diff --git a/c/set-protocol/src/ump_protocol.c b/c/set-protocol/src/ump_protocol.c index 0aaf7e9..e4e3cc2 100644 --- a/c/set-protocol/src/ump_protocol.c +++ b/c/set-protocol/src/ump_protocol.c @@ -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) || diff --git a/python/set_devices/ump_logger.py b/python/set_devices/ump_logger.py index def1a43..dd5e312 100644 --- a/python/set_devices/ump_logger.py +++ b/python/set_devices/ump_logger.py @@ -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 diff --git a/python/set_devices/ump_logger_can.py b/python/set_devices/ump_logger_can.py index 6e23c5e..902de20 100644 --- a/python/set_devices/ump_logger_can.py +++ b/python/set_devices/ump_logger_can.py @@ -27,7 +27,8 @@ 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 + # До 120 слов передают целые 12-словные записи v4; старые блоки v2/v3 остаются разрешены. + (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