Home Wiki Programming & Logic Object-Oriented Programming for Automation Systems
Programming & Logic

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.

OOP class inheritance encapsulation polymorphism design-patterns البرمجة الكائنية الكائن التوريث التغليف تعدد الأشكال أنماط التصميم