Files
templates/python/protocan/gas_slots.py
T
Andrey 876b58517c feat(set-protocol): Раздели GAS и добавь пакетный сбор регистров
Сохрани адресацию отдельных регистров GAS 0x3 и выдели слотам тип 0x4 с маской Word0. Добавь единое C-ядро GAS_WATCH2, тонкие Python/JNI-порты и проверки общих эталонных байтов.

Совместимость: тип 0x4 больше не обслуживает Modbus Coil. Потребителям PCAN_MSG_MODBUS_COIL и ProtoCanMsgToModbusCoil нужно перейти на GAS_SLOT и отдельный API слотов; регистровый GAS остаётся на 0x3.
2026-10-23 14:14:48 +03:00

80 lines
11 KiB
Python

import ctypes # Передаём построение ID, проверку маски и порядок октетов единственному C-кодеку.
from .native import get_native_core # Используем штатный поиск библиотеки без резервного Python-кодека.
class GasSlots: # Тип 0x4 содержит Word0 с маской и три значения в фиксированных позициях.
def __init__(self, library=None): # Тест или приложение может передать ту же собранную библиотеку явно.
self.lib = library or get_native_core().lib # Отсутствующее общее ядро проявляется штатной ошибкой загрузки.
self.lib.pcan_gas_slot_request_id.argtypes = [ctypes.c_uint] * 3 # Обёртка PERIPH принимает mode, локальный слот и Route.
self.lib.pcan_gas_slot_request_id.restype = ctypes.c_uint32 # Extended ID возвращается без знакового преобразования.
self.lib.pcan_gas_slot_id.argtypes = [ctypes.c_uint] * 4 # Общий построитель принимает DeviceType, DeviceID, полный адрес и Route.
self.lib.pcan_gas_slot_id.restype = ctypes.c_uint32 # Любой корректный ID типа 0x4 отличен от нулевого маркера ошибки.
self.lib.pcan_gas_slot_encode.argtypes = [ctypes.POINTER(ctypes.c_uint16), ctypes.c_uint, ctypes.POINTER(ctypes.c_uint8)] # Четыре wire-слова включают маску, а не четвёртый регистр.
self.lib.pcan_gas_slot_encode.restype = ctypes.c_int # Кодек возвращает длину 0/8 либо отрицательный отказ.
self.lib.pcan_gas_slot_decode.argtypes = [ctypes.c_uint32, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint8), ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint16)] # PERIPH-декодер дополнительно проверяет принадлежность странице и последний слот.
self.lib.pcan_gas_slot_decode.restype = ctypes.c_int # Результат 0/4 включает маску при наличии данных.
self.lib.pcan_gas_slot_decode_words.argtypes = [ctypes.c_uint32, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint8), ctypes.c_uint, ctypes.POINTER(ctypes.c_uint16)] # Общий декодер поддерживает остальные классы узлов и все 16 бит адреса.
self.lib.pcan_gas_slot_decode_words.restype = ctypes.c_int # Отрицательное значение означает отказ без публикации частичного результата.
@staticmethod # Проверка представимости Python-чисел не дублирует wire-логику.
def _unsigned(value, maximum): # Проверяем границы до возможного сужения ctypes.
if type(value) is not int or not 0 <= value <= maximum: # bool, дробь и отрицательное число не являются полями протокола.
raise ValueError('Недопустимое целое значение протокола') # Не отправляем усечённое значение к другому адресу.
def _payload(self, words, route): # Общая упаковка используется адресным и PERIPH-построителями.
values = None if words is None else tuple(words) # Фиксируем входной iterable до преобразования к C-массиву.
if values is not None: # Запись или ответ содержат ровно маску и три значения.
if len(values) != 4: # Позиции не сдвигаются при частичной записи.
raise ValueError('Нужны Word0-маска и три значения Word1…Word3') # Не дополняем отсутствующие позиции нулями неявно.
for value in values: # Все wire-слова имеют одинаковую ширину 16 бит.
self._unsigned(value, 0xFFFF) # Корректные ноль и 65535 остаются допустимыми значениями.
if values is None and route != 0: # Пустой ответ прибора не имеет определённого смысла.
raise ValueError('Ответ должен содержать маску и значения') # Отказ возникает до отправки в транспорт.
source = None if values is None else (ctypes.c_uint16 * 4)(*values) # Маску и значения проверяет C до кодирования.
data = (ctypes.c_uint8 * 8)() # Буфер ограничен одним classic CAN кадром.
length = self.lib.pcan_gas_slot_encode(source, 0 if values is None else 4, data) # Единственный кодек проверяет разрешённые биты маски и порядок октетов.
if length < 0: # Неверная маска не заменяется другой командой.
raise ValueError('Неверная маска GAS SLOT: допустимы непустые биты 1…3') # Бит 0 относится к служебному слову и запрещён.
return bytes(data[:length]) # Возвращаем только готовые wire-октеты.
def frame(self, mode, slot, words=None, route=0): # Удобная обёртка для 200 регистров каждого mode PERIPH.
for value in (mode, slot, route): # Исключаем переполнение host ABI до вызова C.
self._unsigned(value, 0xFFFFFFFF) # Семантические пределы mode и local проверяет общий построитель.
can_id = self.lib.pcan_gas_slot_request_id(mode, slot, route) # C формирует MsgType=0x4 и HEX-страницу mode.
if can_id == 0: # Не создаём адрес для резерва страницы или неверного направления.
raise ValueError('Неверный mode, слот или направление') # Исходная ошибка не превращается в обращение к соседнему прибору.
payload = self._payload(words, route) # Данные одинаковы для обоих адресных API.
self.decode(can_id, payload) # Дополнительно проверяем маску двух действующих значений последнего слота PERIPH.
return can_id, payload # Транспорт получает готовые CAN ID и данные.
def frame_address(self, device_type, device_id, address, words=None, route=0): # Полный слотный API для любого узла, включая STM Invertor.
for value in (device_type, device_id, address, route): # Python-числа не должны оборачиваться в C unsigned.
self._unsigned(value, 0xFFFFFFFF) # Конкретную ширину полей идентификатора проверяет C.
can_id = self.lib.pcan_gas_slot_id(device_type, device_id, address, route) # Общий построитель сохраняет все адреса 0…65535.
if can_id == 0: # Нулевой результат означает ошибку адреса узла или слота.
raise ValueError('Неверный адрес узла, слота или направление') # Не скрываем ошибку входа маскированием.
return can_id, self._payload(words, route) # Не привязываем общий формат к странице PERIPH.
def _input(self, can_id, data, flags): # Проверяем размеры и представимость до создания ctypes-буферов.
self._unsigned(can_id, 0xFFFFFFFF) # C затем проверит фактические 29 бит CAN ID.
self._unsigned(flags, 0xFFFFFFFF) # Флаги IDE/RTR/ERR проверяются общим wire-кодеком.
payload = bytes(data) # Сохраняем точную длину без усечения.
if len(payload) > 8: # CAN FD не включается неявно.
raise ValueError('Classic CAN допускает до восьми октетов') # Лишние октеты остаются видимой ошибкой.
return (ctypes.c_uint8 * len(payload)).from_buffer_copy(payload), len(payload) # Нулевой запрос не требует фиктивных данных.
def decode(self, can_id, data, flags=1): # Возвращаем mode, локальный слот и wire-слова PERIPH с маской.
raw, length = self._input(can_id, data, flags) # Получаем проверенный ограниченный буфер.
mode, slot, words = ctypes.c_uint(), ctypes.c_uint(), (ctypes.c_uint16 * 4)() # Отдельные выходы сохраняют ширину host ABI.
count = self.lib.pcan_gas_slot_decode(can_id, flags, raw, length, ctypes.byref(mode), ctypes.byref(slot), words) # C проверяет полный формат и геометрию PERIPH.
if count < 0: # Чужая страница или несуществующая позиция не становятся регистрами другого профиля.
raise ValueError('Неверный кадр GAS SLOT PERIPH') # Никакого автоматического перехода к типу 0x3.
return mode.value, slot.value, tuple(words[:count]) # Пустой кортеж означает запрос чтения.
def decode_address(self, can_id, data, flags=1): # Декодируем маску и три значения любого адресуемого узла.
raw, length = self._input(can_id, data, flags) # Проверяем точную длину до native-вызова.
words = (ctypes.c_uint16 * 4)() # Четыре wire-слова включают служебную маску.
count = self.lib.pcan_gas_slot_decode_words(can_id, flags, raw, length, words) # Общий C-кодек проверяет тип 0x4 и все транспортные условия.
if count < 0: # Ошибка не выдаётся за допустимый пустой слот.
raise ValueError('Неверный кадр GAS SLOT') # Сообщение пригодно для CAN-журнала.
return tuple(words[:count]) # Адрес и класс узла уже доступны вызывающему коду в CAN ID.