TofuPilotTofuPilot

Plugs

Create persistent resources for your phases to access.

Keysight DAQ
Active
EtherCAT Manager
Initializing
Rigol Power Supply
Idle

You can create plugs to manage persistent resources like test equipment or hardware interfaces.

TofuPilot Framework handles plug initialization, lifecycle management, and automatically passes plug instances to your phase functions.

plugs: 
  - name: EtherCAT Manager
    python: hardware.ethercat:EtherCATManager

main:
  - name: Test Motor
    python: phases.motor:test_motor
class EtherCATManager:
    def __init__(self):
        # Initialize connection
        pass

    def send_command(self, cmd):
        # Send command to hardware
        pass
def test_motor(ethercat_manager):
    ethercat_manager.send_command("START")

Plugs are standard Python classes.

Name

You can create a new plug by adding it to the plugs list.

plugs:
  - name: EtherCAT Manager

TofuPilot will automatically trim whitespace and enforce a 100 character limit.

Description

You can add an optional description to explain what the plug is.

plugs:
  - name: EtherCAT Manager
    description: "Manages EtherCAT communication with motors"

TofuPilot will enforce a 1,000 character limit on descriptions.

Key

You can define a key for referencing this plug in your phase function parameters.

plugs:
  - name: EtherCAT Manager
    description: "Manages EtherCAT communication with motors"
    key: ethercat_manager

If not specified, TofuPilot auto-generates a key from the plug name. Keys must be valid Python identifiers: start with a letter or underscore, followed by letters, numbers, or underscores.

Python

You can define plugs as standard Python classes.

class PowerSupply:
    def __init__(self):
        self.address = "192.168.1.100"

    def __del__(self):
        self.write("OUTP OFF")

    def set_voltage(self, volts):
        self.write(f"VOLT {volts}")

    def write(self, command):
        pass  # Actual hardware communication here

And reference the Python class in YAML:

plugs:
  - name: Power Supply
    description: "Programmable power supply"
    python: instruments.power_supply:PowerSupply

TofuPilot will automatically initialize the plug at setup and ensure its destruction at teardown.

Setup / Teardown

Single Slot

TofuPilot automatically manages your plug lifecycle during execution:

  1. Initialize plugs by calling __init__() method
  2. Execute procedure phases
  3. Destroy plugs by calling __del__() method

If your procedure specifies Setup or Teardown Phases, TofuPilot initializes plugs before setup and destroys them after teardown.

TofuPilot will stop the execution immediately on plug initialization Error, even before executing setup phases, but will always destroy plugs to prevent resource leaks no matter the execution status.

Plug Design

Errors

You can raise exceptions in plugs like you would typically do in Python.

class RigolDP832:
    def set_voltage(self, channel, voltage):
        if voltage > self.max_voltage:
            raise ValueError(f"Voltage {voltage}V exceeds maximum {self.max_voltage}V") 
        cmd = f":SOUR{channel}:VOLT {voltage}\n"
        self.socket.send(cmd.encode())

TofuPilot will automatically capture and report errors.

Logs

You can use print statements for plug-level logging.

class RigolDP832:
    def measure_voltage(self, channel):
        cmd = f":MEAS:VOLT? CH{channel}\n"
        self.socket.send(cmd.encode())
        voltage = self.socket.recv(1024).decode().strip()
        print(f"Measured: {voltage}V") 
        return float(voltage)

TofuPilot will capture and report them.

Phase-Based Management

You can use dedicated setup and teardown phases for complex plug management to leverage TofuPilot native phase execution features like UI prompts, timeouts and more.

plugs:
  - name: Power Supply
    python: instruments.rigol:RigolDP832

setup: 
  - name: Connect Power Supply
    python: setup.connect_psu
    timeout: 30
    ui: 
      components: 
        - key: status
          type: text
          label: "Connection Status"

teardown:
  - name: Disconnect Power Supply
    python: teardown.disconnect_psu
class RigolDP832:
    def __init__(self):
        # Minimal initialization
        self.socket = None

    def connect(self, ip, port=5555):
        # Complex connection logic called from phase
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.connect((ip, port))
        return True

    def disconnect(self):
        if self.socket:
            self.socket.close()
def connect_psu(power_supply, run, log):
    run.ui.status = "Connecting to power supply..."

    if power_supply.connect("192.168.1.50"):
        log.info("Power supply connected successfully")
        return True
    else:
        log.error("Failed to connect to power supply")
        return False

This approach gives you full phase capabilities for plug management operations.

Garbage Collection

TofuPilot guarantees cleanup by calling __del__() at plug deletion and terminating the subprocess, unlike Python's non-deterministic garbage collector.

Method Calling

Plugs are automatically passed to phase functions by matching parameter names (case-insensitive) to plug keys.

def test_motor(ethercat_manager):
    ethercat_manager.send_command("START")  # Call plug methods directly

TofuPilot runs plugs in a separate process from phases and uses a TCP server to handle method calls. When you call ethercat_manager.send_command("START"), TofuPilot serializes the method name and arguments, sends them to the plug process, executes the method, and returns the result.

Benefits:

  • Process isolation prevents plug crashes from affecting phases
  • Plug resources (sockets, file handles) stay in one process
  • Automatic cleanup when plug process terminates

Limitations:

You can only pass serializable arguments (strings, numbers, lists, dicts) to plug methods. You cannot pass or return plug instances, functions, or other non-serializable objects:

def measure_voltage(power_supply, measurements):
    # ✅ Pass serializable arguments, get serializable results
    voltage = power_supply.measure_voltage(channel=1)
    measurements.add('voltage', voltage)
    return voltage

    # ❌ Cannot pass plug instance to another plug
    # other_device.configure(power_supply)

    # ❌ Cannot return plug instance
    # return power_supply

We're looking for your feedback

Share your experience with plugs and help shape future improvements. Join the discussion on Discord.

Example

Let's create a power supply plug to control voltage output:

  1. Define the plug in procedure YAML
  2. Implement the plug class
  3. Use it in a phase

We'll create the procedure file and the Python modules:

procedure.yaml
rigol.py
set_voltage.py

We'll define the plug, implement it, and use it in a phase:

plugs:
  - name: Power Supply
    description: Rigol DP832 programmable power supply
    python: instruments.rigol:RigolDP832

main:
  - name: Set Voltage
    python: phases.set_voltage
import socket

class RigolDP832:
    def __init__(self):
        self.ip = "192.168.1.50"
        self.port = 5555

        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.connect((self.ip, self.port))
        print(f"Connected to power supply at {self.ip}")

    def __del__(self):
        if hasattr(self, 'socket'):
            self.socket.close()
        print("Power supply disconnected")

    def set_voltage(self, channel, voltage):
        cmd = f":SOUR{channel}:VOLT {voltage}\n"
        self.socket.send(cmd.encode())
def set_voltage(power_supply, measurements, log):
    power_supply.set_voltage(channel=1, voltage=5.0)

    measurements.add('voltage_set', 5.0)
    log.info("Set channel 1 to 5.0V")

    return True

TofuPilot creates the plug instance when the slot starts, automatically passes it to your phase function by matching the parameter name power_supply to the plug key, and destroys it when the slot completes.

How is this guide?