Skip to content
Eugen Iofciu Vasile edited this page Feb 12, 2024 · 3 revisions

What is FluidPyPLC and how does it work ? 🔍

FluidPyPLC is a Python package designed to assist in solving and testing pneumatic or hydraulic circuits and PLC sequences. The core idea behind this project is a method used to solve FluidSim circuits' sequences, such as "A+/B+/B-/A-", which technically creates 2 blocks:

phases_diagram

  • 1. The limit switch a1 will block the actuator B, because B does a positive and a negative stroke while a1 is active;
  • 2. The limit switch b0 will block the actuator A, because A does a positive and a negative stroke while b0 is active;

There are several methods to solve this problem:

  • 1. Cascade;
  • 2. Relay memories;
  • 3. PLC;

See more about these methods in the Methods' wiki page.

What did I use❓

I utilized a combination of the Relay memories method and the PLC method. Let me explain further in the following section.

Method implementation:

The logic I employed is based on the Relay memories method because it offers precision in processing sequences and can be applied as a rule for all sequences. Having a rule for the submitted sequences is the optimal solution to solve them all.

With a rule in place, I can write a program instead of using many relays (which are not flexible), and the PLC can read and execute the program.

PLC offers a flexible and efficient means of commanding the circuit's actuators and valves. This is the solution I developed for solving blocking signals in pneumatic or hydraulic systems.

Solution 🏆

Sequence Submitter

This is where the user's input is handled and formatted to a correct sequence.

class Sequence:
    def __init__(self):
        self.alphabet = "abcdefghjklmnopqrstuvwyxz"
        self.symbols = "+-"
        self.sequence = []

    def run(self):
        while True:
            stroke = input("Insert stroke:")
            if stroke == "/" and self.close_sequence_handler():
                break

            stroke_check = self.stroke_handler(stroke)
            sequence_check = self.sequence_handler(stroke)
            if stroke_check and sequence_check:
                self.sequence_append(stroke)

    def stroke_handler(self, stroke):
        if len(stroke) != 2:
            print("[!] The piston stroke name must be exactly 2 characters (e.g., A+, B-).")
            return False
        elif stroke == "/":
            print("[!] The sequence is not completed.")
            return False
        elif stroke[0].lower() in self.alphabet and stroke[1] in self.symbols:
            return True
        else:
            print("[!] Invalid stroke format. Expected: LETTER+ or LETTER-.")
            return False

    def sequence_append(self, stroke):
        self.sequence.append(stroke.upper())

    def sequence_handler(self, stroke):
        stroke_upper = stroke.upper()
        if stroke_upper not in self.sequence:
            return True

        last_index = ''.join(self.sequence).rfind(stroke_upper[0]) // 2
        if self.sequence[last_index] == stroke_upper:
            print("[!] The piston is already in that position.")
            return False
        return True

    def close_sequence_handler(self):
        return len(self.sequence) % 2 == 0

Processing the Sequence

This process serves as the core of FluidPyPLC's functionality. It consists of:

  • Groups: Set the groups based on the blocks.
  • Blocks: Individuate the blocking signals.
  • Activations: Set the limit switches of each actuator.

Initial process

# function that founds the number of block in the sequence
def number_of_blocks(s):
    seen = set()
    z = 0
    for i in range(len(s)):
        if s[i][0] in seen:
            z += 1
            seen = set()
            seen.add(s[i][0])
        else:
            seen.add(s[i][0])
    return z

# class that creates the sequence's groups
class Groups():
    def __init__(self, s):
        self.run(s)

    def run(self, s):
        seen = set()
        n_blocks = number_of_blocks(s)
        self.groups_2D = [[] for _ in range(n_blocks + 1)]
        z = 0
        for i in range(len(s)):
            if s[i][0] in seen:
                z += 1
                seen = set()
                self.groups_2D[z].append(s[i])
                seen.add(s[i][0])
            else:
                self.groups_2D[z].append(s[i])
                seen.add(s[i][0])

# array rotation to the right by 1, the last limit switch will activate the sequence, together with the Start button
def rotate(array, n):
    return array[n:] + array[:n]

# the function set the switches labels, e.g. a0, a1, etc ..
def switches_labels(s):
    switch_labels = rotate(s, -1)
    switch_labels = [switch.lower() for switch in switch_labels]
    switch_labels = [words.replace("-", "0") for words in switch_labels]
    switch_labels = [words.replace("+", "1") for words in switch_labels]
    return switch_labels

# function to set the limit switches groups by copying the same format of the sequence's groups
def copy_array(array, groups):
    new_array = deepcopy(groups)
    z = 0
    for i in range(len(groups)):
        for j in range(len(groups[i])):
            new_array[i][j] = array[z]
            z += 1
    return new_array

class Switches():
    def __init__(self, s, g):
        self.run(s, g)

    def run(self, s, g):
        self.limit_switches = switches_labels(s)
        self.limit_s_groups = copy_array(self.limit_switches, g)

Data store and last processes

This is where I can provide the full data about the sequence, later to be used by other classes like Plc and LD, to convert the sequence data in a functional and efficient Structured Text code or Ladder Logic xml file.

# get the number of pistons and their labels
def pistons(s):
    cleaned_sequence = [stroke.strip("+-") for stroke in s]
    piston_labels = sorted(set(cleaned_sequence))
    return len(piston_labels), piston_labels

# function to merge the last group with the first one if it is compatible
def merge_groups(groups):
    last_group_strokes = {stroke[0] for stroke in groups[-1]}
    return not any(stroke[0] in last_group_strokes for stroke in groups[0])

# function to check if there are loops inside the sequence
def check_for_loops(s):
    return any(s.count(stroke) > 1 for stroke in set(s))

# function to understand which limit switches are held down from the beginning, e.g. the limit switches normally open, are closed
def lswitch_boolean(limit_switches):
    lswitch_bool = ['TRUE',]
    seen_letter = []
    seen_switch = []
    for i in range(1, len(limit_switches)):
        limit_switch = limit_switches[i]
        if limit_switch[0] not in seen_letter:
            seen_letter.append(limit_switch[0])
            seen_switch.append(limit_switch)
            lswitch_bool.append('FALSE')
        else:
            if limit_switch not in seen_switch:
                lswitch_bool.append('TRUE')
            else:
                lswitch_bool.append('FALSE')
    return lswitch_bool

# stored data
class Data:
    def __init__(self, s):
        self.sequence = s
        self.run()

    def run(self):
        g = Groups(self.sequence)
        sw = Switches(self.sequence, g.groups_2D)
        self.groups = g.groups_2D
        self.lswitch = sw.limit_switches
        self.lswitch_groups = sw.limit_s_groups
        self.loop = check_for_loops(self.sequence)
        self.merge = merge_groups(g.groups_2D)
        self.number_of_pistons, self.pistons_labels = pistons(self.sequence)
        self.lswitch_bool = lswitch_boolean(sw.limit_switches)

PLC generator and LD converter 🥇

The Plc class generates the ST code to handle the sequence, based on using as many Logic Gates (AND, OR, NAND, NOR,...) and IF ELSE statements as needed.

It also generates the connections of the Inputs and Outputs modules for PLC and the circuit's contacts and solenoids. The ST code logic follows the Relay memories method, using virtual relays K0, K1,... to execute part of the sequence. This can be seen as using functions in ST code, but it is lighter than that.

The LD class, on the other hand, converts the generated ST code into Ladder Logic xml, with a custom uuid4 identity so it can be read and written by CODESYS once imported. This is possible due to the fact that using IF ELSE statements can allow an easy conversion to Ladder Logic.

Installation

To install FluidPyPLC, follow these steps:

  1. Install the package using pip:
pip install fluidpyplc
  1. Set the working directory, this will create 2 folders inside the chosen directory, "Plots" and "plc":
fluidpy --folder <path_to_your_working_directory>