Files
templates/python/set_devices/ump_logger.py
Andrey 95c8b8ac7d Поддержать компактные записи логгера УМП v4
Разрешить 12-словные записи в запросах RTU и CAN. Распознавать статус v4 и декодировать оставшиеся сигналы с сохранением поддержки архивов v2/v3.
2026-10-12 12:26:37 +03:00

122 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Клиент регистратора УМП v2 с выбором аналоговых каналов и чтением снимка. Индексы каналов и
# регистровые адреса разделены; отображение результата не должно менять порядок слов принятого
# снимка.
"""Протокол кольцевого логгера УМП v2.
Технологические регистры 0..127 не изменены. Расширение 0x1000 сначала
проверяется по сигнатуре и версии: старая прошивка игнорировала старший
байт адреса, поэтому диагностическое чтение безопасно попадёт в 0..15,
но управляющие записи до успешной проверки запрещены в панели.
CRC и проверка ответов остаются в общей библиотеке протокола ПМ35.
"""
from dataclasses import dataclass
from protocan import periph28335 as protocol
BASE = 0x1000
PROTECTION = 0x1300 # Balsam 167: versioned voltage protection/test block.
DATA = 0x1010
LIVE = 0x1100
EMU_CONTROL = 0x1200
FAST_CAPABILITY = 0x1220
FAST_DATA = 0x2000
WORDS = 40
CAPACITY = 64000 // (2 * WORDS)
# Компактная запись v4 имеет 12 слов; старые 40/42 слова остаются поддержанными для чтения архивов.
COMPACT_WORDS = 12
# Ёмкость 760 точек сохраняет прежнее число записей постинтервала и поведение кольцевых банков.
COMPACT_CAPACITY = 760
# Ключи соответствуют индексам оставшихся полей v2/v3, значения — индексам этих же каналов в компактном v4.
COMPACT_INDEX = {2: 2, 3: 3, 4: 4, 5: 5, 9: 6, 24: 7, 25: 8, 40: 9, 41: 10}
FIELDS = (
('off_trigger', 'Событие отключения', 2, 8),
('post_truncated', 'Усечение постинтервала', 2, 9),
('start', 'Команда пуск УМП', 3, 7),
('reset', 'Команда сброс УМП', 3, 6),
('omega', 'Вход OMEGA', 4, 0),
('start_in', 'Вход Start_1', 4, 1),
('res_in', 'Вход Res_in', 4, 2),
('start_out', 'Выход START_DPTB', 5, 0),
('stop_out', 'Выход STOP_DPTB', 5, 1),
('res_out', 'Выход RES_OUT_2', 5, 2),
('current', 'Расчётное задание тока, мА', 9, None),
('go', 'Начальная уставка, мА', 24, None),
('stop', 'Конечная уставка, мА', 25, None),
('zpt1', 'ЗПТ1: U1 + U2 (ПМ67, рег. 306)', 40, None),
('zpt2', 'ЗПТ2: U3 + U4 (ПМ67, рег. 9)', 41, None),
)
def request(controller: int, function: int, address: int, value: int, *, record_words: int = WORDS) -> bytes: # Проверяет запрос с учётом ширины записи, прочитанной из статуса платы.
if not 0 <= controller <= 255 or not 0 <= value <= 65535: # Ограничивает адрес одним октетом, значение — 16-битным словом.
raise ValueError('Недопустимый адрес контроллера или значение') # Запрещает усечение входных параметров при упаковке.
if record_words not in (COMPACT_WORDS, 40, 42): # Допускает только известные компактный и прежние форматы записи.
raise ValueError('Неверный размер записи логгера') # Не позволяет строить адреса по неизвестной ширине.
capacity = {COMPACT_WORDS: COMPACT_CAPACITY, 40: 800, 42: 760}[record_words] # Выбирает реальную границу банка по формату.
if function == 3: # Разрешает только существующие диагностические блоки чтения.
valid = ((address == PROTECTION and value == 8) or (address == BASE and value == 16) # Читает полный блок защиты либо дескриптор журнала.
or (address == DATA and 1 <= value <= 120 and (value % COMPACT_WORDS == 0 or value in (40, 80, 42, 84))) # Принимает целые записи v4 и исторические блоки v2/v3.
or (address == LIVE and value == 124) or (address == EMU_CONTROL and value == 4) # Сохраняет фиксированные размеры онлайн-блока и управления эмуляцией.
or (address == FAST_CAPABILITY and value == 4) # Проверяет возможность ускоренного архивного чтения.
or (FAST_DATA <= address < FAST_DATA + capacity * record_words # Ограничивает адрес началом и концом реального банка.
and (address - FAST_DATA) % record_words == 0 # Требует начала блока на границе записи.
and 1 <= value <= 120 and value % record_words == 0 # Передаёт до 120 слов, содержащих целое число записей.
and address + value <= FAST_DATA + capacity * record_words)) # Исключает пересечение конца банка.
else: # Запись разрешена только прежним управляющим адресам.
valid = function == 6 and ((address == PROTECTION + 2 and value in (0, 1, 2, 3)) # Проверяет маску эмуляции защиты.
or (address == PROTECTION + 3 and 1 <= value <= 60000) # Проверяет тайм-аут свежести CAN в миллисекундах.
or address in (BASE, BASE + 1, BASE + 2, BASE + 3) # Оставляет команды управления журналом без запуска УМП.
or (address == EMU_CONTROL + 2 and value in (0, 1))) # Сохраняет двоичное включение эмулятора.
if not valid: # Запрещает запросы вне согласованных адресов и размеров.
raise ValueError('Недопустимый запрос логгера УМП') # Возвращает прежнее понятное сообщение вызывающему коду.
return protocol.with_crc(bytes((controller, function)) + address.to_bytes(2, 'big') + value.to_bytes(2, 'big')) # Передаёт 16-битный адрес и добавляет общий Modbus CRC.
@dataclass(frozen=True)
class Status:
state: int
count: int
period: int
mode: int
frequency: int
generation: int
archive_count: int
post_ms: int
live_state: int
words: int = 40
capacity: int = 800
@classmethod
def decode(cls, words):
if (len(words) != 16 or words[0] != 0x554D or
(words[1], words[4], words[5]) not in ((2, 40, 800), (3, 42, 760), (4, COMPACT_WORDS, COMPACT_CAPACITY))): # Проверяет согласованные версию, ширину и ёмкость.
raise ValueError('Прошивка не поддерживает логгер УМП v2/v3/v4') # Перечисляет известные форматы без разрешения неизвестной прошивки.
if words[9] != 6:
raise ValueError('Логгер УМП доступен только на плате нагрузки dsk_LOAD')
post_limit = max(1000, min(65535, words[5] // 10 * words[6])) if words[1] >= 3 else 1000 # Постинтервал v3/v4 занимает 10% точек; при 760 точках и 20 мс его оценка достигает 1520 мс.
if words[2] not in (0, 1, 2, 3) or words[3] > words[5] or not 1 <= words[6] <= 1000 or words[11] != 1000 or words[13] > words[5] or words[14] > post_limit or words[15] not in (0, 1, 3):
raise ValueError('Повреждён статус логгера')
return cls(words[2], words[3], words[6], words[10], words[11], *words[12:16], words[4], words[5])
def sample(words, frequency=1000):
if len(words) not in (COMPACT_WORDS, 40, 42) or frequency <= 0: # Допускает только известную ширину записи и положительную частоту времени.
raise ValueError('Неверная запись логгера')
result = {'time_ms': (words[0] | words[1] << 16) * 1000 / frequency, 'event': words[2]}
for key, _label, index, bit in FIELDS:
physical_index = COMPACT_INDEX[index] if len(words) == COMPACT_WORDS else index # Выбирает индекс оставшегося поля по фактической версии записи.
value = words[physical_index] if physical_index < len(words) else float('nan') # ЗПТ в старых 40-словных архивах остаётся неизвестным.
if bit is not None:
value = (value >> bit) & 1
# Физические единицы приводим здесь, одинаково для CSV и графика.
if key == 'current': # Сохранившееся задание имеет масштаб 0,1 мА; уставки и напряжения передаются целыми единицами.
value /= 10.0
result[key] = value
return result
def emulator_enabled(words):
if len(words) != 4 or tuple(words[:2]) != (0x454D, 1) or words[2] not in (0, 1):
raise ValueError('Плата не поддерживает управление эмуляцией STM')
return bool(words[2])