From 64c93d992d2352f0875affdf871851c73f7bd0e0 Mon Sep 17 00:00:00 2001 From: dev01 Date: Thu, 4 Jun 2026 13:30:50 +0800 Subject: [PATCH] refactor: replace http.server with Microdot framework in web_node Co-Authored-By: Claude Opus 4.7 --- .../application_pkg/web_node.py | 162 ++++++------------ workspace/src/application_pkg/package.xml | 1 + workspace/src/application_pkg/setup.py | 2 +- 3 files changed, 58 insertions(+), 107 deletions(-) diff --git a/workspace/src/application_pkg/application_pkg/web_node.py b/workspace/src/application_pkg/application_pkg/web_node.py index 7cea9b7..24a13f8 100755 --- a/workspace/src/application_pkg/application_pkg/web_node.py +++ b/workspace/src/application_pkg/application_pkg/web_node.py @@ -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() diff --git a/workspace/src/application_pkg/package.xml b/workspace/src/application_pkg/package.xml index e56ce9e..8f71e81 100755 --- a/workspace/src/application_pkg/package.xml +++ b/workspace/src/application_pkg/package.xml @@ -9,6 +9,7 @@ rclpy std_msgs + python3-microdot ament_copyright ament_flake8 diff --git a/workspace/src/application_pkg/setup.py b/workspace/src/application_pkg/setup.py index 587e4a2..d85eb3e 100755 --- a/workspace/src/application_pkg/setup.py +++ b/workspace/src/application_pkg/setup.py @@ -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',