chore: add documentation RI
Code functionality left unchanged, only added docs where missing close: N25B-298
This commit is contained in:
@@ -2,7 +2,27 @@ from __future__ import unicode_literals
|
||||
|
||||
|
||||
class AgentSettings(object):
|
||||
"""Agent port configuration."""
|
||||
"""
|
||||
Agent port configuration.
|
||||
|
||||
:param actuating_receiver_port: Port for receiving actuation commands.
|
||||
:type actuating_receiver_port: int
|
||||
:param main_receiver_port: Port for receiving main messages.
|
||||
:type main_receiver_port: int
|
||||
:param video_sender_port: Port used for sending video frames.
|
||||
:type video_sender_port: int
|
||||
:param audio_sender_port: Port used for sending audio data.
|
||||
:type audio_sender_port: int
|
||||
|
||||
:ivar actuating_receiver_port: Port for receiving actuation commands.
|
||||
:type actuating_receiver_port: int
|
||||
:ivar main_receiver_port: Port for receiving main messages.
|
||||
:type main_receiver_port: int
|
||||
:ivar video_sender_port: Port used for sending video frames.
|
||||
:type video_sender_port: int
|
||||
:ivar audio_sender_port: Port used for sending audio data.
|
||||
:type audio_sender_port: int
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
actuating_receiver_port=5557,
|
||||
@@ -17,7 +37,35 @@ class AgentSettings(object):
|
||||
|
||||
|
||||
class VideoConfig(object):
|
||||
"""Video configuration constants."""
|
||||
"""
|
||||
Video configuration constants.
|
||||
|
||||
:param camera_index: Index of the camera to use.
|
||||
:type camera_index: int
|
||||
:param resolution: Video resolution mode.
|
||||
:type resolution: int
|
||||
:param color_space: Color space identifier.
|
||||
:type color_space: int
|
||||
:param fps: Frames per second of the video stream.
|
||||
:type fps: int
|
||||
:param stream_name: Name of the video stream.
|
||||
:type stream_name: str
|
||||
:param image_buffer: Internal buffer size for video frames.
|
||||
:type image_buffer: int
|
||||
|
||||
:ivar camera_index: Index of the camera used.
|
||||
:type camera_index: int
|
||||
:ivar resolution: Video resolution mode.
|
||||
:type resolution: int
|
||||
:ivar color_space: Color space identifier.
|
||||
:type color_space: int
|
||||
:ivar fps: Frames per second of the video stream.
|
||||
:type fps: int
|
||||
:ivar stream_name: Name of the video stream.
|
||||
:type stream_name: str
|
||||
:ivar image_buffer: Internal buffer size for video frames.
|
||||
:type image_buffer: int
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
camera_index=0,
|
||||
@@ -36,7 +84,23 @@ class VideoConfig(object):
|
||||
|
||||
|
||||
class AudioConfig(object):
|
||||
"""Audio configuration constants."""
|
||||
"""
|
||||
Audio configuration constants.
|
||||
|
||||
:param sample_rate: Audio sampling rate in Hz.
|
||||
:type sample_rate: int
|
||||
:param chunk_size: Size of audio chunks to capture/process.
|
||||
:type chunk_size: int
|
||||
:param channels: Number of audio channels.
|
||||
:type channels: int
|
||||
|
||||
:ivar sample_rate: Audio sampling rate in Hz.
|
||||
:type sample_rate: int
|
||||
:ivar chunk_size: Size of audio chunks to capture/process.
|
||||
:type chunk_size: int
|
||||
:ivar channels: Number of audio channels.
|
||||
:type channels: int
|
||||
"""
|
||||
def __init__(self, sample_rate=16000, chunk_size=512, channels=1):
|
||||
self.sample_rate = sample_rate
|
||||
self.chunk_size = chunk_size
|
||||
@@ -44,14 +108,46 @@ class AudioConfig(object):
|
||||
|
||||
|
||||
class MainConfig(object):
|
||||
"""Main configuration"""
|
||||
"""
|
||||
Main system configuration.
|
||||
|
||||
:param poll_timeout_ms: Timeout for polling events, in milliseconds.
|
||||
:type poll_timeout_ms: int
|
||||
:param max_handler_time_ms: Maximum allowed handler time, in milliseconds.
|
||||
:type max_handler_time_ms: int
|
||||
|
||||
:ivar poll_timeout_ms: Timeout for polling events, in milliseconds.
|
||||
:type poll_timeout_ms: int
|
||||
:ivar max_handler_time_ms: Maximum allowed handler time, in milliseconds.
|
||||
:type max_handler_time_ms: int
|
||||
"""
|
||||
def __init__(self, poll_timeout_ms=100, max_handler_time_ms=50):
|
||||
self.poll_timeout_ms = poll_timeout_ms
|
||||
self.max_handler_time_ms = max_handler_time_ms
|
||||
|
||||
|
||||
class Settings(object):
|
||||
"""Global settings container."""
|
||||
"""
|
||||
Global settings container.
|
||||
|
||||
:param agent_settings: Agent settings instance or None for defaults.
|
||||
:type agent_settings: AgentSettings | None
|
||||
:param video_config: VideoConfig instance or None for defaults.
|
||||
:type video_config: VideoConfig | None
|
||||
:param audio_config: AudioConfig instance or None for defaults.
|
||||
:type audio_config: AudioConfig | None
|
||||
:param main_config: MainConfig instance or None for defaults.
|
||||
:type main_config: MainConfig | None
|
||||
|
||||
:ivar agent_settings: Agent-related port configuration.
|
||||
:type agent_settings: AgentSettings
|
||||
:ivar video_config: Video stream configuration.
|
||||
:type video_config: VideoConfig
|
||||
:ivar audio_config: Audio stream configuration.
|
||||
:type audio_config: AudioConfig
|
||||
:ivar main_config: Main system-level configuration.
|
||||
:type main_config: MainConfig
|
||||
"""
|
||||
def __init__(self, agent_settings=None, video_config=None, audio_config=None, main_config=None):
|
||||
self.agent_settings = agent_settings or AgentSettings()
|
||||
self.video_config = video_config or VideoConfig()
|
||||
|
||||
@@ -10,22 +10,32 @@ from robot_interface.core.config import settings
|
||||
|
||||
|
||||
class ActuationReceiver(ReceiverBase):
|
||||
"""
|
||||
The actuation receiver endpoint, responsible for handling speech and gesture requests.
|
||||
|
||||
:param zmq_context: The ZeroMQ context to use.
|
||||
:type zmq_context: zmq.Context
|
||||
|
||||
:param port: The port to use.
|
||||
:type port: int
|
||||
|
||||
:ivar _tts_service: The text-to-speech service object from the Qi session.
|
||||
:vartype _tts_service: ssl.SSLSession | None
|
||||
"""
|
||||
def __init__(self, zmq_context, port=settings.agent_settings.actuating_receiver_port):
|
||||
"""
|
||||
The actuation receiver endpoint, responsible for handling speech and gesture requests.
|
||||
|
||||
:param zmq_context: The ZeroMQ context to use.
|
||||
:type zmq_context: zmq.Context
|
||||
|
||||
:param port: The port to use.
|
||||
:type port: int
|
||||
"""
|
||||
super(ActuationReceiver, self).__init__("actuation")
|
||||
self.create_socket(zmq_context, zmq.SUB, port)
|
||||
self.socket.setsockopt_string(zmq.SUBSCRIBE, u"") # Causes block if given in options
|
||||
self._tts_service = None
|
||||
|
||||
def _handle_speech(self, message):
|
||||
"""
|
||||
Handle a speech actuation request.
|
||||
|
||||
:param message: The message to handle, must contain properties "endpoint" and "data".
|
||||
:type message: dict
|
||||
"""
|
||||
text = message.get("data")
|
||||
if not text:
|
||||
logging.warn("Received message to speak, but it lacks data.")
|
||||
@@ -48,5 +58,11 @@ class ActuationReceiver(ReceiverBase):
|
||||
qi.async(self._tts_service.say, text)
|
||||
|
||||
def handle_message(self, message):
|
||||
"""
|
||||
Handle an actuation/speech message with the receiver.
|
||||
|
||||
:param message: The message to handle, must contain properties "endpoint" and "data".
|
||||
:type message: dict
|
||||
"""
|
||||
if message["endpoint"] == "actuate/speech":
|
||||
self._handle_speech(message)
|
||||
|
||||
@@ -14,6 +14,24 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AudioSender(SocketBase):
|
||||
"""
|
||||
Audio sender endpoint, responsible for sending microphone audio data.
|
||||
|
||||
:param zmq_context: The ZeroMQ context to use.
|
||||
:type zmq_context: zmq.Context
|
||||
|
||||
:param port: The port to use.
|
||||
:type port: int
|
||||
|
||||
:ivar thread: Thread used for sending audio.
|
||||
:type thread: threading.Thread | None
|
||||
|
||||
:ivar audio: PyAudio instance.
|
||||
:type audio: pyaudio.PyAudio | None
|
||||
|
||||
:ivar microphone: Selected microphone information.
|
||||
:type microphone: dict | None
|
||||
"""
|
||||
def __init__(self, zmq_context, port=settings.agent_settings.audio_sender_port):
|
||||
super(AudioSender, self).__init__(str("audio")) # Convert future's unicode_literal to str
|
||||
self.create_socket(zmq_context, zmq.PUB, port)
|
||||
@@ -30,7 +48,10 @@ class AudioSender(SocketBase):
|
||||
def start(self):
|
||||
"""
|
||||
Start sending audio in a different thread.
|
||||
|
||||
Will not start if no microphone is available.
|
||||
"""
|
||||
|
||||
if not self.microphone:
|
||||
logger.info("Not listening: no microphone available.")
|
||||
return
|
||||
@@ -41,14 +62,18 @@ class AudioSender(SocketBase):
|
||||
|
||||
def wait_until_done(self):
|
||||
"""
|
||||
Wait until the audio thread is done. Will only be done if `state.exit_event` is set, so
|
||||
make sure to set that before calling this method or it will block.
|
||||
Wait until the audio thread is done.
|
||||
|
||||
Will block until `state.exit_event` is set. If the thread is not running, does nothing.
|
||||
"""
|
||||
if not self.thread: return
|
||||
self.thread.join()
|
||||
self.thread = None
|
||||
|
||||
def _stream(self):
|
||||
"""
|
||||
Internal method to continuously read audio from the microphone and send it over the socket.
|
||||
"""
|
||||
audio_settings = settings.audio_config
|
||||
chunk = audio_settings.chunk_size # 320 at 16000 Hz is 20ms, 512 is required for Silero-VAD
|
||||
|
||||
|
||||
@@ -6,26 +6,47 @@ from robot_interface.state import state
|
||||
from robot_interface.core.config import settings
|
||||
|
||||
class MainReceiver(ReceiverBase):
|
||||
"""
|
||||
The main receiver endpoint, responsible for handling ping and negotiation requests.
|
||||
|
||||
:param zmq_context: The ZeroMQ context to use.
|
||||
:type zmq_context: zmq.Context
|
||||
|
||||
:param port: The port to use.
|
||||
:type port: int
|
||||
"""
|
||||
def __init__(self, zmq_context, port=settings.agent_settings.main_receiver_port):
|
||||
"""
|
||||
The main receiver endpoint, responsible for handling ping and negotiation requests.
|
||||
|
||||
:param zmq_context: The ZeroMQ context to use.
|
||||
:type zmq_context: zmq.Context
|
||||
|
||||
:param port: The port to use.
|
||||
:type port: int
|
||||
"""
|
||||
super(MainReceiver, self).__init__("main")
|
||||
self.create_socket(zmq_context, zmq.REP, port, bind=False)
|
||||
|
||||
@staticmethod
|
||||
def _handle_ping(message):
|
||||
"""A simple ping endpoint. Returns the provided data."""
|
||||
"""
|
||||
Handle a ping request.
|
||||
|
||||
Returns the provided data in a standardized response dictionary.
|
||||
|
||||
:param message: The ping request message.
|
||||
:type message: dict
|
||||
|
||||
:return: A response dictionary containing the original data.
|
||||
:rtype: dict[str, str | list[dict]]
|
||||
"""
|
||||
return {"endpoint": "ping", "data": message.get("data")}
|
||||
|
||||
@staticmethod
|
||||
def _handle_port_negotiation(message):
|
||||
"""
|
||||
Handle a port negotiation request.
|
||||
|
||||
Returns a list of all known endpoints and their descriptions.
|
||||
|
||||
:param message: The negotiation request message.
|
||||
:type message: dict
|
||||
|
||||
:return: A response dictionary with endpoint descriptions as data.
|
||||
:rtype: dict[str, list[dict]]
|
||||
"""
|
||||
endpoints = [socket.endpoint_description() for socket in state.sockets]
|
||||
|
||||
return {"endpoint": "negotiate/ports", "data": endpoints}
|
||||
@@ -33,13 +54,13 @@ class MainReceiver(ReceiverBase):
|
||||
@staticmethod
|
||||
def _handle_negotiation(message):
|
||||
"""
|
||||
Handle a negotiation request. Will respond with ports that can be used to connect to the robot.
|
||||
Handle a negotiation request. Responds with ports that can be used to connect to the robot.
|
||||
|
||||
: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]]
|
||||
:return: A response dictionary with the negotiation result.
|
||||
:rtype: dict[str, str | list[dict]]
|
||||
"""
|
||||
# In the future, the sender could send information like the robot's IP address, etc.
|
||||
|
||||
@@ -49,6 +70,17 @@ class MainReceiver(ReceiverBase):
|
||||
return {"endpoint": "negotiate/error", "data": "The requested endpoint is not implemented."}
|
||||
|
||||
def handle_message(self, message):
|
||||
"""
|
||||
Main entry point for handling incoming messages.
|
||||
|
||||
Dispatches messages to the appropriate handler based on the endpoint.
|
||||
|
||||
:param message: The received message.
|
||||
:type message: dict
|
||||
|
||||
:return: A response dictionary based on the requested endpoint.
|
||||
:rtype: dict[str, str | list[dict]]
|
||||
"""
|
||||
if message["endpoint"] == "ping":
|
||||
return self._handle_ping(message)
|
||||
elif message["endpoint"].startswith("negotiate"):
|
||||
|
||||
@@ -4,7 +4,7 @@ from robot_interface.endpoints.socket_base import SocketBase
|
||||
|
||||
|
||||
class ReceiverBase(SocketBase, object):
|
||||
"""Associated with a ZeroMQ socket."""
|
||||
"""Base class for receivers associated with a ZeroMQ socket."""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -4,16 +4,27 @@ import zmq
|
||||
|
||||
|
||||
class SocketBase(object):
|
||||
"""
|
||||
Base class for endpoints associated with a ZeroMQ socket.
|
||||
|
||||
:ivar identifier: The identifier of the endpoint.
|
||||
:type identifier: str
|
||||
|
||||
:ivar port: The port used by the socket, set by `create_socket`.
|
||||
:type port: int | None
|
||||
|
||||
:ivar socket: The ZeroMQ socket object, set by `create_socket`.
|
||||
:type socket: zmq.Socket | None
|
||||
|
||||
:ivar bound: Whether the socket is bound or connected, set by `create_socket`.
|
||||
:type bound: bool | None
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
name = None
|
||||
socket = None
|
||||
|
||||
def __init__(self, identifier):
|
||||
"""
|
||||
:param identifier: The identifier of the endpoint.
|
||||
:type identifier: str
|
||||
"""
|
||||
self.identifier = identifier
|
||||
self.port = None # Set later by `create_socket`
|
||||
self.socket = None # Set later by `create_socket`
|
||||
|
||||
@@ -7,6 +7,15 @@ from robot_interface.state import state
|
||||
from robot_interface.core.config import settings
|
||||
|
||||
class VideoSender(SocketBase):
|
||||
"""
|
||||
Video sender endpoint, responsible for sending video frames.
|
||||
|
||||
:param zmq_context: The ZeroMQ context to use.
|
||||
:type zmq_context: zmq.Context
|
||||
|
||||
:param port: The port to use for sending video frames.
|
||||
:type port: int
|
||||
"""
|
||||
def __init__(self, zmq_context, port=settings.agent_settings.video_sender_port):
|
||||
super(VideoSender, self).__init__("video")
|
||||
self.create_socket(zmq_context, zmq.PUB, port, [(zmq.CONFLATE,1)])
|
||||
@@ -14,6 +23,8 @@ class VideoSender(SocketBase):
|
||||
def start_video_rcv(self):
|
||||
"""
|
||||
Prepares arguments for retrieving video images from Pepper and starts video loop on a separate thread.
|
||||
|
||||
Will not start of no qi session is available.
|
||||
"""
|
||||
if not state.qi_session:
|
||||
logging.info("No Qi session available. Not starting video loop.")
|
||||
|
||||
@@ -57,6 +57,13 @@ def main_loop(context):
|
||||
continue
|
||||
|
||||
def overtime_callback(time_ms):
|
||||
"""
|
||||
A callback function executed by TimeBlock if the message handling
|
||||
exceeds the allowed time limit.
|
||||
|
||||
:param time_ms: The elapsed time, in milliseconds, that the block took.
|
||||
:type time_ms: float
|
||||
"""
|
||||
logging.warn("Endpoint \"%s\" took too long (%.2f ms) on the main thread.",
|
||||
message["endpoint"], time_ms)
|
||||
|
||||
@@ -68,6 +75,12 @@ def main_loop(context):
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Initializes the ZeroMQ context and the application state.
|
||||
It executes the main event loop (`main_loop`) and ensures that both the
|
||||
application state and the ZeroMQ context are properly cleaned up (deinitialized/terminated)
|
||||
upon exit, including handling a KeyboardInterrupt.
|
||||
"""
|
||||
context = zmq.Context()
|
||||
|
||||
state.initialize()
|
||||
|
||||
@@ -12,14 +12,31 @@ class State(object):
|
||||
|
||||
This class is used to share state between threads. For example, when the program is quit, that all threads can
|
||||
detect this via the `exit_event` property being set.
|
||||
|
||||
:ivar is_initialized: Flag indicating whether the state setup (exit handlers, QI session) has completed.
|
||||
:type is_initialized: bool
|
||||
|
||||
:ivar exit_event: A thread event used to signal all threads that the program is shutting down.
|
||||
:type exit_event: threading.Event | None
|
||||
|
||||
:ivar sockets: A list of ZeroMQ socket wrappers (`SocketBase`) that need to be closed during deinitialization.
|
||||
:type sockets: List[SocketBase]
|
||||
|
||||
:ivar qi_session: The QI session object used for interaction with the robot/platform services.
|
||||
:type qi_session: None | ssl.SSLSession
|
||||
"""
|
||||
def __init__(self):
|
||||
self.is_initialized = False
|
||||
self.exit_event = None
|
||||
self.sockets = [] # type: List[SocketBase]
|
||||
self.qi_session = None # type: None | ssl.SSLSession
|
||||
self.sockets = []
|
||||
self.qi_session = None
|
||||
|
||||
def initialize(self):
|
||||
"""
|
||||
Sets up the application state. Creates the thread exit event, registers
|
||||
signal handlers (`SIGINT`, `SIGTERM`) for graceful shutdown, and
|
||||
establishes the QI session.
|
||||
"""
|
||||
if self.is_initialized:
|
||||
logging.warn("Already initialized")
|
||||
return
|
||||
@@ -36,6 +53,9 @@ class State(object):
|
||||
self.is_initialized = True
|
||||
|
||||
def deinitialize(self):
|
||||
"""
|
||||
Closes all sockets stored in the `sockets` list.
|
||||
"""
|
||||
if not self.is_initialized: return
|
||||
|
||||
for socket in self.sockets:
|
||||
@@ -44,7 +64,17 @@ class State(object):
|
||||
self.is_initialized = False
|
||||
|
||||
def __getattribute__(self, name):
|
||||
# Enforce that the state is initialized before accessing any property (aside from the basic ones)
|
||||
"""
|
||||
Custom attribute access method that enforces a check: the state must be
|
||||
fully initialized before any non-setup attributes (like `sockets` or `qi_session`)
|
||||
can be accessed.
|
||||
|
||||
:param name: The name of the attribute being accessed.
|
||||
:type name: str
|
||||
|
||||
:return: The value of the requested attribute.
|
||||
:rtype: Any
|
||||
"""
|
||||
if name in (
|
||||
"initialize",
|
||||
"deinitialize",
|
||||
|
||||
@@ -8,6 +8,12 @@ except ImportError:
|
||||
|
||||
|
||||
def get_qi_session():
|
||||
"""
|
||||
Create and return a Qi session if available.
|
||||
|
||||
:return: The active Qi session or ``None`` if unavailable.
|
||||
:rtype: qi.Session | None
|
||||
"""
|
||||
if qi is None:
|
||||
logging.info("Unable to import qi. Running in stand-alone mode.")
|
||||
return None
|
||||
|
||||
@@ -5,27 +5,45 @@ class TimeBlock(object):
|
||||
"""
|
||||
A context manager that times the execution of the block it contains. If execution exceeds the
|
||||
limit, or if no limit is given, the callback will be called with the time that the block took.
|
||||
|
||||
:param callback: The callback function that is called when the block of code is over,
|
||||
unless the code block did not exceed the time limit.
|
||||
:type callback: Callable[[float], None]
|
||||
|
||||
:param limit_ms: The number of milliseconds the block of code is allowed to take. If it
|
||||
exceeds this time, or if it's None, the callback function will be called with the time the
|
||||
block took.
|
||||
:type limit_ms: int | None
|
||||
"""
|
||||
def __init__(self, callback, limit_ms=None):
|
||||
"""
|
||||
:param callback: The callback function that is called when the block of code is over,
|
||||
unless the code block did not exceed the time limit.
|
||||
:type callback: Callable[[float], None]
|
||||
|
||||
:param limit_ms: The number of milliseconds the block of code is allowed to take. If it
|
||||
exceeds this time, or if it's None, the callback function will be called with the time the
|
||||
block took.
|
||||
:type limit_ms: int | None
|
||||
"""
|
||||
self.limit_ms = float(limit_ms) if limit_ms is not None else None
|
||||
self.callback = callback
|
||||
self.start = None
|
||||
|
||||
def __enter__(self):
|
||||
"""
|
||||
Enter the context manager and record the start time.
|
||||
|
||||
:return: Returns itself so timing information can be accessed if needed.
|
||||
:rtype: TimeBlock
|
||||
"""
|
||||
self.start = time.time()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
"""
|
||||
Exit the context manager, calculate the elapsed time, and call the callback
|
||||
if the time limit was exceeded or not provided.
|
||||
|
||||
:param exc_type: The exception type, or None if no exception occurred.
|
||||
:type exc_type: Type[BaseException] | None
|
||||
|
||||
:param exc_value: The exception instance, or None if no exception occurred.
|
||||
:type exc_value: BaseException | None
|
||||
|
||||
:param traceback: The traceback object, or None if no exception occurred.
|
||||
:type traceback: TracebackType | None
|
||||
"""
|
||||
elapsed = (time.time() - self.start) * 1000.0 # ms
|
||||
if self.limit_ms is None or elapsed > self.limit_ms:
|
||||
self.callback(elapsed)
|
||||
|
||||
Reference in New Issue
Block a user