Skip to content

Dual Output with Sensor Feedback

In order to add sensor feedback to the CylinderDualOutput component, we could modify the existing interface to include sensor inputs for position feedback. This would allow us to verify whether the cylinder is fully extended or retracted based on the sensor states.

However, modifying the existing interface would violate the Open/Closed Principle (OCP) since we would be changing an existing interface. It could also violate the Interface Segregation Principle (ISP) if not all implementations of the interface require feedback functionality.

1
2
3
4
5
6
INTERFACE I_Cylinder
    METHOD Extend
    METHOD Retract
    PROPERTY IsExtended  : BOOL
    PROPERTY IsRetracted : BOOL
END_INTERFACE

Because making this change will require updating any existing implementations of the I_Cylinder interface to include the new properties, this violates the Open/Closed Principle (OCP), and also the Interface Segregation Principle (ISP). Therefore we could instead create a new interface I_CylinderWithFeedback that extends I_Cylinder to include the sensor properties, allowing existing implementations to remain unchanged.

1
2
3
4
INTERFACE I_CylinderWithFeedback EXTENDS I_Cylinder
    PROPERTY IsExtended  : BOOL
    PROPERTY IsRetracted : BOOL
END_INTERFACE

Another option would be to create a separate interface specifically for feedback, such as I_CylinderFeedback, and have components that require feedback to implement this additional interface. There are two paths to consider here, depending on the use case. We can create an I_Cylinder interface that extends all other interfaces, or we can create separate interfaces for different functionalities. By creating separate interfaces, we adhere more closely to the Interface Segregation Principle (ISP) since components can implement only the interfaces they need.

For this example, we will proceed with creating a new interface I_CylinderWithFeedback that extends I_Cylinder.

But keep in mind that creating a separate feedback interface is also a valid approach. As the complexity of a component grows, the separation of concerns through multiple interfaces can lead to better maintainability and flexibility. As with avoiding inheriting from concrete implementations, it is also beneficial to avoid creating large, monolithic interfaces that force components to implement methods or properties they do not need. Consider the idea of adding I_Reset or I_Home interfaces, where should they be defined in the hierarchy? I_CylinderWithFeedbackAndReset? I_CylinderWithFeedbackAndHome? Or should they be separate interfaces that can be implemented as needed? Maybe I_Cylinder can EXTEND I_Reset and I_Home, but then all cylinders would have to implement those methods even if they don't need them. It could also be that the specific Cylinder classes implements each interface as needed. Such as:

1
Cylinder IMPLEMENTS I_Cylinder, I_DualOutput, I_DualFeedback, I_Reset, I_Home

These are design decisions that should be made based on the specific requirements and use cases of the components being developed. As mentioned in the Machine Code Progression document, planning and designing the architecture of components before the implementation phase is crucial for creating maintainable and scalable systems.

Step 2: Create the CylinderDualOutputWithFeedback Component

Create a function block CylinderDualOutputWithFeedback that implements the I_CylinderWithFeedback interface. This component will depend on two Digital Output components for controlling the cylinder and will also include sensor inputs for feedback.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
FUNCTION_BLOCK CylinderDualOutputWithFeedback IMPLEMENTS I_CylinderWithFeedback
VAR
    ExtendOutput  : I_DigitalOutput; // Dependency Injection of Digital Output component for extending
    RetractOutput : I_DigitalOutput; // Dependency Injection of Digital Output component for retracting

    // Sensor inputs for feedback
    ExtendSensor  : I_DigitalInput;
    RetractSensor : I_DigitalInput;
END_VAR

METHOD FB_Init : BOOL
    VAR_INPUT
        bInitRetains  : BOOL; // TRUE: the retain variables are initialized (reset warm / reset cold)
        bInCopyCode   : BOOL; // TRUE: the instance will be copied to the copy code afterward (online change)   
        ExtendOutput  : I_DigitalOutput;
        RetractOutput : I_DigitalOutput;
        ExtendSensor  : I_DigitalInput;
        RetractSensor : I_DigitalInput;
    END_VAR

    THIS^.ExtendOutput  := ExtendOutput;
    THIS^.RetractOutput := RetractOutput;
    THIS^.ExtendSensor  := ExtendSensor;
    THIS^.RetractSensor := RetractSensor;

END_METHOD

METHOD Extend
    ExtendOutput.On  := TRUE;    
    RetractOutput.On := FALSE; // Ensure retract output is off
END_METHOD

METHOD Retract
    RetractOutput.On := TRUE;
    ExtendOutput.On  := FALSE; // Ensure extend output is off
END_METHOD

PROPERTY IsExtended : BOOL
    GET IsExtended  := ExtendSensor.On;
END_PROPERTY

PROPERTY IsRetracted : BOOL
    GET IsRetracted  := RetractSensor.On;
END_PROPERTY

END_FUNCTION_BLOCK

Step 3 : Using a Null Object for the Digital Output and Digital Input Dependencies

To adhere to the Dependency Inversion Principle (DIP) and avoid null reference issues, we can create Null Object implementations of the I_DigitalOutput and I_DigitalInput interfaces. These Null Objects will provide default behavior when no actual digital outputs or inputs are injected. In the CylinderDualOutputWithFeedback function block declare the Null Object variables for the digital output and digital input dependencies:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
FUNCTION_BLOCK CylinderDualOutputWithFeedback IMPLEMENTS I_CylinderWithFeedback
VAR
    ExtendOutput  : I_DigitalOutput; // Dependency Injection of Digital Output component for extending
    RetractOutput : I_DigitalOutput; // Dependency Injection of Digital Output component for retracting

    // Sensor inputs for feedback
    ExtendSensor  : I_DigitalInput;
    RetractSensor : I_DigitalInput;

    // Null Objects for Digital Output dependencies
    NullEO            : BOOL;
    NullExtendOutput  : DigitalOutput(Output := NullEO);
    NullRO            : BOOL;
    NullRetractOutput : DigitalOutput(Output := NullRO);

    // Null Objects for Digital Input dependencies
    NullES            : BOOL;
    NullExtendSensor  : DigitalInput(Input := NullES);
    NullRS            : BOOL;
    NullRetractSensor : DigitalInput(Input := NullRS);
END_VAR

Then, in the FB_Init method, check if the ExtendOutput, RetractOutput, ExtendSensor, and RetractSensor inputs are provided. If not, assign the respective Null Objects to the references.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
METHOD FB_Init : BOOL
    IF ExtendOutput = 0 THEN
        THIS^.ExtendOutput := NullExtendOutput;
    ELSE
        THIS^.ExtendOutput := ExtendOutput;
    END_IF

    IF RetractOutput = 0 THEN
        THIS^.RetractOutput := NullRetractOutput;
    ELSE
        THIS^.RetractOutput := RetractOutput;
    END_IF

    IF ExtendSensor = 0 THEN
        THIS^.ExtendSensor := NullExtendSensor;
    ELSE
        THIS^.ExtendSensor := ExtendSensor;
    END_IF

    IF RetractSensor = 0 THEN
        THIS^.RetractSensor := NullRetractSensor;
    ELSE
        THIS^.RetractSensor := RetractSensor;
    END_IF

END_METHOD

Step 4: Using the CylinderDualOutputWithFeedback Component

To use the CylinderDualOutputWithFeedback component, create instances of it in your main program or another function block. Initialize it with actual digital output variables for extending and retracting the cylinder, as well as digital input variables for the sensors.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
PROGRAM MAIN
VAR
    ExtendOutputVar  AT %Q* : BOOL; // Must be declared before injected into the component
    RetractOutputVar AT %Q* : BOOL; // Must be declared before injected into the component
    ExtendSensorVar  AT %I* : BOOL; // Must be declared before injected into the component
    RetractSensorVar AT %I* : BOOL; // Must be declared before injected into the component
    ExtendOutput            : DigitalOutput(Output := ExtendOutputVar);
    RetractOutput           : DigitalOutput(Output := RetractOutputVar);
    ExtendSensor            : DigitalInput(Input := ExtendSensorVar);
    RetractSensor           : DigitalInput(Input := RetractSensorVar);
    Cylinder                : CylinderDualOutputWithFeedback(ExtendOutput := ExtendOutput, RetractOutput := RetractOutput, ExtendSensor := ExtendSensor, RetractSensor := RetractSensor);

    ExtendCylinder  : BOOL;
    RetractCylinder : BOOL;
END_VAR

// Extend the cylinder
IF ExtendCylinder THEN
    ExtendCylinder  := FALSE;
    Cylinder.Extend();
END_IF

// Retract the cylinder
IF RetractCylinder THEN
    RetractCylinder := FALSE;
    Cylinder.Retract();
END_IF

// Check if the cylinder is fully extended
IF Cylinder.IsExtended THEN
    // Cylinder is fully extended
END_IF

// Check if the cylinder is fully retracted
IF Cylinder.IsRetracted THEN
    // Cylinder is fully retracted
END_IF

END_PROGRAM

Conclusion

By following these steps, we have successfully created a CylinderDualOutputWithFeedback component that adheres to the SOLID principles, particularly the Dependency Inversion Principle (DIP) and with some consideration for the Interface Segregation Principle (ISP). This component can now be used in a modular and maintainable way, with clear separation of concerns for controlling the cylinder and obtaining feedback from sensors.