diff --git a/config/nginx-aimbot.conf b/config/nginx-aimbot.conf index a511833..02e0364 100644 --- a/config/nginx-aimbot.conf +++ b/config/nginx-aimbot.conf @@ -34,7 +34,7 @@ server { proxy_send_timeout 86400s; } - # 模式切换 API (HTTP → ROS2 web_node) + # API (HTTP → ROS2 web_node) location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; diff --git a/docs/使用指南.md b/docs/使用指南.md index f12c6ec..a2115c9 100644 --- a/docs/使用指南.md +++ b/docs/使用指南.md @@ -109,11 +109,6 @@ curl -X POST http://localhost:8080/api/mode/switch \ curl -X POST http://localhost:8080/api/mode/switch \ -H "Content-Type: application/json" \ -d '{"mode": "debug"}' - -# 激光开关 -curl -X POST http://localhost:8080/api/mode/switch \ - -H "Content-Type: application/json" \ - -d '{"mode": "laser_on"}' ``` 也可直接通过 ROS2 话题发送: @@ -122,7 +117,28 @@ curl -X POST http://localhost:8080/api/mode/switch \ ros2 topic pub /gimbal/mode std_msgs/String "data: 'auto'" ``` -## 6. 手动控制云台 +## 6. 激光控制 + +```bash +# 开激光 +curl -X POST http://localhost:8080/api/laser \ + -H "Content-Type: application/json" \ + -d '{"action": "on"}' + +# 关激光 +curl -X POST http://localhost:8080/api/laser \ + -H "Content-Type: application/json" \ + -d '{"action": "off"}' +``` + +也可直接通过 ROS2 话题发送: + +```bash +ros2 topic pub /gimbal/mode std_msgs/String "data: 'laser_on'" +ros2 topic pub /gimbal/mode std_msgs/String "data: 'laser_off'" +``` + +## 7. 手动控制云台 ```bash # angular.y = pitch 角速度, angular.z = yaw 角速度 @@ -130,7 +146,7 @@ ros2 topic pub /cmd_gimbal geometry_msgs/Twist \ "{angular: {y: 0.0, z: 0.0}}" ``` -## 7. PID 调参(调试模式) +## 8. PID 调参(调试模式) 先切换到 debug 模式,然后通过 `/pid_config` 话题发送参数: @@ -144,13 +160,13 @@ ros2 topic pub /pid_config std_msgs/String \ 参数立即生效,调试完毕后切回 auto 或 manual 模式。 -## 8. 查看视觉结果 +## 9. 查看视觉结果 浏览器打开 `http://<设备IP>:8080`,可看到: - 原始图像 + 轮廓标注(result) - Otsu 二值化 + 闭运算结果(binary) -## 9. 查看话题 +## 10. 查看话题 ```bash # 列出所有活跃话题 @@ -164,7 +180,7 @@ ros2 topic hz /vision/target ros2 topic echo /vision/target ``` -## 10. 测试 +## 11. 测试 ```bash cd workspace @@ -179,7 +195,7 @@ colcon test --packages-select gimbal_control_pkg colcon test-result --all ``` -## 11. 故障排查 +## 12. 故障排查 | 现象 | 检查项 | |------|--------| diff --git a/workspace/src/application_pkg/application_pkg/web_node.py b/workspace/src/application_pkg/application_pkg/web_node.py index 2c7ca10..79e07df 100755 --- a/workspace/src/application_pkg/application_pkg/web_node.py +++ b/workspace/src/application_pkg/application_pkg/web_node.py @@ -89,40 +89,77 @@ class _ImageHandler(BaseHTTPRequestHandler): def do_POST(self): path = self.path.split('?')[0] if path == '/api/mode/switch': - content_length = int(self.headers.get('Content-Length', 0)) - body = self.rfile.read(content_length) if content_length > 0 else b'{}' - try: - data = json.loads(body) - mode = data.get('mode', '') - except json.JSONDecodeError: - self.send_response(400) - self.end_headers() - self.wfile.write(b'{"error":"invalid json"}') - return - - valid_modes = ('auto', 'manual', 'debug', 'laser_on', 'laser_off') - if mode not in valid_modes: - self.send_response(400) - self.end_headers() - self.wfile.write( - f'{{"error":"invalid mode, must be one of {valid_modes}"}}' - .encode() - ) - return - - if _mode_publisher is not None: - msg = String() - msg.data = mode - _mode_publisher.publish(msg) - - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(f'{{"status":"ok","mode":"{mode}"}}'.encode()) + self._handle_mode_switch() + elif path == '/api/laser': + self._handle_laser() else: self.send_response(404) self.end_headers() + def _handle_mode_switch(self): + content_length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(content_length) if content_length > 0 else b'{}' + try: + data = json.loads(body) + mode = data.get('mode', '') + except json.JSONDecodeError: + self.send_response(400) + self.end_headers() + self.wfile.write(b'{"error":"invalid json"}') + return + + valid_modes = ('auto', 'manual', 'debug') + if mode not in valid_modes: + self.send_response(400) + self.end_headers() + self.wfile.write( + f'{{"error":"invalid mode, must be one of {valid_modes}"}}' + .encode() + ) + return + + if _mode_publisher is not None: + msg = String() + msg.data = mode + _mode_publisher.publish(msg) + + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(f'{{"status":"ok","mode":"{mode}"}}'.encode()) + + def _handle_laser(self): + content_length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(content_length) if content_length > 0 else b'{}' + try: + data = json.loads(body) + action = data.get('action', '') + except json.JSONDecodeError: + self.send_response(400) + self.end_headers() + self.wfile.write(b'{"error":"invalid json"}') + return + + if action == 'on': + mode = 'laser_on' + elif action == 'off': + mode = 'laser_off' + else: + self.send_response(400) + self.end_headers() + self.wfile.write(b'{"error":"action must be on or off"}') + return + + if _mode_publisher is not None: + msg = String() + msg.data = mode + _mode_publisher.publish(msg) + + self.send_response(200) + self.send_header('Content-type', 'application/json') + 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