2.7.0 logs

This commit is contained in:
2026-07-06 07:29:56 +08:00
parent fd949fca47
commit f141311173
8 changed files with 442 additions and 33 deletions
@@ -1,6 +1,6 @@
"""
MAVLink ROS2 Nodes
主要包含三個獨立的 ROS2 Node :
主要包含四個獨立的 ROS2 Node :
1. VehicleStatusPublisher - 發布載具狀態到 ROS2 topics
從 vehicle_registry 讀取狀態數據,頻率控制,模組化設計
2. MavlinkCommandService - 提供 MAVLink 指令 service 介面
@@ -8,6 +8,7 @@ MAVLink ROS2 Nodes
並不會包含額外的功能
3. RtcmRelay - 訂閱 RTCM topic 並轉發為 MAVLink GPS_RTCM_DATA 給所有載具
過期丟棄、去重、節流、分片
4. FcNetworkLogPublisher - 將 FC Network logging queue 發布到 /fc_network/logs
與一個節點管理器
- fc_ros_manager
@@ -18,6 +19,7 @@ import os
import time
import math
import hashlib
import queue
import threading
from typing import Dict, Optional
@@ -42,7 +44,7 @@ from fc_interfaces.msg import ServiceAckResult
# 自定義 imports
from . import mavlinkVehicleView as mvv
from . import mavlinkObject as mo
from .utils import setup_logger
from .utils import get_ros_log_queue, setup_logger
logger = setup_logger(os.path.basename(__file__))
MODULE_VER = "2.50"
@@ -1179,6 +1181,58 @@ class RtcmRelay(Node):
# logger.info("RtcmRelay stopped")
# ============================================================================
# FC Network Log Publisher Node
# ============================================================================
class FcNetworkLogPublisher(Node):
"""從 Python logging queue 發布結構化 /fc_network/logs 訊息。"""
def __init__(self):
super().__init__('fc_network_log_publisher')
qos = QoSProfile(
history=HistoryPolicy.KEEP_LAST,
depth=200,
reliability=ReliabilityPolicy.RELIABLE,
durability=DurabilityPolicy.VOLATILE,
)
self.publisher = self.create_publisher(
fcmsg.FcNetworkLog, '/fc_network/logs', qos)
self.log_queue = get_ros_log_queue()
self.running = True
self.timer = self.create_timer(0.05, self._drain_queue)
def _drain_queue(self):
if not self.running:
return
for _ in range(100):
try:
item = self.log_queue.get_nowait()
except queue.Empty:
break
created = max(0.0, float(item.get('created', 0.0)))
seconds = int(created)
nanoseconds = int((created - seconds) * 1_000_000_000)
msg = fcmsg.FcNetworkLog()
msg.stamp.sec = seconds
msg.stamp.nanosec = min(max(nanoseconds, 0), 999_999_999)
msg.level = min(max(int(item.get('level', 20)), 0), 255)
msg.source = str(item.get('source', 'fc_network'))
msg.event_code = str(item.get('event_code', ''))
msg.sysid = min(
max(int(item.get('sysid', -1)), -2_147_483_648),
2_147_483_647
)
msg.message = str(item.get('message', ''))
self.publisher.publish(msg)
def stop(self):
self.running = False
# ============================================================================
# ROS2 節點管理器
# ============================================================================
@@ -1192,10 +1246,11 @@ class fc_ros_manager:
stop 是停下止 ROS2 nodes 的運行,但不銷毀節點實例,允許後續再次 start。
shutdown 是完全關閉 ROS2 並銷毀節點實例。
管理三個獨立的 ROS2 Node :
管理四個獨立的 ROS2 Node :
- VehicleStatusPublisher
- MavlinkCommandService
- RtcmRelay
- FcNetworkLogPublisher
提供統一的啟動/停止介面給 mainOrchestrator
@@ -1215,6 +1270,7 @@ class fc_ros_manager:
self.status_publisher: Optional[VehicleStatusPublisher] = None
self.command_service: Optional[MavlinkCommandService] = None
self.rtcm_relay: Optional[RtcmRelay] = None
self.log_publisher: Optional[FcNetworkLogPublisher] = None
# Executor & Thread
self.spin_thread: Optional[threading.Thread] = None
@@ -1235,12 +1291,19 @@ class fc_ros_manager:
self.status_publisher = VehicleStatusPublisher()
self.command_service = MavlinkCommandService()
self.rtcm_relay = RtcmRelay()
if hasattr(fcmsg, 'FcNetworkLog'):
self.log_publisher = FcNetworkLogPublisher()
else:
logger.warning(
"FcNetworkLog interface unavailable; /fc_network/logs disabled")
# 創建執行者 MultiThreadedExecutor 並把 node 加入其中
self.executor = MultiThreadedExecutor()
self.executor.add_node(self.status_publisher)
self.executor.add_node(self.command_service)
self.executor.add_node(self.rtcm_relay)
if self.log_publisher:
self.executor.add_node(self.log_publisher)
self.initialized = True
# logger.info("fc_ros_manager initialized")
@@ -1266,6 +1329,8 @@ class fc_ros_manager:
self.status_publisher.running = True
self.command_service.running = True
self.rtcm_relay.running = True
if self.log_publisher:
self.log_publisher.running = True
self.spin_thread = threading.Thread(
target=self._spin_executor,
@@ -1391,6 +1456,8 @@ class fc_ros_manager:
self.command_service.stop()
if self.rtcm_relay:
self.rtcm_relay.stop()
if self.log_publisher:
self.log_publisher.stop()
# 等待 spin 執行緒結束
if self.spin_thread and self.spin_thread.is_alive():
@@ -1417,6 +1484,8 @@ class fc_ros_manager:
self.command_service.destroy_node()
if self.rtcm_relay:
self.rtcm_relay.destroy_node()
if self.log_publisher:
self.log_publisher.destroy_node()
# 關閉 ROS2
if rclpy.ok():
@@ -1435,6 +1504,7 @@ class fc_ros_manager:
'status_publisher_active': self.status_publisher is not None and self.status_publisher.running,
'command_service_active': self.command_service is not None,
'rtcm_relay_active': self.rtcm_relay is not None and self.rtcm_relay.running,
'log_publisher_active': self.log_publisher is not None and self.log_publisher.running,
}
@@ -1489,4 +1559,3 @@ TODO
1. service 部分會需要跟 mavlinkobject 大量互動 也許需要考慮對方的生命週期
'''
@@ -2,6 +2,6 @@
共用工具模組
"""
from .ringBuffer import RingBuffer
from .theLogger import setup_logger
from .theLogger import get_ros_log_queue, setup_logger
__all__ = ['RingBuffer', 'setup_logger']
__all__ = ['RingBuffer', 'get_ros_log_queue', 'setup_logger']
@@ -1,9 +1,47 @@
import logging
import os
import queue
from logging.handlers import TimedRotatingFileHandler
# 全域 Logger 實例
_global_logger = None
_ros_log_queue = queue.Queue(maxsize=2000)
class RosTopicQueueHandler(logging.Handler):
"""將 FC Network log record 非阻塞地排入 ROS publisher 佇列。"""
def emit(self, record: logging.LogRecord) -> None:
try:
sysid = int(getattr(record, 'sysid', -1))
except (TypeError, ValueError):
sysid = -1
item = {
'created': float(record.created),
'level': int(record.levelno),
'source': str(record.name),
'event_code': str(getattr(record, 'event_code', '')),
'sysid': sysid,
'message': record.getMessage(),
}
try:
_ros_log_queue.put_nowait(item)
except queue.Full:
# GUI 不應反過來拖慢通訊;滿載時淘汰最舊一筆。
try:
_ros_log_queue.get_nowait()
except queue.Empty:
return
try:
_ros_log_queue.put_nowait(item)
except queue.Full:
pass
def get_ros_log_queue():
"""供 ROS publisher node 取得共用、thread-safe 的紀錄佇列。"""
return _ros_log_queue
def setup_logger(name: str, log_dir: str = "logs", level=logging.DEBUG) -> logging.Logger:
global _global_logger
@@ -37,6 +75,10 @@ def setup_logger(name: str, log_dir: str = "logs", level=logging.DEBUG) -> loggi
console_handler.setFormatter(formatter)
_global_logger.addHandler(console_handler)
ros_queue_handler = RosTopicQueueHandler()
ros_queue_handler.setLevel(logging.INFO)
_global_logger.addHandler(ros_queue_handler)
# 為每個模組建立子 Logger,並設定名稱
module_logger = _global_logger.getChild(name)
module_logger.name = name # 修改子 Logger 的名稱,僅保留子 Logger 名稱