feat: separate laser API endpoint (POST /api/laser)

This commit is contained in:
dev01
2026-06-01 20:46:46 +08:00
parent a281fd115e
commit bb8724035f
3 changed files with 95 additions and 42 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ server {
proxy_send_timeout 86400s; proxy_send_timeout 86400s;
} }
# 模式切换 API (HTTP → ROS2 web_node) # API (HTTP → ROS2 web_node)
location /api/ { location /api/ {
proxy_pass http://127.0.0.1:8080/api/; proxy_pass http://127.0.0.1:8080/api/;
proxy_set_header Host $host; proxy_set_header Host $host;
+27 -11
View File
@@ -109,11 +109,6 @@ curl -X POST http://localhost:8080/api/mode/switch \
curl -X POST http://localhost:8080/api/mode/switch \ curl -X POST http://localhost:8080/api/mode/switch \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"mode": "debug"}' -d '{"mode": "debug"}'
# 激光开关
curl -X POST http://localhost:8080/api/mode/switch \
-H "Content-Type: application/json" \
-d '{"mode": "laser_on"}'
``` ```
也可直接通过 ROS2 话题发送: 也可直接通过 ROS2 话题发送:
@@ -122,7 +117,28 @@ curl -X POST http://localhost:8080/api/mode/switch \
ros2 topic pub /gimbal/mode std_msgs/String "data: 'auto'" 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 ```bash
# angular.y = pitch 角速度, angular.z = yaw 角速度 # 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}}" "{angular: {y: 0.0, z: 0.0}}"
``` ```
## 7. PID 调参(调试模式) ## 8. PID 调参(调试模式)
先切换到 debug 模式,然后通过 `/pid_config` 话题发送参数: 先切换到 debug 模式,然后通过 `/pid_config` 话题发送参数:
@@ -144,13 +160,13 @@ ros2 topic pub /pid_config std_msgs/String \
参数立即生效,调试完毕后切回 auto 或 manual 模式。 参数立即生效,调试完毕后切回 auto 或 manual 模式。
## 8. 查看视觉结果 ## 9. 查看视觉结果
浏览器打开 `http://<设备IP>:8080`,可看到: 浏览器打开 `http://<设备IP>:8080`,可看到:
- 原始图像 + 轮廓标注(result) - 原始图像 + 轮廓标注(result)
- Otsu 二值化 + 闭运算结果(binary) - Otsu 二值化 + 闭运算结果(binary)
## 9. 查看话题 ## 10. 查看话题
```bash ```bash
# 列出所有活跃话题 # 列出所有活跃话题
@@ -164,7 +180,7 @@ ros2 topic hz /vision/target
ros2 topic echo /vision/target ros2 topic echo /vision/target
``` ```
## 10. 测试 ## 11. 测试
```bash ```bash
cd workspace cd workspace
@@ -179,7 +195,7 @@ colcon test --packages-select gimbal_control_pkg
colcon test-result --all colcon test-result --all
``` ```
## 11. 故障排查 ## 12. 故障排查
| 现象 | 检查项 | | 现象 | 检查项 |
|------|--------| |------|--------|
@@ -89,40 +89,77 @@ class _ImageHandler(BaseHTTPRequestHandler):
def do_POST(self): def do_POST(self):
path = self.path.split('?')[0] path = self.path.split('?')[0]
if path == '/api/mode/switch': if path == '/api/mode/switch':
content_length = int(self.headers.get('Content-Length', 0)) self._handle_mode_switch()
body = self.rfile.read(content_length) if content_length > 0 else b'{}' elif path == '/api/laser':
try: self._handle_laser()
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())
else: else:
self.send_response(404) self.send_response(404)
self.end_headers() 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): def _serve_image(self, key):
global _latest_jpeg global _latest_jpeg
jpeg = _latest_jpeg.get(key) if _latest_jpeg else None jpeg = _latest_jpeg.get(key) if _latest_jpeg else None