From 07929c880a2eed774bfffe864d34454d0a6a7708 Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 12 Nov 2026 15:53:12 +0300 Subject: [PATCH] =?UTF-8?q?feat(set-protocol):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D1=8C=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8C=20CAN?= =?UTF-8?q?=20=D0=A3=D0=9A=D0=A1=D0=92=D0=AD=D0=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- c/set-protocol/CMakeLists.txt | 1 + c/set-protocol/README.md | 7 ++ c/set-protocol/docs/UKSVEP_CAN.md | 64 +++++++++++ c/set-protocol/include/balsam_can.h | 3 + c/set-protocol/include/uksvep_can.h | 23 ++++ c/set-protocol/src/balsam_can.c | 44 ++++++-- c/set-protocol/src/uksvep_can.c | 143 +++++++++++++++++++++++++ c/set-protocol/tests/test_uksvep_can.c | 58 ++++++++++ c/set-protocol/tools/build_host.py | 1 + python/protocan/uksvep.py | 110 +++++++++++++++++++ python/tests/test_uksvep_can.py | 37 +++++++ 11 files changed, 482 insertions(+), 9 deletions(-) create mode 100644 c/set-protocol/docs/UKSVEP_CAN.md create mode 100644 c/set-protocol/include/uksvep_can.h create mode 100644 c/set-protocol/src/uksvep_can.c create mode 100644 c/set-protocol/tests/test_uksvep_can.c create mode 100644 python/protocan/uksvep.py create mode 100644 python/tests/test_uksvep_can.py diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index a775d46..2e1021c 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -28,6 +28,7 @@ set(SETPROTOCOL_V2_SOURCES # Совместимые ProtoCAN/SETGUI v1 форматы переходного периода. set(SETPROTOCOL_LEGACY_SOURCES + src/uksvep_can.c # Общий профиль STM32 UKSVEP: маршруты, payload и карта дискретов. src/balsam_can.c src/set_crc.c src/periph28335.c diff --git a/c/set-protocol/README.md b/c/set-protocol/README.md index 5f4a3da..5d4de66 100644 --- a/c/set-protocol/README.md +++ b/c/set-protocol/README.md @@ -131,3 +131,10 @@ FFI-клиенты подключают `setprotocol_abi.h`. Имена функ ## GAS: регистры 0x3 и слоты 0x4 [Адресация GAS](docs/GAS_SLOT.md): `0x3` — отдельные 16-битные регистры `0…65535`, запись 1…4 слов. `0x4` — слот с Word0-маской и тремя значениями; PERIPH сохраняет 200 слов в 67 слотах `0xMM00…0xMM42`. Формат определяется MsgType, без настройки профиля. + +## УКСВЭП STM32F103RC + +Общий профиль `uksvep_can.h/.c` предоставляет карту дискретов и параметров, +маршруты проекта «Ледокол», C-кодирование регистрового CAN и расчёт состояний +для эмулятора SETGUI. Контракт ABI, единицы и проверки: +[UKSVEP_CAN.md](docs/UKSVEP_CAN.md). diff --git a/c/set-protocol/docs/UKSVEP_CAN.md b/c/set-protocol/docs/UKSVEP_CAN.md new file mode 100644 index 0000000..25c8131 --- /dev/null +++ b/c/set-protocol/docs/UKSVEP_CAN.md @@ -0,0 +1,64 @@ +# UKSVEP STM32F103RC: регистровый CAN + +Профиль относится к `UKSVEP_ICE_22220.6`: `Core/Src/can.c`, +`Core/Inc/package.h`, `Core/Src/message.c`, `Core/Src/lampa.c` и `main.h`. +Он подключён в SETGUI как отдельный эмулятор проекта «Ледокол». + +Classic CAN, extended 29-битный ID, DLC=8, 250000 бит/с. +DeviceID 0…15 задают J0…J3; `Mode=DeviceID+1`. +Данные ВЭП: `0x001CE000 | DeviceID`; команды ВЭП: +`0x001CE020 | DeviceID`. Это регистровое приложение, а не ProtoCAN Boot Type5. +Broadcast из старого приложения не выдаётся этим клиентом за адресный ответ. + +Payload общий с историческим `balsam_can`: word2, word3, +адрес/маска, word1, по два байта big endian. В заголовке биты15…13 задают +присутствие слов, биты12…0 — начальный адрес. `uksvep_can` ограничивает +банк R0…R127; отсутствующие слова не должны обнулять предыдущее состояние. +Маршрутизация других Balsam-плат остаётся в их собственном профиле. + +| Регистры | Назначение | +|---|---| +| 0 / 1 / 2 | Входы / неисправности / аварии | +| 8 / 9 | Маски неисправностей / аварий | +| 23 | Перемычки; младший полубайт соответствует DeviceID | +| 48 | Sleep_time, секунды | +| 100 | Brightness, 0…10 тактов ШИМ | +| 101 / 102 | Cancount, быстрый / медленный период по 10 мс | +| 125 / 126 / 127 | Версия протокола / Mode / команды | + +Назначения входов, GPIO, команд и значения по умолчанию доступны через +`uksvep_field_*`, `uksvep_input_name`, `uksvep_command_name`. +`uksvep_evaluate_inputs` моделирует ReadEnteres: R0.7 никогда не является +неисправностью/аварией; R0.8 всегда является неисправностью и становится +аварией по Error_mask.8; R0.9 всегда является неисправностью и аварией. +Вход R0.9/PB12 читается без инверсии, остальные назначенные +сигналы собираются в соответствии с физической картой STM32-проекта. + +## Слои + +`SETGUI → protocan/uksvep.py (ctypes) → uksvep_can.c → общий payload balsam_can.c`. +Ни GPIO, ни Qt, ни часы ОС общему C-профилю не нужны; динамическая память +не используется. Вычисления и wire не дублируются в Python. + +Все выходные буферы принадлежат caller и имеют явную capacity: + +- `uksvep_can_encode`: минимум8 байтов, возвращает8 либо0; +- `uksvep_can_decode`: минимум8 слов uint16 — направление, количество, + затем максимум3 пары адрес/значение; возвращает число пар,0 для чужого ID, + −1 для неверных аргументов/своего повреждённого кадра; +- `uksvep_field_info`: минимум4 слова uint16 — адрес, максимум, default, flags; +- `uksvep_evaluate_inputs`: минимум2 слова uint16 — неисправности и аварии. + +Флаги полей: PARAM=1, COMMAND=2, STATE=4. GUI использует их для выбора +допустимых редакторов роли. Имена возвращаются постоянными строками UTF-8; +владение памятью клиенту не передаётся. Для применения RX клиент дополнительно +отбрасывает STD/RTR и собственное направление/эхо. Физические уровни +LED0…LED3 и PVT по текущему регистровому CAN не передаются; интерфейс не +должен представлять расчётные лампы как измерение этих GPIO. + +## Проверки + +`test_uksvep_can` фиксирует независимые wire-эталоны, все16 адресов, +малый выходной буфер, границы R127, sparse101, неверный DLC, чужие профили, +особые входы и регрессию прежнего decoder Бальзам. Python-тесты используют +ту же DLL, GUI-тесты подменяют только аппаратный драйвер. diff --git a/c/set-protocol/include/balsam_can.h b/c/set-protocol/include/balsam_can.h index 9d32331..8bb55e7 100644 --- a/c/set-protocol/include/balsam_can.h +++ b/c/set-protocol/include/balsam_can.h @@ -43,6 +43,9 @@ typedef struct { uint16_t values[BALSAM_CAN_REGISTER_COUNT]; } balsam_can_frame_t; +size_t balsam_can_encode_payload(uint16_t address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity); /* Общий payload word2/word3/адрес/word1 для исторических регистровых CAN-профилей. */ +int balsam_can_decode_payload(const uint8_t *data, size_t size, balsam_can_frame_t *output); /* Разбирает только три слова и маску, оставляя маршрутизацию конкретному профилю. */ + /** Return non-zero for the command/data/terminal IDs used by CAN_Bal_2812. */ int balsam_can_is_id(uint32_t can_id); diff --git a/c/set-protocol/include/uksvep_can.h b/c/set-protocol/include/uksvep_can.h new file mode 100644 index 0000000..5f7e332 --- /dev/null +++ b/c/set-protocol/include/uksvep_can.h @@ -0,0 +1,23 @@ +#ifndef UKSVEP_CAN_H /* Защищает общий профиль ВЭП от повторного объявления. */ +#define UKSVEP_CAN_H /* Карта соответствует STM32-проекту UKSVEP_ICE_22220.6, а не ВЭП Бальзам. */ +#include "pcan_abi.h" /* Даёт экспортируемый C ABI для Python и других клиентов templates. */ +#ifdef __cplusplus /* Позволяет использовать тот же профиль из C++ и JNI. */ +extern "C" { /* Сохраняет стабильные имена экспортов без C++ mangling. */ +#endif /* Завершает выбор языка связывания. */ +#define UKSVEP_REGISTER_COUNT 128U /* message.h выделяет Modbus_LEN=0x80 слов uint16. */ +#define UKSVEP_FIELD_PARAM 1U /* Поле допускает запись параметра в реальный ВЭП. */ +#define UKSVEP_FIELD_COMMAND 2U /* Поле R127 содержит битовые команды приложения. */ +#define UKSVEP_FIELD_STATE 4U /* Поле состояния доступно для редактирования только в роли эмулируемого ВЭП. */ +PCAN_ABI_API uint32_t uksvep_can_id(unsigned device, unsigned from_device); /* Адрес 0…15: данные 0x1CE000+ID, команды 0x1CE020+ID; UINT32_MAX означает отказ. */ +PCAN_ABI_API size_t uksvep_can_encode(unsigned device, unsigned from_device, unsigned address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity); /* Строит DLC=8 для одного–трёх слов; capacity задаётся в байтах, требуется минимум 8. */ +PCAN_ABI_API int uksvep_can_decode(unsigned device, uint32_t id, const uint8_t *data, size_t size, uint16_t *output, size_t capacity); /* output: направление, число, три пары адрес/значение; capacity в словах uint16, минимум 8; 0=чужой ID, -1=ошибка. */ +PCAN_ABI_API unsigned uksvep_field_count(void); /* Число подтверждённых package.h полей, без выдуманных регистров. */ +PCAN_ABI_API int uksvep_field_info(unsigned index, unsigned device, uint16_t *output, size_t capacity); /* output[4]: адрес, максимум, начальное значение, флаги роли; capacity задаётся в словах uint16. */ +PCAN_ABI_API const char *uksvep_field_name(unsigned index); /* Возвращает постоянное имя UTF-8 без передачи владения памятью. */ +PCAN_ABI_API const char *uksvep_input_name(unsigned bit); /* Возвращает назначение и физический GPIO каждого из четырнадцати входов. */ +PCAN_ABI_API const char *uksvep_command_name(unsigned bit); /* Описывает только команды package.h; пустая строка означает резерв. */ +PCAN_ABI_API void uksvep_evaluate_inputs(uint16_t inputs, uint16_t alarm_mask, uint16_t error_mask, uint16_t *output, size_t capacity); /* Вычисляет R1/R2 по ReadEnteres; capacity задаётся в словах uint16, требуется минимум 2. */ +#ifdef __cplusplus /* Закрывает внешнее C-связывание для C++. */ +} /* Все клиенты вызывают одну реализацию протокола. */ +#endif /* Завершает совместимость с C++. */ +#endif /* Завершает публичный профиль UKSVEP. */ diff --git a/c/set-protocol/src/balsam_can.c b/c/set-protocol/src/balsam_can.c index 0e46c42..7afff8e 100644 --- a/c/set-protocol/src/balsam_can.c +++ b/c/set-protocol/src/balsam_can.c @@ -48,11 +48,44 @@ int balsam_can_is_id(uint32_t can_id) && relative < BALSAM_CAN_DATA_OFFSET + BALSAM_CAN_NODE_COUNT); } +size_t balsam_can_encode_payload(uint16_t address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity) /* Общий регистровый payload не зависит от выбранной платы. */ +{ /* Формат совпадает у Бальзам и UKSVEP, но адреса CAN определяют разные профили. */ + size_t index; /* Перебирает только присутствующие полезные слова. */ + uint16_t header; /* Хранит трёхбитную маску и 13-битный адрес регистра. */ + if (capacity < 8U || address > 0x1FFFU || values == NULL || output == NULL || count == 0U || count > 3U) { /* Проверяет геометрию payload до изменения выходных байтов. */ + return 0U; /* Отказ не создаёт частично готового кадра. */ + } /* Конкретный профиль дополнительно ограничивает размер своего банка. */ + memset(output, 0, 8U); /* Отсутствующие слова получают нулевое заполнение без бита присутствия. */ + header = (uint16_t)(address | (((7U << (3U - count)) & 7U) << 13U)); /* Одно/два/три слова дают маски 100/110/111 в битах 15…13. */ + output[4] = (uint8_t)(header >> 8U); /* Старший байт адреса/маски передаётся первым. */ + output[5] = (uint8_t)header; /* Младший байт завершает big-endian заголовок. */ + for (index = 0U; index < count; ++index) { /* eCAN передаёт word2 и word3 перед word1. */ + size_t offset = index == 0U ? 6U : (index - 1U) * 2U; /* Первое слово находится в байтах 6/7, следующие — в 0/1 и 2/3. */ + output[offset] = (uint8_t)(values[index] >> 8U); /* Записывает старшие восемь бит без зависимости от endian хоста. */ + output[offset + 1U] = (uint8_t)values[index]; /* Записывает младшие восемь бит того же слова. */ + } /* Порядок слов реализован в одном C-модуле для всех потребителей. */ + return 8U; /* Исторический регистровый кадр всегда имеет DLC=8. */ +} /* Завершает общий кодировщик трёх регистров. */ + +int balsam_can_decode_payload(const uint8_t *data, size_t size, balsam_can_frame_t *output) /* Отделяет общий payload от профиля CAN ID. */ +{ /* Маршрут и номер устройства заполняет вызывающий профиль. */ + uint16_t header; /* Получает маску присутствия и начальный адрес слова. */ + if (data == NULL || output == NULL || size != BALSAM_CAN_DLC) { /* Не читает неполные либо отсутствующие восемь байтов. */ + return 0; /* Отказ не изменяет ранее принятые значения клиента. */ + } /* Сохраняет исходный порядок декодирования Balsam без второго codec. */ + header = get_be16(&data[4]); /* Адрес находится в третьем 16-битном слове на проводе. */ + output->present_mask = (uint8_t)((header >> 13U) & 0x07U); /* Три старших бита разрешают отдельные регистры. */ + output->start_address = (uint16_t)(header & 0x1FFFU); /* Остальные 13 бит задают начальный адрес. */ + output->values[0] = get_be16(&data[6]); /* Первое логическое слово передаётся последним. */ + output->values[1] = get_be16(&data[0]); /* Второе логическое слово передаётся первым. */ + output->values[2] = get_be16(&data[2]); /* Третье логическое слово передаётся вторым. */ + return 1; /* Все три значения разобраны; применять можно только присутствующие. */ +} /* Завершает общий разбор payload без назначения платы. */ + int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size, balsam_can_frame_t *output) { uint32_t relative; - uint16_t header; if ((data == NULL) || (output == NULL)) return -1; can_id &= 0x1FFFFFFFUL; if (!balsam_can_is_id(can_id)) return 0; @@ -67,14 +100,7 @@ int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size, output->direction = can_id == 0x00745019UL ? BALSAM_CAN_FROM_NODE : BALSAM_CAN_TO_NODE; } - /* eCAN sends MDL before MDH: value2, value3, address/mask, value1. */ - header = get_be16(&data[4]); - output->present_mask = (uint8_t)((header >> 13) & 0x07U); - output->start_address = (uint16_t)(header & 0x1FFFU); - output->values[0] = get_be16(&data[6]); - output->values[1] = get_be16(&data[0]); - output->values[2] = get_be16(&data[2]); - return 1; + return balsam_can_decode_payload(data, size, output); /* Переиспользует тот же payload decoder, сохраняя прежние проверки и маршрутизацию Бальзам. */ } const char *balsam_can_device_name(uint8_t device) diff --git a/c/set-protocol/src/uksvep_can.c b/c/set-protocol/src/uksvep_can.c new file mode 100644 index 0000000..8723a37 --- /dev/null +++ b/c/set-protocol/src/uksvep_can.c @@ -0,0 +1,143 @@ +#include "uksvep_can.h" /* Объявляет общий формат, метаданные и ABI профиля UKSVEP. */ +#include "balsam_can.h" /* Повторно использует исторический порядок трёх слов без второго кодека. */ +#include /* Обнуляет кадр до заполнения присутствующих слов. */ + +typedef struct { /* Хранит подтверждённые параметры регистра из package.h/message.c. */ + uint16_t address; /* Индекс слова в modbus[128], без смещения Modbus 40001. */ + uint16_t maximum; /* Верхняя граница ввода в исходных единицах прошивки. */ + uint16_t initial; /* Значение для предпросмотра, не считается принятым от платы. */ + uint16_t flags; /* Отделяет параметры, команды и эмулируемое состояние. */ + const char *name; /* Постоянная подпись UTF-8 для всех потребителей профиля. */ +} field_t; /* Структура остаётся внутренней: через ABI передаются скаляры. */ + +static const field_t fields[] = { /* Источник адресов — Core/Inc/package.h текущего STM32-проекта. */ + {0U, 65535U, 0x247FU, UKSVEP_FIELD_STATE, "Дискретные входы"}, /* R0: нейтраль включает исправные питания, 380 В, ЛСУ и СКК; аварийные биты 8/9 выключены. */ + {1U, 65535U, 0U, UKSVEP_FIELD_STATE, "Неисправности"}, /* R1 вычисляется из входов и Alarm_mask, а не является прямым аппаратным входом. */ + {2U, 65535U, 0U, UKSVEP_FIELD_STATE, "Аварии"}, /* R2 вычисляется из входов и Error_mask. */ + {8U, 65535U, 0x277FU, UKSVEP_FIELD_PARAM, "Маска неисправностей"}, /* Default_params: 0x3FFF без битов 7, 11 и 12. */ + {9U, 65535U, 0x0200U, UKSVEP_FIELD_PARAM, "Маска аварий"}, /* По умолчанию выбран бит 9, авария сети 24 В. */ + {23U, 65535U, 0U, UKSVEP_FIELD_STATE, "Перемычки J0…J3 / TEST"}, /* Jumpers=R0x17, младший байт задаёт физические перемычки. */ + {48U, 65535U, 60U, UKSVEP_FIELD_PARAM, "Время до отключения, с"}, /* Millisecond умножает Sleep_time на 1000 мс. */ + {100U, 10U, 9U, UKSVEP_FIELD_PARAM, "Яркость лампы, 0…10"}, /* ШИМ использует десять отсчётов, штатное значение Brightness=9. */ + {101U, 65535U, 20U, UKSVEP_FIELD_PARAM, "Быстрый CAN, ×10 мс"}, /* Cancount[0]=20 соответствует 200 мс между пакетами. */ + {102U, 65535U, 200U, UKSVEP_FIELD_PARAM, "Медленный CAN, ×10 мс"}, /* Cancount[1]=200 соответствует 2000 мс между пакетами. */ + {125U, 65535U, 1U, UKSVEP_FIELD_STATE, "Версия прикладного протокола"}, /* PROTOKOL=1 относится к регистровому приложению, не к ProtoCAN Boot. */ + {126U, 65535U, 1U, UKSVEP_FIELD_STATE, "Mode = DeviceID + 1"}, /* При запросе метаданных значение уточняется по выбранному адресу. */ + {127U, 65535U, 0U, UKSVEP_FIELD_COMMAND, "Команды"} /* cTestLamp/cDefParam/cSaveParam/cLoadParam/cExtLamp/cExtLite/cReset. */ +}; /* Все адреса и значения по умолчанию определены в одном общем модуле. */ + +static const char *const inputs[] = { /* Порядок битов повторяет ReadEnteres в Core/Src/lampa.c. */ + "ИП1 · контроллер · PC13 · активный 0", /* Бит 0 читает инверсию IN_06. */ + "ИП2 · периферия · PC15 · активный 0", /* Бит 1 читает инверсию IN_05. */ + "ИП3 · замки / освещение · PA0 · активный 0", /* Бит 2 читает инверсию IN_04. */ + "ИП4 · датчики + · PA1 · активный 0", /* Бит 3 читает инверсию IN_03. */ + "ИП5 · датчики − · PA2 · активный 0", /* Бит 4 читает инверсию IN_02. */ + "ИП6 · драйверы · PA3 · активный 0", /* Бит 5 читает инверсию IN_01. */ + "Три фазы 380 В · PB15 · активный 0", /* Бит 6 читает инверсию IN_07. */ + "Заряд накопителя · PB14 · активный 0", /* Бит 7 не становится неисправностью либо аварией. */ + "Разряд накопителя · PB13 · активный 0", /* Бит 8 всегда становится неисправностью при единице. */ + "Авария 24 В · PB12 · активный 1", /* Бит 9 единственный читается без инверсии IN_10. */ + "Питание ЛСУ · PB1 · активный 0", /* Бит 10 читает инверсию IN_11. */ + "Резерв 11 · PB0 · активный 0", /* Бит 11 читает инверсию IN_12. */ + "Резерв 12 · PA5 · активный 0", /* Бит 12 читает инверсию IN_13. */ + "Питание СКК · PA4 · активный 0" /* Бит 13 читает инверсию IN_14. */ +}; /* Логическая единица здесь означает установленный бит R0, а не высокий GPIO. */ + +static const char *const commands[16] = { /* Имена битов берутся из package.h, без команд чужого задатчика. */ + "Тест ламп", "Параметры по умолчанию", "Сохранить параметры", "Загрузить параметры", /* Биты 0…3 действительно обслуживаются main. */ + "", "", "ExtLamp · флаг", "ExtLite · флаг", /* Биты 6/7 объявлены, но текущая логика лампы не использует их как отдельные команды. */ + "", "", "", "", "", "", "", "Перезагрузка" /* Бит 15 запрещает продление IWDG и вызывает аппаратный reset. */ +}; /* Пустые имена не создают ложных кнопок для резервных битов. */ + +uint32_t uksvep_can_id(unsigned device, unsigned from_device) /* Повторяет Setup_CAN_addr без связи с адресом boot Type=5. */ +{ /* Весь DeviceID ограничен четырьмя перемычками платы. */ + if (device > 15U || from_device > 1U) { /* Не обрезает ошибочный номер до адреса другого устройства. */ + return UINT32_MAX; /* Явный недопустимый ID исключает скрытую передачу. */ + } /* Направление 1 означает данные от эмулируемого или реального ВЭП. */ + return 0x001CE000UL | device | (from_device ? 0U : 0x20U); /* Командное смещение 0x20 отличается от 0x10 в Бальзам. */ +} /* Завершает единственное вычисление маршрутов UKSVEP. */ + +size_t uksvep_can_encode(unsigned device, unsigned from_device, unsigned address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity) /* Проверяет геометрию до общего кодека. */ +{ /* Один кадр вмещает до трёх последовательных слов uint16. */ + if (capacity < 8U || uksvep_can_id(device, from_device) == UINT32_MAX || address >= UKSVEP_REGISTER_COUNT || count == 0U || count > 3U || count > UKSVEP_REGISTER_COUNT - address) { /* Исключает переполнение банка R0…R127. */ + return 0U; /* Не выдаёт частично собранный пакет ошибочного диапазона. */ + } /* Параметры относятся к существующей карте платы. */ + return balsam_can_encode_payload((uint16_t)address, values, count, output, capacity); /* Общий кодек задаёт порядок word2, word3, адрес/маска, word1. */ +} /* Не содержит второго endian/масочного алгоритма. */ + +int uksvep_can_decode(unsigned device, uint32_t id, const uint8_t *data, size_t size, uint16_t *output, size_t capacity) /* Разбирает только выбранное устройство и обе его роли. */ +{ /* ABI возвращает плоские скаляры без зависимости от выравнивания C-структур. */ + balsam_can_frame_t frame = {0}; /* Общий payload decoder заполняет маску, адрес и три слова. */ + unsigned index; /* Перебирает только реально присутствующие биты маски. */ + unsigned count = 0U; /* Считает адресные пары, которые разрешено применить к RX-снимку. */ + if (capacity < 8U || device > 15U || output == NULL || id > 0x1FFFFFFFUL) { /* Отвергает неверные аргументы до сравнения маршрутов. */ + return -1; /* Ошибка не должна частично изменить снимок клиента. */ + } /* Проверка чужого ID выполняется до DLC и указателя данных. */ + if (id != uksvep_can_id(device, 0U) && id != uksvep_can_id(device, 1U)) { /* Broadcast и чужой ВЭП не выдаются за адресный ответ. */ + return 0; /* Позволяет общему CAN-монитору передать кадр другим потребителям. */ + } /* Дальше кадр гарантированно адресует выбранный профиль. */ + if (data == NULL || size != 8U || !balsam_can_decode_payload(data, size, &frame) || frame.present_mask == 0U) { /* Требует полный classic CAN data payload с полезными словами. */ + return -1; /* Повреждённый собственный кадр учитывается как ошибка RX. */ + } /* Проверяет все адреса до публикации результата. */ + for (index = 0U; index < 3U; ++index) { /* В кадре максимум три последовательных адреса. */ + if ((frame.present_mask & (4U >> index)) != 0U && frame.start_address + index >= UKSVEP_REGISTER_COUNT) { /* Отсутствующее слово не создаёт ложной записи R128/R129. */ + return -1; /* При выходе присутствующего слова за карту отвергается весь пакет. */ + } /* Проверка проходит до заполнения плоского output. */ + } /* Геометрия всех значащих слов проверена. */ + output[0] = id == uksvep_can_id(device, 1U); /* Сохраняет реальное направление, чтобы собственный TX не стал ответом платы. */ + for (index = 0U; index < 3U; ++index) { /* Сохраняет только слова с установленными битами присутствия. */ + if ((frame.present_mask & (4U >> index)) != 0U) { /* Неприсутствующие нули не должны затирать предыдущие RX-значения. */ + output[2U + count * 2U] = (uint16_t)(frame.start_address + index); /* Возвращает точный индекс регистра. */ + output[3U + count * 2U] = frame.values[index]; /* Возвращает полезное uint16 без потери старшего бита. */ + ++count; /* Продвигает плоскую таблицу только после полезной пары. */ + } /* Завершает применение одного бита маски. */ + } /* Все полезные слова готовы к атомарному обновлению модели. */ + output[1] = (uint16_t)count; /* Число пар делает ABI независимым от завершающих неиспользуемых элементов. */ + return (int)count; /* Положительный результат отличает собственный валидный кадр от чужого. */ +} /* Завершает общий декодер профиля без GPIO и Qt. */ + +unsigned uksvep_field_count(void) /* Предоставляет размер карты из единственного источника. */ +{ /* Клиент не хранит вторую таблицу адресов. */ + return (unsigned)(sizeof(fields) / sizeof(fields[0])); /* Размер определяется фактическим C-массивом. */ +} /* Завершает запрос количества полей. */ + +int uksvep_field_info(unsigned index, unsigned device, uint16_t *output, size_t capacity) /* Возвращает метаданные в явном массиве четырёх uint16. */ +{ /* Неверный адрес устройства не должен давать правдоподобные параметры другой платы. */ + if (capacity < 4U || index >= uksvep_field_count() || device > 15U || output == NULL) { /* Проверяет границы всех входных аргументов. */ + return 0; /* При отказе выходной буфер не изменяется. */ + } /* Индекс описывает существующее именованное поле. */ + output[0] = fields[index].address; /* Индекс слова modbus без дополнительного смещения. */ + output[1] = fields[index].maximum; /* Ограничение ввода остаётся общим для GUI и будущих портов. */ + output[2] = fields[index].address == 126U ? (uint16_t)(device + 1U) : fields[index].initial; /* Mode зависит от четырёх перемычек выбранного адреса. */ + if (fields[index].address == 23U) { /* R23 содержит активные логические перемычки J0…J3 в младшем полубайте. */ + output[2] = (uint16_t)device; /* Начальный эмулируемый адрес соответствует выбранному DeviceID без TEST. */ + } /* Профиль не создаёт противоречия между исходящим ID и состоянием перемычек. */ + output[3] = fields[index].flags; /* Роль определяет доступность редактирования реальной платы. */ + return 1; /* Все четыре скаляра успешно заполнены. */ +} /* Завершает запрос метаданных без передачи C padding через ABI. */ + +const char *uksvep_field_name(unsigned index) /* Возвращает UTF-8 имя именованного регистра. */ +{ /* Неверный индекс не должен приводить к чтению произвольного указателя. */ + return index < uksvep_field_count() ? fields[index].name : ""; /* Пустая строка явно означает отсутствие поля. */ +} /* Память строки остаётся во владении библиотеки. */ + +const char *uksvep_input_name(unsigned bit) /* Возвращает назначение физического дискретного входа. */ +{ /* В прошивке используются ровно четырнадцать разрядов R0. */ + return bit < 14U ? inputs[bit] : ""; /* Резервные 14/15 не получают выдуманных GPIO. */ +} /* Завершает запрос общей карты входов. */ + +const char *uksvep_command_name(unsigned bit) /* Возвращает назначение управляющего бита R127. */ +{ /* Сохраняет пустые позиции резервных команд package.h. */ + return bit < 16U ? commands[bit] : ""; /* Проверяет индекс до доступа к таблице. */ +} /* Завершает запрос именованных команд. */ + +void uksvep_evaluate_inputs(uint16_t value, uint16_t alarm_mask, uint16_t error_mask, uint16_t *output, size_t capacity) /* Модель ReadEnteres для исходящих данных эмулируемого ВЭП. */ +{ /* Расчёт используется только по явной кнопке и не считается ответом реальной платы. */ + if (output == NULL || capacity < 2U) { /* Позволяет безопасно отвергнуть неверный буфер клиента. */ + return; /* Не обращается к отсутствующей памяти. */ + } /* Все действия ниже повторяют особые случаи битов 7/8/9 из lampa.c. */ + output[0] = (uint16_t)((uint16_t)~value & alarm_mask & (uint16_t)~0x0380U); /* Обычные неисправности — отсутствие включённых в маску входов; 7/8/9 обрабатываются отдельно. */ + output[0] |= (uint16_t)(value & 0x0300U); /* Разряд и авария 24 В всегда являются неисправностями. */ + output[1] = (uint16_t)((uint16_t)~value & error_mask & (uint16_t)~0x0380U); /* Обычные аварии также определяются инверсией входов и маской. */ + output[1] |= (uint16_t)((value & error_mask & 0x0100U) | (value & 0x0200U)); /* Разряд зависит от Error_mask.8, авария 24 В всегда активна при R0.9=1. */ +} /* Заряд накопителя R0.7 никогда не создаёт неисправность либо аварию. */ diff --git a/c/set-protocol/tests/test_uksvep_can.c b/c/set-protocol/tests/test_uksvep_can.c new file mode 100644 index 0000000..67f4f8b --- /dev/null +++ b/c/set-protocol/tests/test_uksvep_can.c @@ -0,0 +1,58 @@ +#include "uksvep_can.h" /* Проверяет C99-профиль, который вызывают все GUI через ABI. */ +#include "balsam_can.h" /* Сохраняет регрессию существующей маршрутизации Бальзам после выделения общего payload. */ +#include /* Неверный проводной вектор должен завершать тест с отказом. */ +#include /* Сравнивает байты с независимым эталоном, а не только с обратным decoder. */ + +int main(void) /* Фиксирует реальную карту UKSVEP из can.c/lampa.c/package.h. */ +{ /* Проверки не требуют Qt, аппаратного CAN или микроконтроллера. */ + const uint16_t words[] = {0x1234U, 0xABCDU, 0x8001U}; /* Различные слова выявляют перестановку и потерю старшего бита. */ + const uint8_t golden[] = {0xABU, 0xCDU, 0x80U, 0x01U, 0xE0U, 0x08U, 0x12U, 0x34U}; /* R8…R10: word2, word3, маска111/адрес8, word1. */ + const uint8_t sparse[] = {0xAAU, 0x55U, 0x33U, 0x44U, 0xA0U, 0x7DU, 0x11U, 0x22U}; /* Маска101 разрешает R125 и R127, оставляя R126 неизменным. */ + uint8_t payload[8]; /* Выходной буфер реального кодировщика C. */ + uint16_t decoded[8]; /* Плоский ABI буфер направления и пар регистров. */ + uint16_t info[4]; /* Адрес, максимум, начальное значение и роль регистра. */ + uint16_t state[2]; /* Расчётные неисправности и аварии эмулируемого ВЭП. */ + balsam_can_frame_t previous = {0}; /* Независимо проверяет существующий публичный decoder Бальзам. */ + unsigned device; /* Перебирает все положения четырёх перемычек. */ + unsigned index; /* Перебирает общую карту подтверждённых полей. */ + + for (device = 0U; device < 16U; ++device) { /* Адреса не должны наследовать Mode=9 и базу BA0000 другого ВЭП. */ + assert(uksvep_can_id(device, 0U) == 0x001CE020U + device); /* Командный ID берётся из Setup_CAN_addr текущего STM32-проекта. */ + assert(uksvep_can_id(device, 1U) == 0x001CE000U + device); /* Исходящий ID ВЭП не содержит командного смещения 0x20. */ + } /* Проверяет обе роли каждого физического адреса. */ + assert(uksvep_can_id(16U, 0U) == UINT32_MAX && uksvep_can_id(0U, 2U) == UINT32_MAX); /* Ошибочная цель не обрезается до соседнего устройства. */ + assert(uksvep_can_encode(5U, 0U, 8U, words, 3U, payload, 7U) == 0U); /* Малый выходной буфер не допускает записи восьми байтов за его границы. */ + assert(uksvep_can_encode(5U, 0U, 8U, words, 3U, payload, sizeof(payload)) == 8U); /* Строит стандартный трёхсловный командный кадр. */ + assert(memcmp(payload, golden, sizeof(golden)) == 0); /* Эталон проверяет endian, адрес и маску независимо от decoder. */ + assert(uksvep_can_decode(5U, 0x001CE025U, golden, 8U, decoded, 8U) == 3); /* Распознаёт собственное командное направление. */ + assert(decoded[0] == 0U && decoded[2] == 8U && decoded[3] == 0x1234U && decoded[6] == 10U && decoded[7] == 0x8001U); /* Возвращает исходные пары без потери разрядов. */ + assert(uksvep_can_decode(5U, 0x001CE005U, sparse, 8U, decoded, 8U) == 2); /* Частичный RX не требует всех трёх последовательных регистров. */ + assert(decoded[0] == 1U && decoded[2] == 125U && decoded[3] == 0x1122U && decoded[4] == 127U && decoded[5] == 0x3344U); /* Отсутствующий R126 не появляется среди обновлений. */ + assert(uksvep_can_decode(5U, 0x001CE004U, golden, 8U, decoded, 8U) == 0); /* Данные другого адреса игнорируются. */ + assert(uksvep_can_decode(5U, 0x00BA0018U, golden, 8U, decoded, 8U) == 0); /* Старый ВЭП проекта Бальзам не подменяет STM32 УКСВЭП. */ + assert(uksvep_can_decode(5U, 0x001CE005U, golden, 7U, decoded, 8U) == -1); /* Повреждённый DLC собственного узла отвергается. */ + assert(uksvep_can_encode(5U, 0U, 127U, words, 2U, payload, sizeof(payload)) == 0U); /* Запись второго слова не может пересечь R127. */ + assert(uksvep_can_encode(5U, 0U, 127U, words, 1U, payload, sizeof(payload)) == 8U && payload[4] == 0x80U && payload[5] == 0x7FU); /* Одно последнее слово использует маску100 и нулевые соседи. */ + assert(uksvep_can_decode(5U, 0x001CE005U, payload, 8U, decoded, 8U) == 1); /* Последний валидный регистр можно принять отдельно. */ + payload[4] = 0xE0U; /* Искусственно делает присутствующими запрещённые R128/R129. */ + assert(uksvep_can_decode(5U, 0x001CE005U, payload, 8U, decoded, 8U) == -1); /* Такой пакет отвергается целиком, без частичного применения R127. */ + assert(balsam_can_decode(0x00BA0017U, golden, 8U, &previous) == 1 && previous.device == 8U && previous.values[1] == 0xABCDU); /* Общий payload сохраняет прежний маршрут и слова задатчика. */ + for (index = 0U; index < uksvep_field_count(); ++index) { /* Каждое именованное поле должно принадлежать реальной карте. */ + assert(uksvep_field_info(index, 7U, info, 4U) && info[0] < 128U && info[2] <= info[1]); /* Начальное значение не превышает UI-границу поля. */ + assert(strlen(uksvep_field_name(index)) != 0U); /* Нет безымянных полей, которые выглядели бы пропущенной картой. */ + if (info[0] == 126U) { /* Физический адрес отличается от Mode на единицу. */ + assert(info[2] == 8U); /* DeviceID7 соответствует Mode8 независимо от названия устройства. */ + } /* Остальные defaults одинаковы для всех адресов. */ + } /* Размер карты задаёт C, а не отдельный список Python. */ + assert(!uksvep_field_info(999U, 0U, info, 4U) && strlen(uksvep_input_name(14U)) == 0U); /* За границами нет фиктивных регистров и GPIO. */ + assert(strstr(uksvep_input_name(9U), "PB12") != NULL && strlen(uksvep_command_name(4U)) == 0U); /* Особый прямой вход и резервная команда сохраняют реальное назначение. */ + uksvep_evaluate_inputs(0x247FU, 0x277FU, 0x0200U, state, 2U); /* Моделирует исправные штатные входы и маски. */ + assert(state[0] == 0U && state[1] == 0U); /* Исправное состояние не должно вызывать ложную аварию. */ + uksvep_evaluate_inputs(0x247EU, 0x277FU, 0x0200U, state, 2U); /* Убирает только исправность ИП1. */ + assert(state[0] == 1U && state[1] == 0U); /* Маска по умолчанию делает потерю ИП1 неисправностью, а не аварией. */ + uksvep_evaluate_inputs(0x27FFU, 0U, 0U, state, 2U); /* Включает заряд, разряд и аварию 24 В при нулевых масках. */ + assert(state[0] == 0x0300U && state[1] == 0x0200U); /* Заряд не влияет, разряд всегда неисправность, 24 В всегда авария. */ + uksvep_evaluate_inputs(0x25FFU, 0U, 0x0100U, state, 2U); /* Разрешает аварийный разряд отдельным битом маски. */ + assert(state[1] == 0x0100U); /* Особый бит 8 проверяется по своей полярности без инверсии. */ + return 0; /* Все эталоны, границы и регрессия старого профиля выполнены. */ +} /* Завершает независимую host-проверку C-профиля. */ diff --git a/c/set-protocol/tools/build_host.py b/c/set-protocol/tools/build_host.py index f4615d2..35219ba 100644 --- a/c/set-protocol/tools/build_host.py +++ b/c/set-protocol/tools/build_host.py @@ -33,6 +33,7 @@ SOURCES = [ "altera_stream.c", "set_gas_watch.c", # Добавляет общий GAS_WATCH2 в прямые MSVC/CC сборки, чтобы новые ctypes/JNI символы были доступны. "set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c", "set_signal.c", "set_wavegen.c", + "uksvep_can.c", # Включает общий профиль ВЭП также в прямую MSVC/CC сборку DLL. "balsam_can.c", "set_crc.c", "periph28335.c", "tms2812.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c", "pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c", "pcan_gas.c", "pcan_modbus_server.c", diff --git a/python/protocan/uksvep.py b/python/protocan/uksvep.py new file mode 100644 index 0000000..6849ebf --- /dev/null +++ b/python/protocan/uksvep.py @@ -0,0 +1,110 @@ +"""Тонкий Python-порт общей CAN-карты UKSVEP на STM32F103RC.""" # Форматы, GPIO и расчёт неисправностей принадлежат C99 templates. +from __future__ import annotations # Аннотации остаются совместимыми с исходниками Windows 7. +import ctypes as ct # Передаёт скаляры и буферы единственной нативной реализации. +from dataclasses import dataclass # Представляет готовые результаты C без протокольных вычислений. +from .native import get_native_protocol, NativeProtocolUnavailable # Использует штатный поиск и жизненный цикл общей DLL. + +@dataclass(frozen=True) # Метаданные одинаковы для всех представлений GUI. +class Field: # Значения читаются из package.h-совместимой таблицы C. + address: int # Нулевой индекс регистра modbus[128]. + maximum: int # Верхняя граница ввода в исходных единицах. + initial: int # Значение предпросмотра, которое ещё не принято от платы. + flags: int # 1 — параметр, 2 — команды, 4 — эмулируемое состояние. + name: str # Общая подпись UTF-8 из C-профиля. + +@dataclass(frozen=True) # Пакет отделён от физического CAN-драйвера. +class Frame: # Готовые байты нельзя изменять через поля виджета после отправки. + can_id: int # Полный 29-битный ID без аппаратного бита IDE. + data: bytes # Восемь байтов payload, построенных C. + +@dataclass(frozen=True) # Снимок содержит только реально присутствующие слова. +class Packet: # Источник пакета отделяет ответ ВЭП от команды управляющего узла. + from_device: bool # Истина означает ID данных ВЭП, а не собственное эхо команд. + values: tuple[tuple[int, int], ...] # Пары адрес/значение, разрешённые маской присутствия C. + +class UksvepCan: # Каждая панель использует общее нативное ядро без собственного codec. + def __init__(self): # Привязывает только дополнительные экспорты профиля UKSVEP. + self.lib = get_native_protocol().lib # Загрузка DLL и общий ABI уже проверены штатным портом. + signatures = { # Типы описывают ABI, а не байтовый порядок wire-протокола. + "uksvep_can_id": ([ct.c_uint, ct.c_uint], ct.c_uint32), # Возвращает маршрут для физического адреса и роли. + "uksvep_can_encode": ([ct.c_uint, ct.c_uint, ct.c_uint, ct.POINTER(ct.c_uint16), ct.c_size_t, ct.POINTER(ct.c_uint8), ct.c_size_t], ct.c_size_t), # Кодирует до трёх слов в буфер caller. + "uksvep_can_decode": ([ct.c_uint, ct.c_uint32, ct.POINTER(ct.c_uint8), ct.c_size_t, ct.POINTER(ct.c_uint16), ct.c_size_t], ct.c_int), # Возвращает плоский массив пар из C. + "uksvep_field_count": ([], ct.c_uint), # Позволяет получить все поля без таблицы дублей Python. + "uksvep_field_info": ([ct.c_uint, ct.c_uint, ct.POINTER(ct.c_uint16), ct.c_size_t], ct.c_int), # Возвращает четыре скаляра метаданных. + "uksvep_field_name": ([ct.c_uint], ct.c_char_p), # C владеет постоянной строкой UTF-8. + "uksvep_input_name": ([ct.c_uint], ct.c_char_p), # Назначения GPIO остаются общими с профилем. + "uksvep_command_name": ([ct.c_uint], ct.c_char_p), # Пустое имя обозначает резервный бит R127. + "uksvep_evaluate_inputs": ([ct.c_uint16, ct.c_uint16, ct.c_uint16, ct.POINTER(ct.c_uint16), ct.c_size_t], None), # Рассчитывает R1/R2 для эмуляции по алгоритму C. + } # Символы добавлены без изменения существующего ABI версии 1. + try: # Старая DLL должна давать понятную ошибку вместо тихого Python fallback. + for name, (arguments, result) in signatures.items(): # Каждый экспорт получает точную сигнатуру до первого вызова. + function = getattr(self.lib, name) # Отсутствие профиля в старой DLL обнаруживается здесь. + function.argtypes = arguments # ctypes не угадывает размер указателей и целых. + function.restype = result # Сохраняет 32-битные ID и указатели строк на 64-битном хосте. + except AttributeError as error: # Формат wire не реализуется заново в аварийной ветви. + raise NativeProtocolUnavailable("В общей DLL нет профиля UKSVEP. Пересоберите setprotocol.dll из templates.") from error # Поясняет требуемое действие пользователю. + + @staticmethod # Валидация Python-аргумента предотвращает обрезание ctypes до C-типа. + def _device(value): # Адрес хранится целым числом из четырёх перемычек. + if not isinstance(value, int) or not 0 <= value <= 15: # Нельзя молча адресовать соседний узел по младшим битам. + raise ValueError("DeviceID ВЭП должен быть 0…15") # Ошибка возникает до передачи CAN. + return value # Валидный скаляр далее проверяется также C-профилем. + + def can_id(self, device, from_device=False): # Доверяет вычисление маршрута общему C-профилю. + return int(self.lib.uksvep_can_id(self._device(device), bool(from_device))) # Python не повторяет базу и смещение идентификатора. + + def fields(self, device=0): # Получает подтверждённые поля выбранного ВЭП. + device = self._device(device) # Проверяет адрес до создания начальных значений Mode. + result = [] # Список содержит только готовые метаданные ABI. + for index in range(self.lib.uksvep_field_count()): # Размер карты также принадлежит C. + raw = (ct.c_uint16 * 4)() # Caller владеет явно ограниченным выходным буфером. + if not self.lib.uksvep_field_info(index, device, raw, len(raw)): # Проверяет результат вызова вместо чтения частично готового массива. + raise ValueError("Не удалось получить карту UKSVEP") # Ошибка не заменяется выдуманными defaults. + name = self.lib.uksvep_field_name(index).decode("utf-8") # Декодирует только строковое представление общей подписи. + result.append(Field(*map(int, raw), name)) # Не меняет единицы и адреса, возвращённые C. + return tuple(result) # Карта не изменяется виджетами во время работы. + + def names(self, kind): # Читает общие назначения входов либо команд. + function = self.lib.uksvep_input_name if kind == "inputs" else self.lib.uksvep_command_name # Выбор относится к категории отображения, а не к codec. + return tuple(function(bit).decode("utf-8") for bit in range(16)) # Резервные позиции остаются пустыми строками. + + def frames(self, device, address, values, from_device=False): # Делит длинный снимок на штатные вызовы трёхсловного C-кодировщика. + device = self._device(device) # Неверный адрес не может быть обрезан ctypes. + values = tuple(values) # Фиксирует входной снимок до первой физической записи. + if not values or not isinstance(address, int) or not 0 <= address < 128 or len(values) > 128 - address: # Проверяет Python-диапазон до преобразования unsigned. + raise ValueError("Выберите непустой диапазон R0…R127") # Ошибка обнаруживается до формирования первого пакета. + if any(not isinstance(value, int) or not 0 <= value <= 65535 for value in values): # Не обрезает отрицательные или переполненные слова. + raise ValueError("Значения ВЭП должны быть uint16: 0…65535") # Поле пользователя не должно превращаться в другое число на проводе. + result = [] # Все байты создаются до передачи, чтобы не отправить половину неверного снимка. + for offset in range(0, len(values), 3): # Ограничение трёх слов задано существующим историческим форматом. + part = values[offset:offset + 3] # UI-порт только разбивает контейнер, не строит маску/endian. + source = (ct.c_uint16 * len(part))(*part) # Преобразует значения в явно типизированный вход C. + target = (ct.c_uint8 * 8)() # Payload classic CAN ограничен восемью байтами. + size = self.lib.uksvep_can_encode(device, bool(from_device), address + offset, source, len(part), target, len(target)) # Все поля wire заполняет C99. + if size != 8: # Валидный UKSVEP кадр обязан иметь полный DLC. + raise ValueError("C-профиль отклонил регистры UKSVEP") # Ошибка не скрывается частичной передачей. + result.append(Frame(self.can_id(device, from_device), bytes(target))) # Возвращает готовый транспортно-независимый кадр. + return tuple(result) # Физический writer принадлежит существующему BusTab. + + def decode(self, device, can_id, data): # Вызывает общий decoder с ограниченными буферами. + device = self._device(device) # Проверяет адрес до входа в ctypes. + if not isinstance(can_id, int) or not 0 <= can_id <= 0x1FFFFFFF: # Исключает знаковое переполнение и биты вне extended ID. + return None # Такое событие не принадлежит допустимому CAN-профилю. + data = bytes(data) # Фиксирует входной payload на время вызова C. + source = (ct.c_uint8 * len(data)).from_buffer_copy(data) # Caller владеет памятью до завершения синхронного разбора. + target = (ct.c_uint16 * 8)() # Восемь скаляров вмещают направление и три адресные пары. + count = self.lib.uksvep_can_decode(device, can_id, source, len(data), target, len(target)) # C выбирает ID, DLC, маску и границы карты. + if count < 0: # Повреждённый пакет выбранного ВЭП не считается чужим. + raise ValueError("Повреждённый CAN-кадр UKSVEP: DLC, маска или адрес") # UI может увеличить счётчик отказов RX. + if count == 0: # Пакет другого адреса не меняет снимок. + return None # Общая шина продолжает обрабатывать его независимо. + pairs = tuple((int(target[2 + index * 2]), int(target[3 + index * 2])) for index in range(count)) # Переводит плоский ABI в готовые пары без повторного endian-разбора. + return Packet(bool(target[0]), pairs) # Направление не позволяет выдать эхо TX за ответ платы. + + def evaluate(self, inputs, alarm_mask, error_mask): # Предоставляет явный расчёт состояния эмулируемого ВЭП. + values = (inputs, alarm_mask, error_mask) # Передаёт только исходные слова C-алгоритму ReadEnteres. + if any(not isinstance(value, int) or not 0 <= value <= 65535 for value in values): # Защищает параметры от неявного uint16 обрезания. + raise ValueError("Входы и маски должны быть uint16") # Неверный ввод не превращается в правдоподобную аварию. + result = (ct.c_uint16 * 2)() # Буфер содержит R1 и R2, вычисленные C. + self.lib.uksvep_evaluate_inputs(*values, result, len(result)) # Python не повторяет особые случаи заряд/разряд/24 В. + return tuple(map(int, result)) # Результат используется только в исходящем эмулируемом снимке. diff --git a/python/tests/test_uksvep_can.py b/python/tests/test_uksvep_can.py new file mode 100644 index 0000000..fb00758 --- /dev/null +++ b/python/tests/test_uksvep_can.py @@ -0,0 +1,37 @@ +"""Проверки тонкого порта ВЭП на настоящей C DLL templates.""" # Транспорт и Qt здесь не участвуют. +import pytest # Параметризует все физические адреса и неверные аргументы. +from protocan.uksvep import UksvepCan # Проверяет общий production-порт без отдельного Python codec. + +@pytest.fixture(scope="module") # Все сценарии используют одну загруженную нативную библиотеку. +def core(): # Отсутствие C-профиля должно приводить к ошибке теста, а не fallback. + return UksvepCan() # Привязывает те же экспорты, что панель SETGUI. + +@pytest.mark.parametrize("device", range(16)) # Четыре перемычки задают все шестнадцать адресов. +def test_routes_and_mode(core, device): # Разделяет DeviceID, Mode и legacy CAN-пару. + assert core.can_id(device) == 0x001CE020 + device # Команды идут по маршруту Setup_CAN_addr. + assert core.can_id(device, True) == 0x001CE000 + device # Данные устройства не содержат командное смещение. + fields = {item.address: item for item in core.fields(device)} # Карта адресов приходит только из C. + assert fields[126].initial == device + 1 and fields[100].maximum == 10 # Mode и десятиотсчётная яркость совпадают с прошивкой. + +@pytest.mark.parametrize("device,address,values", [(16, 0, (1,)), (-1, 0, (1,)), (0, -1, (1,)), (0, 128, (1,)), (0, 127, (1, 2)), (0, 0, ()), (0, 0, (-1,)), (0, 0, (65536,))]) # Все ошибки должны обнаруживаться до ctypes-обрезания. +def test_invalid_arguments(core, device, address, values): # Неверный ввод не адресует соседнюю плату или слово. + with pytest.raises(ValueError): # Требует явный отказ вместо скрытого modulo. + core.frames(device, address, values) # Использует реальный путь формирования GUI-снимка. + +def test_golden_and_sparse_rx(core): # Эталон независимо проверяет endian, маску и пропущенное слово. + frames = core.frames(5, 8, (0x1234, 0xABCD, 0x8001)) # Значения различаются во всех значащих позициях. + assert [(frame.can_id, frame.data) for frame in frames] == [(0x001CE025, bytes.fromhex("AB CD 80 01 E0 08 12 34"))] # Фиксирует точный wire-vector STM32-проекта. + packet = core.decode(5, 0x001CE005, bytes.fromhex("AA 55 33 44 A0 7D 11 22")) # Маска101 разрешает R125 и R127. + assert packet.from_device and packet.values == ((125, 0x1122), (127, 0x3344)) # Отсутствующий R126 не затирается чужим значением. + assert core.decode(5, 0xBA0018, frames[0].data) is None # Другой ВЭП Бальзам не смешивается с UKSVEP. + with pytest.raises(ValueError): # Повреждённый собственный пакет должен считаться ошибкой RX. + core.decode(5, 0x001CE005, frames[0].data[:7]) # DLC7 недостаточен для исторического payload. + +def test_fields_inputs_and_special_polarity(core): # Карта и расчёт не должны копировать семантику задатчика. + fields = {item.address: item for item in core.fields()} # Все defaults взяты из C-профиля package.h. + assert set(fields) == {0, 1, 2, 8, 9, 23, 48, 100, 101, 102, 125, 126, 127} # Не добавляет несуществующие аналоговые измерения. + assert fields[8].initial == 0x277F and fields[9].initial == 0x200 # Маски повторяют Default_params. + assert "PB12" in core.names("inputs")[9] and "активный 1" in core.names("inputs")[9] # Единственный прямой вход имеет особую полярность. + assert core.names("commands")[0] == "Тест ламп" and not core.names("commands")[4] # Команды задатчика и резервные биты не подставляются. + assert core.evaluate(0x247F, 0x277F, 0x200) == (0, 0) # Исправное состояние не вызывает ложных аварий. + assert core.evaluate(0x27FF, 0, 0) == (0x300, 0x200) # Разряд и 24 В сохраняют особую семантику без масок; заряд не влияет.