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:
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user