refactor: replace http.server with Microdot framework in web_node

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dev01
2026-06-04 13:30:50 +08:00
parent df02657f1d
commit 64c93d992d
3 changed files with 58 additions and 107 deletions
@@ -1,138 +1,88 @@
#!/usr/bin/env python3
import json
import asyncio
import threading
import socket
import select
import socketserver
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
from http.server import HTTPServer, BaseHTTPRequestHandler
_mode_publisher = None
class _APIHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(404)
self.end_headers()
def do_POST(self):
path = self.path.split('?')[0]
if path == '/api/mode/switch':
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._json_response(400, {'code': -1, 'message': 'invalid json'})
return
valid_modes = ('auto', 'manual', 'debug')
if mode not in valid_modes:
self._json_response(400, {
'code': -1,
'message': f'unknown mode: {mode}',
})
return
if _mode_publisher is not None:
msg = String()
msg.data = mode
_mode_publisher.publish(msg)
self._json_response(200, {'code': 0, 'message': 'ok'})
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._json_response(400, {'code': -1, 'message': 'invalid json'})
return
if action == 'on':
mode = 'laser_on'
elif action == 'off':
mode = 'laser_off'
else:
self._json_response(400, {
'code': -1,
'message': 'action must be on or off',
})
return
if _mode_publisher is not None:
msg = String()
msg.data = mode
_mode_publisher.publish(msg)
self._json_response(200, {'code': 0, 'message': 'ok'})
def _json_response(self, status, data):
self.send_response(status)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(data).encode())
def handle_error(self, request, client_address):
pass
def log_message(self, format, *args):
pass
class _ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
daemon_threads = True
from microdot import Microdot
class WebNode(Node):
def __init__(self):
super().__init__('web_node')
global _mode_publisher
self.declare_parameter('port', 8080)
port = self.get_parameter('port').value
self._port = self.get_parameter('port').value
self._mode_publisher = self.create_publisher(
String, '/gimbal/mode', 10
)
_mode_publisher = self._mode_publisher
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
self._app = Microdot()
self._setup_routes()
self._timer = self.create_timer(0.01, self._handle_http_requests)
self._thread = threading.Thread(target=self._run_server, daemon=True)
self._thread.start()
hostname = socket.gethostname()
ip = socket.gethostbyname(hostname)
self.get_logger().info(
f'API server on http://{ip}:{port}'
)
self.get_logger().info(f'API server on http://{ip}:{self._port}')
def _handle_http_requests(self):
for _ in range(20):
def _setup_routes(self):
app = self._app
@app.route('/api/mode/switch', methods=['POST'])
async def mode_switch(request):
try:
self._server.handle_request()
data = request.json
except Exception:
break
return {'code': -1, 'message': 'invalid json'}, 400
mode = data.get('mode', '')
valid_modes = ('auto', 'manual', 'debug')
if mode not in valid_modes:
return {'code': -1, 'message': f'unknown mode: {mode}'}, 400
msg = String()
msg.data = mode
self._mode_publisher.publish(msg)
return {'code': 0, 'message': 'ok'}
@app.route('/api/laser', methods=['POST'])
async def laser(request):
try:
data = request.json
except Exception:
return {'code': -1, 'message': 'invalid json'}, 400
action = data.get('action', '')
if action == 'on':
mode = 'laser_on'
elif action == 'off':
mode = 'laser_off'
else:
return {'code': -1, 'message': 'action must be on or off'}, 400
msg = String()
msg.data = mode
self._mode_publisher.publish(msg)
return {'code': 0, 'message': 'ok'}
def _run_server(self):
try:
self._app.run(host='0.0.0.0', port=self._port, debug=False)
except RuntimeError:
pass
def destroy_node(self):
self._server.server_close()
try:
self._app.shutdown()
except Exception:
pass
super().destroy_node()
@@ -9,6 +9,7 @@
<depend>rclpy</depend>
<depend>std_msgs</depend>
<depend>python3-microdot</depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
+1 -1
View File
@@ -11,7 +11,7 @@ setup(
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
install_requires=['setuptools', 'microdot'],
zip_safe=True,
maintainer='dev01',
maintainer_email='dev01@todo.todo',