Разрешить 12-словные записи в запросах RTU и CAN. Распознавать статус v4 и декодировать оставшиеся сигналы с сохранением поддержки архивов v2/v3.
122 lines
9.9 KiB
Python
122 lines
9.9 KiB
Python
# Клиент регистратора УМП 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])
|