feat: separate laser API endpoint (POST /api/laser)
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user