Object-Oriented Programming for Automation Systems
Object-Oriented Programming: Machines Speak in Objects
Imagine standing in front of a production line at a food processing plant. You see a filling machine, a conveyor belt, a weight sensor, and a control panel. Each component has properties (speed, state, temperature) and behaviors (start, stop, calibrate). This is exactly what Object-Oriented Programming (OOP) does -- it transforms real-world entities into software objects you can control.
OOP is not just a coding style -- it is a way of thinking that makes industrial control software more organized, maintainable, and extensible.
Classes and Objects: The Blueprint and the Real Machine
A Class is the engineering blueprint -- the technical drawing describing how a machine is built. An Object is the actual machine built from that blueprint.
Practical example -- modeling an electric motor:
class Motor:
def __init__(self, rated_power_kw, rated_rpm):
self.rated_power = rated_power_kw
self.rated_rpm = rated_rpm
self.is_running = False
self.current_rpm = 0
def start(self):
if not self.is_running:
self.is_running = True
self.current_rpm = self.rated_rpm
print(f"Motor running at {self.current_rpm} RPM")
def stop(self):
self.is_running = False
self.current_rpm = 0
print("Motor stopped")
def get_status(self):
state = "Running" if self.is_running else "Stopped"
return f"Status: {state} | Speed: {self.current_rpm} RPM"
Now we create actual objects -- each is an independent motor:
pump_motor = Motor(rated_power_kw=5.5, rated_rpm=1450)
conveyor_motor = Motor(rated_power_kw=2.2, rated_rpm=960)
pump_motor.start() # Motor running at 1450 RPM
print(conveyor_motor.get_status()) # Status: Stopped | Speed: 0 RPM
From a single class, we created two completely different motors, each with its own independent properties. This is the core idea of classes and objects.
Encapsulation: Protecting the Internal Parts
Encapsulation means hiding internal details and exposing only a simple interface -- just like operating a machine through its control panel without needing to know the internal circuit details.
class TemperatureController:
def __init__(self, setpoint):
self._setpoint = setpoint
self._current_temp = 25.0
self._kp = 2.0 # Internal PID coefficient
self._output = 0.0
def update_reading(self, sensor_value):
"""Public interface -- called by the user"""
self._current_temp = sensor_value
self._calculate_output()
def _calculate_output(self):
"""Internal logic hidden from the user"""
error = self._setpoint - self._current_temp
self._output = max(0, min(100, self._kp * error))
@property
def output_percent(self):
return self._output
@property
def setpoint(self):
return self._setpoint
@setpoint.setter
def setpoint(self, value):
if 0 <= value <= 200:
self._setpoint = value
else:
raise ValueError("Temperature out of allowed range")
The user interacts only with update_reading and setpoint -- no need to know how the output is calculated internally. This prevents errors and simplifies maintenance.
Inheritance: Building Families of Machines
Inheritance lets you create new classes that build upon existing ones -- just as an electric motor and a hydraulic motor are both "motors" but each has additional specific properties.
class Actuator:
"""Parent class: any industrial actuator"""
def __init__(self, name, max_force_n):
self.name = name
self.max_force = max_force_n
self.position = 0.0 # Percentage 0-100
def move_to(self, target_percent):
self.position = max(0, min(100, target_percent))
print(f"{self.name}: position = {self.position}%")
def emergency_stop(self):
self.position = 0
print(f"{self.name}: EMERGENCY STOP!")
class PneumaticCylinder(Actuator):
"""Pneumatic cylinder -- inherits from Actuator"""
def __init__(self, name, max_force_n, stroke_mm, pressure_bar):
super().__init__(name, max_force_n)
self.stroke_mm = stroke_mm
self.pressure = pressure_bar
def extend(self):
self.move_to(100)
def retract(self):
self.move_to(0)
class ServoMotor(Actuator):
"""Servo motor -- inherits from Actuator, adds position precision"""
def __init__(self, name, max_force_n, resolution_bits):
super().__init__(name, max_force_n)
self.resolution = 2 ** resolution_bits
def move_to_angle(self, degrees):
percent = (degrees / 360.0) * 100
self.move_to(percent)
cylinder = PneumaticCylinder("CYL-01", 500, stroke_mm=200, pressure_bar=6)
servo = ServoMotor("SRV-01", 50, resolution_bits=12)
cylinder.extend() # CYL-01: position = 100%
servo.move_to_angle(90) # SRV-01: position = 25.0%
cylinder.emergency_stop() # CYL-01: EMERGENCY STOP!
Both have move_to and emergency_stop from the parent class, but each adds its own specialized behavior.
Polymorphism: Same Command, Different Execution
Polymorphism means the same command (e.g., "activate") executes differently depending on the object type -- an electric valve behaves differently from a proportional valve when receiving a move command.
class ElectricValve(Actuator):
def move_to(self, target_percent):
if target_percent > 50:
self.position = 100 # Valve is either open or closed
print(f"{self.name}: valve FULLY OPEN")
else:
self.position = 0
print(f"{self.name}: valve CLOSED")
class ProportionalValve(Actuator):
def move_to(self, target_percent):
self.position = target_percent # Continuous proportional opening
print(f"{self.name}: opening = {self.position}%")
devices = [
ElectricValve("V-101", 200),
ProportionalValve("PV-201", 200),
ServoMotor("SRV-02", 50, 12),
]
for device in devices:
device.move_to(75) # Same call -- different results
Output:
V-101: valve FULLY OPEN
PV-201: opening = 75%
SRV-02: position = 75%
This is powerful in automation -- you can write generic control code that works with any type of actuator.
Modeling a Complete Production Line
Let us combine all concepts into a simple production line model:
class ProductionLine:
def __init__(self, name):
self.name = name
self._devices = []
self._is_running = False
def add_device(self, device):
self._devices.append(device)
def start_all(self):
print(f"--- Starting line: {self.name} ---")
self._is_running = True
for device in self._devices:
device.move_to(100) # Polymorphism in action!
def emergency_stop_all(self):
print(f"!!! EMERGENCY STOP -- Line: {self.name} !!!")
self._is_running = False
for device in self._devices:
device.emergency_stop()
def status_report(self):
print(f"\n=== Status Report: {self.name} ===")
for device in self._devices:
print(f" {device.name}: position={device.position}%")
line = ProductionLine("Filling Line")
line.add_device(PneumaticCylinder("CYL-Push", 800, 300, 6))
line.add_device(ProportionalValve("PV-Fill", 150))
line.add_device(ServoMotor("SRV-Seal", 100, 16))
line.start_all()
line.status_report()
line.emergency_stop_all()
Common Design Patterns in Automation
Observer Pattern
When a sensor value changes, multiple subsystems need to be notified:
class Sensor:
def __init__(self, name):
self.name = name
self._value = 0
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def set_value(self, new_value):
self._value = new_value
for obs in self._observers:
obs.on_sensor_update(self.name, new_value)
class AlarmSystem:
def on_sensor_update(self, sensor_name, value):
if value > 80:
print(f"[ALARM] {sensor_name} = {value} -- threshold exceeded!")
class DataLogger:
def on_sensor_update(self, sensor_name, value):
print(f"[LOG] {sensor_name} = {value}")
State Machine Pattern
Industrial machines transition between well-defined states:
class MachineState:
IDLE = "idle"
STARTING = "starting"
RUNNING = "running"
ERROR = "error"
STOPPING = "stopping"
class PackagingMachine:
def __init__(self):
self.state = MachineState.IDLE
def start(self):
if self.state == MachineState.IDLE:
self.state = MachineState.STARTING
# Initialize sensors...
self.state = MachineState.RUNNING
print("Machine is running")
elif self.state == MachineState.ERROR:
print("Must clear error first!")
def report_error(self, code):
self.state = MachineState.ERROR
print(f"Error {code} -- machine stopped")
When to Use OOP and When Not To
| Scenario | Recommended Approach |
|---|---|
| Modeling machines with states and behaviors | OOP |
| Simple math calculations (unit conversions) | Plain functions |
| Complex multi-component control systems | OOP with design patterns |
| Short single-task scripts | No need for OOP |
| Interfaces to different PLC devices | OOP with polymorphism |
Summary
Object-Oriented Programming gives you powerful tools to model industrial automation systems:
- Classes and objects represent real machines and components
- Encapsulation protects internal logic from tampering
- Inheritance builds families of similar devices
- Polymorphism enables flexible, generic control code
- Design patterns provide proven solutions to common problems
Start by modeling the machines you work with -- you will find that OOP translates the industrial world into code very naturally.