Plugs
Create persistent resources for your phases to access.
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_motorclass EtherCATManager:
def __init__(self):
# Initialize connection
pass
def send_command(self, cmd):
# Send command to hardware
passdef 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 ManagerTofuPilot 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_managerIf 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 hereAnd reference the Python class in YAML:
plugs:
- name: Power Supply
description: "Programmable power supply"
python: instruments.power_supply:PowerSupplyTofuPilot 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:
- Initialize plugs by calling
__init__()method - Execute procedure phases
- 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_psuclass 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 FalseThis 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 directlyTofuPilot 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_supplyWe'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:
- Define the plug in procedure YAML
- Implement the plug class
- Use it in a phase
We'll create the procedure file and the Python modules:
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_voltageimport 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 TrueTofuPilot 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?