Сохрани адресацию отдельных регистров GAS 0x3 и выдели слотам тип 0x4 с маской Word0. Добавь единое C-ядро GAS_WATCH2, тонкие Python/JNI-порты и проверки общих эталонных байтов. Совместимость: тип 0x4 больше не обслуживает Modbus Coil. Потребителям PCAN_MSG_MODBUS_COIL и ProtoCanMsgToModbusCoil нужно перейти на GAS_SLOT и отдельный API слотов; регистровый GAS остаётся на 0x3.
80 lines
11 KiB
Python
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.
|