Development of a custom device support package
This guide describes a scenario in which:
-
The Python-based custom support package runs on a local computer, and you need to use it from Engee.
-
It is also assumed that this support package will be used in the model. Engee and his call from the block Engee Function.
-
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_case → CamelCase (Python class) → UPPER_CASE (Julia is a module).
Device Class (Python)
Mandatory requirements:
-
Inheritance from
BaseDevice. -
Full annotations of the types of all arguments and return values for public methods.
-
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:
-
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,connectand so on). Block parameters (ip,port) are taken from the mask and are available as constructor arguments. -
Function
(c::Block)(t::Real)— called at each step of the simulation. This is the body of the block. Heret— current time, ehc.field— accessing the fields of the structure. The returned values correspond to the order of the block’s output ports. -
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
-
The block displays the following message
Method not found.Reason: the method is private (
_…) or does not have complete type annotations. -
The custom support package is not imported into Common code.
Reason: Synchronization failed
syncExtensions(). Executeengee.clear_all()on the command line Engee. -
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, runengee.clear_all()on the command line Engee.