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 #!/usr/bin/env python3
import json import json
import asyncio
import threading
import socket import socket
import select
import socketserver
import rclpy import rclpy
from rclpy.node import Node from rclpy.node import Node
from std_msgs.msg import String from std_msgs.msg import String
from http.server import HTTPServer, BaseHTTPRequestHandler from microdot import Microdot
_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
class WebNode(Node): class WebNode(Node):
def __init__(self): def __init__(self):
super().__init__('web_node') super().__init__('web_node')
global _mode_publisher
self.declare_parameter('port', 8080) self.declare_parameter('port', 8080)
port = self.get_parameter('port').value self._port = self.get_parameter('port').value
self._mode_publisher = self.create_publisher( self._mode_publisher = self.create_publisher(
String, '/gimbal/mode', 10 String, '/gimbal/mode', 10
) )
_mode_publisher = self._mode_publisher
self._server = _ThreadingHTTPServer(('', port), _APIHandler) self._app = Microdot()
self._server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self._setup_routes()
self._server.socket.setblocking(False)
self._server.timeout = 0
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() hostname = socket.gethostname()
ip = socket.gethostbyname(hostname) ip = socket.gethostbyname(hostname)
self.get_logger().info( self.get_logger().info(f'API server on http://{ip}:{self._port}')
f'API server on http://{ip}:{port}'
)
def _handle_http_requests(self): def _setup_routes(self):
for _ in range(20): app = self._app
@app.route('/api/mode/switch', methods=['POST'])
async def mode_switch(request):
try: try:
self._server.handle_request() data = request.json
except Exception: 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): def destroy_node(self):
self._server.server_close() try:
self._app.shutdown()
except Exception:
pass
super().destroy_node() super().destroy_node()
@@ -9,6 +9,7 @@
<depend>rclpy</depend> <depend>rclpy</depend>
<depend>std_msgs</depend> <depend>std_msgs</depend>
<depend>python3-microdot</depend>
<test_depend>ament_copyright</test_depend> <test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend> <test_depend>ament_flake8</test_depend>
+1 -1
View File
@@ -11,7 +11,7 @@ setup(
['resource/' + package_name]), ['resource/' + package_name]),
('share/' + package_name, ['package.xml']), ('share/' + package_name, ['package.xml']),
], ],
install_requires=['setuptools'], install_requires=['setuptools', 'microdot'],
zip_safe=True, zip_safe=True,
maintainer='dev01', maintainer='dev01',
maintainer_email='dev01@todo.todo', maintainer_email='dev01@todo.todo',