Документация Engee

Разработка пользовательского пакета поддержки устройства

Страница в процессе разработки.

Данное руководство описывает сценарий, в котором:

  1. Разработанный на Python пользовательский пакет поддержки работает на локальном компьютере, и необходимо использовать его из Engee.

  2. Также предполагается использование этого пакета поддержки в модели Engee и его вызов из блока Engee Function.

  3. Эту задачу можно решить с помощью Engee.Интеграции.

Структура пакета поддержки устройства

Минимальный шаблон устройства размещается в директории devices:

my_extension/
  devices/
    mydevice/
      __init__.py
      mydevice.py
Пакет поддержки (устройство) должен располагаться в devices/<папка_устройства>/.

Схема именования:

devices/debug_device/           — папка устройства
├── __init__.py                 — from .debug_device import DebugDevice
└── debug_device.py             — class DebugDevice(BaseDevice):
                                      def ping(...) -> str:

Julia: EngeeDeviceManager.Devices.DEBUGDEVICE
Блок:  using .EngeeDeviceManager.Devices.DEBUGDEVICE
       dev = DEBUGDEVICE.DebugDevice()

Имя папки debug_device, имя класса DebugDevice и имя Julia-модуля DEBUGDEVICE связаны правилом преобразования регистра: snake_caseCamelCase (Python класс) → UPPER_CASE (Julia-модуль).

Класс устройства (Python)

Обязательные требования:

  1. Наследование от BaseDevice.

  2. Полные аннотации типов у публичных методов: аргументы, их типы и возвращаемое значение return, если функция возвращает результат.

  3. Методы, предназначенные для вызова из блока, не должны начинаться с _ и __ — такие методы не будут доступны из скриптов и блоков.

Пример класса устройства:

from devices.base_device import BaseDevice
from main_logger import MainLogger


class MyDeviceLogger(MainLogger):
    pass


logger = MyDeviceLogger()


class MyDevice(BaseDevice):
    def __init__(self) -> None:
        pass

    def connect(self, host: str, port: int) -> bool:
        logger.info("Connect {}:{}", host, port)
        return True

    def read_value(self, channel: int) -> float:
        return 12.34

Обратите внимание, что при наследовании от встроенного класса MainLogger появляется возможность реализовать мультиязычное логирование для пользовательского пакета поддержки. Например:

from collections import defaultdict
from typing import Any

from main_logger import MainLogger


class MyDeviceLogger(MainLogger):
    def __init__(self) -> None:
        self.tr: Any = defaultdict(dict)

        self.tr = {
            "Connect {}:{}": {
                "ru": "Подключение к {}:{}",
            },
            "Read failed: {}": {
                "ru": "Ошибка чтения: {}",
            },
        }

Пользовательское устройство добавляется в Engee.Интеграции, синхронизируется c Engee и после этого может использоваться в скриптах и блоках.

Наглядно увидеть, как описанный выше механизм разработки собственного пакета поддержки применяется на практике для работы с реальным оборудованием, можно в примере Сообщества: Разработка пакета поддержки оборудования для Engee.Интеграции.

Вызов из блока Engee Function

В Common code начните с подключения Engee.Интеграции, как в стандартных .nglib блоках Оборудования, исходный код которых открыт.

package_dir = "/internal_persistent_vol/support_packages/locations/Engee-Device-Manager/EngeeDeviceManager.jl"
include("$(package_dir)/src/EngeeDeviceManager.jl")

using .EngeeDeviceManager
using .EngeeDeviceManager.Devices.MYDEVICE

Затем используйте обычный вызов методов:

mutable struct Block <: AbstractCausalComponent
    dev::MYDEVICE.MyDevice
    function Block()
        d = MYDEVICE.MyDevice()
        d.connect(host, Int(port))
        new(d)
    end
end

function (c::Block)(t::Real, channel)
    return c.dev.read_value(Int(channel))
end

Здесь MYDEVICE — это имя модуля пользовательского пакета поддержки, соответствующее имени класса MyDevice в верхнем регистре.

Полный пример блока с параметрами, инициализацией и завершением

Типовой блок оборудования UDP RX использует в Common code три функции:

package_dir = "/internal_persistent_vol/support_packages/locations/Engee-Device-Manager/EngeeDeviceManager.jl"
include("$(package_dir)/src/EngeeDeviceManager.jl")

using .EngeeDeviceManager
using .EngeeDeviceManager.Devices.SOCKET

mutable struct Block <: AbstractCausalComponent
    socket::SOCKET.Socket
    function Block()
        socket = SOCKET.Socket("AF_INET", "SOCK_DGRAM")
        SOCKET.bind(socket, ip, port)
        new(socket)
    end
end

function (c::Block)(t::Real)
    response = SOCKET.receive(c.socket, buf_size)
    if response === nothing
        return 0, zeros(UInt8, buf_size)
    elseif length(response.data) == 0
        return 0, zeros(UInt8, buf_size)
    end
    return length(response.data), resize!(response.data, buf_size)
end

function terminate!(c::Block)
    SOCKET.close(c.socket)
    return nothing
end

Разбор структуры:

  1. Конструктор Block() — вызывается один раз при запуске симуляции. В нем создается экземпляр устройства и вызываются методы инициализации (bind, open, connect и т.д.). Параметры блока (ip, port) берутся из маски и доступны как аргументы конструктора.

  2. Функция (c::Block)(t::Real) — вызывается на каждом шаге симуляции. Это тело блока. Здесь t — текущее время, а c.поле — доступ к полям структуры. Возвращаемые значения соответствуют порядку выходных портов блока.

  3. Функция terminate!(c::Block) — вызывается при завершении симуляции. Она освобождает ресурсы: закрывает сокеты, файлы, соединения. Ее вызов опционален.

Параметры блока задаются через маску (maskValues в .nglib). В Common code ссылайтесь на них по именам из маски: ip, port, buf_size, sample_time.

Пользовательский пакет поддержки подключается аналогично. В начале Common code обязательно укажите путь к модулю:

package_dir = "/internal_persistent_vol/support_packages/locations/Engee-Device-Manager/EngeeDeviceManager.jl"
include("$(package_dir)/src/EngeeDeviceManager.jl")

using .EngeeDeviceManager
using .EngeeDeviceManager.Devices.MYDEVICE

mutable struct Block <: AbstractCausalComponent
    dev::MYDEVICE.MyDevice
    function Block()
        d = MYDEVICE.MyDevice()
        d.connect(host, port)
        new(d)
    end
end

function (c::Block)(t::Real)
    return c.dev.read_value(channel)
end

function terminate!(c::Block)
    # закрытие соединения, если необходимо
    return nothing
end

Частые проблемы

  1. В блоке выводится сообщение Method not found.

    Причина: метод приватный (_…) или не имеет полных аннотаций типов.

  2. Пользовательский пакет поддержки не импортируется в Common code.

    Причина: не выполнена синхронизация syncExtensions(). Выполните engee.clear_all() в командной строке Engee.

  3. Изменили сигнатуры функций в модуле, но функции работают по-старому.

    Причина: в Engee осталась старая версия пакета поддержки. Повторите syncExtensions(). Если функции не обновились, выполните engee.clear_all() в командной строке Engee.