Engee documentation

Development of a custom device support package

This guide describes a scenario in which:

  1. The Python-based custom support package runs on a local computer, and you need to use it from Engee.

  2. It is also assumed that this support package will be used in the model. Engee and his call from the block Engee Function.

  3. This task can be solved using Engee.Integrations.

Structure of the device support package

The minimum device template is placed in the directory devices:

my_extension/
  devices/
    mydevice/
      __init__.py
      mydevice.py
The support package (device) must be located in devices/<device folder>/.

Naming scheme:

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()

Folder Name debug_device, class name DebugDevice and the name of the Julia module DEBUGDEVICE bound by the case conversion rule: snake_caseCamelCase (Python class) → UPPER_CASE (Julia is a module).

Device Class (Python)

Mandatory requirements:

  1. Inheritance from BaseDevice.

  2. Full annotations of the types of all arguments and return values for public methods.

  3. Methods intended to be called from a block should not start with and _ — such methods will not be available from scripts and blocks.

Example of a device class:

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

Note that when inheriting from a built-in class MainLogger it becomes possible to implement multilingual logging for a custom support package. For example:

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": "Ошибка чтения: {}",
            },
        }

User Device added to Engee.Integrations, syncing with Engee and after that it can be used in scripts and blocks.

You can clearly see how the mechanism described above for developing your own support package is applied in practice to work with real hardware using the example of the Community.: Development of a hardware support package for Engee.Integration.

Calling from the Engee Function block

In Common code start by connecting Engee.Integration, as in the standard .nglib blocks Hardware, the source code of which is open.

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

Then use the usual method invocation.:

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

Here MYDEVICE — this is the name of the user support package module corresponding to the class name MyDevice in uppercase.

A complete example of a block with parameters, initialization, and completion

A typical piece of equipment UDP RX uses in Common code Three functions:

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

Analyzing the structure:

  1. Constructor Block() — It is called once when the simulation is running. An instance of the device is created in it and initialization methods are called (bind, open, connect and so on). Block parameters (ip, port) are taken from the mask and are available as constructor arguments.

  2. Function (c::Block)(t::Real) — called at each step of the simulation. This is the body of the block. Here t — current time, eh c.field — accessing the fields of the structure. The returned values correspond to the order of the block’s output ports.

  3. Function terminate!(c::Block) — called at the end of the simulation. It frees up resources: closes sockets, files, and connections. Her challenge is optional.

Block parameters are set via a mask (maskValues in .nglib). In Common code refer to them by their names from the mask.: ip, port, buf_size, sample_time.

A custom support package is enabled in the same way. At the beginning Common code be sure to specify the path to the module.:

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

Common problems

  1. The block displays the following message Method not found.

    Reason: the method is private (_…) or does not have complete type annotations.

  2. The custom support package is not imported into Common code.

    Reason: Synchronization failed syncExtensions(). Execute engee.clear_all() on the command line Engee.

  3. The function signatures in the module have been changed, but the functions work the same way.

    Reason: in Engee the old version of the support package remains. Repeat syncExtensions(). If the functions have not been updated, run engee.clear_all() on the command line Engee.