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.

"""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.

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.

TestBeds:
  - Name: BedroomTestBed
    Controllers:
      SmartLight:
        - name: "BedLight"
          ip: "192.168.1.50"