feat: abstract base classes for endpoints

Introduces EndpointBase and ReceiverBase abstract base classes. Implements a ReceiverBase with the MainReceiver.

ref: N25B-168
This commit is contained in:
Twirre Meulenbelt
2025-10-09 16:04:18 +02:00
parent c4530f0c3a
commit 23805812d5
6 changed files with 201 additions and 42 deletions

View File

@@ -1,65 +1,59 @@
import logging
import time
import zmq
def handle_ping(message):
"""A simple ping endpoint. Returns the provided data."""
return {"endpoint": "ping", "data": message.get("data")}
from robot_interface.endpoints.main_receiver import MainReceiver
from robot_interface.state import state
def handle_negotiation(message):
def main_loop(context):
"""
Handle a negotiation request. Will respond with ports that can be used to connect to the robot.
Run the main loop, handling all incoming requests like pings, negotiation, actuation, etc.
:param message: The negotiation request message.
:type message: dict
:return: A response dictionary with a 'ports' key containing a list of ports and their function.
:rtype: dict[str, list[dict]]
:param context: The ZeroMQ context to use.
:type context: zmq.Context
"""
# TODO: .../error on all endpoints?
return {"endpoint": "negotiation/error", "data": "The requested endpoint is not implemented."}
# When creating endpoints, remember to add them to the endpoint list of the state to ensure they're deinitialized
main_receiver = MainReceiver(context)
state.endpoints.append(main_receiver)
# Define endpoints that can run on the main thread. These endpoints should not block for long (say 50 ms at most).
receivers = [main_receiver]
def route_request(message):
"""
Handle a request message.
poller = zmq.Poller()
for receiver in receivers:
poller.register(receiver.socket, zmq.POLLIN)
:param message: The request message.
:type message: dict
:return: A response message.
:rtype: dict
"""
print("Received request: {}".format(message))
if "endpoint" not in message:
return {"endpoint": "error", "data": "No endpoint provided."}
if message["endpoint"] == "ping":
return handle_ping(message)
elif message["endpoint"] == "negotiation":
return handle_negotiation(message)
return {"endpoint": "error", "data": "The requested endpoint is not implemented."}
def main_loop(socket):
while True:
request = socket.recv_json()
response = route_request(request)
socket.send_json(response)
if state.exit_event.is_set(): break
socks = dict(poller.poll(100))
for receiver in receivers:
if receiver.socket not in socks: continue
start_time = time.time()
message = receiver.socket.recv_json()
response = receiver.handle_message(message)
receiver.socket.send_json(response)
time_spent_ms = (time.time() - start_time) * 1000
if time_spent_ms > 50:
logging.warn("Endpoint \"%s\" took too long (%.2f ms) on the main thread.", receiver.name, time_spent_ms)
def main():
context = zmq.Context()
socket = context.socket(zmq.REP)
socket.connect("tcp://localhost:5555")
state.initialize()
try:
main_loop(socket)
main_loop(context)
except KeyboardInterrupt:
print("User interrupted.")
finally:
socket.close()
state.deinitialize()
context.term()