diff --git a/docs/index.rst b/docs/index.rst index 093c9551..808397b3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,7 +11,7 @@ Welcome to Mobly's documentation! :caption: Contents: mobly - + tutorial_custom_controller Indices and tables ================== diff --git a/docs/tutorial_custom_controller.md b/docs/tutorial_custom_controller.md new file mode 100644 index 00000000..10a72552 --- /dev/null +++ b/docs/tutorial_custom_controller.md @@ -0,0 +1,157 @@ +# Custom Controller Tutorial + +Mobly enables users to control custom hardware devices (e.g., smart lights, switches) by creating custom controller modules. This tutorial explains how to implement a production-ready custom controller. + +## Controller Module Interface + +A Mobly controller module needs to implement specific top-level functions to manage the device lifecycle. + +**Required functions:** + +* **`create(configs)`**: Instantiates controller objects from the configuration. +* **`destroy(objects)`**: Cleans up resources when the test ends. + +**Optional functions:** + +* **`get_info(objects)`**: Returns device information for the test report. If not implemented, no controller information will be included in the result. + +## Implementation Example + +The following example demonstrates a custom controller for a **Smart Light**, featuring type hinting, input validation, and fault-tolerant cleanup. + +### 1. Controller Module (`smart_light.py`) + +Save this code as `smart_light.py`. + +```python +"""Mobly controller module for a Smart Light.""" + +import logging +from typing import Any, Dict, List + +# The key used in the config file to identify this controller. +MOBLY_CONTROLLER_CONFIG_NAME = "SmartLight" + +class SmartLight: + """A class representing a smart light device. + + Attributes: + name: the name of the device. + ip: the IP address of the device. + is_on: True if the light is currently on, False otherwise. + """ + + def __init__(self, name: str, ip: str): + self.name = name + self.ip = ip + self.is_on = False + logging.info("Initialized SmartLight [%s] at %s", self.name, self.ip) + + def power_on(self): + """Turns the light on.""" + self.is_on = True + logging.info("SmartLight [%s] turned ON", self.name) + + def power_off(self): + """Turns the light off.""" + self.is_on = False + logging.info("SmartLight [%s] turned OFF", self.name) + + def close(self): + """Simulates closing the connection.""" + logging.info("SmartLight [%s] connection closed", self.name) + + +def create(configs: List[Dict[str, Any]]) -> List[SmartLight]: + """Creates SmartLight instances from a list of configurations. + + Args: + configs: A list of dicts, where each dict represents a configuration + for a SmartLight device. + + Returns: + A list of SmartLight objects. + + Raises: + ValueError: If a required configuration parameter is missing. + """ + devices = [] + for config in configs: + if "name" not in config or "ip" not in config: + raise ValueError( + f"Invalid config: {config}. 'name' and 'ip' are required." + ) + + devices.append(SmartLight( + name=config["name"], + ip=config["ip"] + )) + return devices + + +def destroy(objects: List[SmartLight]) -> None: + """Cleans up SmartLight instances. + + Args: + objects: A list of SmartLight objects to be destroyed. + """ + for light in objects: + try: + if light.is_on: + light.power_off() + light.close() + except Exception: + # Catching broad exceptions ensures that a failure in one device + # does not prevent others from being cleaned up. + logging.exception("Failed to clean up SmartLight [%s]", light.name) + + +def get_info(objects: List[SmartLight]) -> List[Dict[str, Any]]: + """Returns information for the test result. + + Args: + objects: A list of SmartLight objects. + + Returns: + A list of dicts containing device information. + """ + return [{"name": light.name, "ip": light.ip} for light in objects] + +``` + +### 2. Controller Module (`smart_light.py`) + +To use the custom controller, register it in your test script. + +```python +from mobly import base_test +from mobly import test_runner +import smart_light + +class LightTest(base_test.BaseTestClass): + def setup_class(self): + # Register the custom controller + self.lights = self.register_controller(smart_light) + + def test_turn_on(self): + light = self.lights[0] + light.power_on() + + # Verify the light is on + if not light.is_on: + raise signals.TestFailure(f"Light {light.name} should be on!") + +if __name__ == "__main__": + test_runner.main() +``` + +### 3. Configuration File (config.yaml) +Define the device in your configuration file using the SmartLight key. +```yaml +TestBeds: + - Name: BedroomTestBed + Controllers: + SmartLight: + - name: "BedLight" + ip: "192.168.1.50" +```