|
|
|
|
@ -83,7 +83,10 @@ class JsonTelemetryProcessor:
|
|
|
|
|
"position": {"lat": 24.0, "lon": 120.0},
|
|
|
|
|
"heading": 90
|
|
|
|
|
}
|
|
|
|
|
Serial JSON uses this same shape; only the transport/framing differs.
|
|
|
|
|
Serial JSON also accepts the compact UAV.py shape:
|
|
|
|
|
{"s": 1, "m": "GUIDED", "a": 1, "b": 85, "h": 10.0,
|
|
|
|
|
"v": 4.2, "p": [24.0, 120.0], "ypr": [90.0, 0.0, 0.0],
|
|
|
|
|
"g": 3, "d": [0.8, 1.2]}
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def _emit_json_connection_type(self, drone_id):
|
|
|
|
|
@ -103,19 +106,21 @@ class JsonTelemetryProcessor:
|
|
|
|
|
if not isinstance(data, dict):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
system_id = data.get('system_id', data.get('sysid'))
|
|
|
|
|
system_id = data.get('system_id', data.get('sysid', data.get('s')))
|
|
|
|
|
if system_id is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
drone_id = f"s{self.socket_id}_{system_id}"
|
|
|
|
|
self._emit_json_connection_type(drone_id)
|
|
|
|
|
|
|
|
|
|
mode = data.get('mode', data.get('mode_name'))
|
|
|
|
|
mode = data.get('mode', data.get('mode_name', data.get('m')))
|
|
|
|
|
state = {}
|
|
|
|
|
if mode is not None:
|
|
|
|
|
state['mode'] = mode
|
|
|
|
|
if 'armed' in data:
|
|
|
|
|
state['armed'] = data.get('armed')
|
|
|
|
|
elif 'a' in data:
|
|
|
|
|
state['armed'] = bool(data.get('a'))
|
|
|
|
|
if state:
|
|
|
|
|
self.signals.update_signal.emit('state', drone_id, state)
|
|
|
|
|
|
|
|
|
|
@ -144,6 +149,10 @@ class JsonTelemetryProcessor:
|
|
|
|
|
if 'battery_voltage' in data:
|
|
|
|
|
battery_data['voltage'] = data.get('battery_voltage')
|
|
|
|
|
self.signals.update_signal.emit('battery', drone_id, battery_data)
|
|
|
|
|
elif 'b' in data:
|
|
|
|
|
self.signals.update_signal.emit('battery', drone_id, {
|
|
|
|
|
'percentage': data.get('b')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
pos = data.get('position')
|
|
|
|
|
if isinstance(pos, dict):
|
|
|
|
|
@ -159,6 +168,22 @@ class JsonTelemetryProcessor:
|
|
|
|
|
'lon': data.get('lon', data.get('longitude', 0)),
|
|
|
|
|
'alt': data.get('alt', data.get('altitude', 0))
|
|
|
|
|
})
|
|
|
|
|
elif isinstance(data.get('p'), (list, tuple)) and len(data.get('p')) >= 2:
|
|
|
|
|
dop = data.get('d') if isinstance(data.get('d'), (list, tuple)) else []
|
|
|
|
|
gps_data = {
|
|
|
|
|
'lat': data['p'][0],
|
|
|
|
|
'lon': data['p'][1],
|
|
|
|
|
'alt': data.get('h', 0)
|
|
|
|
|
}
|
|
|
|
|
if 'g' in data:
|
|
|
|
|
gps_data['fix_type'] = data.get('g')
|
|
|
|
|
if len(dop) >= 1:
|
|
|
|
|
gps_data['eph'] = dop[0]
|
|
|
|
|
gps_data['hdop'] = dop[0]
|
|
|
|
|
if len(dop) >= 2:
|
|
|
|
|
gps_data['epv'] = dop[1]
|
|
|
|
|
gps_data['vdop'] = dop[1]
|
|
|
|
|
self.signals.update_signal.emit('gps', drone_id, gps_data)
|
|
|
|
|
|
|
|
|
|
local = data.get('local_position', data.get('local_pose', data.get('local')))
|
|
|
|
|
if isinstance(local, dict):
|
|
|
|
|
@ -183,6 +208,29 @@ class JsonTelemetryProcessor:
|
|
|
|
|
self.signals.update_signal.emit('altitude', drone_id, {
|
|
|
|
|
'altitude': alt
|
|
|
|
|
})
|
|
|
|
|
elif 'h' in data:
|
|
|
|
|
height = data.get('h', 0.0)
|
|
|
|
|
self.signals.update_signal.emit('local_pose', drone_id, {
|
|
|
|
|
'x': 0.0,
|
|
|
|
|
'y': 0.0,
|
|
|
|
|
'z': height
|
|
|
|
|
})
|
|
|
|
|
self.signals.update_signal.emit('altitude', drone_id, {
|
|
|
|
|
'altitude': height
|
|
|
|
|
})
|
|
|
|
|
elif (
|
|
|
|
|
isinstance(data.get('p'), (list, tuple))
|
|
|
|
|
or 'lat' in data
|
|
|
|
|
or 'latitude' in data
|
|
|
|
|
):
|
|
|
|
|
self.signals.update_signal.emit('local_pose', drone_id, {
|
|
|
|
|
'x': 0.0,
|
|
|
|
|
'y': 0.0,
|
|
|
|
|
'z': 0.0
|
|
|
|
|
})
|
|
|
|
|
self.signals.update_signal.emit('altitude', drone_id, {
|
|
|
|
|
'altitude': 0.0
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
velocity = data.get('velocity')
|
|
|
|
|
if isinstance(velocity, dict):
|
|
|
|
|
@ -191,6 +239,12 @@ class JsonTelemetryProcessor:
|
|
|
|
|
'vy': velocity.get('vy', velocity.get('y', 0.0)),
|
|
|
|
|
'vz': velocity.get('vz', velocity.get('z', 0.0))
|
|
|
|
|
})
|
|
|
|
|
elif 'v' in data:
|
|
|
|
|
self.signals.update_signal.emit('velocity', drone_id, {
|
|
|
|
|
'vx': data.get('v', 0.0),
|
|
|
|
|
'vy': 0.0,
|
|
|
|
|
'vz': 0.0
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
attitude = data.get('attitude')
|
|
|
|
|
if isinstance(attitude, dict):
|
|
|
|
|
@ -200,12 +254,28 @@ class JsonTelemetryProcessor:
|
|
|
|
|
'yaw': attitude.get('yaw', 0.0),
|
|
|
|
|
'rates': attitude.get('rates', (0.0, 0.0, 0.0))
|
|
|
|
|
})
|
|
|
|
|
elif isinstance(data.get('ypr'), (list, tuple)) and len(data.get('ypr')) >= 3:
|
|
|
|
|
yaw, pitch, roll = data['ypr'][0], data['ypr'][1], data['ypr'][2]
|
|
|
|
|
self.signals.update_signal.emit('attitude', drone_id, {
|
|
|
|
|
'roll': roll,
|
|
|
|
|
'pitch': pitch,
|
|
|
|
|
'yaw': yaw,
|
|
|
|
|
'rates': (0.0, 0.0, 0.0)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
hud = data.get('hud', {})
|
|
|
|
|
if not isinstance(hud, dict):
|
|
|
|
|
hud = {}
|
|
|
|
|
if 'heading' in data:
|
|
|
|
|
hud['heading'] = data.get('heading')
|
|
|
|
|
elif 'y' in data:
|
|
|
|
|
hud['heading'] = data.get('y')
|
|
|
|
|
elif isinstance(data.get('ypr'), (list, tuple)) and len(data.get('ypr')) >= 1:
|
|
|
|
|
hud['heading'] = data['ypr'][0]
|
|
|
|
|
if 'v' in data and 'groundspeed' not in hud:
|
|
|
|
|
hud['groundspeed'] = data.get('v')
|
|
|
|
|
if 'h' in data and 'alt' not in hud and 'altitude' not in hud:
|
|
|
|
|
hud['alt'] = data.get('h')
|
|
|
|
|
if hud:
|
|
|
|
|
self.signals.update_signal.emit('hud', drone_id, {
|
|
|
|
|
'heading': hud.get('heading', 0.0),
|
|
|
|
|
@ -229,6 +299,7 @@ class UDPMavlinkReceiver(threading.Thread):
|
|
|
|
|
self.connection_name = connection_name
|
|
|
|
|
self.monitor = monitor # 保存 monitor 引用
|
|
|
|
|
self.socket_id = monitor.get_next_socket_id() if monitor else 0
|
|
|
|
|
self._socket_id_released = False
|
|
|
|
|
self.running = False
|
|
|
|
|
self.sock = None
|
|
|
|
|
|
|
|
|
|
@ -258,6 +329,7 @@ class UDPMavlinkReceiver(threading.Thread):
|
|
|
|
|
finally:
|
|
|
|
|
if self.sock:
|
|
|
|
|
self.sock.close()
|
|
|
|
|
self._release_socket_id()
|
|
|
|
|
|
|
|
|
|
def process_mavlink_message(self, msg):
|
|
|
|
|
"""處理 MAVLink 訊息"""
|
|
|
|
|
@ -343,6 +415,12 @@ class UDPMavlinkReceiver(threading.Thread):
|
|
|
|
|
def stop(self):
|
|
|
|
|
"""停止接收器"""
|
|
|
|
|
self.running = False
|
|
|
|
|
self._release_socket_id()
|
|
|
|
|
|
|
|
|
|
def _release_socket_id(self):
|
|
|
|
|
if self.monitor and not self._socket_id_released:
|
|
|
|
|
self.monitor.release_socket_id(self.socket_id)
|
|
|
|
|
self._socket_id_released = True
|
|
|
|
|
|
|
|
|
|
class SerialMavlinkReceiver(threading.Thread, JsonTelemetryProcessor):
|
|
|
|
|
"""串口遙測接收器,可自動處理 MAVLink 或 WebSocket 格式 JSON。"""
|
|
|
|
|
@ -354,6 +432,7 @@ class SerialMavlinkReceiver(threading.Thread, JsonTelemetryProcessor):
|
|
|
|
|
self.connection_name = connection_name
|
|
|
|
|
self.monitor = monitor # 保存 monitor 引用
|
|
|
|
|
self.socket_id = monitor.get_next_socket_id() if monitor else 0
|
|
|
|
|
self._socket_id_released = False
|
|
|
|
|
self.source_type = 'Serial'
|
|
|
|
|
self.running = False
|
|
|
|
|
self.serial_conn = None
|
|
|
|
|
@ -402,6 +481,7 @@ class SerialMavlinkReceiver(threading.Thread, JsonTelemetryProcessor):
|
|
|
|
|
self.serial_conn.close()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
self._release_socket_id()
|
|
|
|
|
|
|
|
|
|
def _process_mavlink_byte(self, byte):
|
|
|
|
|
try:
|
|
|
|
|
@ -562,6 +642,12 @@ class SerialMavlinkReceiver(threading.Thread, JsonTelemetryProcessor):
|
|
|
|
|
self.serial_conn.close()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
self._release_socket_id()
|
|
|
|
|
|
|
|
|
|
def _release_socket_id(self):
|
|
|
|
|
if self.monitor and not self._socket_id_released:
|
|
|
|
|
self.monitor.release_socket_id(self.socket_id)
|
|
|
|
|
self._socket_id_released = True
|
|
|
|
|
|
|
|
|
|
class WebSocketMavlinkReceiver(threading.Thread, JsonTelemetryProcessor):
|
|
|
|
|
"""WebSocket MAVLink 接收器"""
|
|
|
|
|
@ -572,6 +658,7 @@ class WebSocketMavlinkReceiver(threading.Thread, JsonTelemetryProcessor):
|
|
|
|
|
self.connection_name = connection_name
|
|
|
|
|
self.monitor = monitor # 保存 monitor 引用
|
|
|
|
|
self.socket_id = monitor.get_next_socket_id() if monitor else 0 # 一次性分配 socket_id
|
|
|
|
|
self._socket_id_released = False
|
|
|
|
|
self.source_type = 'WS'
|
|
|
|
|
self.running = False
|
|
|
|
|
self.max_retries = 5
|
|
|
|
|
@ -629,6 +716,7 @@ class WebSocketMavlinkReceiver(threading.Thread, JsonTelemetryProcessor):
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
print(f"WebSocket client {self.connection_name} stopped")
|
|
|
|
|
self._release_socket_id()
|
|
|
|
|
|
|
|
|
|
def process_websocket_message(self, data):
|
|
|
|
|
"""處理 WebSocket 訊息"""
|
|
|
|
|
@ -637,6 +725,12 @@ class WebSocketMavlinkReceiver(threading.Thread, JsonTelemetryProcessor):
|
|
|
|
|
def stop(self):
|
|
|
|
|
"""停止接收器"""
|
|
|
|
|
self.running = False
|
|
|
|
|
self._release_socket_id()
|
|
|
|
|
|
|
|
|
|
def _release_socket_id(self):
|
|
|
|
|
if self.monitor and not self._socket_id_released:
|
|
|
|
|
self.monitor.release_socket_id(self.socket_id)
|
|
|
|
|
self._socket_id_released = True
|
|
|
|
|
|
|
|
|
|
class DroneMonitor(Node):
|
|
|
|
|
# Subscribe to drone ROS2 topics
|
|
|
|
|
@ -675,7 +769,7 @@ class DroneMonitor(Node):
|
|
|
|
|
# 【新增】Socket ID 重新分配機制 (從 0 開始)
|
|
|
|
|
# ================================================================================
|
|
|
|
|
self.socket_id_mapping = {} # {原始socket_id: 重新分配的socket_id}
|
|
|
|
|
self.socket_id_counter = 0 # 當前分配到的最大socket_id
|
|
|
|
|
self.active_socket_ids = set() # 目前通訊連線使用中的 socket_id
|
|
|
|
|
self.socket_id_lock = Lock() # 線程安全鎖
|
|
|
|
|
# ================================================================================
|
|
|
|
|
|
|
|
|
|
@ -708,23 +802,55 @@ class DroneMonitor(Node):
|
|
|
|
|
self.create_timer(1.0, self.scan_topics)
|
|
|
|
|
|
|
|
|
|
def get_next_socket_id(self):
|
|
|
|
|
"""获取下一个可用的 socket_id(从 0 开始连续分配)"""
|
|
|
|
|
"""取得目前最小的未使用 socket_id(從 0 開始)。"""
|
|
|
|
|
with self.socket_id_lock:
|
|
|
|
|
current_id = self.socket_id_counter
|
|
|
|
|
self.socket_id_counter += 1
|
|
|
|
|
return current_id
|
|
|
|
|
socket_id = 0
|
|
|
|
|
while socket_id in self.active_socket_ids:
|
|
|
|
|
socket_id += 1
|
|
|
|
|
self.active_socket_ids.add(socket_id)
|
|
|
|
|
return socket_id
|
|
|
|
|
|
|
|
|
|
def release_socket_id(self, socket_id):
|
|
|
|
|
"""釋放通訊連線使用的 socket_id,讓後續連線可重用最小空缺。"""
|
|
|
|
|
try:
|
|
|
|
|
socket_id = int(socket_id)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
with self.socket_id_lock:
|
|
|
|
|
if socket_id in getattr(self, 'socket_id_mapping', {}).values():
|
|
|
|
|
return
|
|
|
|
|
if socket_id in getattr(self, 'sys_to_socket_id', {}).values():
|
|
|
|
|
return
|
|
|
|
|
self.active_socket_ids.discard(socket_id)
|
|
|
|
|
|
|
|
|
|
def get_or_assign_socket_id(self, original_socket_id):
|
|
|
|
|
"""根據原始 socket_id 分配或獲取對應的 socket_id(從 0 開始連續分配)
|
|
|
|
|
同一個原始 socket_id 會得到同一個分配的 ID
|
|
|
|
|
"""ROS2 socket_id 映射。
|
|
|
|
|
|
|
|
|
|
有原始 socket_id=N 時優先使用 N;若 N 已被其他通訊占用,
|
|
|
|
|
才改用目前最小未使用 ID。同一個原始 socket_id 會得到同一個映射。
|
|
|
|
|
"""
|
|
|
|
|
original_socket_id = str(original_socket_id)
|
|
|
|
|
|
|
|
|
|
with self.socket_id_lock:
|
|
|
|
|
if original_socket_id not in self.socket_id_mapping:
|
|
|
|
|
# 分配新的 socket_id
|
|
|
|
|
self.socket_id_mapping[original_socket_id] = self.socket_id_counter
|
|
|
|
|
self.socket_id_counter += 1
|
|
|
|
|
try:
|
|
|
|
|
preferred_socket_id = int(original_socket_id)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
preferred_socket_id = None
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
preferred_socket_id is not None
|
|
|
|
|
and preferred_socket_id >= 0
|
|
|
|
|
and preferred_socket_id not in self.active_socket_ids
|
|
|
|
|
):
|
|
|
|
|
socket_id = preferred_socket_id
|
|
|
|
|
else:
|
|
|
|
|
socket_id = 0
|
|
|
|
|
while socket_id in self.active_socket_ids:
|
|
|
|
|
socket_id += 1
|
|
|
|
|
self.active_socket_ids.add(socket_id)
|
|
|
|
|
self.socket_id_mapping[original_socket_id] = socket_id
|
|
|
|
|
|
|
|
|
|
return self.socket_id_mapping[original_socket_id]
|
|
|
|
|
|
|
|
|
|
@ -796,8 +922,7 @@ class DroneMonitor(Node):
|
|
|
|
|
# 为每个 sys_id 分配 socket_id(如果还没有分配)
|
|
|
|
|
# 注意:如果后续 summary 提供了 socket_id,会使用 summary 的映射覆盖
|
|
|
|
|
if sys_id not in self.sys_to_socket_id:
|
|
|
|
|
# 暂时所有 ROS2 topic 共享同一个 socket_id = 0
|
|
|
|
|
self.sys_to_socket_id[sys_id] = 0
|
|
|
|
|
self.sys_to_socket_id[sys_id] = self.get_next_socket_id()
|
|
|
|
|
|
|
|
|
|
subs_attr = f'drone_{sys_id}_subs'
|
|
|
|
|
if not hasattr(self, subs_attr):
|
|
|
|
|
@ -1194,8 +1319,12 @@ class DroneMonitor(Node):
|
|
|
|
|
# 從 summary 獲取原始 socket_id,並映射到分配的 socket_id
|
|
|
|
|
original_socket_id = data.get('socket_id')
|
|
|
|
|
if original_socket_id is not None:
|
|
|
|
|
fallback_socket_id = self.sys_to_socket_id.pop(sys_id, None)
|
|
|
|
|
if fallback_socket_id is not None:
|
|
|
|
|
self.release_socket_id(fallback_socket_id)
|
|
|
|
|
# 使用原始 socket_id 獲取或分配統一的 socket_id
|
|
|
|
|
assigned_socket_id = self.get_or_assign_socket_id(original_socket_id)
|
|
|
|
|
self.sys_to_socket_id[sys_id] = assigned_socket_id
|
|
|
|
|
else:
|
|
|
|
|
# 如果沒有 socket_id,使用 sys_to_socket_id 映射
|
|
|
|
|
assigned_socket_id = self.sys_to_socket_id.get(sys_id, 0)
|
|
|
|
|
@ -1361,7 +1490,7 @@ class DroneMonitor(Node):
|
|
|
|
|
'ping': msg.data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def start_serial_connection(self, port='/dev/ttyUSB0', baudrate=57600):
|
|
|
|
|
def start_serial_connection(self, port='/dev/ttyUSB0', baudrate=115200):
|
|
|
|
|
"""啟動串口遙測連接(自動辨識 MAVLink / JSON)"""
|
|
|
|
|
connection_name = f"Serial_{port.replace('/', '_')}"
|
|
|
|
|
receiver = SerialMavlinkReceiver(port, baudrate, self.signals, connection_name, self)
|
|
|
|
|
|