Initial commit

This commit is contained in:
2026-01-14 21:33:17 +03:00
commit 40d80ef55e
17 changed files with 2573 additions and 0 deletions

0
owen/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

209
owen/client.py Normal file
View File

@@ -0,0 +1,209 @@
#! /usr/bin/env python3
"""Реализация класса клиента для управления контроллером ОВЕН."""
from __future__ import annotations
from operator import mul, truediv
from typing import TYPE_CHECKING, Callable
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadBuilder, BinaryPayloadDecoder
from owen.protocol import Owen, OwenError
if TYPE_CHECKING:
from pymodbus.client.sync import ModbusSerialClient
from pymodbus.pdu import ModbusResponse
from serial import Serial
from owen.device import MODBUS_PARAMS, OWEN_DEVICE, OWEN_PARAMS
class ClientMixin:
"""Класс-примесь клиента."""
@staticmethod
def check_index(name: str, dev: OWEN_PARAMS | MODBUS_PARAMS,
index: int | None) -> int | None:
"""Проверка индекса."""
if not index:
index = None if None in dev["index"] else 0
if index not in dev["index"]:
msg = f"'{name}' does not support index '{index}'"
raise OwenError(msg)
return index
@staticmethod
def check_value(name: str, dev: OWEN_PARAMS | MODBUS_PARAMS,
value: float | str | None) -> None:
"""Проверка данных."""
if all([value is None, dev["min"] is not None, dev["max"] is not None]) or \
all([value is not None, dev["min"] == dev["max"] is None or
value < dev["min"] or value > dev["max"]]): # type: ignore
msg = f"An '{name}' value of '{value}' is out of range"
raise OwenError(msg)
class OwenSerialClient(ClientMixin):
"""Класс клиента для взаимодействия с устройством по протоколу ОВЕН."""
def __init__(self, transport: Serial, device: OWEN_DEVICE, unit: int, *,
addr_len_8: bool = True) -> None:
"""Инициализация класса клиента с указанными параметрами."""
self.socket = transport
self.unit = unit
self.device = device
self.addr_len_8 = addr_len_8
self._owen = Owen(self.unit, addr_len_8)
def __repr__(self) -> str:
"""Строковое представление объекта."""
return (f"{type(self).__name__}(transport={self.socket}, unit={self.unit}, "
f"addr_len_8={self.addr_len_8})")
def __del__(self) -> None:
"""Закрытие соединения с устройством при удалении объекта."""
if self.socket:
self.socket.close()
def bus_exchange(self, packet: bytes) -> bytes:
"""Обмен по интерфейсу."""
self.socket.reset_input_buffer()
self.socket.reset_output_buffer()
self.socket.write(packet)
return self.socket.read_until(b"\r")
def send_message(self, flag: int, name: str, index: int | None,
data: bytes = b"") -> bytes:
"""Подготовка данных для обмена."""
packet = self._owen.make_packet(flag, name, index, data)
answer = self.bus_exchange(packet)
return self._owen.parse_response(packet, answer)
def get_param(self, name: str, index: int | None = None) -> float | str:
"""Чтение данных из устройства."""
dev = self.device["Owen"][name.upper()]
index = self.check_index(name, dev, index)
result = self.send_message(1, name, index)
return self._owen.unpack_value(dev["type"], result, index)
def set_param(self, name: str, index: int | None = None,
value: float | str | None = None) -> bool:
"""Запись данных в устройство."""
dev = self.device["Owen"][name.upper()]
index = self.check_index(name, dev, index)
self.check_value(name, dev, value)
data = self._owen.pack_value(dev["type"], value)
return bool(self.send_message(0, name, index, data))
class OwenModbusClient(ClientMixin):
"""Класс клиента для взаимодействия с устройством по протоколу Modbus."""
def __init__(self, transport: ModbusSerialClient, device: OWEN_DEVICE,
unit: int) -> None:
"""Инициализация класса клиента с указанными параметрами."""
self.socket = transport
self.unit = unit
self.device = device
self.socket.connect()
def __repr__(self) -> str:
"""Строковое представление объекта."""
return f"{type(self).__name__}(transport={self.socket}, unit={self.unit})"
def __del__(self) -> None:
"""Закрытие соединения с устройством при удалении объекта."""
if self.socket:
self.socket.close()
@staticmethod
def check_error(retcode: ModbusResponse) -> bool:
"""Проверка возвращаемого значения на ошибку."""
if retcode.isError():
raise OwenError(retcode)
return True
def _read(self, dev: MODBUS_PARAMS, index: int | None) -> float | str:
"""Чтение данных из регистра Modbus."""
count = {"U16": 1, "I16": 1, "U32": 2, "I32": 2, "F32": 2, "STR": 4,
}[dev["type"]]
result = self.socket.read_holding_registers(address=dev["index"][index],
count=count,
unit=self.unit)
self.check_error(result)
decoder = BinaryPayloadDecoder.fromRegisters(result.registers, Endian.Big)
return {"U16": decoder.decode_16bit_uint,
"I16": decoder.decode_16bit_int,
"U32": decoder.decode_32bit_uint,
"I32": decoder.decode_32bit_int,
"F32": decoder.decode_32bit_float,
"STR": lambda: decoder.decode_string(8),
}[dev["type"]]()
def modify_value(self, func: Callable[[float, float], float], dev: MODBUS_PARAMS,
index: int | None, value: float) -> float:
"""Преобразование значения к нужной точности."""
if dev["dp"]:
dp_dev = self.device["Modbus"][dev["dp"]]
dp = self._read(dp_dev, index)
value = func(value, 10.0**int(dp))
prec = dev["precision"]
return func(value, 10.0**prec) if prec else value
def get_param(self, name: str, index: int | None = None) -> float | str:
"""Чтение данных из устройства."""
dev = self.device["Modbus"][name.upper()]
index = self.check_index(name, dev, index)
value = self._read(dev, index)
return self.modify_value(truediv, dev, index, value)
def set_param(self, name: str, index: int | None = None,
value: float | str | None = None) -> bool:
"""Запись данных в устройство."""
dev = self.device["Modbus"][name.upper()]
index = self.check_index(name, dev, index)
self.check_value(name, dev, value)
value = self.modify_value(mul, dev, index, value)
builder = BinaryPayloadBuilder(None, Endian.Big)
{"U16": lambda value: builder.add_16bit_uint(int(value)),
"I16": lambda value: builder.add_16bit_int(int(value)),
"U32": lambda value: builder.add_32bit_uint(int(value)),
"I32": lambda value: builder.add_32bit_int(int(value)),
"F32": lambda value: builder.add_32bit_float(float(value)),
"STR": lambda value: builder.add_string(str(value)),
}[dev["type"]](value)
result = self.socket.write_registers(address=dev["index"][index],
values=builder.build(),
skip_encode=True,
unit=self.unit)
return self.check_error(result)
__all__ = ["OwenModbusClient", "OwenSerialClient"]

229
owen/converter.py Normal file
View File

@@ -0,0 +1,229 @@
#! /usr/bin/env python3
"""Функции для упаковки и распаковки разных типов данных."""
from binascii import hexlify, unhexlify
from decimal import Decimal
from struct import pack, unpack
def pack_str(value: str) -> bytes:
"""Упаковка данных типа STR."""
return bytes(value[::-1], encoding="cp1251")
def unpack_str(value: bytes, index: int) -> str:
"""Распаковка данных типа STR."""
return bytes(value[::-1]).decode("cp1251")
def pack_u24(value: tuple[int, int]) -> bytes:
"""Упаковка данных типа U24."""
return pack(">BH", *value)[:3]
def unpack_u24(value: bytes, index: int) -> tuple[int, int]:
"""Распаковка данных типа U24."""
return unpack(">BH", value[:3])
def pack_i8(value: int) -> bytes:
"""Упаковка данных типа I8."""
return pack(">b", value)[:1]
def unpack_i8(value: bytes, index: int) -> int:
"""Распаковка данных типа I8."""
return unpack(">b", value[:1])[0]
def pack_u8(value: int) -> bytes:
"""Упаковка данных типа U8."""
return pack(">B", value)[:1]
def unpack_u8(value: bytes, index: int) -> int:
"""Распаковка данных типа U8."""
return unpack(">B", value[:1])[0]
def pack_i16(value: int) -> bytes:
"""Упаковка данных типа I16."""
return pack(">h", value)[:2]
def unpack_i16(value: bytes, index: int) -> int:
"""Распаковка данных типа I16."""
return unpack(">h", value[:2])[0]
def pack_u16(value: int) -> bytes:
"""Упаковка данных типа U16."""
return pack(">H", value)[:2]
def unpack_u16(value: bytes, index: int) -> int:
"""Распаковка данных типа U16."""
return unpack(">H", value[:2])[0]
def pack_f24(value: float) -> bytes:
"""Упаковка данных типа F24."""
return pack(">f", value)[:3]
def unpack_f24(value: bytes, index: int) -> float:
"""Распаковка данных типа F24."""
return unpack(">f", value[:3] + b"\x00")[0]
def pack_f32(value: float) -> bytes:
"""Упаковка данных типа F32."""
return pack(">f", value)[:4]
def unpack_f32(value: bytes, index: int) -> float:
"""Распаковка данных типа F32."""
return unpack(">f", value[:4])[0]
def pack_f32t(value: tuple[float, int]) -> bytes:
"""Упаковка данных типа F32+T."""
return pack(">fH", *value)[:6]
def unpack_f32t(value: bytes, index: int) -> tuple[float, int]:
"""Распаковка данных типа F32+T."""
return unpack(">fH", value[:6])
def pack_i32(value: int) -> bytes:
"""Упаковка данных типа I32."""
return pack(">i", value)[:4]
def unpack_i32(value: bytes, index: int) -> int:
"""Распаковка данных типа I32."""
return unpack(">i", value[:4])[0]
def pack_u32(value: int) -> bytes:
"""Упаковка данных типа U32."""
return pack(">I", value)[:4]
def unpack_u32(value: bytes, index: int) -> int:
"""Распаковка данных типа U32."""
return unpack(">I", value[:4])[0]
def pack_sdot(value: float) -> bytes:
"""Упаковка данных типа STORED_DOT."""
sign, digits, exponent = Decimal(str(value)).as_tuple()
mantissa = int("".join(map(str, digits)))
frmt, size, chunk = {mantissa < 16: (">B", 4, slice(1)),
mantissa >= 4096: (">I", 20, slice(1, 4)),
}.get(True, (">H", 12, slice(2)))
bin_str = f"{sign:1b}{abs(exponent):03b}{mantissa:0{size}b}"
return pack(frmt, int(bin_str, 2))[chunk]
def unpack_sdot(value: bytes, index: int) -> float:
"""Распаковка данных типа STORED_DOT."""
if index is not None:
value = value[:-2]
data = int.from_bytes(value, "big")
shift = len(value) * 8 - 4
sign = data >> (shift + 3) & 1
exponent = data >> shift & 7
mantissa = data & (2 ** shift - 1)
return (-1) ** sign * 10 ** (-exponent) * mantissa
def _encode_dot_u(value: float) -> bytes:
"""Упаковка данных типа DEC_DOTi."""
value = int(value)
length = len(str(value))
length += length % 2
hexstr = f"{value:0{length}d}"
return unhexlify(hexstr)
def pack_dot0(value: float) -> bytes:
"""Упаковка данных типа DEC_DOT0."""
return _encode_dot_u(value)
def pack_dot3(value: float) -> bytes:
"""Упаковка данных типа DEC_DOT3."""
return _encode_dot_u(value * 1000)
def _decode_dot_u(value: bytes, index: int) -> int:
"""Распаковка данных типа DEC_DOTi."""
if index is not None:
value = value[:-2]
return int(hexlify(value).decode())
def unpack_dot0(value: bytes, index: int) -> int:
"""Распаковка данных типа DEC_DOT0."""
return _decode_dot_u(value, index)
def unpack_dot3(value: bytes, index: int) -> float:
"""Распаковка данных типа DEC_DOT3."""
return _decode_dot_u(value, index) / 1000.0
OWEN_TYPE = {"F32+T": {"pack": pack_f32t, "unpack": unpack_f32t},
"SDOT": {"pack": pack_sdot, "unpack": unpack_sdot},
"DOT0": {"pack": pack_dot0, "unpack": unpack_dot0},
"DOT3": {"pack": pack_dot3, "unpack": unpack_dot3},
"F32": {"pack": pack_f32, "unpack": unpack_f32},
"F24": {"pack": pack_f24, "unpack": unpack_f24},
"U16": {"pack": pack_u16, "unpack": unpack_u16},
"I16": {"pack": pack_i16, "unpack": unpack_i16},
"U32": {"pack": pack_u32, "unpack": unpack_u32},
"I32": {"pack": pack_i32, "unpack": unpack_i32},
"U8": {"pack": pack_u8, "unpack": unpack_u8},
"I8": {"pack": pack_i8, "unpack": unpack_i8},
"U24": {"pack": pack_u24, "unpack": unpack_u24}, # для N.err
"STR": {"pack": pack_str, "unpack": unpack_str}}

1244
owen/device.py Normal file

File diff suppressed because it is too large Load Diff

151
owen/protocol.py Normal file
View File

@@ -0,0 +1,151 @@
#! /usr/bin/env python3
"""Реализация протокола взаимодействия ОВЕН."""
from __future__ import annotations
import logging
from functools import reduce
from struct import error, unpack
from owen.converter import OWEN_TYPE
_logger = logging.getLogger(__name__)
_logger.addHandler(logging.NullHandler())
HEADER = ord("#")
FOOTER = ord("\r")
OWEN_ASCII = {"0": 0, "1": 2, "2": 4, "3": 6, "4": 8,
"5": 10, "6": 12, "7": 14, "8": 16, "9": 18,
"A": 20, "B": 22, "C": 24, "D": 26, "E": 28,
"F": 30, "G": 32, "H": 34, "I": 36, "J": 38,
"K": 40, "L": 42, "M": 44, "N": 46, "O": 48,
"P": 50, "Q": 52, "R": 54, "S": 56, "T": 58,
"U": 60, "V": 62, "W": 64, "X": 66, "Y": 68,
"Z": 70, "-": 72, "_": 74, "/": 76, " ": 78}
class OwenError(Exception):
pass
class Owen:
"""Класс, описывающий протокол ОВЕН."""
def __init__(self, unit: int, addr_len_8: bool) -> None:
"""Инициализация класса клиента с указанными параметрами."""
self.unit = unit
self.addr_len_8 = addr_len_8
@staticmethod
def fast_calc(value: int, crc: int, bits: int) -> int:
"""Вычисление значения полинома."""
return reduce(lambda crc, i: crc << 1 & 0xFFFF ^ (0x8F57
if (value << i ^ crc >> 8) & 0x80 else 0), range(bits), crc)
def owen_crc16(self, packet: tuple[int, ...]) -> int:
"""Вычисление контрольной суммы."""
return reduce(lambda crc, val: self.fast_calc(val, crc, 8), packet, 0)
def owen_hash(self, packet: tuple[int, ...]) -> int:
"""Вычисление hash-функции."""
return reduce(lambda crc, val: self.fast_calc(val << 1, crc, 7), packet, 0)
@staticmethod
def name2code(name: str) -> tuple[int, ...]:
"""Преобразование локального идентификатора в числовой код."""
code: list[int] = reduce(lambda x, ch: [*x[:-1], x[-1] + 1] if ch == "."
else [*x, OWEN_ASCII[ch]], name.upper(), [])
return (*code, *[OWEN_ASCII[" "]] * (4 - len(code)))
@staticmethod
def encode_frame(frame: tuple[int, ...]) -> bytes:
"""Преобразование пакета из числового вида в строковый."""
chars = ([71 + (num >> 4), 71 + (num & 0xF)] for num in frame)
return bytes([HEADER, *sum(chars, []), FOOTER])
@staticmethod
def decode_frame(frame: bytes) -> tuple[int, ...]:
"""Преобразование пакета из строкового вида в числовой."""
pairs = zip(*[iter(frame[1:-1])] * 2)
return tuple((i - 71 << 4) + (j - 71 & 0xF) for i, j in pairs)
@staticmethod
def pack_value(frmt: str, value: float | str | None) -> bytes:
"""Упаковка данных заданного формата."""
return b"" if value is None else OWEN_TYPE[frmt]["pack"](value)
@staticmethod
def unpack_value(frmt: str, value: bytes, index: int | None) -> float | str:
"""Распаковка данных заданного формата."""
try:
return OWEN_TYPE[frmt]["unpack"](value, index)
except error:
errcode = OWEN_TYPE["U8"]["unpack"](value, index)
msg = f"Device error={errcode:02X}"
raise OwenError(msg) from None
def make_packet(self, flag: int, name: str, index: int | None,
data: bytes) -> bytes:
"""Формирование пакета для записи."""
addr0, addr1 = (self.unit & 0xFF, 0) if self.addr_len_8 else \
(self.unit >> 3 & 0xFF, (self.unit & 0x07) << 5)
if index is not None:
data = bytes([*data, *index.to_bytes(2, "big")])
cmd = self.owen_hash(self.name2code(name))
frame = (addr0, addr1 | flag << 4 | len(data), *cmd.to_bytes(2, "big"), *data)
crc = self.owen_crc16(frame)
packet = self.encode_frame((*frame, *crc.to_bytes(2, "big")))
_logger.debug("Send param: address=%d, flag=%d, size=%d, cmd=%04X, "
"index=%s, data=%s, crc=%04X", self.unit, flag, len(data),
cmd, index, tuple(data), crc)
_logger.debug("Send frame: %r, size=%d", packet, len(packet))
return packet
def parse_response(self, packet: bytes, answer: bytes) -> bytes:
"""Расшифровка прочитанного пакета."""
_logger.debug("Recv frame: %r, size=%d", answer, len(answer))
if not answer or answer[0] != HEADER or answer[-1] != FOOTER:
msg = "Invalid message format"
raise OwenError(msg)
frame = self.decode_frame(answer)
address = frame[0] if self.addr_len_8 else frame[0] << 3 | frame[1] >> 5
flag = frame[1] >> 4 & 1
size = frame[1] & 0xF
cmd, *data, crc = unpack(f">H{size}BH", bytes(frame[2:]))
_logger.debug("Recv param: address=%d, flag=%d, size=%d, cmd=%04X, data=%s, "
"crc=%04X", address, flag, size, cmd, tuple(data), crc)
if self.owen_crc16(frame[:-2]) != crc:
msg = "Checksum error"
raise OwenError(msg)
if address != self.unit:
msg = "Sender and receiver addresses mismatch"
raise OwenError(msg)
if packet[7:9] != answer[7:9]: # hash mismatch
msg = "Network error={:02X}, hash={:02X}{:02X}".format(*data)
raise OwenError(msg)
return bytes(data)
__all__ = ["Owen"]