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