Примеры ======= Первая программа ---------------- Напишем и разберём программу, которая мигает светодиодом RUN каждый цикл работы. Стиль написания: Structured Text-подобный. .. highlight:: python .. code-block:: python #Моргаем светодиодом RUN from pyplc.config import plc,board def hello(): board.run = not board.run plc.run( instances=[ hello ], ctx=globals() ) Первая строчка подключает и импортирует из модуля pyplc 2 объекта: plc и board. plc необходим для организации цикла работы, board имеет атрибуты, которые отвечают за светодиоды (4 шт) и переключатели (2 шт) контроллера. Далее идет объявление функции с именем hello, в теле которой атрибуту run присваивается логическое отрицание его текущего значения. `board.run` отвечает за светодиод `RUN`, имеет тип bool (логический, может быть False -выключен или True — включён). Последняя строка используя объект `plc`` запускает цикл работы программы. Для этого вызывается метод `run`` с 2 параметрами: instances и ctx. - instances имеет тип списка (встроенный тип python). Все элементы этого списка вызываются каждый цикл работы программы. Мы в списке только одну функцию указали (обратите внимание, что мы не результат выполнения функции, а саму функцию передали) - ctx имеет тип словаря (dict), и мы ему присваиваем словарь глобальных переменных текущего контекста. plc.run выполняется до прерывания цикла работы (в случае ошибки или исключения). Включить исполнительный механизм по срабатыванию датчика -------------------------------------------------------- Такая задача встречается как вспомогательная, например в случае набора расходной емкости водой по нижнему уровню. В нашем случае нам необходимо подключить 1 дискретный вход (сигнал с датчика) и 1 дискретный выход (управление механизмом, например насосом). Для этого потребуется 8-ми канальный KRAX DI-430 и 8-ми канальный KRAX DO-530. .. highlight:: python :linenothreshold: 5 .. code-block:: python from pyplc.config import plc from pyplc.channel import IBool,QBool from pyplc.ld import LD SWITCH_ON_1 = IBool.at('%IX8.0') POWER_ON_1 = QBool.at('%QX9.0') # SWITCH_ON_1 POWER_ON_1 # ├───────┤├───────────────(S)───────┤ turn_on = LD.no(SWITCH_ON_1).set(POWER_ON_1).end() plc.run( instances=[turn_on], ctx=globals() ) В строке №3 подключаем модуль, который позволяет писать код, напоминающий Ladder Diagram (строка 8,9) В строке 5,6 объявлены переменные ввода-вывода. Значение этих переменных синхронизируется с состояниями каналов модулей ввода вывода. Для того чтобы объявить переменную ввода вывода мы выбираем ей имя (SWITCH_ON_1 & POWER_ON_1), и присваиваем им результат выполнения метода `at`` соответствующего типа. Например дискретный вход это IBool, дискретный выход QBool. В качестве параметра указываем строку, которая указывает по какому адресу в памяти ввода вывода она находится. Подробную информацию об адресации каналов ввода вывода смотрите в документации к библиотеки `PYPLC`. Обратите внимание, turn_on тоже использована как функция. Дело в том, что в python объекты могут переопределять метод `__call__`, и тогда можно написать turn_on() - это будет вызов нашей логики один раз. А просто turn_on — это наша программа. Необходимо отметить, что переменные ввода-вывода (SWITCH_ON_1 & POWER_ON_1) представляют механизм доступа к физическим сигналом, но это тоже объекты. И это может приводить к путанице. Например код .. highlight:: python .. code-block:: python if SWITCH_ON_1: POWER_ON_1 = True сработает не как ожидается. Первая часть (if SWITCH_ON_1:) работает вполне ожидаемо, то есть проверит какое состояние дискретного сигнала SWITCH_ON_1 и если он True, то выполнит код после :, а именно .. highlight:: python .. code-block:: python POWER_ON_1 = True. Можно ожидать, что наш дискретный выход будет установлен в True, но в действительности наш объект POWER_ON_1 будет заменен на новый объект, тип которого будет bool. Чтобы избежать этого, лучше к переменным ввода-вывода обращаться через атрибуты объекта hw, который находится в `pyplc.config`. Наш пример будет выглядеть так: .. highlight:: python .. code-block:: python if SWITCH_ON_1: hw.POWER_ON_1 = True Еще возможны следующие способы изменения значения переменных вывода, все равнозначные: .. highlight:: python .. code-block:: python POWER_ON_1(True) POWER_ON_1.write(True) hw.POWER_ON_1 = True Подробная информация по работе с переменными ввода-вывода в документации к `PYPLC`. Дискретный вход, выход и аналоговый вход ---------------------------------------- Задача: включить по дискретному входу №1, выключить по входу №2 или уровню аналогового сигнала. Потребуется еще модуль подключения токового сигнала 4-20 мА KRAX-AI455. Он 4-х канальный. Порядок сопряжения с модулями такой: 932 — 455 — 430 — 530. .. highlight:: python .. code-block:: python from pyplc.config import plc,hw from pyplc.channel import IBool,QBool,IWord from pyplc.ld import LD SWITCH_ON_1 = IBool.at( '%IX8.0' ) SWITCH_OFF_1= IBool.at( '%IX8.1' ) LEVEL_1 = IWord.at( '%IW0' ) POWER_ON_1 = QBool.at( '%QX9.0' ) # SWITCH_ON_1 POWER_ON_1 # ├───────┤ ├──────────────(S)────────┤ turn_on = LD.no(SWITCH_ON_1).set(POWER_ON_1).end() # в Ladder Diagram аналога нет, можно описать как несколько параллельных # цепей (тут 2), и результатом их выполнения будет True если они все False # SWITCH_OFF_1 POWER_ON_1 # ├──┤ ├────────────┬───┤NOT├─────────────────────────────────────────(R)─────────┤ # LEVEL_1>32767 │ # ├──┤ ├────────────┘ turn_off=LD.nor(LD.no(SWITCH_OFF_1),LD.no(lambda:LEVEL_1>32767)).rst(POWER_ON_1).end() plc.run( instances=[turn_on,turn_off], ctx=globals() ) Обратите внимание, в качестве условия `LEVEL_1>32767` используется `lambda: LEVEL_1>32767`. Если написать `LEVEL_1>32767`, то результат будет вычислен сразу же, будет True если в момент вычисления `LEVEL_1>32767` или False иначе. А надо каждый цикл работы вычислять. Для этого нужно создать функцию .. highlight:: python .. code-block:: python def check_level()->bool: return LEVEL_1>32767 и её поставить. Не очень удобно создавать функцию для любого выражения. На счастье в python есть возможность создавать анонимные функции, используя ключевое слово lambda. Этим пользуемся. Аналоговый вход, дискретный выход и функциональные блоки -------------------------------------------------------- В модуле pyplc есть набор часто используемых блоков. Разберем как их использовать. Задача: включить исполнительный механизм если уровень на аналоговом входе >75%, выключить если <25%. .. highlight:: python .. code-block:: python from pyplc.config import plc,hw from pyplc.utils.latch import RS from pyplc.channel import QBool,IWord LEVEL_1 = IWord.at( '%IW0' ) POWER_ON_1 = QBool.at( '%QX8.0' ) logic = RS(reset = lambda: LEVEL_1<0x3FFF, set = lambda: LEVEL_1>0x7FFF, q = POWER_ON_1 ) plc.run( instances=[logic], ctx=globals() ) Применен блок RS. У него 2 входа (set,reset) и один выход (q). Если `set==True` и `reset==False`, то `q` становится True. Если `reset==True`, то `q` становится `False`. Настраиваем, что поступает на входы `set`/`reset` и куда выход `q` подключить. На вход и выход все программы могут принимать функции, каждый цикл работы они будет вызываться. Функции которые мы подключаем к входам должны работать без аргументов, а те что подключаются к выходам напротив должны работать как минимум с одним аргументом. Мы создаем 2 безымянные функции с помощью ключевого слова lambda. А выход мы подключаем к POWER_ON_1, который как упоминалось выше тоже может использоваться как функция (POWER_ON_1(True) как раз устанавливает новое значение True). Если бы мы на set подключали дискретный вход (как в примере №3) SWITCH_ON_1, то использовать lambda нет необходимости. Было бы просто set = SWITCH_ON_1. Блоки можно самостоятельно писать и потом их повторно использовать. Создание блока и его использование ---------------------------------- Рассмотрим как создать свой блок и как его использовать. Назначение блока: включение двигателя с авто-подхватом, перед включением дать звонок 2 сек. Включение производится дискретным выходом длительностью не менее 1 сек. Выключение другим дискретным выходом длительностью не менее 1 сек. Следовательно, наш блок должен иметь 3 выхода (ON,OFF,BELL) и один вход (MANUAL) для активации. .. highlight:: python .. code-block:: python from pyplc.sfc import SFC,POU class Motor(SFC): manual = POU.input( False ) ison = POU.input(False,hidden=True) on = POU.output(False,hidden=True) off= POU.output(False,hidden=True) bell = POU.output(False,hidden=True) def __init__(self,manual: bool= False, on:bool = False,off:bool = False,bell: bool=False, id:str=None, parent:POU = None ) -> None: super().__init__( ) self.on = on self.off= off self.bell=bell self.manual = manual def main(self): yield from self.until(lambda: self.manual,step='Ждем manual') self.bell = True yield from self.pause(2000,step='Звонок') self.bell = False self.on = True yield from self.till(lambda: self.manual,max=1000,step='Импульс ON') self.on = False yield from self.till(lambda: self.manual,step='Работаем') self.off = True yield from self.pause(2000,step='Импульс OFF') self.off = False Программа должна каждый цикл опроса должна завершать свою работу. Надо при получении сигнала manual надо включить звонок, подождать 2 сек и выключить его. SFC-блоки за счет использования ключевого слова yield позволяют функцию выполнять частями, например: .. highlight:: python .. code-block:: python yield from self.until(lambda: self.manual,step='Ждем manual') каждое сканирование проверяет состояние self.manual, пока оно не станет ==True. А как только оно выполнится перейдет к следующей части кода: .. highlight:: python .. code-block:: python self.bell = True yield from self.pause(2000,step='Звонок') self.bell = False В первой строке на выход bell подаем True, и потом снова конструкция с циклом на 2000 мсек. По окончанию которой на bell выдаем False. Обратите внимание: если self.bell = True нужно удерживать, то выглядеть это может так: .. highlight:: python .. code-block:: python self.bell = True for _ in self.pause(2000,step='Звонок'): self.bell = True self.bell = False А так если к выходу bell подключен физический сигнал он изменится всего 2 раза (в начале и конце). Пример использования нашего блока: .. highlight:: python .. code-block:: python from pyplc.config import plc from pyplc.channel import IBool,QBool from motor import Motor SWITCH_ON_1 = IBool.at( '%IX0.0' ) POWER_ON_1 = QBool.at( '%QX1.0' ) POWER_OFF_1 = QBool.at( '%QX1.1' ) BELL_ON_1 = QBool.at( '%QX1.2' ) motor = Motor(manual=SWITCH_ON_1,on=POWER_ON_1,off=POWER_OFF_1, bell = BELL_ON_1) plc.run(instances=[motor],ctx=globals()) Тестирование и отладка ---------------------- Для тестирования и отладки используется следующая методика: программа пишется, отлаживается на компьютере (без контроллера), так как синтаксис Python и MicroPython одинаковый, модуль pyplc написан на python и работает на любом ПК. В качестве среды разработки используется VSCode с дополнением для обмена файлами с контроллером KRAX. В VSCode доступны все привычные методы отладки (контрольные точки, просмотр значений любой переменной, пошаговое выполнение и т.п.). Модуль PyPLC позволяет прозрачно подключиться к контроллеру (если он есть), и тогда сигналы с датчиков и управляющие сигналы синхронизируются. Советы по поиску и устранению проблем ------------------------------------- #. Каждый цикл работы программы моргайте индикатором RUN. Это позволит с первого взгляда определить работает программа сейчас или закончила своё выполнение. #. Пользуйтесь выводом на консоль (функция `print`, или `POU.log` ). #. Если надо проверить состояние переменных во время выполнения программы в контроллере можно воспользоваться telnet подключением (доступно на порту 2455). После установки соединения можно написать выражение на python и оно будет выполнено или вычислено. Например если написать board.run вам в ответ будет показано текущее состояние светодиода RUN.