Поддержать компактные записи логгера УМП v4

Разрешить 12-словные записи в запросах RTU и CAN. Распознавать статус v4 и декодировать оставшиеся сигналы с сохранением поддержки архивов v2/v3.
This commit is contained in:
2026-10-12 12:26:37 +03:00
parent bbb4bc2b9d
commit 95c8b8ac7d
3 changed files with 40 additions and 64 deletions

View File

@@ -18,7 +18,8 @@ unsigned ump_validate(unsigned fn, uint16_t a, uint16_t n, int can)
return can ? ((a == UMP_FIRMWARE_INFO && n == UMP_FIRMWARE_INFO_WORDS) ? 0 : 2)
: ((n && n <= UMP_FIRMWARE_INFO + UMP_FIRMWARE_INFO_WORDS - a) ? 0 : 2);
if (can) return ((a == UMP_BASE && n == 16) ||
(a == UMP_DATA && (n == 40 || n == 80 || n == 120 || n == 42 || n == 84)) ||
/* Разрешает целые 12-словные записи v4 до 120 слов; размеры v2/v3 сохраняются для прежних устройств. */
(a == UMP_DATA && (n == 40 || n == 80 || n == 42 || n == 84 || (n && n <= 120 && !(n % 12U)))) ||
(a == UMP_LIVE && n == 124)) ? 0 : 2;
if (!n || n > 125) return 2;
return ((a >= UMP_BASE && a < UMP_BASE+16 && n <= UMP_BASE+16-a) ||

View File

@@ -22,6 +22,12 @@ 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),
@@ -33,68 +39,37 @@ FIELDS = (
('start_out', 'Выход START_DPTB', 5, 0),
('stop_out', 'Выход STOP_DPTB', 5, 1),
('res_out', 'Выход RES_OUT_2', 5, 2),
('impulse', 'Остаток импульса, мс', 6, None),
('wake', 'Инициализация ЦАП, тики ЦАП', 7, None),
('dac', 'Код ЦАП', 8, None),
('current', 'Расчётное задание тока, мА', 9, None),
('sensor0', 'Измерение 0', 10, None),
('sensor1', 'Измерение 1', 11, None),
('sensor2', 'Измерение 2', 12, None),
('sensor3', 'Измерение 3', 13, None),
('error0', 'Ошибки 0 (маска)', 14, None),
('error1', 'Ошибки 1 (маска)', 15, None),
('error2', 'Ошибки 2 (маска)', 16, None),
('error3', 'Ошибки 3 (маска)', 17, None),
('can_header', 'CAN адрес/маска', 18, None),
('can0', 'CAN слово 0', 19, None),
('can1', 'CAN слово 1', 20, None),
('can2', 'CAN слово 2', 21, None),
('can_count', 'Количество CAN RX (mod 65536)', 22, None),
('can_timeout', 'Таймауты CAN TX (mod 65536)', 23, None),
('commands', 'Команды (маска)', 3, None),
('go', 'Начальная уставка, мА', 24, None),
('stop', 'Конечная уставка, мА', 25, None),
('dac_min', 'Код калибровки 4 мА', 26, None),
('dac_max', 'Код калибровки 20 мА', 27, None),
('dac_cal', 'Калибровочный код ЦАП', 28, None),
('dac04', 'DAC_04 (регистр)', 29, None),
('dac20', 'DAC_20 (регистр)', 30, None),
('ramp', 'Счётчик разгона', 31, None),
('duration', 'Длительность разгона, тики ЦАП', 32, None),
('effective_go', 'Начальная уставка после ограничений, мА', 33, None),
('effective_stop', 'Конечная уставка после ограничений, мА', 34, None),
('position', 'Положение разгона (kod)', 35, None),
('adc0', 'АЦП канал 0', 36, None),
('adc1', 'АЦП канал 1', 37, None),
('adc2', 'АЦП канал 2', 38, None),
('adc3', 'АЦП канал 3', 39, 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:
raise ValueError('Недопустимый адрес контроллера или значение')
if record_words not in (40, 42):
raise ValueError('Неверный размер записи логгера')
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 % WORDS == 0 or value in (42, 84))) 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
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) 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'))
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)
@@ -114,27 +89,27 @@ class Status:
@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))):
raise ValueError('Прошивка не поддерживает логгер УМП v2/v3')
(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')
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] > 1000 or words[15] not in (0, 1, 3):
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 (40, 42) or frequency <= 0:
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:
value = words[index] if index < len(words) else float('nan')
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
elif 10 <= index <= 13 and value >= 32768:
value -= 65536
# Физические единицы приводим здесь, одинаково для CSV и графика.
if key in ('current', 'effective_go', 'effective_stop'):
if key == 'current': # Сохранившееся задание имеет масштаб 0,1 мА; уставки и напряжения передаются целыми единицами.
value /= 10.0
result[key] = value
return result

View File

@@ -27,7 +27,7 @@ def build_request(mode, token, function, address, value):
if not 1 <= token <= 255:
raise ValueError('Неверный номер CAN-запроса')
valid = ((function == 3 and ((address == PROTECTION and value == 8) or (address == BASE and value == 16) or
(address == DATA and value in (40, 80, 120, 42, 84)) or
(address == DATA and (value in (40, 80, 42, 84) or 0 < value <= 120 and value % 12 == 0)) or # Читает целые компактные записи и прежние архивы.
(address == LIVE and value == MAX_WORDS) or (address == EMU_CONTROL and value == 4))) or
(function == 6 and ((BASE <= address <= BASE+3 and 0 <= value <= 65535) or
(address == PROTECTION+2 and value in (0, 1, 2, 3)) or