-
Notifications
You must be signed in to change notification settings - Fork 205
Docs: Add tutorial for writing custom controllers #993
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ffb6f67
Docs: Add tutorial for writing custom controllers
amytsengtw 53f1f3a
Docs: Add tutorial for writing custom controllers
amytsengtw 81dd83a
Add missing 'is_on' attribute to SmartLight docstring and remove redu…
amytsengtw fc6c301
Merge branch 'master' into docs-custom-controller
ko1in1u File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| ``` | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.