diff --git a/README.md b/README.md index 03d5403..aca05d3 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ │ │ application_pkg │ │ │ │ ┌───────────────────────────────┐ │ │ │ │ │ web_node (:8080) │ │ │ -│ │ │ - MJPEG 预览页 │ │ │ │ │ │ - POST /api/mode/switch │──┼──▶ /gimbal/mode │ +│ │ │ - POST /api/laser │ │ │ │ │ └───────────────────────────────┘ │ │ │ └─────────────────────────────────────┘ │ │ │ @@ -50,8 +50,6 @@ | 阶段 | 话题 | 方向 | 消息类型 | 说明 | |------|------|------|----------|------| | 图像采集 | `/camera/image_raw` | camera_node → vision_node | `sensor_msgs/Image` | 原始相机图像 | -| 视觉结果 | `/vision/result_image` | vision_node → web_node | `sensor_msgs/Image` | 带轮廓标注的结果图 | -| 二值调试 | `/vision/binary_image` | vision_node → web_node | `sensor_msgs/Image` | 二值化调试图像 | | 目标追踪 | `/vision/target` | vision_node → gimbal_node | `geometry_msgs/Point` | 归一化目标偏移 (x,y) | | 模式切换 | `/gimbal/mode` | web_node → gimbal_node | `std_msgs/String` | auto/manual/debug | | 手动控制 | `/cmd_gimbal` | 前端 → gimbal_node | `geometry_msgs/Twist` | yaw/pitch 角度 | @@ -88,9 +86,6 @@ source install/setup.bash # 启动全流程 ros2 launch bringup_pkg vision_pipeline.launch.py - -# 浏览器预览 -# http://<设备IP>:8080 ``` 详细使用方法见 [使用指南](docs/使用指南.md)。 diff --git a/docs/使用指南.md b/docs/使用指南.md index a2115c9..ed1e856 100644 --- a/docs/使用指南.md +++ b/docs/使用指南.md @@ -160,13 +160,7 @@ ros2 topic pub /pid_config std_msgs/String \ 参数立即生效,调试完毕后切回 auto 或 manual 模式。 -## 9. 查看视觉结果 - -浏览器打开 `http://<设备IP>:8080`,可看到: -- 原始图像 + 轮廓标注(result) -- Otsu 二值化 + 闭运算结果(binary) - -## 10. 查看话题 +## 9. 查看话题 ```bash # 列出所有活跃话题 @@ -180,7 +174,7 @@ ros2 topic hz /vision/target ros2 topic echo /vision/target ``` -## 11. 测试 +## 10. 测试 ```bash cd workspace @@ -195,7 +189,7 @@ colcon test --packages-select gimbal_control_pkg colcon test-result --all ``` -## 12. 故障排查 +## 11. 故障排查 | 现象 | 检查项 | |------|--------| diff --git a/workspace/src/application_pkg/application_pkg/web_node.py b/workspace/src/application_pkg/application_pkg/web_node.py index 79e07df..1505aec 100755 --- a/workspace/src/application_pkg/application_pkg/web_node.py +++ b/workspace/src/application_pkg/application_pkg/web_node.py @@ -3,88 +3,20 @@ import json import socket import select import socketserver -import cv2 import rclpy from rclpy.node import Node -from sensor_msgs.msg import Image from std_msgs.msg import String -from cv_bridge import CvBridge from http.server import HTTPServer, BaseHTTPRequestHandler -_latest_jpeg = None - -_HTML_PAGE = """ - - - - -Vision Pipeline - - - -

Vision Pipeline Results

-
-
-

Result Image (contours)

- Result -
-
-

Binary (Otsu + Close)

- Binary -
-
-
Live
- - -""" - - _mode_publisher = None -class _ImageHandler(BaseHTTPRequestHandler): +class _APIHandler(BaseHTTPRequestHandler): def do_GET(self): - path = self.path.split('?')[0] - if path == '/' or path == '/index.html': - self.send_response(200) - self.send_header('Content-type', 'text/html; charset=utf-8') - self.end_headers() - self.wfile.write(_HTML_PAGE.encode()) - elif path in ('/result', '/binary'): - self._serve_image(path.lstrip('/')) - else: - self.send_response(404) - self.end_headers() + self.send_response(404) + self.end_headers() def do_POST(self): path = self.path.split('?')[0] @@ -160,22 +92,6 @@ class _ImageHandler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(f'{{"status":"ok","laser":"{action}"}}'.encode()) - def _serve_image(self, key): - global _latest_jpeg - jpeg = _latest_jpeg.get(key) if _latest_jpeg else None - if jpeg is None: - self.send_response(204) - self.end_headers() - return - self.send_response(200) - self.send_header('Content-type', 'image/jpeg') - self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') - self.end_headers() - try: - self.wfile.write(jpeg) - except (BrokenPipeError, ConnectionResetError): - pass - def handle_error(self, request, client_address): pass @@ -191,56 +107,32 @@ class WebNode(Node): def __init__(self, port=8080): super().__init__('web_node') global _mode_publisher - self._bridge = CvBridge() - - self._result_sub = self.create_subscription( - Image, '/vision/result_image', self._make_jpeg_callback('result'), 10 - ) - self._binary_sub = self.create_subscription( - Image, '/vision/binary_image', self._make_jpeg_callback('binary'), 10 - ) self._mode_publisher = self.create_publisher( String, '/gimbal/mode', 10 ) _mode_publisher = self._mode_publisher - self._server = _ThreadingHTTPServer(('', port), _ImageHandler) + self._server = _ThreadingHTTPServer(('', port), _APIHandler) self._server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self._server.socket.setblocking(False) self._server.timeout = 0 - # 定时器在主线程中轮询 accept,连接交给工作线程处理 self._timer = self.create_timer(0.01, self._handle_http_requests) hostname = socket.gethostname() ip = socket.gethostbyname(hostname) self.get_logger().info( - f'Web server on http://{ip}:{port} (http://localhost:{port})' + f'API server on http://{ip}:{port}' ) def _handle_http_requests(self): - # 一次 tick 内处理所有积压连接 for _ in range(20): try: self._server.handle_request() except Exception: break - def _make_jpeg_callback(self, key): - def callback(msg): - global _latest_jpeg - try: - cv_image = self._bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8') - _, jpeg = cv2.imencode('.jpg', cv_image, - [cv2.IMWRITE_JPEG_QUALITY, 85]) - if _latest_jpeg is None: - _latest_jpeg = {} - _latest_jpeg[key] = jpeg.tobytes() - except Exception as e: - self.get_logger().error(f'JPEG encode error ({key}): {e}') - return callback - def destroy_node(self): self._server.shutdown() super().destroy_node() diff --git a/workspace/src/application_pkg/package.xml b/workspace/src/application_pkg/package.xml index 0583f12..e56ce9e 100755 --- a/workspace/src/application_pkg/package.xml +++ b/workspace/src/application_pkg/package.xml @@ -8,8 +8,6 @@ TODO: License declaration rclpy - sensor_msgs - cv_bridge std_msgs ament_copyright