diff --git a/README.md b/README.md index 0abb7db..8341feb 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,28 @@ for t in threads: t.join() ``` +### Custom Robot Platforms (DIY Robot) + +For engineers with custom robotic platforms that aren't directly supported, `armctl` provides a flexible `DIYRobot` factory class: + +```python +from armctl import DIYRobot +from armctl.templates import SocketController + +# Create a blank robot factory +robot = DIYRobot() + +# Configure with any communication method +robot(SocketController, "192.168.1.100", 8080) + +# Use basic communication methods for custom protocols +robot.connect() +response = robot.send_command("CUSTOM_COMMAND") +robot.disconnect() +``` + +The `DIYRobot` provides only basic communication methods (`connect`, `disconnect`, `send_command`) without any template commands, allowing full customization for any robotic platform. + ## API Reference > [!NOTE] diff --git a/armctl/__init__.py b/armctl/__init__.py index c614a24..a1b289b 100644 --- a/armctl/__init__.py +++ b/armctl/__init__.py @@ -10,6 +10,7 @@ - Grippers: OnRobot (Ethernet) - Vention (Ethernet) - Jaka Robotics: Jaka (Ethernet) +- DIY/Custom Robots: DIYRobot factory class for custom platforms """ # from .dobot import Dobot @@ -18,6 +19,7 @@ # from .fanuc import Fanuc from .vention import Vention from .jaka import Jaka +from .diy_robot import DIYRobot __all__ = [ "ElephantRobotics", @@ -30,4 +32,5 @@ "Vention", "Jaka", + "DIYRobot", ] \ No newline at end of file diff --git a/armctl/diy_robot.py b/armctl/diy_robot.py new file mode 100644 index 0000000..de09d4b --- /dev/null +++ b/armctl/diy_robot.py @@ -0,0 +1,153 @@ +""" +This module provides the DIYRobot factory class for creating custom robotic platforms +with pick-and-choose communication systems. + +The DIYRobot class allows users to: +1. Create a blank factory instance: Robot = DIYRobot() +2. Configure communication dynamically: Robot(SocketController, "192.168.1.1", 8080) +3. Access basic communication methods: connect(), disconnect(), send_command() +""" + +from typing import Union, Any, Type +from .templates.communication import Communication + + +class DIYRobot: + """ + A factory class for creating custom robotic platforms with flexible communication. + + This class provides a blank factory that can be configured with any communication + method via the __call__ function. It does not inherit from Communication or Commands + initially, but provides the basic communication interface after configuration. + + Example Usage: + ------------- + from armctl import DIYRobot + from armctl.templates import SocketController + + # Create blank factory + robot = DIYRobot() + + # Configure communication + robot(SocketController, "192.168.1.100", 8080) + + # Use basic communication methods + robot.connect() + response = robot.send_command("custom_command") + robot.disconnect() + """ + + def __init__(self): + """ + Initialize a blank DIY robot factory. + + No communication or command inheritance is established at this point. + The robot must be configured using the __call__ method before use. + """ + self._communication_instance = None + self._is_configured = False + + def __call__(self, communication_class: Type[Communication], *args, **kwargs) -> None: + """ + Configure the robot with a specific communication method and parameters. + + Parameters + ---------- + communication_class : Type[Communication] + A Communication subclass (e.g., SocketController, SerialController) + *args + Positional arguments to pass to the communication class constructor + **kwargs + Keyword arguments to pass to the communication class constructor + + Example: + -------- + robot(SocketController, "192.168.1.100", 8080) + robot(SerialController, "/dev/ttyUSB0", baudrate=115200) + """ + if not issubclass(communication_class, Communication): + raise TypeError(f"communication_class must be a subclass of Communication, got {communication_class}") + + # Create instance of the communication class with provided arguments + self._communication_instance = communication_class(*args, **kwargs) + self._is_configured = True + + def _ensure_configured(self): + """Ensure the robot has been configured with a communication method.""" + if not self._is_configured or self._communication_instance is None: + raise RuntimeError("DIYRobot must be configured with a communication method first. " + "Use robot(CommunicationClass, *args, **kwargs) to configure.") + + def connect(self) -> None: + """ + Connect to the robot using the configured communication method. + + Raises: + ------- + RuntimeError + If the robot has not been configured with a communication method. + """ + self._ensure_configured() + return self._communication_instance.connect() + + def disconnect(self) -> None: + """ + Disconnect from the robot using the configured communication method. + + Raises: + ------- + RuntimeError + If the robot has not been configured with a communication method. + """ + self._ensure_configured() + return self._communication_instance.disconnect() + + def send_command(self, command: Union[str, dict], timeout: float = 5.0, **kwargs) -> Union[dict, str]: + """ + Send a command to the robot using the configured communication method. + + This is the primary method for custom robot control. Engineers can use this + to send any custom commands specific to their robotic platform. + + Parameters + ---------- + command : Union[str, dict] + The command to send to the robot + timeout : float, optional + Timeout for the command in seconds (default: 5.0) + **kwargs + Additional arguments to pass to the communication's send_command method + + Returns + ------- + Union[dict, str] + The response from the robot + + Raises: + ------- + RuntimeError + If the robot has not been configured with a communication method. + """ + self._ensure_configured() + return self._communication_instance.send_command(command, timeout, **kwargs) + + def __enter__(self): + """Context manager support for automatic connection management.""" + self.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Ensure disconnection when leaving the context.""" + self.disconnect() + + @property + def is_configured(self) -> bool: + """Check if the robot has been configured with a communication method.""" + return self._is_configured + + @property + def communication_type(self) -> str: + """Get the type of communication method configured, if any.""" + if self._is_configured and self._communication_instance: + return self._communication_instance.__class__.__name__ + return "Not configured" \ No newline at end of file diff --git a/armctl/templates/__init__.py b/armctl/templates/__init__.py index 754e053..92071ca 100644 --- a/armctl/templates/__init__.py +++ b/armctl/templates/__init__.py @@ -12,4 +12,5 @@ from .socket_controller import SocketController from .serial_controller import SerialController from .plc_controller import PLCController -from .commands import Commands \ No newline at end of file +from .commands import Commands +from .communication import Communication \ No newline at end of file