diff --git a/c/protocan-boot/README.md b/c/protocan-boot/README.md index 367e984..4e1db0e 100644 --- a/c/protocan-boot/README.md +++ b/c/protocan-boot/README.md @@ -20,6 +20,11 @@ Linker script, startup, тактирование, GPIO и переход в пр область приложения, callback `set_pending_slot` не требуется, а аппаратный rollback недоступен. Значение `slot_count = 0` сохраняет прежний A/B-режим. +Для [HOPE G474VET6](ports/stm32g474-hope/README.md) готовый BOOT CANdle +использует тот же main/startup/Flash-порт, но аппаратный адаптер подключает +FDCAN1 к PD0/PD1 по схеме платы. USART1 PA9/PA10 и слот приложения +0x08008000 сохраняются; исходники проекта подключаются из templates. + ## Слои ```text diff --git a/c/protocan-boot/ports/stm32g474-hope/README.md b/c/protocan-boot/ports/stm32g474-hope/README.md new file mode 100644 index 0000000..2f2b9d9 --- /dev/null +++ b/c/protocan-boot/ports/stm32g474-hope/README.md @@ -0,0 +1,34 @@ +# HOPE G474VET6: готовый BOOT CANdle + +Аппаратный адаптер готового CAN/UART-загрузчика из +`CANdle_stm32` (`https://git.rd12.ru/setcorp/CANdle_stm32.git`). +Основная программа, startup, проверка образа и Flash-порт повторно используются +из `../stm32g474`; протоколы — из общих `protocan-boot` и `rs485-boot`. +Новой загрузочной машины состояний в этом каталоге нет. + +По [заводской схеме HOPE от 14.04.2025](../../../set-protocol/ports/stm32g474-hope/docs/HOPE_G474VE_schematic_20250414.pdf) +CAN1 перенесён с PB8/PB9 исходного CANdle на **PD0 RX / PD1 TX, AF9**. +PB8 на этой плате соединён с BOOT0. USART1 **PA9 TX / PA10 RX, AF7** +соединён с CH340N через JP2 и сохраняет **512000 бод, 8N1**. + +CAN: Classic CAN, 500 кбит/с, extended ID, ProtoCAN Type=7/ID=15. +Тактирование сохранено из CANdle: HSI16/PLL → CPU 144 МГц, +PLLQ 48 МГц → FDCAN. USB CDC в BOOT не реализован; USB-UART работает через CH340N. + +BOOT занимает **0x08000000..0x08007FFF, 32 КиБ**. +Приложение размещается с **0x08008000**, максимум 480 КиБ. +Общая память загрузчика: SRAM 0x20000000..0x20013FF7, +стек 2 КиБ заканчивается на 0x20013FF8; маркер CANB — 0x20013FFC. +DBANK=0/1 поддерживаются готовым Flash-портом; bank swap отвергается. + +При наличии приложения после reset открывается 10-секундное окно обновления. +При отсутствии приложения или маркере CANB загрузчик остаётся ждать UART/CAN. +Перед подтверждением записи проверяются CRC и векторы сохранённого образа. +После независимого reset используется исходная проверка векторов CANdle; +постоянного manifest и A/B-слотов этот профиль не добавляет. + +Интеграция Keil: `stm32f407_emulator/G474/HOPE/Boot/MDK-ARM/HOPE_G474VE_BOOT.uvprojx`. +Все C-файлы проекта подключаются непосредственно из templates; ST HAL/CMSIS +подготавливаются из установленного Keil.STM32G4xx_DFP 1.6.0. +Первичная запись BOOT выполняется через SWD по адресам HEX, затем прошивается +профиль приложения `UMP_Emulator_HOPE_G474VE` с сохранением первых 32 КиБ. diff --git a/c/protocan-boot/ports/stm32g474-hope/device.c b/c/protocan-boot/ports/stm32g474-hope/device.c new file mode 100644 index 0000000..5768359 --- /dev/null +++ b/c/protocan-boot/ports/stm32g474-hope/device.c @@ -0,0 +1,31 @@ +#include "device.h" /* Подключает интерфейс аппаратного слоя готового загрузчика CANdle. */ +static void clock_error(void) { for (;;) {} } /* Останавливает запуск при отказе RCC, чтобы не принимать прошивку с неверной скоростью CAN. */ +void device_sysclock_config(void) /* Повторяет тактирование device_g4.c из CANdle без изменения готового CAN/UART-протокола. */ +{ /* HSI16 оставлен исходным источником boot; приложение самостоятельно переключается на кварц HOPE 8 МГц. */ + RCC_OscInitTypeDef osc = {0}; RCC_ClkInitTypeDef clk = {0}; RCC_PeriphCLKInitTypeDef peripheral = {0}; /* Полностью определяет структуры HAL до запуска PLL и шин. */ + __HAL_RCC_SYSCFG_CLK_ENABLE(); __HAL_RCC_PWR_CLK_ENABLE(); /* Разрешает проверку Flash bank swap и настройку регулятора питания. */ + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1_BOOST) != HAL_OK) clock_error(); /* Boost Range1 обеспечивает допустимый режим CPU 144 МГц. */ + osc.OscillatorType = RCC_OSCILLATORTYPE_HSI | RCC_OSCILLATORTYPE_HSI48; /* Сохраняет источники оригинального аппаратного профиля CANdle. */ + osc.HSIState = RCC_HSI_ON; osc.HSI48State = RCC_HSI48_ON; osc.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; /* Использует заводскую калибровку HSI16 и включает исходный HSI48. */ + osc.PLL.PLLState = RCC_PLL_ON; osc.PLL.PLLSource = RCC_PLLSOURCE_HSI; osc.PLL.PLLM = RCC_PLLM_DIV4; /* HSI16 / 4 формирует вход PLL 4 МГц, как в готовом CANdle. */ + osc.PLL.PLLN = 72U; osc.PLL.PLLP = RCC_PLLP_DIV2; osc.PLL.PLLQ = RCC_PLLQ_DIV6; osc.PLL.PLLR = RCC_PLLR_DIV2; /* VCO 288 МГц создаёт CPU 144 МГц и FDCAN kernel 48 МГц через PLLQ. */ + if (HAL_RCC_OscConfig(&osc) != HAL_OK) clock_error(); /* Не продолжает загрузку при отказе внутренних генераторов или PLL. */ + clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; /* Согласует частоты ядра и обеих периферийных шин. */ + clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider = RCC_SYSCLK_DIV1; /* Передаёт CPU и AHB исходные 144 МГц без деления. */ + clk.APB1CLKDivider = RCC_HCLK_DIV1; clk.APB2CLKDivider = RCC_HCLK_DIV1; /* USART1 получает PCLK2 144 МГц для расчёта прежних 512000 бод. */ + if (HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_4) != HAL_OK) clock_error(); /* Сохраняет исходные четыре wait-state Flash и проверяет переключение SYSCLK. */ + peripheral.PeriphClockSelection = RCC_PERIPHCLK_USB | RCC_PERIPHCLK_FDCAN; /* Повторяет независимые источники USB и CAN исходного CANdle. */ + peripheral.UsbClockSelection = RCC_USBCLKSOURCE_HSI48; peripheral.FdcanClockSelection = RCC_FDCANCLKSOURCE_PLL; /* FDCAN использует PLLQ 48 МГц, а не PCLK1 144 МГц. */ + if (HAL_RCCEx_PeriphCLKConfig(&peripheral) != HAL_OK) clock_error(); /* Исключает запуск CAN с неподтверждённым kernel clock. */ + HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / 1000U); HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK); /* Обеспечивает миллисекундные тайм-ауты готовой загрузочной машины состояний. */ + HAL_NVIC_SetPriority(SysTick_IRQn, 0U, 0U); /* Сохраняет приоритет SysTick оригинального загрузчика CANdle. */ +} /* Завершает аппаратный clock-профиль без изменения Flash Option Bytes. */ +void HAL_FDCAN_MspInit(FDCAN_HandleTypeDef *can) /* Привязывает существующий FDCAN-загрузчик к штатному CAN1 платы HOPE. */ +{ /* Единственное изменение маршрута CAN относительно device_g4.c: PD0/PD1 вместо PB8/PB9. */ + GPIO_InitTypeDef gpio = {0}; /* Не оставляет неопределённых параметров скорости и альтернативной функции GPIO. */ + if (can->Instance != FDCAN1) return; /* Не перенастраивает дополнительные CAN2/CAN3 платы. */ + __HAL_RCC_FDCAN_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); /* Разрешает общий FDCAN kernel и порт D штатного трансивера U6. */ + gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1; gpio.Mode = GPIO_MODE_AF_PP; gpio.Pull = GPIO_NOPULL; /* PD0 RX и PD1 TX соединены с TJA1051T/3 по схеме HOPE от 14.04.2025. */ + gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate = GPIO_AF9_FDCAN1; /* AF9 выбирает FDCAN1 на PD0/PD1 без занятия BOOT0 на PB8. */ + HAL_GPIO_Init(GPIOD, &gpio); /* Подключает готовый CAN-загрузчик к CAN1L/CAN1H разъёма J4. */ +} /* Завершает аппаратный адаптер HOPE; кадры, CRC и запись Flash реализованы готовыми модулями templates. */ diff --git a/c/protocan-boot/ports/stm32g474-hope/device.h b/c/protocan-boot/ports/stm32g474-hope/device.h new file mode 100644 index 0000000..72a37fd --- /dev/null +++ b/c/protocan-boot/ports/stm32g474-hope/device.h @@ -0,0 +1,5 @@ +#ifndef HOPE_BOOT_DEVICE_H /* Защищает аппаратный интерфейс HOPE от повторного включения. */ +#define HOPE_BOOT_DEVICE_H /* Обозначает плату HOPE STM32G474VET6 для готового main CANdle. */ +#include "stm32g4xx_hal.h" /* Предоставляет общему загрузчику HAL FDCAN и системные регистры STM32G4. */ +void device_sysclock_config(void); /* Сохраняет вызов настройки платы, ожидаемый готовым загрузчиком CANdle. */ +#endif /* Завершает интерфейс платы без копирования загрузочного протокола. */ diff --git a/c/protocan-boot/ports/stm32g474-hope/stm32g4xx_hal_conf.template.h b/c/protocan-boot/ports/stm32g474-hope/stm32g4xx_hal_conf.template.h new file mode 100644 index 0000000..2dd3a41 --- /dev/null +++ b/c/protocan-boot/ports/stm32g474-hope/stm32g4xx_hal_conf.template.h @@ -0,0 +1,32 @@ +#ifndef STM32G4xx_HAL_CONF_H /* Защищает минимальную конфигурацию HAL загрузчика от повторного включения. */ +#define STM32G4xx_HAL_CONF_H /* Отделяет BOOT от настроек моделей, дисплея и USB CDC приложения. */ +#define HAL_MODULE_ENABLED /* Разрешает базовый HAL_Init и миллисекундные аппаратные таймеры. */ +#define HAL_CORTEX_MODULE_ENABLED /* Подключает HAL NVIC и SysTick, используемые готовым startup CANdle. */ +#define HAL_RCC_MODULE_ENABLED /* Подключает управление HSI16, PLL и kernel clock FDCAN. */ +#define HAL_GPIO_MODULE_ENABLED /* Подключает UART PA9/PA10 и CAN PD0/PD1. */ +#define HAL_FLASH_MODULE_ENABLED /* Разрешает готовому Flash-порту G474 стирать и записывать только область приложения. */ +#define HAL_PWR_MODULE_ENABLED /* Подключает Boost Range1 для исходных 144 МГц boot. */ +#define HAL_FDCAN_MODULE_ENABLED /* Разрешает Classic CAN 500 кбит/с готового ProtoCAN-загрузчика. */ +#define HSE_VALUE 8000000UL /* Сохраняет номинал внешнего кварца HOPE в герцах, хотя boot использует HSI16. */ +#define HSE_STARTUP_TIMEOUT 100UL /* Ограничивает ожидание внешнего кварца 100 мс при использовании HAL RCC. */ +#define HSI_VALUE 16000000UL /* Внутренний источник исходного CANdle имеет частоту 16 МГц. */ +#define HSI48_VALUE 48000000UL /* Отдельный внутренний генератор USB имеет частоту 48 МГц. */ +#define LSE_VALUE 32768UL /* RTC-кварц HOPE имеет частоту 32768 Гц и не участвует в загрузочной PLL. */ +#define LSE_STARTUP_TIMEOUT 5000UL /* Стандартный HAL допускает до 5 секунд запуска RTC-кварца. */ +#define LSI_VALUE 32000UL /* Определяет номинальную частоту внутреннего низкочастотного RC в герцах. */ +#define EXTERNAL_CLOCK_VALUE 12288000UL /* Сохраняет стандартный номинал неиспользуемого внешнего аудиотактирования HAL. */ +#define VDD_VALUE 3300UL /* Логика платы питается напряжением 3300 мВ через AMS1117-3.3. */ +#define TICK_INT_PRIORITY 0UL /* Повторяет приоритет SysTick готового загрузчика CANdle. */ +#define USE_RTOS 0U /* Загрузчик обслуживает UART/CAN опросом без операционной системы. */ +#define PREFETCH_ENABLE 0U /* Сохраняет стандартный выключенный prefetch HAL G4. */ +#define INSTRUCTION_CACHE_ENABLE 1U /* Разрешает аппаратный кэш инструкций при выполнении boot из Flash. */ +#define DATA_CACHE_ENABLE 1U /* Разрешает стандартный кэш данных HAL G4. */ +#define USE_HAL_FDCAN_REGISTER_CALLBACKS 0U /* Использует статическую HAL_FDCAN_MspInit аппаратного адаптера HOPE. */ +#include "stm32g4xx_hal_rcc.h" /* Предоставляет HAL RCC и расширенные функции выбора источников частоты. */ +#include "stm32g4xx_hal_gpio.h" /* Предоставляет структуры и альтернативные функции GPIO. */ +#include "stm32g4xx_hal_cortex.h" /* Предоставляет HAL NVIC и управление SysTick. */ +#include "stm32g4xx_hal_flash.h" /* Предоставляет операции разблокировки, doubleword-записи и стирания Flash. */ +#include "stm32g4xx_hal_pwr.h" /* Предоставляет управление регулятором и Boost Range1. */ +#include "stm32g4xx_hal_fdcan.h" /* Предоставляет транспортные структуры и функции FDCAN. */ +#define assert_param(expression) ((void)0U) /* Сохраняет штатное отключение отладочных assert HAL в рабочем загрузчике. */ +#endif /* Завершает самостоятельную HAL-конфигурацию шаблона HOPE BOOT. */ diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index e017090..04fc604 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -46,6 +46,8 @@ set(SETPROTOCOL_LEGACY_SOURCES ) set(SETPROTOCOL_SOURCES + src/deif_xdi.c # Общее C99-ядро DEIF для всех платформ. + src/set_invertor_gas.c # Подключает две проекции одной карты STM Invertor: GAS 0x3 и масочный GAS SLOT 0x4. src/set_regmap.c src/set_emu_ump.c src/set_emu_tms.c @@ -96,6 +98,12 @@ endif() option(SETP_BUILD_TESTS "Build host tests" ON) if(SETP_BUILD_TESTS) enable_testing() + add_executable(test_deif_xdi tests/test_deif_xdi.c) # Прямые проверки C-контракта DEIF. + target_link_libraries(test_deif_xdi PRIVATE setprotocol_static) # Используется единственное общее ядро. + add_test(NAME deif_xdi COMMAND test_deif_xdi "${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/deif-xdi-v1.tsv") # Векторы общие с JNI и Python. + add_executable(test_invertor_gas tests/test_invertor_gas.c) # Проверяет два формата, общие значения, маски, границы и транзакционные callbacks. + target_link_libraries(test_invertor_gas PRIVATE setprotocol_static) # Проверяем ту же реализацию, что подключается к прошивке. + add_test(NAME invertor_gas COMMAND test_invertor_gas) # Новый контракт выполняется стандартным CTest вместе с остальными протоколами. add_executable(test_gas_register tests/test_gas_register.c) # Проверяет запись одной уставки без соседей, границы 0…65535 и изоляцию типа 0x3. target_link_libraries(test_gas_register PRIVATE setprotocol_static) # Запускает проверки на том же ядре, которое используют прошивки и host-клиенты. add_test(NAME protocan_gas_register COMMAND test_gas_register) # Отдельный тест защищает восстановленный регистровый контракт. diff --git a/c/set-protocol/docs/DEIF_XDI.md b/c/set-protocol/docs/DEIF_XDI.md new file mode 100644 index 0000000..598cb13 --- /dev/null +++ b/c/set-protocol/docs/DEIF_XDI.md @@ -0,0 +1,104 @@ +# Общее ядро DEIF XDi / CANopen + +Единственная реализация профиля DEIF находится в `include/deif_xdi.h` и +`src/deif_xdi.c`. Desktop использует её через `python/set_devices/deif_native.py`, +Android — через `ports/android/set_deif_jni.c` и `deif/DeifXdi.kt`. +Кодеки, адресация, числовой каталог, NMT, сроки, SDO и выбор азимута +не реализуются повторно в Python или Kotlin. + +Основание профиля: DEIF XDi-net/CANopen reference manual 4189350066D, +§§1–3, §9.11, Appendix 3 и Appendix 5. Набор переменных соответствует +наблюдаемой библиотеке: азимут, RPM, RPM%, мощность AC2 и POWER%. +Абсолютная мощность остаётся RAW до подтверждения её физической единицы. + +## Два режима + +- **Пассивный наблюдатель** принимает данные без включения симуляции, + ничего не отправляет, скрывает устаревшее показание через пять секунд. +- **CAN-SLAVE** принимает данные от внешнего MASTER и производит Boot-up, + heartbeat, EMCY и ответы на явно поддерживаемые SDO. После 3000 мс без + обновления поля сохраняет последнее валидное число и сообщает Data lost; + мигание выполняет GUI по готовой маске устаревших входов. + +Пассивный режим предназначен в том числе для обычного приёма показаний +без занятия собственного Node-ID в сети. На MPDO в активном режиме +также нет ответного PDO. В нормальном обмене SLAVE передаёт heartbeat. + +## Азимут + +Фактическое значение `0x3001:02` и сетевое задание `0x3001:07` хранятся +независимо. `setp_deif_angle` возвращает значение и его тип: + +- `0` — пригодного показания нет; +- `1` — фактический угол; +- `2` — принятое задание, которое GUI обязан подписывать как задание. + +При исходной привязке фактического угла до первого кадра `0x02` допускается +показывать принятое `0x07`. После первого фактического кадра задание его +не подменяет. Ошибочный фактический I16 также не заменяется старым заданием. +Явная ручная привязка к `0x07` остаётся индикатором задания. +Локальный черновик MASTER не входит в состояние C-модели. + +Эталонный кадр `STD 0x201 / 01 01 30 07 84 03 00 00` должен показывать +**90° · задание** и при выключенной симуляции. Node-ID физического SLAVE +не является источником этой SAM-рассылки: источник здесь Node 1. + +## Адресация и сервисы + +SAM согласует источник с COB-ID и поддерживает legacy broadcast `0x27F`. +DAM проверяет собственный Node-ID и выбранный RPDO. XDi-net резервирует +RPDO1 для SAM и Node 127 для сервисного прибора. Чистый CANopen допускает +DAM RPDO1. Все кадры профиля являются Classic CAN STD data frames. + +После Boot-up выдаётся Pre-operational; стандартный self-start занимает +1000 мс. Исходный период heartbeat — 500 мс. NMT Start/Stop/Pre-operational, +Reset node и Reset communication принимаются только для собственного узла +либо broadcast 0. Reset communication сохраняет прикладные значения и +возвращает несохранённые коммуникационные параметры к исходным значениям. + +SDO обслуживается в Operational и Pre-operational, в Stopped не отвечает. +Чтение: `0x1000:00`, `0x1001:00`, `0x1017:00`, `0x1018:00/01` и привязанные +I16-переменные. До обновления переменной возвращается `0x7FFF`. +Запись: U16 `0x1017:00`. Ноль отключает HB только вне XDi-net. +Неизвестный индекс, подиндекс, неподдерживаемая запись или команда дают +стандартный SDO abort. Серийный номер и код конкретного изделия не выдумываются. + +Это явный поднабор профиля индикации, а не полный EDS прошивки. LSS, +segmented SDO, EEPROM, произвольные конфигурационные записи, TPDO mapping, +двухканальное резервирование и механика привода не моделируются. + +## Контракт портов + +Память состояния выделяет вызывающий через `setp_deif_state_size`. +Буфер должен иметь выравнивание не слабее `uint64_t`; ctypes использует +массив `uint64`, JNI — direct ByteBuffer. Private-layout состояния не +передаётся между языками и не сериализуется. + +Все часы передаются как монотонные миллисекунды. C не вызывает ОС, +не использует Qt/JVM и не выделяет динамическую память. Каждый прибор +имеет отдельный контекст. JNI-порт сериализует операции экземпляра. + +`setp_deif_poll` выдаёт готовые кадры однократно. Порт обслуживает его +регулярно и после запросов SDO; очередь ограничена 16 событиями. Экранная +пауза CAN-журнала не должна останавливать протокол. Потеря подключения, +ошибка TX и закрытие владельца останавливают активный сервер. + +`setp_deif_make/master/service/angle_command` создают готовые сообщения. +`setp_deif_selector_info/readdress/route` обслуживают редакторы профиля: +UI не извлекает endian, смещения или адресные маски самостоятельно. +Локализованные тексты и мигание готовых показаний остаются в GUI. + +При отсутствии нативного ядра порт выдаёт явную ошибку. Python/Kotlin +fallback протокола отсутствует. Windows source build может использовать +дополнительную сборку того же C-ядра `setprotocol_deif.dll`; путь задаёт +приложение либо стандартный загрузчик находит компонент рядом с основной +DLL. Новый автономный EXE использует собственную основную DLL с DEIF. + +## Проверки + +`tests/fixtures/deif-xdi-v1.tsv` — единые эталонные байты для C, Python и JNI. +Их используют `tests/test_deif_xdi.c`, `python/tests/test_deif_shared.py` и +`ports/android/tests/.../deif/DeifXdiTest.kt`. Отдельно проверяются пассивные +90°, независимый фактический угол, Boot-up, NMT, heartbeat и отсутствие +выдуманного ответного PDO. Desktop дополнительно проверяет миграцию старой +пустой привязки без включения симуляции и без физического TX. diff --git a/c/set-protocol/docs/STM_INVERTOR_GAS.md b/c/set-protocol/docs/STM_INVERTOR_GAS.md new file mode 100644 index 0000000..d0a34d6 --- /dev/null +++ b/c/set-protocol/docs/STM_INVERTOR_GAS.md @@ -0,0 +1,38 @@ +# STM Invertor: GAS 0x03 и GAS SLOT 0x04 + +Узел: DeviceType `0x7`, DeviceID `0xE`. Обработчик `set_invertor_gas_handle()` выбирает формат только по MsgType и использует существующие кодеки `pcan_gas` и `pcan_gas_slot`. + +| Доступ | Адреса | Содержимое | +| --- | --- | --- | +| GAS `0x03` | Регистры `0x0000…0x02ED` / 0…749 | По 1…4 последовательных 16-битных значения; DLC 2/4/6/8 | +| GAS SLOT `0x04` | Слоты `0x0000…0x00F9` / 0…249 | Word0 — маска; Word1…Word3 — три значения; DLC 8 | + +Это две проекции **одних 750 значений**. Отдельного массива слотов нет. Для слота `s` и полезного слова `w=1…3`: `Register=3*s+w-1`. Обратное отображение: `Slot=Register/3`, `Word=1+Register%3`. Word0 не является регистром. + +Начальные адреса относятся к узлу 7/E и не перенумеровывают PERIPH. Наличие 750 адресов не требует выделения 1500 байт сплошной RAM: provider обслуживает только существующие данные и явно отклоняет неподключённые значения. + +## Примеры + +Запись регистра 1 значением `0x1234`: ID `0x17E30001`, DLC 2, Data `34 12`. + +Запись того же значения через слот 0 / Word2: ID `0x17E40000`, DLC 8, Data `04 00 00 00 34 12 00 00`. Word1 и Word3 сохраняются. + +Чтение любого представления использует DLC=0, Route=0. Ответ имеет Route=1. Первый регистровый ответ использует ID `0x1FE30000`, слотный — `0x1FE40000`. Прикладного ACK записи нет; при необходимости результат применения проверяется чтением статуса устройства. + +## Прикладной порт + +`read(context, address, value)` возвращает ноль для достоверного значения. Ошибка не подменяется успешным нулём. Регистровое чтение останавливается на первом недоступном адресе; слотный ответ отмечает маской каждую доступную позицию. Полностью недоступный слот не возвращает пустую успешную маску. + +`write(context, addresses, values, count)` получает выбранные адреса единым набором. Provider сначала проверяет все права, диапазоны, состояния и значения, затем применяет набор под общей блокировкой. Неотмеченные позиции слота ему не передаются. Масочная запись не делает скрытого чтения и не требует перезаписывать readonly-соседей. + +Регистровый адаптер сначала собирает все слова через существующий кодек 0x03. Он не вызывает write-provider при частичном выходе за 750 регистров. Усиленная проверка относится к этому адаптеру изделия и не меняет общий контракт блочной записи `pcan_gas` для остальных устройств. + +Вызовы обработки одного экземпляра сериализуются приложением. Снимок чтения и применение записи защищает приложение; библиотека не включает прерывания, аппаратные разрешения или HAL. Наличие запроса на запись не подтверждает физическое выполнение команды силовой частью. + +## Клиентские привязки + +`set_invertor_gas_build()` возвращает готовые CAN ID и октеты; `set_invertor_gas_decode()` — проверенные тип, адрес и wire-слова ответа. Python/JNI не повторяют endian, маски идентификатора или формулу адресации. Размеры карт получают через `set_invertor_gas_limit()`. + +`set_invertor_gas_register()` и `set_invertor_gas_coordinate()` возвращают проверенные координаты, не меняя выходы при отказе. Диспетчер не переводит повреждённый 0x04 в 0x03 и не принимает запросы соседнего узла либо эхо ответа как запись. + +Тест `test_invertor_gas.c` проверяет оба wire-вектора, общий источник данных, маски, сохранение соседей, readonly, единственное применение набора, границы 249/749, ошибочные длины, флаги и адреса, а также клиентский ABI. diff --git a/c/set-protocol/include/deif_xdi.h b/c/set-protocol/include/deif_xdi.h new file mode 100644 index 0000000..7924820 --- /dev/null +++ b/c/set-protocol/include/deif_xdi.h @@ -0,0 +1,41 @@ +#ifndef SETP_DEIF_XDI_H /* Защита переносимого ABI от повторного включения. */ +#define SETP_DEIF_XDI_H /* Контракт DEIF одинаков для ctypes, JNI и прошивок. */ +#include "pcan_abi.h" /* Общий макрос экспорта и целочисленные типы стабильного ABI. */ +#ifdef __cplusplus /* C++-потребители должны видеть тот же немодифицированный ABI символов. */ +extern "C" { /* Реализация остаётся C99 для ctypes, JNI и MCU. */ +#endif /* Начало совместимого C ABI. */ +typedef struct { /* Готовый стандартный CAN-кадр; упаковка транспортного моста остаётся отдельным слоем. */ + uint32_t can_id; /* 11-битный COB-ID, вычисленный ядром. */ + uint32_t dlc; /* Число используемых байтов Classic CAN, от нуля до восьми. */ + uint8_t data[8]; /* Готовые wire-байты без преобразований в GUI. */ +} setp_deif_frame_t; /* Размер структуры фиксирован: 16 байт без указателей. */ +PCAN_ABI_API int setp_deif_version(void); /* Версия этого независимого расширения ABI. */ +PCAN_ABI_API size_t setp_deif_state_size(void); /* Размер состояния, память выделяет вызывающая сторона. */ +PCAN_ABI_API int setp_deif_catalog(unsigned field, int32_t *metadata); /* base, subindex, десятичное разрешение; поле 0…7. */ +PCAN_ABI_API int setp_deif_selector(unsigned field, unsigned node, unsigned instance, unsigned dam, uint8_t *out); /* Четырёхбайтный селектор без дублирования endian в портах. */ +PCAN_ABI_API int setp_deif_inspect(uint32_t id, const uint8_t *data, size_t size, unsigned flags, int32_t *out); /* Метаданные кадра и детерминированный код ошибки. */ +PCAN_ABI_API int setp_deif_make(unsigned field, unsigned node, unsigned instance, int32_t value, uint32_t id, unsigned dam, setp_deif_frame_t *out); /* Данные MASTER, яркость и палитра. */ +PCAN_ABI_API int setp_deif_master(unsigned node, const int32_t *values, uint32_t id, unsigned dam, unsigned angle_subindex, setp_deif_frame_t *out); /* Порядок пяти объектов MASTER хранится только в C-каталоге. */ +PCAN_ABI_API int setp_deif_service(unsigned kind, unsigned node, unsigned value, setp_deif_frame_t *out); /* kind=0 строит heartbeat, kind=1 — локальную команду NMT. */ +PCAN_ABI_API int setp_deif_angle_command(const setp_deif_frame_t *actual, setp_deif_frame_t *out); /* Строит отдельное задание из фактического угла без изменения wire-байтов в UI. */ +PCAN_ABI_API int setp_deif_selector_info(const uint8_t *selector, int32_t *out); /* Node, index, subindex и DAM для редактора привязок. */ +PCAN_ABI_API int setp_deif_readdress(const uint8_t *selector, unsigned node, unsigned mode, uint8_t *out); /* Переносит только адрес; mode=2 сохраняет текущий SAM/DAM. */ +PCAN_ABI_API int setp_deif_route(unsigned node, unsigned dam, uint32_t previous, unsigned operation, uint32_t *out); /* COB-ID, Node и адрес; operation=0 согласовать, 1 сохранить, 2 извлечь Node. */ +PCAN_ABI_API int setp_deif_validate(uint32_t id, const uint8_t *data, size_t size, unsigned flags, unsigned slave); /* slave=0 проверяет MASTER, иначе серверные ответы своего узла. */ +PCAN_ABI_API int setp_deif_init(void *state, size_t size, unsigned node, unsigned source, unsigned xdi, unsigned self_start, unsigned pdo, unsigned group, unsigned services, uint32_t monitor_id); /* Инициализация профиля без системных часов и heap. */ +PCAN_ABI_API int setp_deif_bind(void *state, unsigned field, const uint8_t *selector, double scale); /* Привязка индикатора к явному объекту библиотеки. */ +PCAN_ABI_API int setp_deif_get_binding(void *state, unsigned field, uint8_t *selector, double *scale); /* Готовая привязка и масштаб текущего C-профиля. */ +PCAN_ABI_API int setp_deif_default_angle(void *state, uint8_t *selector, double *scale); /* Восстановление отсутствующей старой привязки с адресацией существующих полей. */ +PCAN_ABI_API int setp_deif_start(void *state, uint64_t now, unsigned reset_values); /* Время задаётся монотонными миллисекундами порта. */ +PCAN_ABI_API void setp_deif_stop(void *state); /* Выключенный узел прекращает все служебные события. */ +PCAN_ABI_API void setp_deif_reset_values(void *state); /* Сбрасывает только измерения и их временные отметки. */ +PCAN_ABI_API int setp_deif_accept(void *state, uint32_t id, const uint8_t *data, size_t size, unsigned flags, uint64_t now); /* Приём STD MPDO, NMT и expedited SDO. */ +PCAN_ABI_API size_t setp_deif_poll(void *state, uint64_t now, setp_deif_frame_t *out, size_t capacity); /* Таймеры и однократная выдача очереди Boot-up/HB/EMCY/SDO. */ +PCAN_ABI_API int setp_deif_read(void *state, unsigned field, uint64_t now, unsigned expiry_ms, double *value, int64_t *metadata); /* metadata: seen_ms, raw, статус, флаги seen/valid/stale. */ +PCAN_ABI_API unsigned setp_deif_lost(void *state, uint64_t now); /* Маска входов без обновлений 3000 мс. */ +PCAN_ABI_API int setp_deif_angle(void *state, uint64_t now, unsigned expiry_ms, double *value); /* 1 — фактический угол, 2 — явно помеченное задание, 0 — нет данных. */ +PCAN_ABI_API int setp_deif_meta(void *state, int64_t *out); /* active, NMT, start, lastHB, period, accepted, group, brightness, palette, error. */ +#ifdef __cplusplus /* Закрываем C ABI перед возвратом в C++-контекст клиента. */ +} /* extern C не влияет на чистый C99-компилятор. */ +#endif /* Совместимость с C++-портами. */ +#endif /* SETP_DEIF_XDI_H: все алгоритмы находятся в единственном C99-модуле. */ diff --git a/c/set-protocol/include/set_invertor_gas.h b/c/set-protocol/include/set_invertor_gas.h new file mode 100644 index 0000000..d23a038 --- /dev/null +++ b/c/set-protocol/include/set_invertor_gas.h @@ -0,0 +1,32 @@ +#ifndef SET_INVERTOR_GAS_H /* Общий C-контракт связывает два вида GAS с одним источником значений инвертора. */ +#define SET_INVERTOR_GAS_H /* Защищаем объявления от повторного подключения прошивкой и host-клиентом. */ +#include "set_invertor_profile.h" /* Число слотов и пара DeviceType/DeviceID определены единственным профилем Templates. */ +#include "pcan_gas.h" /* Тип 0x3 использует существующую карту регистров и общий разбор Classic CAN. */ +#include "pcan_gas_slot.h" /* Тип 0x4 сохраняет маску Word0 и три выбранных значения. */ +#ifdef __cplusplus /* Клиенты C++ должны обращаться к обычным C-символам общей библиотеки. */ +extern "C" { /* Открываем совместимую область связывания без изменения wire-формата. */ +#endif /* Последующие объявления также доступны компилятору C99. */ +#define SET_INVERTOR_GAS_REGISTER_FIRST 0U /* В отдельном узле 7/E регистровая карта начинается с нуля независимо от PERIPH. */ +#define SET_INVERTOR_GAS_SLOT_FIRST 0U /* Выделенные 250 слотов отдельного инвертора имеют адреса 0x0000…0x00F9. */ +#define SET_INVERTOR_GAS_REGISTER_COUNT SET_INVERTOR_GAS_WORD_COUNT /* 750 регистров 0x0000…0x02ED хранят те же значения, что слоты типа 0x4. */ +#if SET_INVERTOR_GAS_WORDS_PER_SLOT != PCAN_GAS_SLOT_DATA_WORDS /* Геометрия профиля обязана совпадать с актуальным масочным кодеком Templates. */ +#error "Invertor GAS profile and slot codec disagree" /* При несовместимом изменении профиля прекращаем сборку вместо тихого смещения регистров. */ +#endif /* Проверка действует одинаково для host и STM32. */ +typedef int (*set_invertor_gas_read_fn)(void *context, uint16_t address, uint16_t *value); /* Ноль подтверждает одно достоверное значение; ошибка не подменяется успешным нулём. */ +typedef int (*set_invertor_gas_write_fn)(void *context, const uint16_t *addresses, const uint16_t *values, unsigned count); /* Provider проверяет все 1…4 адреса, права и значения до единственного согласованного применения. */ +typedef struct { /* Приложение владеет данными и отвечает за блокировку общего снимка на время одного вызова обработчика. */ + void *context; /* Контекст связывает регистровый и слотный доступ с одним состоянием изделия. */ + set_invertor_gas_read_fn read; /* NULL отключает чтение, сохраняя возможность явно разрешённой записи. */ + set_invertor_gas_write_fn write; /* NULL полностью запрещает запись, в том числе команды через регистры. */ +} set_invertor_gas_port_t; /* Структура не требует выделения массива из всех 750 слов или аппаратных заголовков. */ +PCAN_ABI_API int32_t set_invertor_gas_register(unsigned slot, unsigned word); /* Возвращает регистр для Word1…Word3 либо -1 для маски и координат за пределами 250 слотов. */ +PCAN_ABI_API int set_invertor_gas_coordinate(unsigned address, uint16_t *slot, uint16_t *word); /* Обратное отображение не изменяет выходы при ошибке; Word0 никогда не является значением. */ +PCAN_ABI_API uint32_t set_invertor_gas_request_id(unsigned message_type, unsigned address, unsigned route); /* Общий кодек строит только MsgType 0x3/0x4 узла 7/E с адресами внутри соответствующей карты. */ +PCAN_ABI_API int set_invertor_gas_handle(const set_invertor_gas_port_t *port, const pcan_frame_t *request, pcan_frame_t *response); /* Возвращает 1 для готового ответа, 0 для применённой записи, -1 при отказе; автоматического fallback нет. */ +PCAN_ABI_API unsigned set_invertor_gas_limit(unsigned message_type); /* Возвращает 750 регистров для 0x3, 250 слотов для 0x4 либо ноль для неизвестного типа. */ +PCAN_ABI_API int set_invertor_gas_build(unsigned message_type, unsigned address, const uint16_t *words, unsigned count, uint32_t *identifier, uint8_t data[8]); /* Тонкие языковые порты получают готовые ID и октеты: count=0 означает чтение, результат — DLC либо -1. */ +PCAN_ABI_API int set_invertor_gas_decode(uint32_t identifier, unsigned flags, const uint8_t *data, unsigned length, unsigned metadata[2], uint16_t words[4]); /* Проверяет ответ 7/E; metadata содержит тип и адрес, результат — число 16-битных wire-слов либо -1. */ +#ifdef __cplusplus /* Закрываем область C ABI только при использовании C++-компилятора. */ +} /* Завершаем публичные объявления общей библиотеки. */ +#endif /* Условие не меняет смысл C99-интерфейса. */ +#endif /* Конец контракта двух представлений одних данных инвертора. */ diff --git a/c/set-protocol/include/setprotocol.h b/c/set-protocol/include/setprotocol.h index 18f86e3..1e32880 100644 --- a/c/set-protocol/include/setprotocol.h +++ b/c/set-protocol/include/setprotocol.h @@ -11,6 +11,7 @@ #ifndef SETPROTOCOL_H #define SETPROTOCOL_H #include "set_regmap.h" +#include "set_invertor_gas.h" /* Предоставляет обе карты одного инвертора через единую точку подключения общей библиотеки. */ #include "set_emu_server.h" #include "balsam_can.h" diff --git a/c/set-protocol/ports/android/Android.mk b/c/set-protocol/ports/android/Android.mk index 9458a79..c221a7a 100644 --- a/c/set-protocol/ports/android/Android.mk +++ b/c/set-protocol/ports/android/Android.mk @@ -31,6 +31,8 @@ LOCAL_SRC_FILES := \ setprotocol_jni.c LOCAL_SRC_FILES += ../../src/set_gas_watch.c # Включает общий GAS_WATCH2 в NDK-библиотеку для новых тонких JNI-вызовов. LOCAL_SRC_FILES += ../../src/pcan_gas_slot.c gas_slots_jni.c # Android собирает общий слотный кодек и тонкую JNI-границу без второго алгоритма. +LOCAL_SRC_FILES += ../../src/set_invertor_gas.c # Android получает ту же C-реализацию нового профиля без копирования алгоритмов. LOCAL_CFLAGS := -std=c99 -Wall -Wextra -Wpedantic -fvisibility=hidden LOCAL_LDLIBS := -llog -lm +LOCAL_SRC_FILES += ../../src/deif_xdi.c set_deif_jni.c # Общий DEIF и тонкий JNI без второй реализации. include $(BUILD_SHARED_LIBRARY) diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifXdi.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifXdi.kt new file mode 100644 index 0000000..77c42b9 --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifXdi.kt @@ -0,0 +1,60 @@ +package ru.setcorp.setprotocol.deif // Общий Android/JVM-порт находится в templates, без зависимости от приложения. +import java.nio.ByteBuffer // Caller-owned direct buffer обеспечивает выравнивание и управляемое JVM время жизни. +import ru.setcorp.setprotocol.NativeSetProtocol // Загрузку общей нативной библиотеки выполняет существующий порт. + +internal object NativeDeifBridge { // Только JNI-объявления; здесь нет второго кодека или автомата. + external fun size(): Int // Размер private C-состояния сообщает само ядро. + external fun initialize(state: ByteBuffer, node: Int, source: Int, xdi: Boolean, automatic: Boolean, pdo: Int, group: Int, services: Boolean, monitorId: Long): Boolean // Профиль проверяет C. + external fun start(state: ByteBuffer, nowMs: Long, reset: Boolean): Boolean // Время передаётся явно для воспроизводимых тестов. + external fun stop(state: ByteBuffer) // Выключенный сервер не производит heartbeat. + external fun accept(state: ByteBuffer, id: Long, data: ByteArray, flags: Int, nowMs: Long): Boolean // Байты обрабатывает общий C-код. + external fun poll(state: ByteBuffer, nowMs: Long): IntArray // Готовые события возвращаются скалярами id, dlc, data[8]. + external fun snapshot(state: ByteBuffer, nowMs: Long, expiryMs: Int): DoubleArray // Числа, статусы и сроки уже вычислены в C. + external fun inspect(id: Long, data: ByteArray, flags: Int): IntArray // Журнал получает единый результат декодирования. +} // ABI не раскрывает Kotlin layout внутренних структур. +data class DeifFrame(val canId: Long, val data: ByteArray) // Готовый STD data frame передаётся существующему CAN-транспорту приложения. +data class DeifSnapshot( // Неизменяемая модель пригодна для Compose, другого GUI и JVM-тестов. + val active: Boolean, val nmt: Int, val accepted: Int, // Жизненный цикл и число действительно принятых MPDO. + val brightness: Int, val palette: Int, // Значения текущего профиля подсветки. + val angle: Double?, val angleIsSetpoint: Boolean, // Фактический угол отделён от явно помеченного принятого задания. + val values: List, val statuses: List, val lostFields: Set, // Порядок полей задаёт общий ABI C, недействительные числа не подменяются нулями. +) // Снимок не содержит указателя на состояние C. +class DeifXdi(node: Int = 13, source: Int = 1, xdi: Boolean = true, automatic: Boolean = true, pdo: Int = 2, group: Int = 1, private val services: Boolean = false, monitorId: Long = 0) : AutoCloseable { // Один независимый прибор на экземпляр. + private val state: ByteBuffer // Память остаётся управляемой JVM, C не использует malloc. + init { // Отсутствующее ядро является явной ошибкой сборки, fallback на Kotlin запрещён. + check(NativeSetProtocol.available) { "Не загружено общее C-ядро DEIF" } // Приложение не должно молча использовать иной алгоритм. + state = ByteBuffer.allocateDirect(NativeDeifBridge.size()) // Размер и выравнивание не дублируют private C-структуру. + require(NativeDeifBridge.initialize(state, node, source, xdi, automatic, pdo, group, services, monitorId)) { "DEIF: неверный Node-ID, источник или RPDO" } // Ограничения XDi-net проверяет C. + } // Создание пассивного наблюдателя ничего не отправляет. + @Synchronized fun start(nowMs: Long, resetValues: Boolean = true) { // NMT и таймеры безопасны при приёме и тиках из разных потоков. + check(NativeDeifBridge.start(state, nowMs, resetValues)) { "Не удалось запустить DEIF" } // Boot-up создаётся только в C-сервере. + } // Физическую отправку выбирает приложение по результату poll. + @Synchronized fun accept(id: Long, data: ByteArray, extended: Boolean = false, remote: Boolean = false, nowMs: Long): Boolean { // Имя транспорта фильтруется приложением до этого вызова. + return NativeDeifBridge.accept(state, id, data, (if (extended) 1 else 0) or (if (remote) 2 else 0), nowMs) // Флаги описывают тип CAN, payload не разбирается в Kotlin. + } // C различает NMT, MPDO, heartbeat и SDO. + @Synchronized fun poll(nowMs: Long): List { // События выдаются один раз, даже если экран журнала стоит на паузе. + return NativeDeifBridge.poll(state, nowMs).asList().chunked(10).map { row -> DeifFrame(row[0].toLong(), ByteArray(row[1]) { row[it + 2].toByte() }) } // Это раскладка скалярного ABI JNI, а не CAN-кодек. + } // Готовые byte arrays не меняются в транспортном слое. + @Synchronized fun snapshot(nowMs: Long): DeifSnapshot { // Тайм-аут наблюдателя сохраняет совместимость Desktop; сервер хранит число при Data lost. + val raw = NativeDeifBridge.snapshot(state, nowMs, if (services) 0 else 5000) // Правило выбора и свежести вычисляет C. + return DeifSnapshot(raw[0] != 0.0, raw[1].toInt(), raw[5].toInt(), raw[7].toInt(), raw[8].toInt(), if (raw[34] != 0.0) raw[35] else null, raw[34] == 2.0, // Метаданные профиля и готовый азимут. + List(6) { field -> val flags = raw[13 + field * 4].toInt(); if (flags and 2 != 0 && flags and 4 == 0) raw[10 + field * 4] else null }, // Порт отображает valid/stale-флаги, не повторяя таймеры. + List(6) { raw[12 + it * 4].toInt() }, (0..5).filter { raw[36].toInt() and (1 shl it) != 0 }.toSet()) // Имена и цвета ошибок остаются ответственностью GUI. + } // Снимок можно использовать в UI без обращения к байтам протокола. + @Synchronized override fun close() { NativeDeifBridge.stop(state) } // Жизненный цикл отменяет heartbeat и очередь до закрытия адаптера. + companion object { // Диагностическое представление не содержит второго парсера. + fun inspect(id: Long, data: ByteArray, extended: Boolean = false, remote: Boolean = false): IntArray { // Приложение передаёт целый транспортный кадр. + check(NativeSetProtocol.available) { "Не загружено общее C-ядро DEIF" } // Без C-декодера никакого угадывания формата нет. + return NativeDeifBridge.inspect(id, data, (if (extended) 1 else 0) or (if (remote) 2 else 0)) // Индексы результата закреплены ABI deif_xdi.h. + } // Один и тот же вход даёт одинаковые поля в Python и Android. + fun summary(id: Long, data: ByteArray, extended: Boolean = false, remote: Boolean = false): String { // Только локализованный текст поверх метаданных C. + val m = inspect(id, data, extended, remote) // Никакого endian-разбора в Kotlin. + return when (m[0]) { // Код результата сообщает общий декодер. + 2 -> "Heartbeat/Boot-up · Node ${m[2]} · NMT ${m[11]}" // Состояние относится к узлу, а не к подтверждению показаний. + 1 -> "DEIF Node ${m[2]} · 0x%04X:%02X · RAW %d".format(m[3], m[4], m[5]) // Физические единицы показывает отдельный снимок модели. + 3 -> "DEIF · яркость/Color · ${m[5]}" // U8 подсветки не получает масштаб RPM. + else -> "DEIF: кадр не принят, код ${m[0]}" // Неизвестный объект не интерпретируется заново портом. + } // Текст не влияет на протокольное принятие. + } // Журнал и индикатор используют один C-каталог. + } // Общий порт не зависит от конкретного Android-приложения. +} // Контекст освобождается JVM после закрытия владельца. diff --git a/c/set-protocol/ports/android/set_deif_jni.c b/c/set-protocol/ports/android/set_deif_jni.c new file mode 100644 index 0000000..a637be5 --- /dev/null +++ b/c/set-protocol/ports/android/set_deif_jni.c @@ -0,0 +1,32 @@ +#include /* JNI здесь преобразует только контейнеры Java и указатели C. */ +#include "deif_xdi.h" /* Все правила DEIF принадлежат общему переносимому ядру. */ +static void *deif_context(JNIEnv *e,jobject b) { /* Direct ByteBuffer владеет памятью состояния и освобождается JVM. */ + if(!b || (*e)->GetDirectBufferCapacity(e,b)<(jlong)setp_deif_state_size()) return NULL; return (*e)->GetDirectBufferAddress(e,b); /* Не допускаем короткий либо недиректный буфер. */ +} /* JNI не копирует private-layout структуры. */ +static int deif_input(JNIEnv *e,jbyteArray a,uint8_t *d) { /* Classic CAN ограничен восемью байтами. */ + jsize n; if(!a) return -1; n=(*e)->GetArrayLength(e,a); if(n>8) return -1; (*e)->GetByteArrayRegion(e,a,0,n,(jbyte *)d); return (int)n; /* Буфер локален текущему вызову JNI. */ +} /* Время жизни Java-указателей не пересекает вызов. */ +JNIEXPORT jint JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_size(JNIEnv *e,jobject self) { (void)e; (void)self; return (jint)setp_deif_state_size(); } /* Kotlin выделяет точно нужный объём без heap в C. */ +JNIEXPORT jboolean JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_initialize(JNIEnv *e,jobject self,jobject b,jint node,jint source,jboolean xdi,jboolean automatic,jint pdo,jint group,jboolean services,jlong id) { /* Параметры профиля передаются без повторения правил. */ + void *s=deif_context(e,b); (void)self; if(!s || id<0 || id>0x1FFFFFFF) return JNI_FALSE; return setp_deif_init(s,setp_deif_state_size(),(unsigned)node,(unsigned)source,xdi!=0,automatic!=0,(unsigned)pdo,(unsigned)group,services!=0,(uint32_t)id)?JNI_TRUE:JNI_FALSE; /* Сервисный Node и RPDO проверяет C. */ +} /* Источником исходных привязок остаётся C-каталог. */ +JNIEXPORT jboolean JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_start(JNIEnv *e,jobject self,jobject b,jlong now,jboolean reset) { (void)self; return now>=0 && setp_deif_start(deif_context(e,b),(uint64_t)now,reset!=0)?JNI_TRUE:JNI_FALSE; } /* Часы Android передаются в монотонных миллисекундах. */ +JNIEXPORT void JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_stop(JNIEnv *e,jobject self,jobject b) { (void)self; setp_deif_stop(deif_context(e,b)); } /* Закрытие сервера прекращает очередь событий. */ +JNIEXPORT jboolean JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_accept(JNIEnv *e,jobject self,jobject b,jlong id,jbyteArray a,jint flags,jlong now) { /* Принимается готовый транспортом CAN-кадр. */ + uint8_t d[8]; int n=deif_input(e,a,d); (void)self; if(n<0 || id<0 || id>0x1FFFFFFF || now<0) return JNI_FALSE; return setp_deif_accept(deif_context(e,b),(uint32_t)id,d,(size_t)n,(unsigned)flags,(uint64_t)now)?JNI_TRUE:JNI_FALSE; /* JNI не разбирает индекс, подиндекс или signed I16. */ +} /* C обслуживает адресацию, NMT и SDO одинаково для Desktop и Android. */ +JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_poll(JNIEnv *e,jobject self,jobject b,jlong now) { /* Очередь возвращается массивом скаляров ABI, а не вторым CAN-пакетом. */ + setp_deif_frame_t frames[16]; jint values[160]; size_t n,i,j; jintArray result; (void)self; if(now<0) return NULL; n=setp_deif_poll(deif_context(e,b),(uint64_t)now,frames,16); /* Все исходящие байты уже сформированы C-ядром. */ + for(i=0;iNewIntArray(e,(jsize)(n*10)); if(result) (*e)->SetIntArrayRegion(e,result,0,(jsize)(n*10),values); return result; /* Владение результатом передаётся JVM. */ +} /* JNI не создаёт фиктивного ответного PDO. */ +JNIEXPORT jdoubleArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_snapshot(JNIEnv *e,jobject self,jobject b,jlong now,jint expiry) { /* Универсальный скалярный снимок пригоден без Compose. */ + void *s=deif_context(e,b); int64_t meta[10],m[4]; jdouble values[37]; double v; unsigned i; jdoubleArray result; (void)self; if(now<0 || expiry<0 || !setp_deif_meta(s,meta)) return NULL; /* Layout состояния не дублируется в Kotlin. */ + for(i=0;i<10;i++) values[i]=(double)meta[i]; for(i=0;i<6;i++) { setp_deif_read(s,i,(uint64_t)now,(unsigned)expiry,&v,m); values[10+i*4]=v; values[11+i*4]=(double)m[0]; values[12+i*4]=(double)m[2]; values[13+i*4]=(double)m[3]; } /* Масштабы, статусы и свежесть вычислены C. */ + values[34]=(double)setp_deif_angle(s,(uint64_t)now,(unsigned)expiry,&v); values[35]=v; values[36]=setp_deif_lost(s,(uint64_t)now); /* Факт, задание и Data lost определяются общим алгоритмом. */ + result=(*e)->NewDoubleArray(e,37); if(result) (*e)->SetDoubleArrayRegion(e,result,0,37,values); return result; /* Java получает независимый неизменяемый снимок. */ +} /* Здесь нет системных часов и транспортного ввода-вывода. */ +JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_inspect(JNIEnv *e,jobject self,jlong id,jbyteArray a,jint flags) { /* Общий декодер доступен также журналу Android. */ + uint8_t d[8]; int32_t m[12]={0}; jint values[13]; int n=deif_input(e,a,d),code; unsigned i; jintArray result; (void)self; code=n<0 || id<0 || id>0x1FFFFFFF?-1:setp_deif_inspect((uint32_t)id,d,(size_t)n,(unsigned)flags,m); /* Только C знает wire-формат DEIF. */ + values[0]=code; for(i=0;i<12;i++) values[i+1]=m[i]; result=(*e)->NewIntArray(e,13); if(result) (*e)->SetIntArrayRegion(e,result,0,13,values); return result; /* Метаданные индексируются по ABI, не по смещениям CAN-data. */ +} /* Локализованные подписи формирует Kotlin. */ diff --git a/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/deif/DeifXdiTest.kt b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/deif/DeifXdiTest.kt new file mode 100644 index 0000000..c43038d --- /dev/null +++ b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/deif/DeifXdiTest.kt @@ -0,0 +1,34 @@ +package ru.setcorp.setprotocol.deif // JVM-тест использует тот же пакет и JNI-порт, что Android GUI. +import org.junit.Assert.* // Сверяются значения, тип индикации и точные служебные байты. +import org.junit.Test // Обычный локальный JUnit запускает нативную host-библиотеку. +class DeifXdiTest { // В тесте отсутствует Kotlin-копия протокола. + private fun bytes(hex: String) = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() // Конвертируется только текст тестовой fixture. + @Test fun sameVectorsAsCAndPython() { // Общий TSV фиксирует межъязыковую совместимость кодека. + val lines = javaClass.getResourceAsStream("/deif-xdi-v1.tsv")!!.bufferedReader().use { it.readLines() } // Fixture поставляется тем же submodule templates. + lines.filter { it.isNotBlank() && !it.startsWith("#") }.forEach { line -> // Комментарии не являются тестовыми пакетами. + val row = line.trim().split(Regex("\\s+")) // Столбцы описывают ожидаемый результат независимо от реализации C. + val flags = row[2].toInt() // Метаданные теста задают STD/EXT/RTR. + val result = DeifXdi.inspect(row[0].toLong(16), bytes(row[1]), flags and 1 != 0, flags and 2 != 0) // JNI вызывает единственный C-декодер. + assertEquals(line, row[3].toInt(), result[0]); assertEquals(line, row[4].toInt(), result[5]) // Сверяем тип принятия и знаковый RAW. + assertEquals(line, row[5].toInt(16), result[3]); assertEquals(line, row[6].toInt(16), result[4]) // Индекс и подиндекс не декодируются повторно в Kotlin. + } // Каждый ошибочный формат проверяется тем же контрактом. + } // Тест не зависит от Android Runtime. + @Test fun passiveNinetyDegreesDoesNotRequireSimulation() { // Воспроизводит выключенный флаг симуляции на снимке пользователя. + DeifXdi().use { model -> // Пассивный наблюдатель не создаёт heartbeat. + assertTrue(model.accept(0x201, bytes("0101300784030000"), nowMs = 10000)) // MASTER Node 1 передаёт сетевое задание RAW 900. + assertEquals(90.0, model.snapshot(10000).angle!!, 0.0); assertTrue(model.snapshot(10000).angleIsSetpoint) // 90° видны с честной пометкой задания. + assertTrue(model.poll(10000).isEmpty()) // Показание не требует физической отправки или включённого SLAVE-сервера. + assertTrue(model.accept(0x201, bytes("01013002E5010000"), nowMs = 10100)) // Отдельное фактическое сообщение передаёт 48,5°. + assertEquals(48.5, model.snapshot(10100).angle!!, 0.0); assertFalse(model.snapshot(10100).angleIsSetpoint) // Угол и тип отображения совпадают с C- и Python-тестами. + } // Close освобождает протокольный сеанс, память direct buffer управляется JVM. + } // Черновика MASTER в API общего ядра вообще нет. + @Test fun slaveOnlySendsHeartbeatForNormalMpdo() { // Обычный приём рассылки не должен создавать ответный PDO. + DeifXdi(node = 13, services = true).use { model -> // Сервисный режим использует адрес Node 13. + model.start(0); val boot = model.poll(0).single() // Boot-up должен быть выдан ровно один раз. + assertEquals(0x70DL, boot.canId); assertArrayEquals(byteArrayOf(0), boot.data) // COB-ID и DLC совпадают с физическим DEIF-профилем. + assertArrayEquals(byteArrayOf(5), model.poll(1000).single().data) // Self-start завершается через 1000 мс. + assertTrue(model.accept(0x201, bytes("0101300784030000"), nowMs = 1000)) // Измерение принимается через общий C-автомат. + assertTrue(model.poll(1000).isEmpty()); assertEquals(90.0, model.snapshot(1000).angle!!, 0.0) // Есть обновлённое показание, но отсутствует выдуманный ACK-PDO. + } // Нативная очередь останавливается при закрытии владельца. + } // Все события проверены на точных монотонных сроках. +} // Этот же класс теста подключается к Android sourceSets из templates. diff --git a/c/set-protocol/ports/stm32g474-hope/README.md b/c/set-protocol/ports/stm32g474-hope/README.md new file mode 100644 index 0000000..ce14587 --- /dev/null +++ b/c/set-protocol/ports/stm32g474-hope/README.md @@ -0,0 +1,91 @@ +# HOPE STM32G474VET6: HAL-порт эмуляторов + +Общий аппаратный порт платы HOPE G474VET6, опознанной пользователем по +[фотографии платы с тремя CAN-разъёмами](https://www.ebay.com/itm/397096571989). +Найдена [исходная схема от 14.04.2025](docs/HOPE_G474VE_schematic_20250414.pdf): +USART1 PA9/PA10 соединён с CH340N через JP2, FDCAN1 PD0/PD1 — с U6 +TJA1051T/3 и CAN1L/CAN1H разъёма J4. Кварц JT1 имеет частоту 8 МГц. +Источник PDF: пользовательская папка +`F:/programm/utilits/china/aliexpresы/stm32g474vet_fdcan`, файл +`G474VET6板子原理图20250414.pdf`. Копия сохранена побайтно без изменения схемы. +Публичный репозиторий производителя и исходники заводских примеров пока не найдены. +Аппаратный запуск на физической плате ещё не выполнен. + +Этот порт находится в общем репозитории `templates`. Пример интеграции Keil: +`stm32f407_emulator/G474/HOPE`. Локальные `Src` — include-обёртки; +модели УМП, 2812 и IGBT, DAC-протокол, GAS и кодеки не копируются. + +## Подключение + +Включите этот каталог в include path перед портом USB boot G474RB. +Предоставьте `board_config.h`, включающий `board_config.template.h`. +Компилируйте `board.c`, общий `stm32g474-periph/main.c`, +`stm32g474-periph/transport.c`, `stm32g474-periph/wave_hal.c` и +`stm32g474-usb-boot/boot_usb_port.c`. Нужны HAL G4 UART/FDCAN/PCD/RCC/PWR, +DAC/DMA/TIM, CMSIS startup G474, ST USB Device CDC и обычная композиция моделей. +`EMU_CAPACITY=400`: один архив 32000 байт — в SRAM, второй — в CCM 32 КиБ; +таблица DAC 8192 байта должна оставаться в SRAM, доступной DMA. + +| Интерфейс | Назначение | +| --- | --- | +| USART1 PA9 TX / PA10 RX AF7 | RTU и SETGUI v1/GAS, 115200 бод, 8N1 | +| FDCAN1 PD0 RX / PD1 TX AF9 | U6 TJA1051T/3, CAN1L/CAN1H J4; Classic CAN, 250 кбит/с, extended RX 0xBA0004 / TX 0xBA0024 | +| USB PA11 D− / PA12 D+ | Нативный USB CDC SET v2, отдельный от USB–UART платы | +| DAC1 channel 1 PA4 | TIM6 TRGO → DMAMUX → DMA1 channel 1, до 1 млн отсчётов/с | +| USART3 PD8 TX / PD9 RX, PD10 DE | Встроенный RS-485 SP3485EN-L; порт его не инициализирует | +| FDCAN2 PB12 RX / PB13 TX | U7 TJA1051T/3; порт его не инициализирует | +| FDCAN3 PA8 RX / PA15 TX | U8 TJA1051T/3; порт его не инициализирует | +| SPI4 PE4 CS / PE2 SCK / PE5 MISO / PE6 MOSI | W25Q64; порт его не инициализирует | +| KEY1/2/3 PE7/8/9; LED1/2/3 PF2/9/10 | Заводские органы управления; порт их не инициализирует | + +По умолчанию CPU=160 МГц от кварца HSE 8 МГц/PLL; APB1=80 МГц, TIM6=160 МГц, +USART1 от APB2=160 МГц. USB использует HSI48 с CRS по SOF. +HSE определяет точность CAN и DAC; CRS подстраивает только USB. +`EMU_USE_HSE=0` выбирает резервный HSI16. Порт также допускает HSE 24 МГц +при физической замене кварца и соответствующем `EMU_HSE_HZ`. +RTC-кварц JT2 32,768 кГц не участвует в системной PLL. + +## Совместимость загрузчика + +Найденный исходный проект: `https://git.rd12.ru/setcorp/CANdle_stm32.git`. +Название `candle_474` не подтверждено как отдельный доступный репозиторий. +Сам reusable загрузчик уже находится в актуальном общем `templates`: +[`c/protocan-boot/ports/stm32g474`](../../../protocan-boot/ports/stm32g474/README.md). +Его модули `pcan_boot` и `rs485-boot` обслуживают CAN и UART. Данный порт +эмулятора не включает загрузочную машину состояний в основное приложение. + +Для установленного CANdle boot задайте `EMU_FLASH_BASE=0x08008000`, +Flash приложения 480 КиБ и RAM `0x20000000..0x20013FF7`. +Исключите последние восемь байт прежних 80 КиБ SRAM: загрузчик проверяет +initial MSP до `0x20014000`, маркер CANB хранится в `0x20013FFC`. +Начальные векторы должны находиться по тому же адресу, что VTOR. +Образ с `0x08010000`, рассчитанный на RB USB boot, здесь несовместим. + +Только для CANdle-профиля компилируйте `boot_request.c` и определите +`EMU_USB_BOOT_ENABLE`. Адресная USB SET v2 команда `0x1003` без payload +записывает `0x43414E42` в SRAM и через 500 мс перезагружает MCU. +После reset работает существующий CAN/UART boot: FDCAN1 500 кбит/с, +ProtoCAN Type=7/ID=15; USART1 512000 бод. USB-прошивальщик RB +`flash-usb.py --target g474` этому загрузчику не соответствует. +Исходный общий boot использовал PB8/PB9, которые не соединены с CAN1 HOPE: +PB8 занят BOOT0. Готовый [адаптер BOOT HOPE](../../../protocan-boot/ports/stm32g474-hope/README.md) +подключает тот же загрузчик к PD0/PD1 AF9. Он собирается отдельным проектом +`HOPE_G474VE_BOOT`, включённым в `474-hope.uvmpw`. UART PA9/PA10, Flash-слот +и SRAM-маркер сохранены. Для обновления установленного BOOT нужен его новый HEX через SWD. +Автоматический вход по CAN ENTER_BOOT или UART BEGIN из работающего эмулятора +в этой интеграции не добавлен; для них используйте окно загрузчика после reset. + +Самостоятельный профиль задаёт `EMU_FLASH_BASE=0x08000000` и не определяет +`EMU_USB_BOOT_ENABLE`. Он заменяет загрузчик, если записывается через SWD. +512 КиБ Flash проверяются при запуске; DBANK=0 и DBANK=1 допускаются, +перестановка BFB2/FB_MODE отвергается. Option Bytes порт не изменяет. + +## IGBT + +Модель доступна во всех профилях через регистры: `0x140C=1` выбирает +программный TX, `0x1403` задаёт маску, `0x1404` возвращает RX. +Физические GPIO по умолчанию отключены (`EMU_IGBT_GPIO_ENABLE=0`): +PB12/PB13 прежнего ручного профиля соединены со штатным трансивером CAN2. +Профиль Vin=PB0/1/2/10..14 и Vstat=PC0..PC7 допустим только после физического +устранения этого соединения или изменения профиля на свободные выводы. +Адреса и алгоритм IGBT остаются общими для всех MCU. diff --git a/c/set-protocol/ports/stm32g474-hope/board.c b/c/set-protocol/ports/stm32g474-hope/board.c new file mode 100644 index 0000000..aa49daa --- /dev/null +++ b/c/set-protocol/ports/stm32g474-hope/board.c @@ -0,0 +1,121 @@ +#include "board.h" /* Подключает публичный интерфейс аппаратного HAL-слоя HOPE. */ +#include "board_config.h" /* Выбирает размещение Flash, USART1 и разрешение физических выводов IGBT. */ +#include "transport.h" /* Объявляет запуск общего UART/FDCAN-транспорта после настройки GPIO. */ +#include "emulator.h" /* Подключает единый шаг моделей УМП, 2812, IGBT и генератора. */ +#include "igbt_emulator.h" /* Передаёт физическую маску Vin и получает маску ответов Vstat. */ +#include "stm32g4xx_hal.h" /* Предоставляет HAL RCC/PWR/GPIO и определения регистров Cortex-M4. */ +volatile uint32_t clock_fault; /* Хранит диагностический код запуска для просмотра через SWD. */ +uint32_t SystemCoreClock = 16000000u; /* После reset HSI16 работает на 16 МГц до настройки PLL HAL. */ +const uint8_t AHBPrescTable[16] = {0,0,0,0,0,0,0,0,1,2,3,4,6,7,8,9}; /* CMSIS-кодирует делители HPRE как степени двойки по RM0440. */ +const uint8_t APBPrescTable[8] = {0,0,0,0,1,2,3,4}; /* HAL использует кодировку PPRE для вычисления частоты периферии G4. */ +static uint8_t usb_clock_ready; /* До настройки HSI48/CRS предотвращает преждевременный запуск USB. */ +#if EMU_IGBT_GPIO_ENABLE /* Не занимает выводы FLASH/CAN2/CAN3 на неподтверждённой ревизии HOPE по умолчанию. */ +static const uint16_t input_pins[8] = {GPIO_PIN_0,GPIO_PIN_1,GPIO_PIN_2,GPIO_PIN_10,GPIO_PIN_11,GPIO_PIN_12,GPIO_PIN_13,GPIO_PIN_14}; /* Ручной профиль Vin PB0/1/2/10..14 совпадает с прежним портом G474. */ +#endif /* Без физического порта все команды IGBT остаются доступны через общую карту регистров. */ +void SystemInit(void) /* Startup вызывает функцию до инициализации глобальных переменных C runtime. */ +{ /* До runtime доступны только константы сборки и регистры, поэтому HAL здесь не вызывается. */ + SCB->CPACR |= 0xFu << 20; /* Разрешает CP10/CP11 FPU: четыре бита доступа 20..23 по ARM Cortex-M4 TRM. */ + SCB->VTOR = EMU_FLASH_BASE; /* Переносит векторы на 0x08008000 для CANdle либо 0x08000000 для самостоятельного образа. */ + __DSB(); /* Завершает запись VTOR до потенциальной доставки аппаратного исключения. */ + __ISB(); /* Обновляет контекст инструкций после включения FPU и смены таблицы векторов. */ + SET_BIT(RCC->CR, RCC_CR_HSION); /* Включает HSI16, поскольку CANdle boot передаёт управление с PLL 144 МГц без сброса RCC. */ + while ((RCC->CR & RCC_CR_HSIRDY) == 0u) {} /* Ждёт готовности внутреннего источника до переключения SYSCLK без зависимости от неинициализированного HAL tick. */ + MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_HSI); /* Переводит SYSCLK на HSI16 до изменения PLL, запрещённого при её использовании ядром. */ + while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_HSI) {} /* Подтверждает аппаратное завершение переключения SYSCLK до отключения прежней PLL. */ + CLEAR_BIT(RCC->CR, RCC_CR_PLLON); /* Выключает PLL предыдущего загрузчика, чтобы HAL мог задать новые M/N/R для 160 МГц. */ + while ((RCC->CR & RCC_CR_PLLRDY) != 0u) {} /* Дожидается фактической остановки PLL перед дальнейшей инициализацией C runtime. */ + CLEAR_BIT(RCC->CFGR, RCC_CFGR_HPRE | RCC_CFGR_PPRE1 | RCC_CFGR_PPRE2); /* Возвращает AHB/APB к делению 1: глобальная SystemCoreClock=16 МГц и первый SysTick HAL совпадают с железом. */ +} /* Завершает минимальную настройку процессора без зависимости от BSS. */ +void SystemCoreClockUpdate(void) /* Поддерживает интерфейс CMSIS, используемый HAL при изменении дерева часов. */ +{ /* Частота определяется фактическим RCC, а не только константой желаемой частоты CPU. */ + RCC_ClkInitTypeDef config; uint32_t latency; /* Получает делитель AHB и обязательный выходной параметр задержки Flash. */ + HAL_RCC_GetClockConfig(&config, &latency); /* Читает активную конфигурацию RCC штатным HAL G4. */ + SystemCoreClock = HAL_RCC_GetSysClockFreq() >> AHBPrescTable[config.AHBCLKDivider >> RCC_CFGR_HPRE_Pos]; /* Переводит SYSCLK в HCLK с аппаратным делителем AHB по RM0440. */ +} /* Завершает обновление частоты ядра в герцах. */ +void SysTick_Handler(void) { HAL_IncTick(); } /* Прерывание только увеличивает счётчик миллисекунд; модели выполняются в main. */ +void board_fatal(uint32_t code) { clock_fault = code; for (;;) {} } /* Останавливает дальнейшую настройку после отказа, сохраняя его причину в Watch. */ +static void peripheral_clock(void) /* Создаёт одинаковую частоту таймера DAC для обоих вариантов Flash. */ +{ /* Штатная PLL использует кварц JT1 8 МГц по схеме HOPE от 14.04.2025; HSI16 остаётся резервным профилем сборки. */ + RCC_OscInitTypeDef osc = {0}; RCC_ClkInitTypeDef clk = {0}; /* Нулевая инициализация отключает неиспользуемые ветви настройки генераторов и шин. */ + RCC_PeriphCLKInitTypeDef peripheral = {0}; RCC_CRSInitTypeDef crs = {0}; /* Готовит независимые источники USB, USART1 и FDCAN. */ + __HAL_RCC_PWR_CLK_ENABLE(); /* Разрешает HAL доступ к регулятору питания перед повышением частоты. */ + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1_BOOST) != HAL_OK) board_fatal(3); /* Boost Range1 требуется для CPU 160 МГц по ограничениям ST. */ + osc.OscillatorType = RCC_OSCILLATORTYPE_HSI | RCC_OSCILLATORTYPE_HSI48; /* Включает HSI16 для CPU и отдельный HSI48 для USB FS. */ + osc.HSIState = RCC_HSI_ON; osc.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; /* Сохраняет заводскую подстройку внутреннего 16-МГц источника. */ + osc.HSI48State = RCC_HSI48_ON; /* USB получает собственный 48-МГц источник, корректируемый USB SOF через CRS. */ + osc.PLL.PLLState = RCC_PLL_ON; osc.PLL.PLLSource = RCC_PLLSOURCE_HSI; /* Задаёт внутренний источник для резервного профиля; штатная ветвь ниже выбирает HSE. */ + osc.PLL.PLLM = RCC_PLLM_DIV4; /* HSI16 / 4 даёт вход PLL 4 МГц, допустимый для PLLN=80. */ +#if EMU_USE_HSE /* Штатный профиль использует подтверждённый схемой кварц JT1 8 МГц для точности CAN и DAC. */ + osc.OscillatorType |= RCC_OSCILLATORTYPE_HSE; /* Добавляет внешний источник, сохраняя HSI48 для USB. */ + osc.HSEState = EMU_HSE_BYPASS ? RCC_HSE_BYPASS : RCC_HSE_ON; /* Различает внешний генератор и пассивный кварц по настройке платы. */ + osc.PLL.PLLSource = RCC_PLLSOURCE_HSE; /* Переключает источник PLL, от которого зависят CAN и частота генератора. */ +#if EMU_HSE_HZ == 8000000u /* Кварц 8 МГц требует делитель два для того же входа PLL 4 МГц. */ + osc.PLL.PLLM = RCC_PLLM_DIV2; /* Сохраняет итоговые 160 МГц при HSE 8 МГц. */ +#elif EMU_HSE_HZ == 24000000u /* Кварц 24 МГц требует делитель шесть для входа PLL 4 МГц. */ + osc.PLL.PLLM = RCC_PLLM_DIV6; /* Сохраняет итоговые 160 МГц при HSE 24 МГц. */ +#else /* Другие частоты требуют явного расчёта PLL вместо незаметного неверного тайминга. */ +#error Unsupported HOPE HSE frequency /* Допускает только проверяемые варианты внешнего источника 8 и 24 МГц. */ +#endif /* Завершает выбор делителя по физической частоте кварца. */ +#endif /* При EMU_USE_HSE=0 внешний источник вообще не запускается. */ + osc.PLL.PLLN = 80; osc.PLL.PLLP = RCC_PLLP_DIV2; /* Вход 4 МГц умножается до VCO 320 МГц; неиспользуемый PLLP имеет допустимый делитель. */ + osc.PLL.PLLQ = RCC_PLLQ_DIV2; osc.PLL.PLLR = RCC_PLLR_DIV2; /* PLLR / 2 создаёт CPU 160 МГц; USB не использует PLLQ. */ + if (HAL_RCC_OscConfig(&osc) != HAL_OK) board_fatal(3); /* Не продолжает запуск при отказе генератора или захвата PLL. */ + clk.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; /* Перенастраивает все шины, от которых зависит используемая периферия. */ + clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider = RCC_SYSCLK_DIV1; /* CPU и AHB получают 160 МГц без дополнительного деления. */ + clk.APB1CLKDivider = RCC_HCLK_DIV2; clk.APB2CLKDivider = RCC_HCLK_DIV1; /* PCLK1=80 МГц, TIM6=160 МГц и USART1 на PCLK2=160 МГц. */ + if (HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_4) != HAL_OK) board_fatal(3); /* Четыре wait-state соответствуют 160 МГц в Boost Range1 по RM0440. */ + peripheral.PeriphClockSelection = RCC_PERIPHCLK_USB | RCC_PERIPHCLK_FDCAN | RCC_PERIPHCLK_USART1; /* Выбирает только реальные блоки HOPE, исключая USART2 профиля Nucleo. */ + peripheral.UsbClockSelection = RCC_USBCLKSOURCE_HSI48; /* Подаёт независимые 48 МГц на USB FS. */ + peripheral.FdcanClockSelection = RCC_FDCANCLKSOURCE_PCLK1; /* Общий CAN-порт рассчитывает 250 кбит/с из фактических 80 МГц PCLK1. */ + peripheral.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; /* HAL вычисляет делитель USART1 из 160 МГц APB2. */ + if (HAL_RCCEx_PeriphCLKConfig(&peripheral) != HAL_OK) board_fatal(3); /* Ошибка выбора kernel clock обнаруживается до включения транспорта. */ + __HAL_RCC_CRS_CLK_ENABLE(); /* Разрешает блок автоматической подстройки HSI48 по USB SOF. */ + crs.Prescaler = RCC_CRS_SYNC_DIV1; crs.Source = RCC_CRS_SYNC_SOURCE_USB; /* Использует каждый входящий SOF USB без деления. */ + crs.Polarity = RCC_CRS_SYNC_POLARITY_RISING; /* Сохраняет рекомендованный HAL фронт синхронизации USB. */ + crs.ReloadValue = __HAL_RCC_CRS_RELOADVALUE_CALCULATE(48000000u, 1000u); /* Рассчитывает 47999 тактов HSI48 на период SOF 1 мс. */ + crs.ErrorLimitValue = RCC_CRS_ERRORLIMIT_DEFAULT; crs.HSI48CalibrationValue = RCC_CRS_HSI48CALIBRATION_DEFAULT; /* Использует заводские начальные значения допусков и подстройки. */ + HAL_RCCEx_CRSConfig(&crs); usb_clock_ready = 1; /* Разрешает main запуск CDC после подготовки независимого USB clock. */ +} /* Завершает настройку часов без обращения к Flash Option Bytes. */ +void board_init(void) /* Вызывается после создания общих моделей и миллисекундного HAL tick. */ +{ /* Геометрия VE отличается от RB, но оба состояния DBANK при 512 КиБ имеют непрерывную карту Flash. */ + GPIO_InitTypeDef gpio = {0}; /* Не оставляет неинициализированных параметров альтернативных функций GPIO. */ + __HAL_RCC_SYSCFG_CLK_ENABLE(); /* Даёт доступ к признаку фактической перестановки Flash-банков. */ + if (*(const uint16_t *)FLASHSIZE_BASE != 512u || (FLASH->OPTR & FLASH_OPTR_BFB2) || (SYSCFG->MEMRMP & SYSCFG_MEMRMP_FB_MODE)) board_fatal(5); /* Требует реальные 512 КиБ VE и обычное отображение банков; DBANK не изменяет. */ + peripheral_clock(); /* Стабилизирует частоты до запуска GPIO, USART1, FDCAN и USB. */ + __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); /* Порт A обслуживает USB/UART/DAC, порт D — штатный CAN1, порт B оставлен опциональным GPIO. */ + __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_FDCAN_CLK_ENABLE(); /* Подаёт часы только на выбранный USART1 и общий FDCAN kernel. */ + gpio.Pin = GPIO_PIN_9 | GPIO_PIN_10; gpio.Mode = GPIO_MODE_AF_PP; /* PA9 TX и PA10 RX подключены через JP2 к CH340N по схеме HOPE от 14.04.2025. */ + gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Pull = GPIO_PULLUP; gpio.Alternate = GPIO_AF7_USART1; /* AF7 назначает USART1; подтяжка сохраняет idle RX при отсутствии адаптера. */ + HAL_GPIO_Init(GPIOA, &gpio); /* Применяет HAL-настройку UART к порту A одним вызовом. */ + gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1; gpio.Alternate = GPIO_AF9_FDCAN1; /* PD0 RX и PD1 TX соединены с U6 TJA1051T/3; PB8 на этой плате занят BOOT0. */ + HAL_GPIO_Init(GPIOD, &gpio); /* Подключает FDCAN1 к штатным CAN1L/CAN1H разъёма J4 по схеме от 14.04.2025. */ +#if EMU_IGBT_GPIO_ENABLE /* Физический профиль включается вручную после проверки занятых выводов FLASH и других разъёмов. */ + __HAL_RCC_GPIOC_CLK_ENABLE(); /* Порт C нужен только для восьми физических выходов Vstat. */ + gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14; /* Сохраняет отключённый ручной профиль Vin; PB12/PB13 соединены с CAN2 и требуют изменения внешних соединений. */ + gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_PULLDOWN; HAL_GPIO_Init(GPIOB, &gpio); /* Задаёт нулевой Vin на неподключённых входах выбранного ручного профиля. */ + gpio.Pin = 0xFFu; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; /* Биты 0..7 порта C становятся восемью ответами Vstat IGBT. */ + HAL_GPIO_WritePin(GPIOC, 0xFFu, GPIO_PIN_RESET); HAL_GPIO_Init(GPIOC, &gpio); /* Сначала записывает нули в ODR, исключая импульс при включении выходов. */ +#endif /* Без включённого профиля IGBT GPIO сохраняют своё состояние после reset. */ +#if EMU_RS485_DE /* Направление разрешается только для отдельно подключённого внешнего RS-485. */ + gpio.Pin = SERIAL_DE_PIN; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; /* Настраивает выбранный внешний вывод DE, не меняя UART TX/RX. */ + HAL_GPIO_WritePin(SERIAL_DE_PORT, SERIAL_DE_PIN, GPIO_PIN_RESET); HAL_GPIO_Init(SERIAL_DE_PORT, &gpio); /* Начинает в режиме приёма с выключенным передатчиком RS-485. */ +#endif /* Штатный TTL UART не занимает вывод DE. */ + transport_init(); /* Запускает HAL UART IRQ и Classic CAN после полной настройки GPIO/clock. */ +} /* Завершает аппаратный запуск HOPE, не запуская генерацию DAC без команды. */ +uint8_t board_usb_ready(void) { return usb_clock_ready; } /* Возвращает main готовность источника USB, общую для обоих вариантов Flash. */ +void board_tick(void) /* Выполняется главным циклом один раз на каждую обслуженную миллисекунду. */ +{ /* Протокольная инъекция IGBT работает и при отключённом физическом GPIO. */ +#if EMU_IGBT_GPIO_ENABLE /* Считывает восемь физических Vin только для явно включённого ручного профиля. */ + uint8_t inputs = 0, outputs; unsigned i; /* Накопленная маска привязана к порядку input_pins, а не к номерам PB. */ + for (i = 0; i < 8; ++i) if (HAL_GPIO_ReadPin(GPIOB, input_pins[i]) == GPIO_PIN_SET) inputs |= (uint8_t)(1u << i); /* Переводит восемь уровней в логические Vin0..Vin7. */ + igbt_emu_set_tx(inputs); /* Передаёт физическую маску общей модели, сохраняя её выбор источника через регистр 0x140C. */ +#else /* Штатный профиль HOPE не использует неподтверждённые внешние дискретные линии. */ + igbt_emu_set_tx(0); /* При выборе GPIO задаёт нулевой Vin; регистровый источник остаётся независимым. */ +#endif /* Далее все модели продвигаются одинаково независимо от наличия дискретного порта. */ + emu_tick(); /* Продвигает выбранную модель и общие отложенные настройки ровно на 1 мс. */ +#if EMU_IGBT_GPIO_ENABLE /* Выдаёт Vstat только после завершения шага модели. */ + outputs = igbt_emu_get_rx(); /* Получает одну согласованную восьмибитную маску ответов IGBT. */ + HAL_GPIO_WritePin(GPIOC, (uint16_t)(~outputs & 0xFFu), GPIO_PIN_RESET); /* Сбрасывает только неактивные PC0..PC7 без изменения остальных выводов C. */ + HAL_GPIO_WritePin(GPIOC, outputs, GPIO_PIN_SET); /* Устанавливает активные ответы Vstat текущего шага модели. */ +#endif /* Завершает опциональное обновление физических ответов. */ +} /* Завершает один миллисекундный шаг без обработки моделей из прерываний. */ diff --git a/c/set-protocol/ports/stm32g474-hope/board.h b/c/set-protocol/ports/stm32g474-hope/board.h new file mode 100644 index 0000000..06fe653 --- /dev/null +++ b/c/set-protocol/ports/stm32g474-hope/board.h @@ -0,0 +1,10 @@ +#ifndef HOPE_BOARD_H /* Защищает интерфейс платы от повторных объявлений в общей композиции. */ +#define HOPE_BOARD_H /* Отделяет аппаратную реализацию HOPE от моделей и транспортных кодеков. */ +#include /* Предоставляет фиксированные типы времени и диагностических кодов. */ +extern volatile uint32_t clock_fault; /* Watch: 2 — CAN, 3 — HAL/часы, 4 — UART, 5 — плотность Flash или перестановка банков. */ +void board_init(void); /* После HAL_Init и emu_init настраивает тактирование, GPIO и HAL-транспорт. */ +void board_tick(void); /* Выполняет один шаг моделей длительностью 1 мс в основном контексте. */ +uint8_t board_usb_ready(void); /* Разрешает запуск USB CDC после подготовки HSI48 и CRS. */ +void board_fatal(uint32_t code); /* Сохраняет причину отказа и останавливает main до аппаратного сброса. */ +void SysTick_Handler(void); /* Обслуживает миллисекундный счётчик HAL без исполнения моделей из IRQ. */ +#endif /* Завершает публичный интерфейс аппаратного слоя HOPE. */ diff --git a/c/set-protocol/ports/stm32g474-hope/board_config.template.h b/c/set-protocol/ports/stm32g474-hope/board_config.template.h new file mode 100644 index 0000000..679ee14 --- /dev/null +++ b/c/set-protocol/ports/stm32g474-hope/board_config.template.h @@ -0,0 +1,45 @@ +#ifndef HOPE_BOARD_CONFIG_H /* Защищает единый профиль платы HOPE от повторного включения. */ +#define HOPE_BOARD_CONFIG_H /* Используется всеми аппаратными адаптерами и общей композицией моделей. */ +#define EMU_USB_SET_V2 1 /* Нативный USB MCU переносит SET v2; UART сохраняет RTU и SETGUI v1/GAS. */ +#define USB_CDC_IRQn USB_LP_IRQn /* G474 обслуживает USB FS через низкоприоритетный вектор USB_LP. */ +#define USB_CDC_PRODUCT_NAME "SET HOPE G474VE Emulator" /* Отличает виртуальный COM эмулятора от USB–UART CH340N на плате. */ +#define EMU_PRODUCT_MODEL "STM32G474VE-HOPE-EMU" /* Возвращается в DEVICE_INFO и проверяется host-тестом SET v2. */ +#define EMU_BOOT_REQUEST 0x1003u /* Сохраняет прикладную команду входа в загрузчик из USB SET v2. */ +#define EMU_REQUEST_BOOT(now) g474_request_boot(now) /* Адаптер HOPE записывает CANB в SRAM, совместимую с CANdle. */ +#define EMU_RS_ADDRESS 16u /* Узел управления моделями использует прежний адрес SetGUI, отличный от адреса CMD_STD. */ +#define EMU_TMS_ADDRESS 1u /* Отдельный адрес 2812 позволяет отличать CMD_STD от команд настройки эмулятора. */ +#define EMU_CAN_MODE 5u /* Общий протокол эмулятора использует extended RX 0xBA0004 и TX 0xBA0024. */ +#define EMU_UART_BAUD 115200u /* Начальная скорость приложения в бодах; загрузчик CANdle отдельно использует 512000. */ +#define EMU_CAN_BITRATE 250000u /* Начальная скорость приложения в бит/с; CAN-загрузчик отдельно использует 500000. */ +#define EMU_FLAGS_DEFAULT 1u /* После сброса разрешает синтетический сценарий УМП; модель по умолчанию остаётся УМП. */ +#define EMU_UART_INSTANCE USART1 /* Использует USART1 PA9/PA10, подключённый через JP2 к CH340N по схеме HOPE от 14.04.2025. */ +#define EMU_UART_IRQn USART1_IRQn /* Направляет RX/TX HAL в аппаратный вектор USART1 вместо USART2 Nucleo. */ +#define SERIAL_UART_IRQ_HANDLER USART1_IRQHandler /* Общий HAL UART публикует правильное имя вектора startup G474. */ +#define EMU_RS485_DE 0 /* Прямой TTL UART PA9/PA10 не требует управления направлением внешнего трансивера. */ +#define SERIAL_DE_PORT GPIOB /* При ручном включении RS-485 используется внешний DE на порту B. */ +#define SERIAL_DE_PIN GPIO_PIN_15 /* PB15 оставлен только опциональному внешнему DE; встроенный RS-485 HOPE не предполагается. */ +#ifndef EMU_USE_HSE /* Позволяет выбрать внешний кварц через настройки компилятора, не меняя переносимый порт. */ +#define EMU_USE_HSE 1 /* Использует кварц JT1 8 МГц, указанный в найденной схеме G474VET6 от 14.04.2025. */ +#endif /* Завершает выбор источника системной PLL. */ +#ifndef EMU_HSE_HZ /* Частота задаётся в герцах и должна соответствовать фактически установленному кварцу. */ +#define EMU_HSE_HZ 8000000u /* Номинал JT1 равен 8 МГц по схеме; HAL и PLL используют одну частоту в герцах. */ +#endif /* Сохраняет явно переданную частоту внешнего источника. */ +#define EMU_HSE_BYPASS 0 /* Внешний режим рассчитан на кварц; для готового генератора нужно отдельно выбрать bypass. */ +#ifndef EMU_IGBT_GPIO_ENABLE /* Ручной профиль Vin требует устранить подтверждённое схемой пересечение PB12/PB13 с CAN2. */ +#define EMU_IGBT_GPIO_ENABLE 0 /* Модель IGBT доступна через регистры; штатные линии CAN2 не становятся дискретными Vin. */ +#endif /* Позволяет включить физические Vin/Vstat параметром компилятора. */ +#ifndef EMU_FLASH_BASE /* Адрес задаётся одинаково в профиле Keil и его scatter-файле. */ +#define EMU_FLASH_BASE 0x08008000u /* Резервирует первые 32 КиБ под найденный CAN/UART-загрузчик CANdle. */ +#endif /* Самостоятельный профиль переопределяет адрес на 0x08000000. */ +#define HOPE_BOOT_REQUEST_ADDRESS 0x20013FFCu /* Совпадает с последним словом 80 КиБ SRAM в boot_profile.h CANdle. */ +#define HOPE_BOOT_REQUEST_MAGIC 0x43414E42u /* ASCII CANB запрашивает вход в существующий CAN/UART-загрузчик после reset. */ +#if EMU_RS_ADDRESS < 1 || EMU_RS_ADDRESS > 247 || EMU_TMS_ADDRESS < 1 || EMU_TMS_ADDRESS > 255 || EMU_TMS_ADDRESS == EMU_RS_ADDRESS /* Исключает недопустимые или совпадающие адреса двух обслуживаемых узлов. */ +#error Invalid HOPE emulator addresses /* Останавливает сборку до получения прошивки с неоднозначной маршрутизацией. */ +#endif /* Завершает проверку адресов протокола. */ +#if EMU_CAN_MODE < 1 || EMU_CAN_MODE > 16 /* Номер режима входит в младшую часть extended CAN ID общего протокола. */ +#error Invalid HOPE CAN mode /* Не допускает CAN ID за пределами поддерживаемых шестнадцати режимов. */ +#endif /* Завершает проверку режима CAN. */ +#if EMU_FLASH_BASE != 0x08000000u && EMU_FLASH_BASE != 0x08008000u /* Поддерживаются только два проверяемых варианта размещения Flash. */ +#error Invalid HOPE vector address /* Ошибка адреса VTOR не должна обнаруживаться только после прошивки платы. */ +#endif /* Завершает проверку размещения таблицы векторов. */ +#endif /* Завершает аппаратный профиль HOPE STM32G474VET6. */ diff --git a/c/set-protocol/ports/stm32g474-hope/boot_port.h b/c/set-protocol/ports/stm32g474-hope/boot_port.h new file mode 100644 index 0000000..dd9d455 --- /dev/null +++ b/c/set-protocol/ports/stm32g474-hope/boot_port.h @@ -0,0 +1,6 @@ +#ifndef HOPE_BOOT_PORT_H /* Защищает интерфейс входа в существующий CANdle boot от повторного включения. */ +#define HOPE_BOOT_PORT_H /* Общий USB SET-адаптер использует этот профиль вместо несовместимого RB USB boot. */ +#include /* Определяет тип миллисекундного времени отложенного reset. */ +void g474_request_boot(uint32_t now); /* Сохраняет CANB в исключённом слове SRAM и назначает reset через 500 мс. */ +void g474_reset_poll(uint32_t now); /* Выполняет назначенный reset из main после отправки ответа USB. */ +#endif /* Завершает интерфейс входа HOPE в найденный загрузчик CANdle. */ diff --git a/c/set-protocol/ports/stm32g474-hope/boot_request.c b/c/set-protocol/ports/stm32g474-hope/boot_request.c new file mode 100644 index 0000000..73a5909 --- /dev/null +++ b/c/set-protocol/ports/stm32g474-hope/boot_request.c @@ -0,0 +1,16 @@ +#include "boot_port.h" /* Сохраняет общий API отложенного reset для USB SET v2 и main эмулятора. */ +#include "board_config.h" /* Получает адрес и magic CANB, совпадающие с исходным профилем CANdle. */ +#include "stm32g4xx_hal.h" /* Предоставляет барьер памяти Cortex-M4 и NVIC_SystemReset. */ +static uint32_t reset_at; /* Содержит срок reset в миллисекундах HAL с арифметикой по модулю 2^32. */ +static uint8_t reset_pending; /* Разделяет отсутствие запроса и допустимый срок reset_at=0 после переполнения. */ +void g474_request_boot(uint32_t now) /* Вызывается только после корректного адресного запроса USB SET v2. */ +{ /* Маркер живёт вне RW/ZI и стека обоих профилей scatter HOPE. */ + *(volatile uint32_t *)(uintptr_t)HOPE_BOOT_REQUEST_ADDRESS = HOPE_BOOT_REQUEST_MAGIC; /* Записывает CANB именно в SRAM 0x20013FFC, а не в TAMP->BKP0R RB-загрузчика. */ + __DSB(); /* Завершает запись SRAM до любого последующего системного сброса. */ + reset_at = now + 500u; /* Даёт USB CDC 500 мс для передачи положительного ответа до переподключения. */ + reset_pending = 1u; /* Публикует запрос после подготовки его маркера и времени. */ +} /* Завершает подготовку входа, не прерывая текущую передачу ответа. */ +void g474_reset_poll(uint32_t now) /* Вызывается главным циклом независимо от текущей активности транспорта. */ +{ /* Знаковая разность корректна при переполнении HAL tick на коротком интервале 500 мс. */ + if (reset_pending && (int32_t)(now - reset_at) >= 0) NVIC_SystemReset(); /* Передаёт управление существующему загрузчику только после завершения задержки. */ +} /* Завершает обслуживание отложенного reset без собственных операций Flash. */ diff --git a/c/set-protocol/ports/stm32g474-hope/docs/HOPE_G474VE_schematic_20250414.pdf b/c/set-protocol/ports/stm32g474-hope/docs/HOPE_G474VE_schematic_20250414.pdf new file mode 100644 index 0000000..0852057 Binary files /dev/null and b/c/set-protocol/ports/stm32g474-hope/docs/HOPE_G474VE_schematic_20250414.pdf differ diff --git a/c/set-protocol/ports/stm32g474-hope/transport.h b/c/set-protocol/ports/stm32g474-hope/transport.h new file mode 100644 index 0000000..65830ca --- /dev/null +++ b/c/set-protocol/ports/stm32g474-hope/transport.h @@ -0,0 +1,9 @@ +#ifndef HOPE_TRANSPORT_H /* Исключает повторное объявление транспорта HOPE при включении общего UART. */ +#define HOPE_TRANSPORT_H /* Сохраняет одинаковый poll API у всех аппаратных портов эмулятора. */ +#include /* Объявляет миллисекундное время и диагностические счётчики фиксированной ширины. */ +extern volatile uint32_t uart_errors, uart_overflows, can_timeouts, can_overflows; /* Показывает ошибки HAL, потери очереди UART и отказы CAN в Watch. */ +void transport_init(void); /* Инициализирует USART1 PA9/PA10 и штатный FDCAN1 PD0/PD1 после настройки GPIO и часов по схеме HOPE. */ +void transport_uart_poll(uint32_t now); /* Обслуживает RTU/SETGUI v1/GAS; now передаётся в миллисекундах HAL_GetTick. */ +void transport_can_poll(uint32_t now); /* За один проход обрабатывает не более одного CAN RX и одного фрагмента TX. */ +void USART1_IRQHandler(void); /* Предоставляет startup G474 правильный обработчик RX/TX и ошибок выбранного UART. */ +#endif /* Завершает интерфейс транспорта HOPE. */ diff --git a/c/set-protocol/ports/stm32g474-periph/transport.c b/c/set-protocol/ports/stm32g474-periph/transport.c index 307f74f..cda4271 100644 --- a/c/set-protocol/ports/stm32g474-periph/transport.c +++ b/c/set-protocol/ports/stm32g474-periph/transport.c @@ -13,6 +13,15 @@ #include "board_config.h" #include "emulator.h" #include "stm32g4xx_hal.h" +#ifndef EMU_UART_INSTANCE /* Сохраняет USART2 у существующей Nucleo и позволяет HOPE выбрать USART1. */ +#define EMU_UART_INSTANCE USART2 /* Штатный порт Nucleo использует VCP на PA2/PA3. */ +#endif /* Экземпляр UART может быть явно задан аппаратным профилем другой платы G474. */ +#ifndef EMU_UART_IRQn /* Номер IRQ выбирается согласованно с экземпляром UART конкретной платы. */ +#define EMU_UART_IRQn USART2_IRQn /* Сохраняет прежний вектор существующей сборки G474RB. */ +#endif /* HOPE передаёт USART1_IRQn из своего board_config.h. */ +#ifndef SERIAL_UART_IRQ_HANDLER /* Сохраняет общий автомат UART при разных аппаратных экземплярах USART. */ +#define SERIAL_UART_IRQ_HANDLER USART2_IRQHandler /* Существующие порты F103/F407/G474RB сохраняют прежний вектор USART2. */ +#endif /* HOPE задаёт USART1_IRQHandler в конфигурации платы. */ static UART_HandleTypeDef uart; static FDCAN_HandleTypeDef can; static volatile uint8_t transmitting; @@ -21,13 +30,13 @@ volatile uint32_t uart_errors,uart_overflows,can_timeouts,can_overflows; void transport_init(void) { FDCAN_FilterTypeDef filter={0}; - uart.Instance=USART2; uart.Init.BaudRate=emu_uart_baud(); + uart.Instance=EMU_UART_INSTANCE; uart.Init.BaudRate=emu_uart_baud(); /* Выбирает физический UART платы и актуальную скорость общей конфигурации сервера. */ uart.Init.WordLength=UART_WORDLENGTH_8B; uart.Init.StopBits=UART_STOPBITS_1; uart.Init.Parity=UART_PARITY_NONE; uart.Init.Mode=UART_MODE_TX_RX; uart.Init.HwFlowCtl=UART_HWCONTROL_NONE; uart.Init.OverSampling=UART_OVERSAMPLING_16; uart.Init.ClockPrescaler=UART_PRESCALER_DIV1; if(HAL_UART_Init(&uart)!=HAL_OK) board_fatal(4); - HAL_NVIC_SetPriority(USART2_IRQn,6,0); HAL_NVIC_EnableIRQ(USART2_IRQn); + HAL_NVIC_SetPriority(EMU_UART_IRQn,6,0); HAL_NVIC_EnableIRQ(EMU_UART_IRQn); /* Приоритет 6 оставляет SysTick/USB доступными, а номер вектора соответствует USART1 HOPE либо USART2 Nucleo. */ can.Instance=FDCAN1; can.Init.ClockDivider=FDCAN_CLOCK_DIV1; can.Init.FrameFormat=FDCAN_FRAME_CLASSIC; can.Init.Mode=FDCAN_MODE_NORMAL; can.Init.AutoRetransmission=ENABLE; can.Init.TransmitPause=DISABLE; @@ -103,7 +112,7 @@ void transport_can_poll(uint32_t now) } /* HAL обслуживает TXE/TC и вызывает callback завершения; прикладной протокол в ISR не выполняется. */ -void USART2_IRQHandler(void) { HAL_UART_IRQHandler(&uart); } +void SERIAL_UART_IRQ_HANDLER(void) { HAL_UART_IRQHandler(&uart); } /* Передаёт RXNE/TXE/TC и ошибки в HAL только выбранного UART; имя вектора согласовано со startup платы. */ /* Вызывается HAL после TC: последний стоп-бит уже вышел на линию, можно отключить передатчик RS-485. * Также используется локально для возврата в приём, если запуск передачи завершился ошибкой. */ void HAL_UART_TxCpltCallback(UART_HandleTypeDef *h) diff --git a/c/set-protocol/src/deif_xdi.c b/c/set-protocol/src/deif_xdi.c new file mode 100644 index 0000000..dc23a45 --- /dev/null +++ b/c/set-protocol/src/deif_xdi.c @@ -0,0 +1,185 @@ +#include "deif_xdi.h" /* Публичный ABI не зависит от Python, Kotlin, Qt или Android. */ +#include /* Фиксированные структуры копируются без динамической памяти. */ +#include /* Проверка конечности пользовательского масштаба предотвращает недопустимые показания. */ +#define DEIF_MAGIC 0x44454946U /* Метка DEIF отличает валидный caller-owned контекст. */ +#define DEIF_FIELDS 6U /* POWER, RPM, фактический угол, POWER%, RPM%, задание угла. */ +#define DEIF_QUEUE 16U /* Ограниченная очередь покрывает Boot-up, heartbeat и ответы без heap. */ +typedef struct { /* Привязка и последнее сообщение одного индикатора. */ + uint8_t selector[4]; /* SAM/DAM, индекс LE и подиндекс сохраняются в wire-порядке. */ + double scale; /* Множитель физической величины задаётся библиотекой прибора. */ + uint64_t seen; /* Монотонные миллисекунды последнего принятого объекта. */ + int32_t raw; /* Исходный I16 хранится без обратного округления показания. */ + unsigned bound, received, status; /* Привязка, наличие кадра и служебный статус I16. */ +} deif_binding; /* Внутренний layout не является частью публичного ABI. */ +typedef struct { /* Всё состояние принадлежит порту, часы и транспорт в ядре отсутствуют. */ + uint32_t magic, monitor_id; /* Метка контекста и точный CAN ID пассивного наблюдателя. */ + unsigned node, source, xdi, self_start, pdo, group, services; /* Профиль по DEIF 4189350066D. */ + unsigned active, nmt, heartbeat, accepted, brightness, palette, error, queued; /* Изменяемые коммуникационные параметры сеанса. */ + uint64_t started, last_hb, auto_at; /* Независимые сроки в монотонных миллисекундах. */ + deif_binding field[DEIF_FIELDS]; /* Каждый вход имеет собственные данные и срок свежести. */ + setp_deif_frame_t queue[DEIF_QUEUE]; /* Служебные кадры выдаются только один раз через poll. */ +} deif_state; /* Размер сообщает state_size, порты не повторяют эту структуру. */ +static const uint16_t bases[8] = {0x3420,0x3080,0x3000,0x3430,0x3090,0x3000,0x3500,0x3500}; /* Опубликованные семейства переменных и диммера. */ +static const uint8_t subs[8] = {2,2,2,2,2,7,2,3}; /* Фактическое значение, отдельное задание, яркость и палитра. */ +static deif_state *context(void *p) { /* Все публичные функции проверяют инициализацию состояния. */ + deif_state *s = (deif_state *)p; /* Порт выделяет выровненную память через uint64 либо direct ByteBuffer. */ + return s && s->magic == DEIF_MAGIC ? s : NULL; /* Невалидный указатель контекста не трактуется как работающий узел. */ +} /* Проверка не обращается к ОС. */ +static uint16_t le16(const uint8_t *p) { return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); } /* Единственная реализация little-endian U16. */ +static void put16(uint8_t *p, uint16_t v) { p[0]=(uint8_t)v; p[1]=(uint8_t)(v>>8); } /* Обратное преобразование принадлежит тому же C-кодеку. */ +static void put32(uint8_t *p, uint32_t v) { unsigned i; for(i=0;i<4;i++) p[i]=(uint8_t)(v>>(8U*i)); } /* SDO и EMCY используют младший байт первым. */ +static unsigned raw_status(int32_t raw) { return raw == -32768 ? 1U : raw == 32766 ? 2U : raw == 32767 ? 3U : 0U; } /* §2.1.2: три зарезервированных значения I16. */ +static int address(uint32_t id, unsigned node, unsigned dam) { /* Проверка адреса применяется одинаково в обоих GUI. */ + if(!node || node>127U) return 0; /* Нулевой адрес не является источником или получателем MPDO. */ + return dam ? (id==0x200U+node || id==0x300U+node || id==0x400U+node || id==0x500U+node) : (id==0x200U+node || id==0x27FU); /* RPDO1…4 либо SAM с исключением service broadcast. */ +} /* Номер получателя не подменяет источник SAM. */ +int setp_deif_version(void) { return 1; } /* Независимая версия расширения ABI. */ +size_t setp_deif_state_size(void) { return sizeof(deif_state); } /* Клиент не зависит от внутренних паддингов структуры. */ +int setp_deif_catalog(unsigned f, int32_t *o) { /* Каталог является единственным источником числовых индексов. */ + if(f>=8U || !o) return 0; /* Неизвестное поле не создаёт фиктивный объект. */ + o[0]=bases[f]; o[1]=subs[f]; o[2]=(f==0U ? 127 : f<6U ? -1 : 0); return 1; /* 127 означает неизвестную физическую единицу POWER, прочие разрешения заданы в десятичной степени. */ +} /* Тексты и локализация остаются в GUI-портах. */ +int setp_deif_selector(unsigned f,unsigned node,unsigned instance,unsigned dam,uint8_t *o) { /* Формирует четыре байта привязки из каталога. */ + if(f>=8U || !o || node<1U || node>127U || instance<1U || instance>(f<6U?15U:9U) || dam>1U) return 0; /* Проверяет границы Node, экземпляра и сетевой группы. */ + o[0]=(uint8_t)(node|(dam?128U:0U)); put16(o+1,(uint16_t)(bases[f]+instance)); o[3]=subs[f]; return 1; /* Индекс и режим MPDO собираются только здесь. */ +} /* В портах нет копии wire-разметки. */ +int setp_deif_inspect(uint32_t id,const uint8_t *d,size_t n,unsigned flags,int32_t *o) { /* Коды ошибок сохраняют причину для локализованной диагностики. */ + unsigned i,node,index,sub,instance; int field=-1; /* Промежуточные поля ограничены размерами Classic CAN. */ + if(!o || (!d && n)) return -1; /* Отсутствующий вход не разыменовывается. */ + memset(o,0,12U*sizeof(*o)); if(flags) return -1; /* EXT и RTR не интерпретируются как XDi STD data frames. */ + if(id>=0x701U && id<=0x77FU) { /* Heartbeat и Boot-up имеют отдельный формат DLC1. */ + o[1]=(int32_t)(id-0x700U); if(n!=1U) return -2; o[10]=d[0]; o[0]=2; /* Сохраняем Node-ID и исходный NMT-байт даже при неизвестном коде. */ + return d[0]==0U || d[0]==4U || d[0]==5U || d[0]==127U ? 2 : -3; /* Toggle-бит node guarding не снимается автоматически. */ + } /* Переменные и диммер разбираются следующим общим путём. */ + if(id<0x201U || id>0x57FU) return 0; if(n!=8U) return -4; /* Неизвестный COB-ID отделён от неправильного DLC MPDO. */ + node=d[0]&127U; index=le16(d+1); sub=d[3]; instance=index&15U; /* Семейства переменных имеют по пятнадцать экземпляров. */ + o[1]=(int32_t)node; o[2]=(int32_t)index; o[3]=(int32_t)sub; o[4]=(int16_t)le16(d+4); o[6]=(int32_t)instance; o[8]=(d[0]&128U)!=0; o[9]=(d[6]==0U && d[7]==0U); /* Метаданные не требуют повторного endian-разбора в Python или Kotlin. */ + if(!address(id,node,(unsigned)o[8])) return -5; /* Включает согласование источника SAM с COB-ID. */ + for(i=0;i<6U;i++) if((index&0xFFF0U)==bases[i] && instance && (sub==2U || sub==7U)) { field=(int)i; break; } /* Известные переменные разрешают фактическое значение и задание. */ + if(index>=0x3501U && index<=0x3509U && (sub==2U || sub==3U)) { o[0]=3; o[5]=sub==2U?6:7; o[4]=d[4]; return 3; } /* Диммер имеет U8, а не I16. */ + if(field<0) return -6; o[0]=1; o[5]=field; o[7]=(int32_t)raw_status(o[4]); return 1; /* Служебный I16 не становится физическим измерением. */ +} /* Ненулевые старшие байты отмечаются отдельно, приёмник их отклонит. */ +int setp_deif_make(unsigned f,unsigned node,unsigned instance,int32_t v,uint32_t id,unsigned dam,setp_deif_frame_t *o) { /* Сборка данных, яркости и Color для всех платформ. */ + if(!id) id=0x200U+node; /* Нулевой аргумент выбирает исходный RPDO1 без копии формулы в портах. */ + if(!o || !address(id,node,dam) || (f<6U?(v< -32768 || v>32767):(f==6U?(v<0 || v>100):(f!=7U || v<0 || v>2)))) return 0; /* Значения проверяются до сужения разрядности. */ + memset(o,0,sizeof(*o)); o->can_id=id; o->dlc=8; if(!setp_deif_selector(f,node,instance,dam,o->data)) return 0; /* Неизвестное поле или экземпляр не создаёт частичный кадр. */ + if(f<6U) put16(o->data+4,(uint16_t)v); else o->data[4]=(uint8_t)v; return 1; /* Старшие байты всегда нулевые согласно §3.2.1. */ +} /* Передача готового кадра остаётся обязанностью транспорта. */ +int setp_deif_master(unsigned node,const int32_t *v,uint32_t id,unsigned dam,unsigned angle_subindex,setp_deif_frame_t *o) { /* Цикл MASTER использует единственный каталог и единственную сборку MPDO. */ + static const unsigned order[5]={2,1,4,0,3}; unsigned i,f; if(!v || !o || (angle_subindex!=2U && angle_subindex!=7U)) return 0; /* Подиндекс азимута выбирается явно, неизвестные коды не подставляются автоматически. */ + for(i=0;i<5U;i++) { f=i==0U && angle_subindex==7U?5U:order[i]; if(!setp_deif_make(f,node,1,v[i],id,dam,&o[i])) return 0; } return 5; /* Все кадры сохраняют один Node-ID, режим и COB-ID. */ +} /* Неуспешный цикл не должен передаваться портом частично. */ +int setp_deif_service(unsigned kind,unsigned node,unsigned value,setp_deif_frame_t *o) { /* Готовые служебные кадры не собираются повторно в GUI. */ + if(!o || node>127U || kind>1U) return 0; memset(o,0,sizeof(*o)); /* Границы проверяются до сужения байтов. */ + if(!kind) { if(!node || (value!=0U && value!=4U && value!=5U && value!=127U)) return 0; o->can_id=0x700U+node; o->dlc=1; o->data[0]=(uint8_t)value; } /* Boot-up и heartbeat своего CANopen-узла. */ + else { if(value!=1U && value!=2U && value!=128U && value!=129U && value!=130U) return 0; o->dlc=2; o->data[0]=(uint8_t)value; o->data[1]=(uint8_t)node; } return 1; /* NMT допускает broadcast 0 и передаёт два байта в COB-ID 0. */ +} /* Отправка физическому или локальному получателю остаётся за портом. */ +int setp_deif_angle_command(const setp_deif_frame_t *a,setp_deif_frame_t *o) { /* Перевод фактического азимута в независимый set-point использует тот же каталог. */ + int32_t m[12]; if(!a || setp_deif_inspect(a->can_id,a->data,a->dlc,0,m)!=1 || (m[2]&0xFFF0)!=0x3000) return 0; return setp_deif_make(5,(unsigned)m[1],(unsigned)m[6],m[4],a->can_id,(unsigned)m[8],o); /* Источник, экземпляр и RAW не вычисляются повторно в Python. */ +} /* Это второй отдельный CAN-кадр, не подмена фактического объекта. */ +int setp_deif_selector_info(const uint8_t *s,int32_t *o) { /* Общий разбор пользовательского селектора для всех редакторов. */ + if(!s || !o || !(s[0]&127U)) return 0; o[0]=s[0]&127U; o[1]=le16(s+1); o[2]=s[3]; o[3]=(s[0]&128U)!=0; return 1; /* Текст HEX остаётся форматом настроек, wire-геометрия находится в C. */ +} /* Неизвестные библиотечные индексы могут быть явно привязаны пользователем. */ +int setp_deif_readdress(const uint8_t *s,unsigned node,unsigned mode,uint8_t *o) { /* Перенос старого профиля не требует wire-масок в приложении. */ + if(!s || !o || !node || node>127U || mode>2U) return 0; memcpy(o,s,4); o[0]=(uint8_t)(node|(mode==2U?(s[0]&128U):(mode?128U:0U))); return 1; /* Индекс и подиндекс сохраняются побайтно, включая ручные объекты библиотеки. */ +} /* Адресный байт меняется единственным общим правилом. */ +int setp_deif_route(unsigned node,unsigned dam,uint32_t id,unsigned op,uint32_t *o) { /* Общая настройка COB-ID не смешивается с физическим каналом приложения. */ + if(!o || dam>1U || op>2U || (op==2U && (id<0x201U || id>0x57FU))) return 0; if(op==2U && (id!=0x27FU || dam)) node=id&127U; if(!node) node=1; if(node>127U) return 0; /* Legacy SAM 0x27F сохраняет явно выбранный источник. */ + if(!op) id=dam?(id/0x100U)*0x100U+node:(id==0x27FU?id:0x200U+node); o[0]=id; o[1]=node; o[2]=node|(dam?128U:0U); return 1; /* RPDO-база DAM сохраняется, SAM использует исходный диапазон XDi-net. */ +} /* Точный допустимый адрес проверяется кодеком перед отправкой. */ +int setp_deif_validate(uint32_t id,const uint8_t *d,size_t n,unsigned flags,unsigned slave) { /* Разделяет выход MASTER и события собственного SLAVE. */ + int32_t m[12]; int k; if(flags || (!d && n)) return 0; /* RTR/EXT запрещены в обоих маршрутах. */ + if(slave) { /* Сервер не может посылать запросы конфигурации другим узлам. */ + if(slave>127U) return 0; if(id==0x700U+slave && n==1U) return d[0]==0U || d[0]==4U || d[0]==5U || d[0]==127U; /* Только собственные NMT-состояния. */ + if(id==0x580U+slave && n==8U) return d[0]==0x43U || d[0]==0x47U || d[0]==0x4BU || d[0]==0x4FU || d[0]==0x60U || d[0]==0x80U; /* Только ответы expedited SDO либо abort. */ + if(id==0x80U+slave && n==8U) { static const uint8_t lost[8]={0,0x81,0x81,6,0,0,0,0}; static const uint8_t clear[8]={0}; return !memcmp(d,lost,8) || !memcmp(d,clear,8); } /* Data lost и сброс по Appendix 5. */ + return 0; /* Чужие идентификаторы и ответные PDO не разрешены. */ + } /* MASTER сохраняет отдельный ограниченный набор команд. */ + k=setp_deif_inspect(id,d,n,0,m); if(k==2) return d[0]==5U; if(k!=1 && k!=3) return 0; /* Обычный MASTER не передаёт NMT, SDO и Boot-up. */ + if(!m[9]) return 0; if(k==3) return d[5]==0U && (m[5]==6 ? d[4]<=100U : d[4]<=2U); return 1; /* Значения диммера проверяются как U8 с нулевым хвостом. */ +} /* Проверка не производит физический TX. */ +static void event(deif_state *s,uint32_t id,const uint8_t *d,unsigned n) { /* Очередь имеет явную ограниченную вместимость. */ + setp_deif_frame_t *f; if(s->queued>=DEIF_QUEUE) return; f=&s->queue[s->queued++]; memset(f,0,sizeof(*f)); f->can_id=id; f->dlc=n; memcpy(f->data,d,n); /* Один event соответствует одному готовому CAN-кадру. */ +} /* Порты регулярно выгружают очередь через poll. */ +int setp_deif_init(void *p,size_t size,unsigned node,unsigned source,unsigned xdi,unsigned self_start,unsigned pdo,unsigned group,unsigned services,uint32_t monitor_id) { /* Общий конструктор наблюдателя и серверного SLAVE. */ + deif_state *s=(deif_state *)p; unsigned f; uint8_t selector[4]; /* Селекторы строятся тем же общим каталогом. */ + if(!s || size(services?126U:127U) || group>9U || pdo<1U || pdo>4U || (services && (!node || node>(xdi?126U:127U) || (xdi && pdo==1U)))) return 0; /* Источник обычной рассылки не занимает сервисный Node 127; RPDO1 резервируется в XDi-net. */ + memset(s,0,sizeof(*s)); s->magic=DEIF_MAGIC; s->node=node; s->source=source; s->xdi=xdi; s->self_start=xdi||self_start; s->pdo=pdo; s->group=group; s->services=services; s->monitor_id=monitor_id?monitor_id:0x200U+source; /* Исходный CAN ID источника вычисляется в C, без копии формулы в Kotlin. */ + s->heartbeat=500; s->brightness=100; s->nmt=services?127U:5U; s->active=!services; /* Пассивный наблюдатель не выдаёт heartbeat и сразу принимает данные. */ + for(f=0;f=DEIF_FIELDS || !isfinite(scale)) return 0; /* Неверный масштаб не попадает в снимок индикатора. */ + b=&s->field[f]; bound=selector && (selector[0]||selector[1]||selector[2]||selector[3]); if(b->bound==bound && b->scale==scale && (!bound || !memcmp(b->selector,selector,4))) return 1; /* Повторная синхронизация порта не стирает принятые значения. */ + memset(b,0,sizeof(*b)); b->bound=bound; b->scale=scale; if(bound) memcpy(b->selector,selector,4); /* Новая привязка начинает собственный срок ожидания. */ + if(f==2U) { uint8_t cmd[4]; if(bound && (le16(selector+1)&0xFFF0U)==0x3000U) { memcpy(cmd,selector,4); cmd[3]=7; setp_deif_bind(s,5,cmd,scale); } else setp_deif_bind(s,5,NULL,scale); } return 1; /* Задание угла выводится отдельно даже при пассивном наблюдении. */ +} /* Выбор фактического или заданного объекта сохраняется в исходной привязке. */ +int setp_deif_default_angle(void *p,uint8_t *o,double *scale) { /* Миграция старой пустой привязки использует уже настроенную адресацию прибора. */ + deif_state *s=context(p); unsigned f,addr; if(!s || !o || !scale) return 0; addr=s->source; /* При отсутствии других полей применяется источник профиля. */ + for(f=0;ffield[f].bound) { addr=s->field[f].selector[0]; break; } /* SAM-источник либо DAM-получатель берётся без предположений UI. */ + *scale=0.1; return setp_deif_selector(2,addr&127U,1,(addr&128U)!=0,o); /* Стандартный азимут экземпляра 1 имеет разрешение 0,1°. */ +} /* Остальные пользовательские привязки не меняются. */ +int setp_deif_get_binding(void *p,unsigned f,uint8_t *o,double *scale) { /* Порт не содержит второй таблицы исходных коэффициентов. */ + deif_state *s=context(p); if(!s || f>=DEIF_FIELDS || !o || !scale) return 0; memcpy(o,s->field[f].selector,4); *scale=s->field[f].scale; return (int)s->field[f].bound; /* Публичный ABI выдаёт только селектор и скаляр масштаба. */ +} /* Внутренний layout deif_binding не раскрывается клиентам. */ +void setp_deif_reset_values(void *p) { /* Сброс данных не переключает коммуникационное состояние. */ + deif_state *s=context(p); unsigned f; if(!s) return; for(f=0;ffield[f].received=0; s->field[f].status=0; s->field[f].seen=0; s->field[f].raw=0; } /* Профиль и масштабы сохраняются. */ +} /* Этот путь используется при изменении маршрута наблюдателя. */ +int setp_deif_start(void *p,uint64_t now,unsigned reset_values) { /* NMT Reset communication сохраняет прикладные значения. */ + deif_state *s=context(p); uint8_t boot=0; if(!s) return 0; if(reset_values) setp_deif_reset_values(s); /* Reset node дополнительно очищает прикладную область. */ + s->active=1; s->nmt=s->services?127U:5U; s->accepted=0; s->queued=0; s->error=0; s->heartbeat=500; s->started=s->last_hb=now; s->auto_at=s->services && s->self_start?now+1000U:0; /* DEIF §1.8 и §1.10: 1000 мс self-start, 500 мс heartbeat. */ + if(s->services) event(s,0x700U+s->node,&boot,1); return 1; /* Наблюдатель не объявляет себя узлом сети. */ +} /* Включение само по себе не создаёт выдуманных измерений. */ +void setp_deif_stop(void *p) { deif_state *s=context(p); if(s) { s->active=0; s->queued=0; } } /* Выключение отменяет в том числе невыданные служебные кадры. */ +unsigned setp_deif_lost(void *p,uint64_t now) { /* Каждый используемый вход имеет независимый таймер. */ + deif_state *s=context(p); unsigned f,mask=0; if(!s || !s->active) return 0; /* Неактивный узел не генерирует ошибку Data lost. */ + for(f=0;f<5U;f++) if(s->field[f].bound) { uint64_t t=s->field[f].received?s->field[f].seen:s->started; if(f==2U && !s->field[2].received && s->field[5].received) t=s->field[5].seen; if(now>=t && now-t>=3000U) mask|=1U<raw=raw; b->seen=now; b->received=1; b->status=raw_status(raw); } /* Служебная ошибка отличается от отсутствия обновлений. */ +static int sdo(deif_state *s,const uint8_t *d,uint64_t now) { /* Явный ограниченный сервер expedited SDO без выдуманного полного EDS. */ + uint16_t idx=le16(d+1); unsigned sub=d[3],bytes=0,found_index=0,f; uint32_t value=0,abort=0; uint8_t out[8]={0}; /* Ответ всегда содержит исходный мультиплексор запроса. */ + memcpy(out+1,d+1,3); if(d[0]==0x80U) return 0; /* Клиентский abort не вызывает ответного abort. */ + if(idx==0x1000U || idx==0x1001U || idx==0x1017U || idx==0x1018U) found_index=1; /* Стандартные объекты объявлены явно. */ + if(idx==0x1000U && !sub) { bytes=4; value=0; } else if(idx==0x1001U && !sub) { bytes=1; value=s->error?0x81U:0U; } /* Тип устройства и error register по Appendix 1. */ + else if(idx==0x1017U && !sub) { bytes=2; value=s->heartbeat; } else if(idx==0x1018U && sub<=1U) { bytes=sub?4U:1U; value=sub?0x100000B2U:1U; } /* DEIF vendor ID опубликован; продукт и серийный номер не выдумываются. */ + for(f=0;ffield[f].bound && le16(s->field[f].selector+1)==idx) { found_index=1; if(s->field[f].selector[3]==sub) { bytes=2; value=s->field[f].received?(uint16_t)s->field[f].raw:0x7FFFU; } } /* До обновления I16 возвращается «данные недействительны». */ + if(d[0]!=0x40U && d[0]!=0x2FU && d[0]!=0x2BU && d[0]!=0x27U && d[0]!=0x23U) abort=0x05040001U; else if(!bytes) abort=found_index?0x06090011U:0x06020000U; /* Различаем неподдерживаемую команду, подиндекс и объект. */ + else if(d[0]==0x40U) { out[0]=(uint8_t)(0x43U|((4U-bytes)<<2)); put32(out+4,value); event(s,0x580U+s->node,out,8); return 1; } /* Upload возвращает фактическую ширину U8/U16/U32. */ + else if(idx!=0x1017U || sub) abort=0x06010002U; else if(d[0]!=0x2BU) abort=0x06070010U; /* Запись данного профиля разрешена только для U16 heartbeat producer. */ + else { value=le16(d+4); if(s->xdi && !value) abort=0x06090030U; else { s->heartbeat=value; s->last_hb=now; out[0]=0x60; event(s,0x580U+s->node,out,8); return 1; } } /* В XDi-net heartbeat обязателен, в CANopen ноль его отключает. */ + out[0]=0x80; put32(out+4,abort); event(s,0x580U+s->node,out,8); return 0; /* Неподдерживаемый запрос не меняет значения или NMT-состояние. */ +} /* На каждый запрос получается не более одного ответа. */ +int setp_deif_accept(void *p,uint32_t id,const uint8_t *d,size_t n,unsigned flags,uint64_t now) { /* Основной общий автомат приёма внешнего MASTER. */ + deif_state *s=context(p); int32_t m[12]; int kind,changed=0; unsigned f,dam; if(!s || !s->active || flags || (!d && n)) return 0; /* Неверный канал отфильтровывает порт, EXT/RTR — ядро. */ + if(s->auto_at && now>=s->auto_at) { s->nmt=5; s->auto_at=0; } /* Автозапуск действует и перед приёмом очередного кадра. */ + if(s->services && id==0 && n==2U && (!d[1] || d[1]==s->node)) { /* NMT адресует свой узел либо broadcast 0. */ + if(d[0]==129U || d[0]==130U) return setp_deif_start(s,now,d[0]==129U); /* Два вида сброса различаются сохранением прикладных данных. */ + if(d[0]==1U || d[0]==2U || d[0]==128U) { s->nmt=d[0]==1U?5U:d[0]==2U?4U:127U; s->auto_at=0; return 1; } return 0; /* Явная NMT-команда отменяет отложенный self-start. */ + } /* Остальные служебные кадры не являются измерениями. */ + if(s->services && id==0x600U+s->node && n==8U && (s->nmt==5U || s->nmt==127U)) return sdo(s,d,now); /* SDO доступен в Operational и Pre-operational. */ + if(s->nmt!=5U || n!=8U) return 0; kind=setp_deif_inspect(id,d,n,0,m); if(kind!=1 && kind!=3 && kind!=-6) return 0; /* Явная пользовательская привязка разрешает неизвестный каталогу I16, но не неверную адресацию. */ + dam=(unsigned)m[8]; if(dam) { if(s->node && (unsigned)m[1]!=s->node) return 0; if(!s->node && id!=s->monitor_id) return 0; if(s->services && id!=(s->pdo+1U)*0x100U+s->node) return 0; } /* Без заданного физического Node пассивный монитор сохраняет точную привязку CAN ID и селектора. */ + else if(s->services ? (!s->xdi || m[1]>=127) : id!=s->monitor_id) return 0; /* Пассивный монитор сохраняет точную настройку CAN ID, CANopen без XDi-net игнорирует SAM. */ + if(kind==3 && s->services && s->group && (unsigned)m[2]==0x3500U+s->group) { if(d[5]||d[6]||d[7] || ((m[3]==2 && d[4]>100U)||(m[3]==3 && d[4]>2U))) return 0; if(m[3]==2) s->brightness=d[4]; else s->palette=d[4]; s->accepted++; return 1; } /* Яркость и Color не продлевают свежесть измерений. */ + if(d[6]||d[7]) return 0; for(f=0;ffield[f].bound && !memcmp(s->field[f].selector+1,d+1,3) && ((dam && s->node) || s->field[f].selector[0]==d[0])) { store(&s->field[f],m[4],now); changed=1; } /* DAM без настроенного физического получателя не превращается в приём всех узлов сети. */ + if(changed) s->accepted++; return changed; /* Приём PDO не создаёт выдуманного ответного PDO. */ +} /* Словарь прикладных значений не зависит от способа доставки кадра. */ +size_t setp_deif_poll(void *p,uint64_t now,setp_deif_frame_t *o,size_t cap) { /* Таймеры используют только переданные портом монотонные часы. */ + deif_state *s=context(p); size_t count; uint8_t state; unsigned lost; if(!s || !s->active || !s->services) return 0; /* Пассивное наблюдение не передаёт ничего в сеть. */ + if(s->auto_at && now>=s->auto_at) { s->nmt=5; s->auto_at=0; } /* Стандартный self-start заканчивается через 1000 мс. */ + if(s->heartbeat && now>=s->last_hb && now-s->last_hb>=s->heartbeat) { state=(uint8_t)s->nmt; event(s,0x700U+s->node,&state,1); s->last_hb=now; } /* После задержки создаётся один актуальный heartbeat без пачки пропущенных тиков. */ + lost=setp_deif_lost(s,now)!=0; if(s->nmt==5U && lost!=s->error) { uint8_t emcy[8]={0}; if(lost) { emcy[1]=0x81; emcy[2]=0x81; emcy[3]=6; } event(s,0x80U+s->node,emcy,8); s->error=lost; } /* Appendix 5: Data lost 0x8100/0x81/0x06 и нулевой Error reset. */ + count=s->queuedqueued:cap; if(!o || !count) return 0; memcpy(o,s->queue,count*sizeof(*o)); s->queued-=(unsigned)count; memmove(s->queue,s->queue+count,s->queued*sizeof(*o)); return count; /* Непоместившиеся события сохраняются для следующего вызова. */ +} /* Владение выходным буфером остаётся у Python/JNI/MCU. */ +int setp_deif_read(void *p,unsigned f,uint64_t now,unsigned expiry,double *value,int64_t *m) { /* Возвращает данные без подмены ошибочного кода числом. */ + deif_state *s=context(p); deif_binding *b; unsigned flags=0; if(!s || f>=DEIF_FIELDS || !value || !m) return 0; b=&s->field[f]; /* Границы поля проверяются до доступа к массиву. */ + if(b->bound && b->received) { flags=1U; if(!b->status) flags|=2U; if(expiry && now>=b->seen && now-b->seen>expiry) flags|=4U; } /* Пассивный UI скрывает данные через пять секунд, сервер хранит последнее значение для мигания. */ + *value=b->raw*b->scale; m[0]=(int64_t)b->seen; m[1]=b->raw; m[2]=b->status; m[3]=flags; return (int)flags; /* Разрешение применяется в единственном общем ядре. */ +} /* Метаданные позволяют отличить invalid от устаревания и отсутствия кадра. */ +int setp_deif_angle(void *p,uint64_t now,unsigned expiry,double *v) { /* Единое правило выбора азимута для Desktop и Android. */ + deif_state *s=context(p); int64_t m[4]; unsigned f,kind; int flags; if(!s || !v) return 0; f=s->field[2].received?2U:5U; kind=f==5U || s->field[2].selector[3]==7U?2U:1U; /* До первого фактического кадра используется только принятое сетевое задание с явной пометкой. */ + flags=setp_deif_read(s,f,now,expiry,v,m); return (flags&2) && !(flags&4) ? (int)kind : 0; /* Ошибочное фактическое сообщение не заменяется старым заданием. */ +} /* Черновиков GUI в этом API нет. */ +int setp_deif_meta(void *p,int64_t *o) { /* Копирует скалярный снимок без раскрытия private-layout состояния. */ + deif_state *s=context(p); if(!s || !o) return 0; o[0]=s->active; o[1]=s->nmt; o[2]=(int64_t)s->started; o[3]=(int64_t)s->last_hb; o[4]=s->heartbeat; o[5]=s->accepted; o[6]=s->group; o[7]=s->brightness; o[8]=s->palette; o[9]=s->error; return 1; /* Индексы массива документированы в публичном заголовке. */ +} /* API пригоден для прямого буфера JNI и ctypes без платформенных зависимостей. */ diff --git a/c/set-protocol/src/set_invertor_gas.c b/c/set-protocol/src/set_invertor_gas.c new file mode 100644 index 0000000..5f2c90d --- /dev/null +++ b/c/set-protocol/src/set_invertor_gas.c @@ -0,0 +1,175 @@ +#include "set_invertor_gas.h" /* Переиспользуем общий регистровый и масочный GAS вместо нового wire-кодека. */ +#include "pcan_id.h" /* Поля идентификатора упаковываются и проверяются существующей реализацией ProtoCAN. */ +#include /* NULL позволяет явно различать отсутствующие callbacks и выходные кадры. */ +typedef struct { /* Небольшая локальная транзакция существует только во время обработки одного CAN-кадра. */ + const set_invertor_gas_port_t *port; /* Provider остаётся единственным владельцем состояния изделия. */ + uint16_t addresses[PCAN_GAS_REGS_PER_FRAME]; /* Восемь октетов типа 0x3 содержат максимум четыре адресованных значения. */ + uint16_t values[PCAN_GAS_REGS_PER_FRAME]; /* Сначала собираем все изменения без обращения к write-provider. */ + unsigned count; /* Количество подготовленных слов не превышает фиксированные массивы. */ +} invertor_transaction; /* Нет глобального состояния, heap или обращения к HAL из переносимого модуля. */ +int32_t set_invertor_gas_register(unsigned slot, unsigned word) /* Отображение одинаково для прошивки и всех клиентов общей библиотеки. */ +{ /* Word0 содержит маску и исключён из полезной регистровой карты. */ + if (slot >= SET_INVERTOR_GAS_SLOT_COUNT || word == 0U || word > SET_INVERTOR_GAS_WORDS_PER_SLOT) { return -1; } /* Отвергаем несуществующие координаты до вычисления смещения. */ + return (int32_t)(slot * SET_INVERTOR_GAS_WORDS_PER_SLOT + word - 1U); /* Три значения каждого слота занимают три последовательных регистра. */ +} /* Максимальная координата слот 249/Word3 соответствует регистру 749. */ +int set_invertor_gas_coordinate(unsigned address, uint16_t *slot, uint16_t *word) /* Переводим отдельный регистр в слот и полезное слово без усечения адреса. */ +{ /* Проверяем оба выхода до первого изменения состояния вызывающего кода. */ + if (address >= SET_INVERTOR_GAS_REGISTER_COUNT || slot == NULL || word == NULL || slot == word) { return 0; } /* Алиас двух выходов не может дать корректную пару координат. */ + *slot = (uint16_t)(address / SET_INVERTOR_GAS_WORDS_PER_SLOT); /* Целая часть выбирает один из 250 слотов отдельного узла. */ + *word = (uint16_t)(1U + address % SET_INVERTOR_GAS_WORDS_PER_SLOT); /* Остаток выбирает Word1…Word3, пропуская служебную маску. */ + return 1; /* Оба выхода опубликованы после проверки всех аргументов. */ +} /* Заканчиваем единственную общую формулу преобразования координат. */ +uint32_t set_invertor_gas_request_id(unsigned message_type, unsigned address, unsigned route) /* Функция не назначает другой DeviceID по номеру слота. */ +{ /* Ноль является признаком отказа, а не допустимым ID этого устройства. */ + pcan_id_t id = {0}; /* Заполняем официальную структуру общего кодека без собственных сдвигов битов. */ + if (route > PCAN_ROUTE_FROM_DEVICE || address > UINT16_MAX) { return 0U; } /* Не допускаем усечения маршрута или адреса. */ + if (message_type == PCAN_MSG_GAS_SLOT) { /* Слотный тип проверяется по собственной ёмкости, а не по числу регистров. */ + if (address >= SET_INVERTOR_GAS_SLOT_COUNT) { return 0U; } /* Адрес 250 уже находится за выделенным блоком 0…249. */ + return pcan_gas_slot_id(SET_INVERTOR_DEVICE_TYPE, SET_INVERTOR_DEVICE_ID, address, route); /* Существующий кодек 0x4 задаёт нужные поля без нового формата. */ + } /* Другие типы не должны автоматически трактоваться как слоты. */ + if (message_type != PCAN_MSG_GAS || address >= SET_INVERTOR_GAS_REGISTER_COUNT) { return 0U; } /* Для типа 0x3 разрешены только 750 отдельных регистров. */ + id.priority = PCAN_PRIORITY_STANDARD; /* Обычная настройка и телеметрия используют стандартный приоритет протокола. */ + id.route = (uint_least8_t)route; /* Route явно различает команду верхнего уровня и ответ прибора. */ + id.device_type = SET_INVERTOR_DEVICE_TYPE; /* Пользователь выбрал класс устройств 0x7. */ + id.device_id = SET_INVERTOR_DEVICE_ID; /* Проверенный свободный экземпляр 0xE не меняет назначений 0xD/0xF. */ + id.msg_type = PCAN_MSG_GAS; /* Тип 0x3 обозначает отдельные регистры согласно новой инструкции Templates. */ + id.msg_body = (uint16_t)address; /* Уже проверенное значение помещается в 16-битный MsgBody. */ + return pcan_id_pack(&id); /* Публикуем ID единственной реализацией существующей битовой раскладки. */ +} /* Конец построения адресуемого идентификатора. */ +static pcan_gas_status_t ReadRegister(const pcan_gas_region_t *region, uint16_t offset, uint16_t *value) /* Связываем общий GAS-регион с данными приложения. */ +{ /* Offset региона равен адресу, поскольку начало отдельной карты равно нулю. */ + invertor_transaction *transaction = (invertor_transaction *)region->user; /* Контекст принадлежит только текущему кадру. */ + if (transaction->port->read == NULL || transaction->port->read(transaction->port->context, offset, value) != 0) { return PCAN_GAS_NO_REG; } /* Недоступное или недостоверное значение останавливает последовательное чтение. */ + return PCAN_GAS_OK; /* Достоверный ноль остаётся обычным успешным результатом provider. */ +} /* Чтение не обращается к write-provider и не изменяет уставки. */ +static pcan_gas_status_t StageRegister(const pcan_gas_region_t *region, uint16_t offset, uint16_t value) /* Откладываем применение всего входного блока до завершения общего декодера. */ +{ /* Callback вызывается существующим pcan_gas_handle для каждого разобранного слова. */ + invertor_transaction *transaction = (invertor_transaction *)region->user; /* Указатель не сохраняется за пределами текущего вызова. */ + if (transaction->port->write == NULL) { return PCAN_GAS_READ_ONLY; } /* Отсутствующий write-provider означает явный запрет записи. */ + if (transaction->count >= PCAN_GAS_REGS_PER_FRAME) { return PCAN_GAS_REJECTED; } /* Фиксированный массив не может переполниться от ошибочного callback. */ + transaction->addresses[transaction->count] = offset; /* Запоминаем отдельный логический адрес без арифметики указателей RAM. */ + transaction->values[transaction->count] = value; /* Значение ещё не попадает в рабочее состояние устройства. */ + transaction->count++; /* Следующее слово использует следующую свободную позицию локальной транзакции. */ + return PCAN_GAS_OK; /* Успех означает подготовку, а окончательное применение происходит после проверки всего кадра. */ +} /* Конец подготовки одного слова. */ +static int HandleRegisters(const set_invertor_gas_port_t *port, const pcan_frame_t *request, pcan_frame_t *response) /* Используем регистровый GAS 0x3 без повторного endian-разбора payload. */ +{ /* Все локальные структуры живут до возвращения общего обработчика. */ + invertor_transaction transaction = {0}; /* Пустая транзакция не может случайно повторить предыдущую запись. */ + pcan_gas_region_t region = {0}; /* Один логический регион не требует физического массива из 750 слов. */ + pcan_gas_map_t map; /* Карта ссылается на локальный регион только на время обработки. */ + pcan_frame_t out = {0}; /* Ошибка или запись не изменяет переданный вызывающим выходной кадр. */ + transaction.port = port; /* Все callbacks используют один согласованный источник данных приложения. */ + region.base = SET_INVERTOR_GAS_REGISTER_FIRST; /* Нулевое начало относится только к отдельному узлу инвертора 7/E. */ + region.count = SET_INVERTOR_GAS_REGISTER_COUNT; /* Последний действующий регистр имеет адрес 0x02ED, то есть 749. */ + region.read = ReadRegister; /* Общая библиотека проверяет диапазон прежде, чем читать приложение. */ + region.write = StageRegister; /* Ни одно слово не применяется до окончания подготовки всего блока. */ + region.user = &transaction; /* Provider получает контекст без глобальной памяти и зависимостей от ОС. */ + map.regions = ®ion; /* Единственный регион не пересекается с картами других CAN-узлов. */ + map.count = 1U; /* Карта содержит ровно один подключённый диапазон значений. */ + if (request->dlc == 0U) { /* Чтение использует стандартное правило ответа до четырёх последовательных регистров. */ + if (response == NULL || !pcan_gas_handle(&map, request, &out)) { return -1; } /* Пустой или недоступный диапазон не превращается в фиктивный ответ. */ + *response = out; return 1; /* Публикуем полностью сформированный существующим кодеком ответ. */ + } /* Непустой payload далее является только запросом записи. */ + if (request->dlc > PCAN_DATA_MAX || (request->dlc & 1U) != 0U) { return -1; } /* Отбрасываем нечётную длину и превышение восьми октетов до подготовки слов. */ + (void)pcan_gas_handle(&map, request, &out); /* Весь wire-пакет разбирает тот же кодек, который используется другими устройствами. */ + if (port->write == NULL || transaction.count != request->dlc / 2U) { return -1; } /* Частично попавший в карту блок не меняет даже свои первые корректные адреса. */ + return port->write(port->context, transaction.addresses, transaction.values, transaction.count) == 0 ? 0 : -1; /* Provider проверяет весь набор прав/значений и применяет его одним вызовом без скрытого ACK. */ +} /* Конец регистрового пути с единым состоянием приложения. */ +static int ReadSlot(void *context, uint16_t slot, uint16_t words[4]) /* Масочный ответ проецирует те же значения в Word1…Word3. */ +{ /* Word0 формируется только из реально прочитанных значений. */ + const set_invertor_gas_port_t *port = (const set_invertor_gas_port_t *)context; /* Указатель относится к тому же provider, что используется обычным GAS. */ + unsigned word; /* Индекс перебирает три полезных позиции, исключая маску. */ + if (slot >= SET_INVERTOR_GAS_SLOT_COUNT || port->read == NULL) { return -1; } /* Слот вне выделенного блока либо отсутствующий источник не порождает нулевую телеметрию. */ + words[0] = 0U; /* До успешного чтения ни одно поле не отмечено достоверным. */ + for (word = 1U; word <= SET_INVERTOR_GAS_WORDS_PER_SLOT; ++word) { /* Каждое полезное слово получает независимую проверку доступности. */ + uint16_t value = 0U; /* Неиспользуемое место кадра заполняется нулём без утверждения его достоверности. */ + const uint16_t address = (uint16_t)set_invertor_gas_register(slot, word); /* Проверенные координаты отображаются единственной общей формулой. */ + words[word] = 0U; /* Не допускаем утечки неинициализированного значения в ответ CAN. */ + if (port->read(port->context, address, &value) == 0) { /* Только положительный результат provider разрешает соответствующий бит. */ + words[0] |= (uint16_t)(1U << word); /* Биты 1…3 имеют смысл, заданный новым контрактом GAS SLOT 0x4. */ + words[word] = value; /* Сохраняем достоверное значение, в том числе корректный числовой ноль. */ + } /* Недоступный сосед не делает доступные значения ошибочными. */ + } /* Полностью неподключённый слот имеет пустую маску и не публикуется. */ + return words[0] != 0U ? 0 : -1; /* Общий кодек отправляет только ответ с непустой разрешённой маской. */ +} /* Чтение слота не создаёт второе хранилище значений. */ +static int WriteSlot(void *context, uint16_t slot, const uint16_t words[4]) /* Запись передаёт provider только выбранные маской позиции. */ +{ /* Предварительное чтение соседних readonly-полей не требуется. */ + const set_invertor_gas_port_t *port = (const set_invertor_gas_port_t *)context; /* Используем единственный источник состояния обоих типов GAS. */ + uint16_t addresses[PCAN_GAS_SLOT_DATA_WORDS], values[PCAN_GAS_SLOT_DATA_WORDS]; /* Один слот содержит максимум три изменяемых значения. */ + unsigned count = 0U, word; /* Подготавливаем компактный список только отмеченных слов. */ + if (slot >= SET_INVERTOR_GAS_SLOT_COUNT || port->write == NULL) { return -1; } /* Слот вне диапазона и отсутствие права записи не вызывают приложение. */ + for (word = 1U; word <= SET_INVERTOR_GAS_WORDS_PER_SLOT; ++word) { /* Маска уже полностью проверена общим кодеком 0x4. */ + if ((words[0] & (1U << word)) != 0U) { /* Неотмеченные поля сохраняются и не участвуют в проверке прав записи. */ + addresses[count] = (uint16_t)set_invertor_gas_register(slot, word); /* Переводим полезное слово в общий регистровый адрес. */ + values[count] = words[word]; /* Значение передаётся без изменения знака или масштаба поля. */ + count++; /* Число выбранных позиций ограничено тремя размерами маски. */ + } /* readonly-сосед не запрещает запись другой отмеченной позиции слота. */ + } /* До этого места ни одно значение приложения не изменялось. */ + return port->write(port->context, addresses, values, count) == 0 ? 0 : -1; /* Provider целиком проверяет выбранную транзакцию перед применением. */ +} /* Успех записи не создаёт не предусмотренный базовым протоколом ответ ACK. */ +int set_invertor_gas_handle(const set_invertor_gas_port_t *port, const pcan_frame_t *request, pcan_frame_t *response) /* Публичный диспетчер выбирает формат исключительно по MsgType. */ +{ /* Все проверки адреса узла выполняются раньше callbacks приложения. */ + pcan_id_t id; /* Поля извлекает существующая общая реализация ProtoCAN. */ + pcan_gas_slot_frame_t slot_request = {0}, slot_response = {0}; /* Масочный путь переиспользует готовый тип независимого Classic CAN-кодека. */ + pcan_gas_slot_port_t slot_port; /* Локальный адаптер соединяет три значения слота с единой картой. */ + pcan_frame_t out = {0}; /* Ошибка оставляет выходной кадр вызывающего без изменений. */ + unsigned i; int result; /* Размеры всех копируемых буферов заранее ограничены восемью октетами. */ + if (port == NULL || request == NULL || request->dlc > PCAN_DATA_MAX || (request->id & ~PCAN_ID_MASK) != 0U || (request->flags & PCAN_FLAG_IDE) == 0U || (request->flags & (PCAN_FLAG_RTR | PCAN_FLAG_ERR)) != 0U) { return -1; } /* Отвергаем нецелевые типы физических кадров и неверный размер до разбора данных. */ + pcan_id_unpack(request->id, &id); /* Не дублируем битовые сдвиги адресации в адаптере изделия. */ + if (id.device_type != SET_INVERTOR_DEVICE_TYPE || id.device_id != SET_INVERTOR_DEVICE_ID || id.route != PCAN_ROUTE_FROM_PM) { return -1; } /* Ответы других приборов и запросы к соседям не могут менять состояние инвертора. */ + if (id.msg_type == PCAN_MSG_GAS) { return HandleRegisters(port, request, response); } /* Тип 0x3 интерпретирует MsgBody как отдельный регистр. */ + if (id.msg_type != PCAN_MSG_GAS_SLOT || id.msg_body >= SET_INVERTOR_GAS_SLOT_COUNT || (request->dlc == 0U && response == NULL)) { return -1; } /* Тип 0x4 использует только выделенные 250 слотов; других форматов или fallback нет. */ + slot_port.context = (void *)port; /* Callback не изменяет структуру порта и не сохраняет указатель после возврата. */ + slot_port.read = ReadSlot; /* Общий слотный кодек получает маску достоверности от нашего отображения данных. */ + slot_port.write = WriteSlot; /* Общая проверка wire-маски предшествует вызову прикладной записи. */ + slot_request.id = request->id; /* Адрес узла и MsgType сохраняются без повторной упаковки. */ + slot_request.flags = request->flags; /* Служебные признаки уже проверены публичной границей. */ + slot_request.dlc = request->dlc; /* Длина остаётся количеством октетов физического кадра. */ + for (i = 0U; i < request->dlc; ++i) { slot_request.data[i] = request->data[i]; } /* Копируем готовые октеты, не разбирая их вторым endian-кодеком. */ + result = pcan_gas_slot_handle_address(SET_INVERTOR_DEVICE_TYPE, SET_INVERTOR_DEVICE_ID, &slot_port, &slot_request, &slot_response); /* Существующий C-кодек проверяет маску и строит ответ 0x4. */ + if (result == 1) { /* Только завершённое чтение публикует CAN-ответ. */ + out.id = slot_response.id; out.flags = (uint8_t)slot_response.flags; out.dlc = (uint8_t)slot_response.dlc; /* Переносим проверенный транспортный результат без изменения его смысла. */ + for (i = 0U; i < out.dlc; ++i) { out.data[i] = (uint8_t)slot_response.data[i]; } /* Восемь готовых октетов копируются в обычный кадр проекта STM32/host. */ + *response = out; /* Выход изменяется только после успешного выполнения всех общих проверок. */ + } /* Запись и ошибка сохраняют предыдущий выход вызывающего кода. */ + return result; /* 1 означает ответ, 0 — запись, -1 — отказ без изменения соседнего протокольного профиля. */ +} /* Конец общего адаптера двух GAS-интерфейсов одного инвертора. */ +unsigned set_invertor_gas_limit(unsigned message_type) /* Единый C-источник границ не допускает расхождения Python, прошивки и другого GUI. */ +{ /* Значение ноль обозначает неизвестный тип, а не пустой действующий блок. */ + if (message_type == PCAN_MSG_GAS) { return SET_INVERTOR_GAS_REGISTER_COUNT; } /* Обычный GAS адресует каждое из 750 значений отдельно. */ + return message_type == PCAN_MSG_GAS_SLOT ? SET_INVERTOR_GAS_SLOT_COUNT : 0U; /* Масочный GAS имеет 250 слотов, остальные типы не поддерживаются этим профилем. */ +} /* Завершаем получение размера карты без аппаратных зависимостей. */ +int set_invertor_gas_build(unsigned message_type, unsigned address, const uint16_t *words, unsigned count, uint32_t *identifier, uint8_t data[8]) /* Все endian-преобразования остаются внутри общего C-кодека. */ +{ /* При отказе не меняем ни идентификатор, ни выходные октеты вызывающего кода. */ + pcan_frame_t frame = {0}; uint_least8_t slot_data[8] = {0}; unsigned i; int length; /* Временный результат ограничен одним восьмиоктетным кадром. */ + const uint32_t id = set_invertor_gas_request_id(message_type, address, PCAN_ROUTE_FROM_PM); /* Проверяем тип, узел и соответствующую единицу адресации централизованно. */ + if (id == 0U || identifier == NULL || data == NULL || count > 4U || (count != 0U && words == NULL)) { return -1; } /* Проверяем буферы и число слов до обращения к существующим builders. */ + if (message_type == PCAN_MSG_GAS) { /* У типа 0x3 допустима запись от одного до четырёх последовательных значений. */ + if (address + count > SET_INVERTOR_GAS_REGISTER_COUNT) { return -1; } /* Даже начало последнего регистра не позволяет записать несуществующего соседа. */ + pcan_gas_regs_to_frame(&frame, id, (uint16_t)address, words, (uint16_t)count); length = frame.dlc; /* Полностью переиспользуем существующую сериализацию обычного GAS. */ + } else { /* Оставшийся проверенный тип равен GAS SLOT 0x4. */ + length = pcan_gas_slot_encode(words, count, slot_data); if (length < 0) { return -1; } /* Общий кодек требует четыре wire-слова с корректной маской либо пустой запрос. */ + for (i = 0U; i < (unsigned)length; ++i) { frame.data[i] = (uint8_t)slot_data[i]; } /* Копируем готовые октеты без повторного разбора расположения слов. */ + } /* После успешного кодирования можно публиковать результат вызывающему. */ + *identifier = id; for (i = 0U; i < 8U; ++i) { data[i] = frame.data[i]; } /* Выход всегда полностью инициализирован, включая неиспользуемый хвост. */ + return length; /* Ноль обозначает корректное чтение с DLC=0, отрицательное значение — отказ. */ +} /* Конец builder для тонких ctypes/JNI-портов. */ +int set_invertor_gas_decode(uint32_t identifier, unsigned flags, const uint8_t *data, unsigned length, unsigned metadata[2], uint16_t words[4]) /* Клиент получает уже проверенные значения без собственной битовой и endian-логики. */ +{ /* Временные выходы защищают данные вызывающего при некорректном ответе. */ + pcan_frame_t frame = {0}; pcan_id_t id; uint16_t decoded[4] = {0}; uint_least8_t slot_data[8]; unsigned i; int count; /* Все структуры локальны и имеют фиксированную вместимость. */ + if (metadata == NULL || words == NULL || data == NULL || length == 0U || length > 8U || flags > UINT8_MAX || (identifier & ~PCAN_ID_MASK) != 0U || (flags & PCAN_FLAG_IDE) == 0U || (flags & (PCAN_FLAG_RTR | PCAN_FLAG_ERR)) != 0U) { return -1; } /* Ответ должен быть непустым допустимым Extended data frame. */ + pcan_id_unpack(identifier, &id); /* Существующий кодек раскрывает поля идентификатора. */ + if (id.device_type != SET_INVERTOR_DEVICE_TYPE || id.device_id != SET_INVERTOR_DEVICE_ID || id.route != PCAN_ROUTE_FROM_DEVICE) { return -1; } /* Эхо запроса или ответ другого узла не обновляет телеметрию инвертора. */ + if (id.msg_type == PCAN_MSG_GAS) { /* Обычный GAS допускает один, два, три или четыре значения. */ + frame.id = identifier; frame.flags = (uint8_t)flags; frame.dlc = (uint8_t)length; /* Устанавливаем проверенные транспортные метаданные существующего frame-кодека. */ + for (i = 0U; i < length; ++i) { frame.data[i] = data[i]; } /* Исходные октеты не разбираются повторно на языковой стороне. */ + count = (int)pcan_gas_frame_to_regs(&frame, decoded, 4U); /* Длину и little-endian проверяет существующая реализация GAS. */ + if (count == 0 || (unsigned)id.msg_body + (unsigned)count > SET_INVERTOR_GAS_REGISTER_COUNT) { return -1; } /* Не публикуем нечётный payload либо блок, пересекающий конец 750 регистров. */ + } else if (id.msg_type == PCAN_MSG_GAS_SLOT && id.msg_body < SET_INVERTOR_GAS_SLOT_COUNT) { /* Масочный ответ проверяется по собственной карте 250 слотов. */ + for (i = 0U; i < length; ++i) { slot_data[i] = data[i]; } /* Передаём готовые октеты общему слотному декодеру. */ + count = pcan_gas_slot_decode_words(identifier, flags, slot_data, length, decoded); if (count != 4) { return -1; } /* Проверяем DLC=8 и допустимую непустую маску Word0. */ + } else { return -1; } /* Другой MsgType или несуществующий слот не вызывают fallback к обычным регистрам. */ + metadata[0] = id.msg_type; metadata[1] = id.msg_body; /* Публикуем тип и адрес только после проверки всего ответа. */ + for (i = 0U; i < (unsigned)count; ++i) { words[i] = decoded[i]; } /* Передаём только реально декодированные wire-слова. */ + return count; /* GUI работает со словами и типом, а не повторяет wire-протокол. */ +} /* Завершаем единый клиентский декодер нового профиля. */ diff --git a/c/set-protocol/tests/fixtures/deif-xdi-v1.tsv b/c/set-protocol/tests/fixtures/deif-xdi-v1.tsv new file mode 100644 index 0000000..3f8633e --- /dev/null +++ b/c/set-protocol/tests/fixtures/deif-xdi-v1.tsv @@ -0,0 +1,15 @@ +# COB-ID payload flags result RAW index subindex +201 0101300784030000 0 1 900 3001 7 +201 0101300284030000 0 1 900 3001 2 +201 0181300239FC0000 0 1 -967 3081 2 +201 0191300203FE0000 0 1 -509 3091 2 +201 01213402CEF70000 0 1 -2098 3421 2 +201 01313402D4FE0000 0 1 -300 3431 2 +30D 8D01300784030000 0 1 900 3001 7 +201 0101300784030000 1 -1 0 0 0 +201 0101300784030000 2 -1 0 0 0 +202 0101300784030000 0 -5 900 3001 7 +201 0181 0 -4 0 0 0 +701 05 0 2 0 0 0 +70D 00 0 2 0 0 0 +70D 7F 0 2 0 0 0 diff --git a/c/set-protocol/tests/test_deif_xdi.c b/c/set-protocol/tests/test_deif_xdi.c new file mode 100644 index 0000000..39b253c --- /dev/null +++ b/c/set-protocol/tests/test_deif_xdi.c @@ -0,0 +1,26 @@ +#include "deif_xdi.h" /* Эталонные векторы проверяют непосредственно единственное C-ядро. */ +#include /* Любое расхождение байтов или сроков делает тест неуспешным. */ +#include /* Файловый ввод нужен только хостовому тесту, не переносимому ядру. */ +#include /* Выравнивание контекста обеспечивает стандартный хостовый allocator. */ +#include /* Сравниваются точные CAN-последовательности. */ +int main(int argc,char **argv) { /* Один набор векторов используется также Python и Android/JVM. */ + FILE *input; char line[160],hex[17]; unsigned id,flags,index,sub,byte,i; int expected,raw; int32_t meta[12]; uint8_t data[8]; size_t n; void *state; double value; setp_deif_frame_t out[16]; /* Все размеры ограничены форматом Classic CAN и ABI. */ + assert(argc==2); input=fopen(argv[1],"r"); assert(input); /* Путь fixtures передаётся сборщиком без зависимости от cwd. */ + while(fgets(line,sizeof line,input)) { /* Таблица содержит положительные и ошибочные кадры. */ + if(line[0]=='#') continue; assert(sscanf(line,"%x %16s %u %d %d %x %x",&id,hex,&flags,&expected,&raw,&index,&sub)==7); /* Это чтение текстовой fixture, а не второй CAN-декодер. */ + n=strlen(hex)/2U; for(i=0;i /* Ошибка теста выводит строку и возвращает ненулевой код также в Release. */ +#include /* Сравниваем исходные данные и неизменность выходного кадра при отказе. */ +#define CHECK(condition) do { if (!(condition)) { printf("invertor_gas:%d: %s\n", __LINE__, #condition); return 1; } } while (0) /* Проверки не зависят от assert или NDEBUG. */ +typedef struct { /* Этот массив существует только в тесте и не задаёт способ хранения прошивки. */ + uint16_t values[SET_INVERTOR_GAS_REGISTER_COUNT]; /* Отличимые значения позволяют обнаружить запись лишнего соседа. */ + unsigned reads; /* Счётчик выявляет скрытый read-modify-write при масочной записи. */ + unsigned commits; /* Каждая успешная запись должна иметь один вызов прикладной транзакции. */ + int unavailable; /* Отдельный адрес моделирует отсутствующее или недостоверное измерение. */ + int readonly; /* Независимый адрес запрещён для записи, но остаётся доступен для чтения. */ +} model; /* Все данные принадлежат тесту и отделены от общего wire-кодека. */ +static int Read(void *context, uint16_t address, uint16_t *value) /* Моделируем provider одного значения с явным отказом. */ +{ /* Чтение не меняет сами значения карты. */ + model *state = (model *)context; state->reads++; /* Считаем обращения независимо от успеха для проверки пути записи. */ + if (address >= SET_INVERTOR_GAS_REGISTER_COUNT || (int)address == state->unavailable) { return -1; } /* Отсутствие значения отличается от достоверного нуля. */ + *value = state->values[address]; return 0; /* Возвращаем текущее значение общей модели. */ +} /* Конец диагностического read-provider. */ +static int Write(void *context, const uint16_t *addresses, const uint16_t *values, unsigned count) /* Выполняем согласованное применение только полностью проверенного набора. */ +{ /* Тестовый provider реализует обязательный контракт проверки до изменения данных. */ + model *state = (model *)context; unsigned i; /* Контекст и индекс не обращаются к аппаратуре или динамической памяти. */ + if (count == 0U || count > PCAN_GAS_REGS_PER_FRAME) { return -1; } /* Пустая транзакция не считается успешной командой. */ + for (i = 0U; i < count; ++i) { if (addresses[i] >= SET_INVERTOR_GAS_REGISTER_COUNT || (int)addresses[i] == state->readonly || values[i] == UINT16_MAX) { return -1; } } /* Граница, readonly и запрещённое тестовое значение проверяются для всего набора до первой записи. */ + for (i = 0U; i < count; ++i) { state->values[addresses[i]] = values[i]; } /* Только полностью валидная транзакция изменяет выбранные адреса. */ + state->commits++; return 0; /* Подтверждаем ровно одно применение набора без протокольного ACK. */ +} /* Завершаем тестовый write-provider. */ +static pcan_frame_t RegisterFrame(unsigned address, const uint16_t *values, unsigned count) /* Сериализация обычного GAS выполняется существующей функцией Templates. */ +{ /* Нулевое число слов обозначает запрос чтения. */ + pcan_frame_t frame = {0}; /* Избегаем неинициализированных флагов и неиспользуемых октетов. */ + pcan_gas_regs_to_frame(&frame, set_invertor_gas_request_id(PCAN_MSG_GAS, address, 0U), (uint16_t)address, values, (uint16_t)count); /* Кодек задаёт little-endian и точную длину 0/2/4/6/8. */ + return frame; /* Возвращаем готовый транспортный кадр без второго wire-builder в тесте. */ +} /* Конец создания регистрового запроса. */ +static pcan_frame_t SlotFrame(unsigned slot, const uint16_t words[4]) /* Масочный payload формируется тем же общим кодеком, что используют клиенты. */ +{ /* NULL обозначает чтение слота без данных. */ + pcan_frame_t frame = {0}; uint_least8_t data[8] = {0}; unsigned i; /* Локальные буферы ограничены одним Classic CAN-кадром. */ + frame.id = set_invertor_gas_request_id(PCAN_MSG_GAS_SLOT, slot, 0U); frame.flags = PCAN_FLAG_IDE; /* Выбираем тип 0x4 отдельно от типа 0x3. */ + frame.dlc = (uint8_t)pcan_gas_slot_encode(words, words == NULL ? 0U : 4U, data); /* Существующий кодек проверяет непустую маску перед формированием октетов. */ + for (i = 0U; i < frame.dlc; ++i) { frame.data[i] = (uint8_t)data[i]; } /* Переносим готовые октеты в стандартный кадр тестового транспорта. */ + return frame; /* Ни одной формулы endian или CRC здесь нет. */ +} /* Конец создания слотного запроса. */ +int main(void) /* Проверяем согласованность, границы, права и отсутствие fallback между двумя типами. */ +{ /* Модель используется без подключения силовой электроники. */ + model state = {0}; set_invertor_gas_port_t port = {&state, Read, Write}; /* Оба типа GAS используют один и тот же provider. */ + pcan_frame_t request, response, unchanged; uint16_t pair[2], words[4], slot, word; unsigned i, reads, commits; pcan_id_t id; /* Все временные данные имеют фиксированные размеры. */ + state.unavailable = -1; state.readonly = -1; /* В начале все значения доступны и разрешены для тестовой записи. */ + for (i = 0U; i < SET_INVERTOR_GAS_REGISTER_COUNT; ++i) { state.values[i] = (uint16_t)(1000U + i); } /* Отличающиеся соседи показывают любую лишнюю запись. */ + CHECK(SET_INVERTOR_GAS_REGISTER_COUNT == 750U && SET_INVERTOR_GAS_SLOT_COUNT == 250U); /* Word0-маски не считаются дополнительными хранимыми значениями. */ + CHECK(set_invertor_gas_request_id(PCAN_MSG_GAS, 0U, 0U) == UINT32_C(0x17E30000)); /* Проверяем эталон первого запроса обычного GAS к узлу 7/E. */ + CHECK(set_invertor_gas_request_id(PCAN_MSG_GAS_SLOT, 0U, 0U) == UINT32_C(0x17E40000)); /* Тип слота отличается только назначенным полем MsgType. */ + CHECK(set_invertor_gas_request_id(PCAN_MSG_GAS, 749U, 1U) == UINT32_C(0x1FE302ED)); /* Проверяем последний регистр и направление ответа. */ + CHECK(set_invertor_gas_request_id(PCAN_MSG_GAS, 750U, 0U) == 0U && set_invertor_gas_request_id(PCAN_MSG_GAS_SLOT, 250U, 0U) == 0U); /* Первые адреса за двумя картами отвергаются без усечения. */ + CHECK(set_invertor_gas_request_id(5U, 0U, 0U) == 0U && set_invertor_gas_request_id(3U, 0U, 2U) == 0U); /* Другой MsgType и недопустимый Route не подменяются GAS. */ + CHECK(set_invertor_gas_register(249U, 3U) == 749 && set_invertor_gas_register(250U, 1U) == -1 && set_invertor_gas_register(0U, 0U) == -1); /* Маска не адресует регистр, а последняя полезная координата существует. */ + for (i = 0U; i < SET_INVERTOR_GAS_REGISTER_COUNT; ++i) { CHECK(set_invertor_gas_coordinate(i, &slot, &word)); CHECK(set_invertor_gas_register(slot, word) == (int32_t)i); } /* У каждой из 750 позиций есть ровно согласованная полезная координата. */ + slot = 99U; word = 88U; CHECK(!set_invertor_gas_coordinate(750U, &slot, &word)); CHECK(slot == 99U && word == 88U); /* Ошибка координат не изменяет выходы вызывающего. */ + pair[0] = 0x1234U; request = RegisterFrame(1U, pair, 1U); reads = state.reads; /* Одна уставка записывается обычным GAS без выбора слота. */ + CHECK(set_invertor_gas_handle(&port, &request, NULL) == 0); CHECK(state.values[0] == 1000U && state.values[1] == 0x1234U && state.values[2] == 1002U); CHECK(state.reads == reads); /* Соседние регистры не меняются и предварительное чтение не выполняется. */ + request = SlotFrame(0U, NULL); CHECK(set_invertor_gas_handle(&port, &request, &response) == 1); /* Тот же результат сразу виден через слотный доступ. */ + CHECK(pcan_gas_slot_decode_words(response.id, response.flags, response.data, response.dlc, words) == 4); CHECK(words[0] == 0xEU && words[1] == 1000U && words[2] == 0x1234U && words[3] == 1002U); /* Маска и три значения не создают второй копии данных. */ + words[0] = 4U; words[1] = 55U; words[2] = 4321U; words[3] = 66U; request = SlotFrame(0U, words); reads = state.reads; /* Выбираем только Word2; ненужные значения специально отличаются от текущих. */ + CHECK(set_invertor_gas_handle(&port, &request, NULL) == 0); CHECK(state.values[0] == 1000U && state.values[1] == 4321U && state.values[2] == 1002U && state.reads == reads); /* Неотмеченные поля сохраняются без чтения слота на клиенте. */ + request = RegisterFrame(1U, NULL, 0U); CHECK(set_invertor_gas_handle(&port, &request, &response) == 1); CHECK(pcan_gas_frame_to_regs(&response, words, 4U) == 4U && words[0] == 4321U); /* Изменение через 0x4 видно через обычный GAS 0x3. */ + state.readonly = 3; words[0] = 4U; words[2] = 2222U; request = SlotFrame(1U, words); /* В слоте 1 Word1 соответствует readonly-регистру 3, но маска выбирает регистр 4. */ + CHECK(set_invertor_gas_handle(&port, &request, NULL) == 0 && state.values[3] == 1003U && state.values[4] == 2222U); /* Неотмеченный readonly-сосед не мешает разрешённой записи. */ + pair[0] = 77U; pair[1] = 88U; state.readonly = 7; commits = state.commits; request = RegisterFrame(6U, pair, 2U); /* Второе слово блока намеренно запрещено для записи. */ + CHECK(set_invertor_gas_handle(&port, &request, NULL) == -1 && state.values[6] == 1006U && state.values[7] == 1007U && state.commits == commits); /* Весь блок отклоняется до применения первого корректного слова. */ + state.readonly = -1; request = RegisterFrame(749U, pair, 2U); CHECK(set_invertor_gas_handle(&port, &request, NULL) == -1 && state.values[749] == 1749U && state.commits == commits); /* Пересечение конца карты не вызывает частичную запись. */ + request = RegisterFrame(749U, NULL, 0U); CHECK(set_invertor_gas_handle(&port, &request, &response) == 1 && response.dlc == 2U); /* Конец обычной карты читается коротким корректным ответом из одного слова. */ + request = SlotFrame(249U, NULL); CHECK(set_invertor_gas_handle(&port, &request, &response) == 1 && response.dlc == 8U); /* Последний слот содержит все три реальные позиции 747…749. */ + state.unavailable = 1; request = SlotFrame(0U, NULL); CHECK(set_invertor_gas_handle(&port, &request, &response) == 1); /* Частичный ответ отмечает только реально доступные значения. */ + CHECK(pcan_gas_slot_decode_words(response.id, response.flags, response.data, response.dlc, words) == 4 && words[0] == 10U && words[2] == 0U); /* Недостоверный Word2 отличается от достоверного числового нуля отсутствием бита 2. */ + state.unavailable = -1; memset(&unchanged, 0xA5, sizeof(unchanged)); response = unchanged; /* Отмечаем выходной кадр для проверки всех отказов. */ + words[0] = 4U; words[2] = 200U; request = SlotFrame(0U, words); request.data[0] = 0x10U; commits = state.commits; /* Устанавливаем зарезервированный бит маски в заранее корректном wire-кадре. */ + CHECK(set_invertor_gas_handle(&port, &request, &response) == -1 && state.commits == commits && memcmp(&response, &unchanged, sizeof(response)) == 0); /* Отказ не меняет ни данные, ни предыдущий ответ. */ + request = SlotFrame(0U, words); request.dlc = 2U; CHECK(set_invertor_gas_handle(&port, &request, &response) == -1); /* У типа 0x4 нет формата короткой записи отдельного значения. */ + request = RegisterFrame(0U, pair, 1U); request.dlc = 1U; CHECK(set_invertor_gas_handle(&port, &request, &response) == -1); /* Нечётный обычный GAS не обрабатывается как корректное первое слово. */ + request.dlc = 9U; CHECK(set_invertor_gas_handle(&port, &request, &response) == -1); /* Payload больше Classic CAN не выходит за фиксированные массивы. */ + request = RegisterFrame(0U, pair, 1U); request.flags |= PCAN_FLAG_RTR; CHECK(set_invertor_gas_handle(&port, &request, &response) == -1); /* RTR не является командой записи. */ + request.flags = PCAN_FLAG_IDE | PCAN_FLAG_ERR; CHECK(set_invertor_gas_handle(&port, &request, &response) == -1); /* Ошибка транспорта не становится прикладной командой. */ + request.flags = 0U; CHECK(set_invertor_gas_handle(&port, &request, &response) == -1); /* Стандартный 11-битный кадр не совпадает с расширенным адресом инвертора. */ + request.flags = PCAN_FLAG_IDE; pcan_id_unpack(request.id, &id); id.device_id = 13U; request.id = pcan_id_pack(&id); /* Меняем адрес на существующий конфигуратор 7/D. */ + CHECK(set_invertor_gas_handle(&port, &request, &response) == -1 && state.commits == commits); /* Инвертор не исполняет запросы соседнего прибора. */ + request = RegisterFrame(0U, pair, 1U); pcan_id_unpack(request.id, &id); id.route = PCAN_ROUTE_FROM_DEVICE; request.id = pcan_id_pack(&id); /* Ответ другого прибора не должен исполняться повторно как запись. */ + CHECK(set_invertor_gas_handle(&port, &request, &response) == -1); /* Запросом считается только направление от верхнего уровня. */ + request = RegisterFrame(0U, NULL, 0U); CHECK(set_invertor_gas_handle(&port, &request, NULL) == -1); CHECK(set_invertor_gas_handle(NULL, &request, &response) == -1); /* Проверяем отсутствие буфера ответа и отсутствующий provider. */ + port.write = NULL; request = RegisterFrame(0U, pair, 1U); CHECK(set_invertor_gas_handle(&port, &request, NULL) == -1); /* Readonly-порт запрещает запись обоими представлениями данных. */ + request = SlotFrame(0U, words); CHECK(set_invertor_gas_handle(&port, &request, NULL) == -1); /* Слотная маска не обходит отсутствие write-provider. */ + { /* Клиентский ABI использует те же wire-векторы, не создавая второго кодека для GUI. */ + uint32_t identifier = 0U; uint8_t data[8] = {0}; unsigned metadata[2] = {99U, 88U}; int length; /* Буферы имеют точные пределы Classic CAN и одного ответа. */ + CHECK(set_invertor_gas_limit(3U) == 750U && set_invertor_gas_limit(4U) == 250U && set_invertor_gas_limit(5U) == 0U); /* Размеры берутся из единственного профиля, а не дублируются клиентом. */ + pair[0] = 0x1234U; length = set_invertor_gas_build(3U, 1U, pair, 1U, &identifier, data); /* Одна обычная уставка передаётся двумя октетами. */ + CHECK(length == 2 && identifier == UINT32_C(0x17E30001) && data[0] == 0x34U && data[1] == 0x12U && data[2] == 0U); /* Проверяем эталонные ID, little-endian и очищенный хвост. */ + CHECK(set_invertor_gas_decode(UINT32_C(0x1FE30001), 1U, data, 2U, metadata, words) == 1 && metadata[0] == 3U && metadata[1] == 1U && words[0] == 0x1234U); /* Клиентский разбор восстанавливает тот же тип, адрес и значение. */ + words[0] = 4U; words[1] = 0U; words[2] = 0x1234U; words[3] = 0U; /* Масочный wire-набор выбирает только второе полезное значение. */ + CHECK(set_invertor_gas_build(4U, 0U, words, 4U, &identifier, data) == 8 && identifier == UINT32_C(0x17E40000)); /* Builder использует именно слотный MsgType 0x4. */ + CHECK(data[0] == 4U && data[1] == 0U && data[4] == 0x34U && data[5] == 0x12U); /* Фиксированный тестовый вектор подтверждает положение маски и Word2. */ + CHECK(set_invertor_gas_decode(UINT32_C(0x1FE40000), 1U, data, 8U, metadata, words) == 4 && words[0] == 4U && words[2] == 0x1234U); /* Ответ проверяется тем же масочным кодеком. */ + CHECK(set_invertor_gas_build(4U, 0U, words, 3U, &identifier, data) == -1); /* Неполный масочный payload не упаковывается как обычный GAS. */ + CHECK(set_invertor_gas_decode(UINT32_C(0x17E40000), 1U, data, 8U, metadata, words) == -1); /* Эхо исходящего запроса не считается обновлением телеметрии. */ + CHECK(set_invertor_gas_decode(UINT32_C(0x1FE30000), 1U, data, 3U, metadata, words) == -1); /* Нечётный обычный payload отвергается клиентской границей. */ + } /* Клиентская проверка не обращается к реальному устройству. */ + puts("invertor GAS 0x3 + GAS SLOT 0x4: OK"); return 0; /* Все проверки выполнены на реальном общем C-кодеке без оборудования. */ +} /* Конец сценариев согласованности и защиты границ новой карты инвертора. */ diff --git a/c/set-protocol/tools/build_host.py b/c/set-protocol/tools/build_host.py index 3c6dcda..c587c88 100644 --- a/c/set-protocol/tools/build_host.py +++ b/c/set-protocol/tools/build_host.py @@ -24,6 +24,8 @@ INCLUDE = ROOT / "include" JNI_INCLUDES: list[Path] = [] SOURCES = [ ROOT / "src" / name for name in ( + "deif_xdi.c", # Общий DEIF входит в host-библиотеку. + "set_invertor_gas.c", # Собирает общий адаптер двух GAS для STM Invertor при прямом запуске host-builder. "set_regmap.c", "set_emu_ump.c", "set_emu_tms.c", "set_emu_igbt.c", "set_emu_server.c", "set_boot.c", "firmware_image.c", "altera_logic.c", "altera_stream.c", @@ -95,6 +97,7 @@ def main() -> int: JNI_INCLUDES.extend([include, include / {"Windows": "win32", "Darwin": "darwin"}.get(platform.system(), "linux")]) SOURCES.extend([ ROOT / "ports" / "android" / "set_plot_jni.c", + ROOT / "ports" / "android" / "set_deif_jni.c", # JNI общего протокольного ядра для JVM-тестов. ROOT / "ports" / "android" / "setprotocol_jni.c", ROOT / "ports" / "android" / "gas_slots_jni.c", # Даёт host JVM тот же адаптер слотной адресации, который используется Android. ]) diff --git a/c/st7789/st7789_disabled.h b/c/st7789/st7789_disabled.h new file mode 100644 index 0000000..c2e770c --- /dev/null +++ b/c/st7789/st7789_disabled.h @@ -0,0 +1,6 @@ +#ifndef ST7789_DISABLED_H /* Защищает отключённый аппаратный интерфейс от повторного включения. */ +#define ST7789_DISABLED_H /* Позволяет общему main обслуживать одинаковый API на плате без подключённого дисплея. */ +#include /* Время общего опроса передаётся как 32-битное число миллисекунд. */ +static inline void ST7789_BoardInit(void) { } /* Профиль без дисплея не включает SPI и не занимает GPIO, сохраняя работу всех моделей DEIF. */ +static inline void ST7789_BoardPoll(uint32_t now_ms) { (void)now_ms; } /* Опрос без экрана не блокирует UART/CAN/USB и не требует графических исходников в прошивке. */ +#endif /* Завершает отключённый backend общего интерфейса дисплея. */ diff --git a/python/protocan/stm_invertor.py b/python/protocan/stm_invertor.py new file mode 100644 index 0000000..f1f9005 --- /dev/null +++ b/python/protocan/stm_invertor.py @@ -0,0 +1,28 @@ +import ctypes # Привязка передаёт готовые слова общему C-кодеку без повторения wire-формата на Python. +from .native import _load_library # Стандартный поиск сохраняет существующий механизм доставки SETProtocol. +class StmInvertorCodec: # Один экземпляр владеет только сигнатурами функций общей библиотеки. + def __init__(self, library=None): # Явная DLL позволяет новой панели не заменять библиотеку работающего приложения. + self.lib = library if library is not None else _load_library() # Нет Python fallback при отсутствии нового C-кодека. + self.lib.set_invertor_gas_limit.argtypes = [ctypes.c_uint] # Тип сообщения передаётся без вычисления геометрии карты в Python. + self.lib.set_invertor_gas_limit.restype = ctypes.c_uint # Границы определяются профилем C из Templates. + self.lib.set_invertor_gas_build.argtypes = [ctypes.c_uint, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint16), ctypes.c_uint, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint8)] # Типы совпадают с публичным C ABI общего адаптера. + self.lib.set_invertor_gas_build.restype = ctypes.c_int # C возвращает фактический DLC либо отрицательную ошибку. + self.lib.set_invertor_gas_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_uint16)] # Декодер проверяет ID, тип, направление, длину и маску. + self.lib.set_invertor_gas_decode.restype = ctypes.c_int # Число принятых wire-слов приходит только из C. + def limit(self, message_type): # Интерфейс не хранит отдельную копию константы 250 или 750. + return int(self.lib.set_invertor_gas_limit(message_type)) # Передаём актуальную границу виджету адреса. + def build(self, message_type, address, words=()): # Пустой набор слов означает чтение DLC0 в обоих протоколах. + if not 0 <= address <= 0xFFFF or len(words) > 4 or any(not 0 <= word <= 0xFFFF for word in words): # Проверяем представимость Python-чисел перед сужением до типов ABI. + raise ValueError('Адрес или значение вне диапазона ABI') # Не допускаем тихого усечения и циклического переноса ctypes. + source = (ctypes.c_uint16 * len(words))(*words) if words else None # Значения остаются словами, endian выбирает общий кодек. + identifier = ctypes.c_uint32(); data = (ctypes.c_uint8 * 8)() # Локальные буферы имеют размеры Classic CAN и не переживают вызов. + length = self.lib.set_invertor_gas_build(message_type, address, source, len(words), ctypes.byref(identifier), data) # Ни CAN ID, ни маска не строятся здесь вручную. + if length < 0: # C отклоняет неверную геометрию, длину блока и маску. + raise ValueError('GAS: неверный адрес, длина блока или маска Word0') # Пользователь видит причину отказа до передачи на шину. + return identifier.value, bytes(data[:length]) # Транспорт получает полностью готовые ID и октеты. + def decode(self, identifier, flags, payload): # Входной кадр проверяется до публикации значений интерфейсу. + if not 0 <= identifier <= 0x1FFFFFFF or len(payload) > 8: # Не допускаем сужения большого CAN ID и переполнения временного буфера. + return None # Кадры другого транспорта не считаются ответом инвертора. + data = (ctypes.c_uint8 * 8)(*payload); metadata = (ctypes.c_uint * 2)(); words = (ctypes.c_uint16 * 4)() # ABI работает с фиксированными массивами без указателей на объекты Python. + count = self.lib.set_invertor_gas_decode(identifier, flags, data, len(payload), metadata, words) # Wire-разбор остаётся единственной C-реализацией. + return (metadata[0], metadata[1], tuple(words[:count])) if count > 0 else None # Недопустимый кадр не изменяет отображённые значения. diff --git a/python/set_devices/deif_controls.py b/python/set_devices/deif_controls.py new file mode 100644 index 0000000..440435b --- /dev/null +++ b/python/set_devices/deif_controls.py @@ -0,0 +1,52 @@ +"""Готовые операции DEIF для GUI без знания wire-разметки и адресных масок.""" # Все числовые правила реализованы единственным C99-ядром templates. +import ctypes as C # Преобразование фиксированных ABI-структур не является вторым кодеком. +from dataclasses import dataclass # Именованные поля исключают индексы метаданных в GUI. +from .deif_native import get_deif_native, Frame # Общий декодер и builders используются независимо от приложения. +from .can_transport import build_frame # Транспорт получает уже готовые CAN-кадры. +@dataclass(frozen=True) # Неизменяемые описания безопасно используются диагностикой и редакторами. +class SelectorInfo: # Это скалярный результат C, а не wire-структура. + node: int; index: int; subindex: int; dam: bool; legacy_key: str # Последний элемент сохраняет только формат прежних ключей настроек Desktop. +@dataclass(frozen=True) # Описание входного кадра отделено от его транспортного контейнера. +class PacketInfo: # GUI не обращается к смещениям payload. + kind: int; node: int; index: int; subindex: int; raw: int; status: int; dam: bool # Все значения получены из C-декодера. + @property # Диагностический ключ независим от адресного байта SAM/DAM. + def object_key(self): # Для TX-эхо достаточно индекса и подиндекса готового объекта. + return self.index, self.subindex # Это идентичность объекта, а не повторная распаковка CAN-data. +def selector_info(selector): # Редактор получает имена полей вместо ручных масок. + raw = bytes.fromhex(selector) if isinstance(selector, str) else bytes(selector) # HEX — только формат хранения пользовательской настройки. + if len(raw) != 4: return None # Буфер неправильного размера не передаётся C. + metadata = (C.c_int32 * 4)() # Node, index, subindex и DAM определены публичным ABI. + if not get_deif_native().selector_info(raw, metadata): return None # Некорректная привязка не становится источником на рисунке. + return SelectorInfo(metadata[0], metadata[1], metadata[2], bool(metadata[3]), raw[1:].hex()) # Срез используется только для совместимости непрозрачного старого ключа настроек. +def readdress_selector(selector, node, dam=None): # Меняет адрес пользовательской привязки общим алгоритмом C. + if not selector: return "" # Явно отключённое поле остаётся отключённым. + raw = bytes.fromhex(selector) if isinstance(selector, str) else bytes(selector) # Порт преобразует текст настройки в буфер ABI. + output = (C.c_uint8 * 4)() # Готовый селектор создаёт C-код. + if len(raw) != 4 or not get_deif_native().readdress(raw, node, 2 if dam is None else int(bool(dam)), output): raise ValueError("Неверный селектор или Node-ID DEIF") # Индекс не пересобирается в Python. + return bytes(output).hex(" ").upper() # Формат вывода совместим с прежним редактором HEX. +def route_parameters(node, mode, identifier, operation=0): # Согласует параметры редактора с C-правилами адресации. + output = (C.c_uint32 * 3)() # COB-ID, Node и адресный байт возвращаются одним вызовом. + if not get_deif_native().route(node, mode == "dam", identifier, operation, output): return None # Неверный диапазон не вызывает произвольную перенастройку привязок. + return tuple(output) # GUI хранит и отображает готовые значения. +def data_node(identifier, fallback=1): # Получает узел из CAN ID без повторения маски в GUI. + result = route_parameters(fallback or 1, "sam", identifier, 2) # Legacy SAM сохраняет явно выбранный источник. + return result[1] if result else fallback # Вне диапазона данных остаётся пользовательское исходное значение. +def packet_info(frame): # Метаданные для подтверждения команд и TX-эхо. + code, metadata = get_deif_native().inspect_frame(frame.can_id, frame.data, frame.ide, frame.rtr) # Wire-разбор находится в C. + return PacketInfo(metadata[0], metadata[1], metadata[2], metadata[3], metadata[4], metadata[7], bool(metadata[8])) if code > 0 else None # Некорректный кадр не получает выдуманных полей. +def heartbeat_description(identifier, data, extended=False, rtr=False): # Приложение не содержит собственного классификатора CANopen error control. + code, _ = get_deif_native().inspect_frame(identifier, data, extended, rtr) # Проверку COB-ID, DLC и состояния выполняет C. + if code not in (2, -3): return None # Обычные MPDO не попадают в поле heartbeat. + from .deif_xdi import decode # Общий локализованный formatter используется обоими CAN-мониторами и прибором. + return decode(build_frame(identifier, bytes(data), ide=extended, rtr=rtr)).summary # Готовая подпись не подтверждает получение конкретного измерения. +def _frame(output): # Общий адаптер переводит структуру C в существующий транспортный контейнер. + return build_frame(output.can_id, bytes(output.data[:output.dlc]), ide=False) # Идентификатор, длина и данные уже вычислены ядром. +def service_frame(node, value, nmt=False): # Строит heartbeat либо локальную NMT-команду через C. + output = Frame() # Память результата принадлежит Python-порту. + if not get_deif_native().service(bool(nmt), node, value, C.byref(output)): raise ValueError("Неверный Node-ID или CANopen-сервис") # Ни COB-ID, ни служебные байты не собираются в GUI. + return _frame(output) # Физическую отправку выполняет выбранный приложением транспорт. +def angle_setpoint_frame(actual): # Формирует отдельный кадр задания с тем же значением фактического азимута. + source, output = Frame(), Frame() # Оба контейнера имеют стабильный layout ABI. + source.can_id, source.dlc, source.data[:] = actual.can_id, len(actual.data), bytes(actual.data) # Это копирование готового CAN-кадра, не декодирование его полей. + if not get_deif_native().angle_command(C.byref(source), C.byref(output)): raise ValueError("Кадр не содержит поддерживаемый азимут DEIF") # Проверку семейства и сборку 0x07 выполняет C. + return _frame(output) # Штатный передатчик получает второй независимый MPDO. diff --git a/python/set_devices/deif_display.py b/python/set_devices/deif_display.py new file mode 100644 index 0000000..ebf5206 --- /dev/null +++ b/python/set_devices/deif_display.py @@ -0,0 +1,129 @@ +"""Независимая от GUI Python-модель индикатора поверх общего C99-ядра DEIF.""" # Настройки и транспортные имена остаются в порту, wire-правила находятся в C. +import ctypes as C # Обмен с ABI использует фиксированные массивы и caller-owned память. +from dataclasses import dataclass # Привязка является простой моделью пользовательской настройки. +from time import monotonic # Порт передаёт часы ядру явно в миллисекундах. +from .deif_native import get_deif_native, Frame # Единственная реализация кодека и автомата загружается без Qt. + +FIELDS = ("power", "rpm", "angle", "power_percent", "rpm_percent", "angle_setpoint") # Порядок полей публичного ABI C; это имена модели, не wire-селекторы. +STATUS = {1: "ниже диапазона", 2: "выше диапазона", 3: "ошибка данных"} # Локализованные подписи готовых статусов C. + + +@dataclass # Привязки можно сохранять в существующих пользовательских профилях JSON. +class Binding: # Четыре HEX-байта и коэффициент поступают из настроек либо C-каталога. + selector: str = "" # Пустая строка означает явно отключённое поле. + scale: float = 1.0 # Коэффициент применяется только в общем C-ядре. + + +def _milliseconds(now): # Перевод единиц системных часов относится к платформенному порту. + return max(0, round((monotonic() if now is None else now) * 1000)) # ABI использует целые монотонные миллисекунды для одинаковых границ на всех платформах. + + +def validate_deif_transmit(frame): # Сохраняет прежний публичный Python API ограниченного маршрута MASTER. + if not get_deif_native().validate(frame.can_id, bytes(frame.data), len(frame.data), int(bool(frame.ide)) | (int(bool(frame.rtr)) << 1), 0): # Проверка адреса, объектов и размеров полностью выполняется в C. + raise ValueError("DEIF: разрешены только данные, яркость, Color и heartbeat; команды настройки через Node-ID запрещены") # Текст остаётся локализованной ответственностью приложения. + + +def brightness_frame(node_id, group, percent, can_id=None, mode="sam"): # Готовит один кадр без отправки или изменения прибора. + return get_deif_native().make_frame(6, node_id, percent, 0 if can_id is None else can_id, mode, group) # Поле ABI 6 обозначает яркость, исходный COB-ID и U8 определяет C. + + +def colour_palette_frame(node_id, group, palette, can_id=None, mode="sam"): # Готовит независимый от яркости кадр палитры. + return get_deif_native().make_frame(7, node_id, palette, 0 if can_id is None else can_id, mode, group) # Поле ABI 7 обозначает Color; исходный COB-ID и кодирование Day/Night/Dusk находятся в C. + + +class DeifDisplay: # Пассивная модель пригодна для Desktop, CLI и тестов без Qt. + services = False # Наблюдатель не объявляет себя узлом CAN и никогда не выдаёт heartbeat. + expiry_ms = 5000 # Прежний режим наблюдения скрывает показания после пяти секунд без обновления. + + def __init__(self, source="DevBoard", can_id=0x201, power_unit="RAW", device_node=None, bindings=None, xdi_net=True, self_start=True, dam_pdo=2, dimmer_group=1, source_node=1): # Дополнительные параметры передаются серверным портом, прежние профили остаются совместимыми. + self.source, self.can_id, self.power_unit, self.device_node = source, can_id, power_unit, device_node # Только имя транспорта и UI-единица остаются вне C. + self.xdi_net, self.self_start, self.dam_pdo, self.dimmer_group, self._source_node = xdi_net, self_start, dam_pdo, dimmer_group, source_node # Параметры профиля передаются C без копии правил протокола. + self._api, self._context, self._route, self._bindings_signature = get_deif_native(), None, None, None # Один контекст на прибор исключает смешивание показаний MASTER и SLAVE. + self.bindings = {} # Контейнер поддерживает прямое редактирование существующим диалогом привязок. + self._ensure_context() # Исходные селекторы и коэффициенты создаются единственным C-каталогом. + if bindings is None: # Новый профиль получает штатные привязки, включая азимут. + for index, key in enumerate(FIELDS[:5]): # Отдельное задание угла автоматически производно от привязки азимута в C. + selector, factor = (C.c_uint8 * 4)(), C.c_double() # Порт принимает готовые значения без разбора endian. + self._api.get_binding(self._context, index, selector, C.byref(factor)) # Настройки по умолчанию читаются из C-профиля. + self.bindings[key] = Binding(bytes(selector).hex(" ").upper(), factor.value) # HEX нужен только редактору настроек. + else: # Существующая библиотека сохраняет пользовательские объекты и масштабы. + self.bindings = {key: Binding(item.selector, item.scale) for key, item in bindings.items()} # Словари двух приборов не должны делить изменяемые Binding. + + def _ensure_context(self): # Изменение адреса требует нового C-контекста, а смена имени транспорта — нет. + route = (self.can_id, self.device_node, self.xdi_net, self.self_start, self.dam_pdo, self.dimmer_group, self.services, self._source_node) # Изменение источника также требует согласованной переинициализации. + if route == self._route: # Повторное чтение снимка не переинициализирует данные. + return # Сохраняет сроки и очередь работающего узла. + size = self._api.state_size() # Размер private C-структуры сообщает библиотека. + self._context = (C.c_uint64 * ((size + 7) // 8))() # uint64 обеспечивает необходимое выравнивание на Windows и ARM. + source = self._source_node # Источник профиля не вычисляется из имени GUI или физического Node-ID SLAVE. + if not self._api.init(self._context, size, self.device_node or 0, source, bool(self.xdi_net), bool(self.self_start), self.dam_pdo, self.dimmer_group, bool(self.services), self.can_id): # C проверяет reserved Node, RPDO и сетевые группы. + raise ValueError("DEIF: неверный профиль SLAVE, Node-ID или RPDO") # Частично инициализированный узел не используется. + self._route, self._bindings_signature = route, None # Привязки повторно синхронизируются после изменения профиля. + + def _sync_bindings(self): # Прямые изменения словаря UI передаются в C только при фактическом изменении настройки. + self._ensure_context() # Проверяет, не был ли изменён физический адрес либо CAN ID наблюдения. + signature = tuple((key, binding.selector, binding.scale) for key, binding in self.bindings.items()) # Это сравнение настроек, а не интерпретация wire-формата. + if signature == self._bindings_signature: # Частые тики и приём кадров не должны сбрасывать значения. + return # Состояние C остаётся прежним. + for index, key in enumerate(FIELDS): # Порядок соответствует публичным field IDs ядра. + if index == 5 and key not in self.bindings: # Производное задание азимута настраивает C при изменении поля angle. + continue # Не отключает автоматически созданную независимую привязку 0x07. + binding = self.bindings.get(key, Binding()) # Отсутствующее поле явно отключается. + selector = bytes.fromhex(binding.selector) if binding.selector else None # HEX является форматом хранения настроек, а не CAN-декодером. + if selector is not None and len(selector) != 4: # Порт проверяет размер буфера до передачи указателя C. + raise ValueError("Селектор DEIF должен содержать четыре байта") # Некорректная пользовательская настройка не создаёт чтение за пределами буфера. + if not self._api.bind(self._context, index, selector, binding.scale): # Интерпретация селектора и применение масштаба находятся в C. + raise ValueError("DEIF: неверная привязка или масштаб") # Нечисловой коэффициент не скрывается как отсутствие пакета. + self._bindings_signature = signature # Последующая обработка кадра использует уже настроенный контекст. + + def bind_default_angle(self): # Миграция старого профиля с пустым азимутом не требует знания адресов в GUI. + self._sync_bindings() # Источник и режим берутся из уже настроенных остальных полей. + selector, factor = (C.c_uint8 * 4)(), C.c_double() # C возвращает готовую библиотечную привязку. + if not self._api.default_angle(self._context, selector, C.byref(factor)): # Ни SAM/DAM, ни индекс не вычисляются в Python. + raise ValueError("Не удалось восстановить привязку азимута DEIF") # Ошибка миграции остаётся видимой. + self.bindings["angle"] = Binding(bytes(selector).hex(" ").upper(), factor.value) # Обновляется только отсутствующая настройка азимута. + self._sync_bindings() # Новая привязка применяется сразу в текущем сеансе. + + def reset(self): # Сохраняет API очистки показаний при смене пользовательского канала. + self._sync_bindings() # Настройки должны соответствовать очищаемому контексту. + self._api.reset_values(self._context) # Очистка выполняется общим ядром. + + def accept(self, source, identifier, data, extended=False, rtr=False, now=None): # Порт связывает транспортное имя с независимой моделью прибора. + if source != self.source or not 0 <= identifier <= 0x1FFFFFFF or len(data) > 8: # Имя канала относится к приложению, границы — к безопасному ABI. + return False # Кадры другого транспорта не обновляют C-контекст. + self._sync_bindings() # Диалог мог изменить привязки с момента предыдущего кадра. + accepted = bool(self._api.accept(self._context, identifier, bytes(data), len(data), int(bool(extended)) | (int(bool(rtr)) << 1), _milliseconds(now))) # Единственное место протокольной обработки находится в C. + return accepted and (self.services or not self._api.inspect_frame(identifier, data, extended, rtr)[1][7]) # Пассивный API сохраняет прежний признак отказа только для текущего ошибочного I16, не для других полей. + + def _snapshots(self, now=None): # Снимок переводит готовые C-поля в контейнеры Python без повторения алгоритма. + self._sync_bindings() # Требуется также после прямого редактирования масштаба в настройках. + result, moment = {}, _milliseconds(now) # Все поля одного снимка читаются в один момент времени. + for index, key in enumerate(FIELDS): # Порядок ABI не зависит от порядка ключей пользовательского словаря. + value, metadata = C.c_double(), (C.c_int64 * 4)() # C выдаёт масштабированное значение и флаги seen/valid/stale. + self._api.read(self._context, index, moment, self.expiry_ms, C.byref(value), metadata) # Свежесть и служебные статусы вычисляются ядром. + result[key] = (value.value, metadata[0] / 1000, metadata[2], metadata[3]) # Деление переводит только единицы системных часов порта. + return result # Снимок не раскрывает внутренний layout C. + + @property # Совместимый read-only снимок прежнего словаря показаний. + def values(self): # Значения остаются в C, Python не хранит вторую копию состояния. + return {key: item[0] for key, item in self._snapshots().items() if item[3] & 2} # Готовый valid-флаг исключает служебные I16. + + @property # Временные отметки нужны существующей отрисовке и диагностике. + def received(self): # Даже invalid является принятым обновлением объекта, а не отсутствием связи. + return {key: item[1] for key, item in self._snapshots().items() if item[3] & 1} # Решение о наличии сообщения уже принято C-ядром. + + def value(self, key, now=None): # Сохраняет интерфейс получения одного свежего показания. + item = self._snapshots(now).get(key) # Масштаб и свежесть получены из общего C-снимка. + return item[0] if item and item[3] & 2 and not item[3] & 4 else None # Python лишь отображает valid/stale флаги ABI. + + def angle_selection(self, now=None): # Одно правило азимута используется и пассивным монитором, и активным SLAVE. + self._sync_bindings() # Профиль мог быть загружен из старого пользовательского файла. + value = C.c_double() # Владение выходным скаляром остаётся у порта. + kind = self._api.angle(self._context, _milliseconds(now), self.expiry_ms, C.byref(value)) # C выбирает фактический угол либо принятое сетевое задание. + return (value.value if kind else None, kind == 2) # Второй элемент нужен только для подписи «задание» в GUI. + + @property # Список источников нужен только заголовку прибора, не алгоритму приёма. + def sam_sources(self): # UI получает готовые адреса без масок и смещений селектора. + from .deif_controls import selector_info # Разбор адреса селектора делегируется общему C-порту. + descriptors = [selector_info(binding.selector) for binding in self.bindings.values() if binding.selector] # Сохраняет индивидуальные привязки каждого поля. + return sorted({item.node for item in descriptors if item is not None and not item.dam}) # DAM-получатель не выдаётся за источник SAM. diff --git a/python/set_devices/deif_emulator.py b/python/set_devices/deif_emulator.py new file mode 100644 index 0000000..5b29355 --- /dev/null +++ b/python/set_devices/deif_emulator.py @@ -0,0 +1,36 @@ +"""Регистровый контракт аппаратного эмулятора DEIF из set_emu_deif.h.""" # Один контракт используется всеми MCU и настольными клиентами. +from dataclasses import dataclass # Неизменяемый снимок отделяет подтверждённое состояние платы от редактируемой формы. + +DEIF_BASE = 0x1600 # Начало окна управления DEIF в set_emu_server.c, адрес измеряется в 16-битных словах. +DEIF_WORDS = 52 # SET_DEIF_WORDS версии 1 включает профиль, RAW, привязки OD и диагностику. +DEIF_FIELDS = ("Угол, RAW", "Обороты, RAW", "Обороты %, RAW", "Мощность, RAW", "Мощность %, RAW", "Задание угла, RAW") # Порядок соответствует raw[6] переносимой C-модели; физические единицы зависят от словаря прибора. +DEIF_INDICES = (0x3001, 0x3081, 0x3091, 0x3421, 0x3431, 0x3001) # Начальные OD экземпляра 1 взяты из set_deif_init; последний объект имеет подиндекс 7. + + +@dataclass(frozen=True) # Клиент публикует только целиком проверенный снимок без последующих изменений. +class DeifSnapshot: # Тип не зависит от Qt, HAL, конкретного MCU и способа доставки регистров. + words: tuple # Содержит 52 беззнаковых слова, прочитанных одной командой FC03. + mode: int # Режим из конфигурации STM: 4 Master, 5 Slave либо иной остановленный режим. + + @property # Роль берётся из ответа платы, а не из ранее выбранного пункта GUI. + def role(self): # Возвращает 1 Master либо 2 Slave согласно set_emu_deif.h. + return self.words[3] # Слово 3 не совпадает с номерами режимов 4 и 5 блока STM. + + @property # Проверка запущенного состояния нужна для запрета изменения сетевого профиля. + def running(self): # Возвращает логическое состояние модели без запуска либо остановки. + return bool(self.words[2]) # Слово 2 равно 1 только после подтверждённой команды пуска. + + @property # Знаковое представление сохраняет отрицательные RAW и служебные коды DEIF. + def raw(self): # Возвращает шесть значений I16 в порядке DEIF_FIELDS. + return tuple(value if value < 0x8000 else value - 0x10000 for value in self.words[16:22]) # Старший бит задаёт знак 16-битного дополнения до двух. + + +def decode_snapshot(words, mode): # Проверяет совместимость прошивки до разрешения любых записей DEIF. + values = tuple(words) # Не сохраняет ссылку на изменяемый транспортный буфер. + if len(values) != DEIF_WORDS or values[:2] != (0x4446, 1): # Сигнатура DF и версия 1 заданы set_deif_read, не совпадают с конфигурацией STM. + raise ValueError("Нужна прошивка с сервисом DEIF Master/Slave версии 1") # Старая прошивка не получает команд в неизвестное окно. + if values[2] not in (0, 1) or values[3] not in (1, 2): # Повреждённые running и role не должны превращаться в разрешение записи. + raise ValueError("STM вернула неверное состояние DEIF") # Отказ сохраняет форму неподтверждённой. + if mode in (4, 5) and values[3] != mode - 3: # Конфигурация и окно состояния должны описывать одну роль текущего экземпляра. + raise ValueError("Роль DEIF не совпадает с режимом STM; повторите чтение") # Между чтениями другая сессия могла изменить режим платы. + return DeifSnapshot(values, mode) # Единственная точка создания проверенного снимка для UI и тестов. diff --git a/python/set_devices/deif_native.py b/python/set_devices/deif_native.py new file mode 100644 index 0000000..cd5a311 --- /dev/null +++ b/python/set_devices/deif_native.py @@ -0,0 +1,75 @@ +"""Тонкий ctypes-порт единственного C99-ядра DEIF из templates/c/set-protocol.""" # В Python нет второго кодека или автомата NMT. +import ctypes as C # Фиксированные типы повторяют только публичный ABI. +import os # Путь дополнительной библиотеки передаёт приложение, не общая библиотека. +from pathlib import Path # Дополнительный нативный компонент может лежать рядом с основной библиотекой любого приложения. +from protocan.native import _load_library # Стандартный механизм загрузки используется при отсутствии явного пути DEIF. + + +class Frame(C.Structure): # Структура содержит готовые STD-кадры, а не wire-структуру транспорта. + _fields_ = [("can_id", C.c_uint32), ("dlc", C.c_uint32), ("data", C.c_uint8 * 8)] # Layout размером 16 байт совпадает с deif_xdi.h. + + +class NativeDeif: # Экземпляр владеет только сигнатурами C, состояние выделяется отдельными моделями. + def __init__(self): # Отсутствие общего ядра является ошибкой сборки, алгоритмического fallback нет. + self.lib = C.CDLL(os.environ["SETPROTOCOL_DEIF_LIBRARY"]) if os.environ.get("SETPROTOCOL_DEIF_LIBRARY") else _load_library() # Выбор пути относится к загрузке, не к протоколу. + if not hasattr(self.lib, "setp_deif_version"): # Во время обновления основная DLL может оставаться занятой работающим приложением. + primary = Path(self.lib._name) # Путь сообщает стандартный загрузчик, структура gui_desktop здесь неизвестна. + self.lib = C.CDLL(str(primary.with_name(primary.stem + "_deif" + primary.suffix))) # Это дополнительная сборка того же C-ядра, а не алгоритмический Python fallback. + signatures = { # Сигнатуры скалярного ABI не зависят от внутренних C-структур. + "version": ([], C.c_int), "state_size": ([], C.c_size_t), # Проверка версии и caller-owned памяти. + "catalog": ([C.c_uint, C.POINTER(C.c_int32)], C.c_int), # Каталог числовых индексов и разрешений. + "selector": ([C.c_uint, C.c_uint, C.c_uint, C.c_uint, C.c_void_p], C.c_int), # C формирует готовый селектор. + "inspect": ([C.c_uint32, C.c_void_p, C.c_size_t, C.c_uint, C.POINTER(C.c_int32)], C.c_int), # Единственный разбор входящего wire-кадра. + "make": ([C.c_uint, C.c_uint, C.c_uint, C.c_int32, C.c_uint32, C.c_uint, C.POINTER(Frame)], C.c_int), # Единственная сборка MPDO. + "master": ([C.c_uint, C.POINTER(C.c_int32), C.c_uint32, C.c_uint, C.c_uint, C.POINTER(Frame)], C.c_int), # Единственный порядок пяти объектов цикла. + "service": ([C.c_uint, C.c_uint, C.c_uint, C.POINTER(Frame)], C.c_int), "angle_command": ([C.POINTER(Frame), C.POINTER(Frame)], C.c_int), # Служебные кадры и задание азимута собираются общим C-кодом. + "selector_info": ([C.c_void_p, C.POINTER(C.c_int32)], C.c_int), "route": ([C.c_uint, C.c_uint, C.c_uint32, C.c_uint, C.POINTER(C.c_uint32)], C.c_int), # Редакторы не разбирают адресный байт или COB-ID самостоятельно. + "readdress": ([C.c_void_p, C.c_uint, C.c_uint, C.c_void_p], C.c_int), # Перенос селектора сохраняет ручной объект без масок и endian в UI. + "validate": ([C.c_uint32, C.c_void_p, C.c_size_t, C.c_uint, C.c_uint], C.c_int), # Изолированные выходы MASTER и SLAVE. + "init": ([C.c_void_p, C.c_size_t] + [C.c_uint] * 7 + [C.c_uint32], C.c_int), # Семь параметров профиля и отдельный CAN ID наблюдателя. + "bind": ([C.c_void_p, C.c_uint, C.c_void_p, C.c_double], C.c_int), # Селектор и библиотечный масштаб передаются без интерпретации. + "get_binding": ([C.c_void_p, C.c_uint, C.c_void_p, C.POINTER(C.c_double)], C.c_int), # Исходные значения профиля приходят из единственного C-каталога. + "default_angle": ([C.c_void_p, C.c_void_p, C.POINTER(C.c_double)], C.c_int), # Общая миграция отсутствующей привязки. + "start": ([C.c_void_p, C.c_uint64, C.c_uint], C.c_int), "stop": ([C.c_void_p], None), "reset_values": ([C.c_void_p], None), # Жизненный цикл без системных часов C. + "accept": ([C.c_void_p, C.c_uint32, C.c_void_p, C.c_size_t, C.c_uint, C.c_uint64], C.c_int), # C проверяет NMT, адресацию, объекты и длину. + "poll": ([C.c_void_p, C.c_uint64, C.POINTER(Frame), C.c_size_t], C.c_size_t), # Порт владеет выходным буфером событий. + "read": ([C.c_void_p, C.c_uint, C.c_uint64, C.c_uint, C.POINTER(C.c_double), C.POINTER(C.c_int64)], C.c_int), # Готовое значение и временные метаданные индикатора. + "lost": ([C.c_void_p, C.c_uint64], C.c_uint), # Маска устаревших входов вычисляется в C. + "angle": ([C.c_void_p, C.c_uint64, C.c_uint, C.POINTER(C.c_double)], C.c_int), "meta": ([C.c_void_p, C.POINTER(C.c_int64)], C.c_int), # Единый выбор азимута и снимок состояния. + } # Эти объявления описывают ABI, а не копию алгоритма. + for name, (arguments, result) in signatures.items(): # Все отсутствующие символы выявляются при загрузке расширения. + function = getattr(self.lib, "setp_deif_" + name) # Названия совпадают с публичным заголовком templates. + function.argtypes, function.restype = arguments, result # ctypes не должен угадывать размеры указателей и целых. + setattr(self, name, function) # Короткие имена оставляют порты моделей читаемыми. + if self.version() != 1: # Несовместимый ABI не используется с предположением о layout. + raise RuntimeError("Нужна общая библиотека DEIF ABI 1") # Сообщение указывает на необходимость пересборки нативного ядра. + + def selector_bytes(self, field, node, instance=1, dam=False): # Возвращает готовый C-селектор для настроек UI. + output = (C.c_uint8 * 4)() # Размер мультиплексора задан публичным контрактом. + if not self.selector(field, node, instance, bool(dam), output): # Проверка диапазонов находится в C. + raise ValueError("DEIF: неверный Node-ID, объект или экземпляр") # Недопустимая привязка не сохраняется в настройках. + return bytes(output) # Порт преобразует только тип контейнера. + + def inspect_frame(self, identifier, data, extended=False, rtr=False): # Передаёт целый CAN-кадр общему декодеру. + if not 0 <= identifier <= 0x1FFFFFFF or len(data) > 8: # Проверяет представимость на границе ABI до сужения Python-чисел. + return -1, [0] * 12 # Переполнение нельзя превратить в другой допустимый кадр. + output = (C.c_int32 * 12)() # Индексы метаданных определены в контракте C-декодера. + code = self.inspect(identifier, bytes(data), len(data), int(bool(extended)) | (int(bool(rtr)) << 1), output) # Флаги относятся к типу CAN-кадра, не к содержимому протокола. + return code, list(output) # Локализованный текст формируется поверх готовых полей C. + + def make_frame(self, field, node, value, identifier, mode="sam", instance=1): # Возвращает кадр существующего транспортного API. + from .can_transport import build_frame # Пакет транспорта не импортируется до фактической сборки кадра. + if mode not in ("sam", "dam") or type(instance) is not int or not 0 <= instance <= 4294967295 or type(value) is not int or not -2147483648 <= value <= 2147483647 or type(node) is not int or not 0 <= node <= 127 or type(identifier) is not int or not 0 <= identifier <= 0x1FFFFFFF: # Порт проверяет только типы и представимость ABI. + raise ValueError("DEIF: неверный Node-ID, режим, CAN ID или значение") # Значения не усекаются до вызова C. + output = Frame() # Память выходного CAN-кадра принадлежит Python. + if not self.make(field, node, instance, value, identifier, mode == "dam", C.byref(output)): # Wire-байты и границы протокола определяются исключительно C. + raise ValueError("DEIF: Node-ID, RPDO, экземпляр или значение вне диапазона") # Причина отклонения остаётся видимой в интерфейсе. + return build_frame(output.can_id, bytes(output.data[:output.dlc]), ide=False) # Транспорт AA55/адаптер получает уже готовые данные STD. + + +_native = None # Общая библиотека загружается один раз на процесс. +def get_deif_native(): # Ленивое получение не требует Qt и пригодно для CLI. + global _native # Экземпляр хранит сигнатуры, состояния приборов остаются раздельными. + if _native is None: # Первая реальная операция DEIF проверяет наличие нового нативного ядра. + _native = NativeDeif() # Отсутствие DLL или ABI не скрывается Python-реализацией протокола. + return _native # Все модели используют один и тот же проверенный C-код. diff --git a/python/set_devices/deif_slave.py b/python/set_devices/deif_slave.py new file mode 100644 index 0000000..dacd573 --- /dev/null +++ b/python/set_devices/deif_slave.py @@ -0,0 +1,56 @@ +"""Серверный Python-порт DEIF: протокольное состояние целиком принадлежит общему C99.""" # Пакет не импортирует gui_desktop и пригоден без Qt. +import ctypes as C # Структуры ABI содержат только готовые кадры и скаляры. +from time import monotonic # Порт поставляет монотонные часы, чтобы C оставался независим от ОС. +from .deif_display import DeifDisplay, FIELDS, STATUS, _milliseconds # Пассивный и активный режимы используют одинаковые снимки индикаторов. +from .deif_native import Frame, get_deif_native # Нет Python-копии адресации, NMT, сроков или SDO. + + +def validate_slave_transmit(frame, node): # Серверный маршрут имеет отдельный контракт по собственному Node-ID. + if type(node) is not int or not 1 <= node <= 127 or not get_deif_native().validate(frame.can_id, bytes(frame.data), len(frame.data), int(bool(frame.ide)) | (int(bool(frame.rtr)) << 1), node): # C разрешает только собственные heartbeat, EMCY и ответы SDO. + raise ValueError("Разрешены только heartbeat, EMCY и ответы SDO своего SLAVE") # Недопустимый кадр не попадёт в физический транспорт. + + +class DeifSlave(DeifDisplay): # Отличие от монитора состоит в включённых сервисах C и сохранении последнего числа при Data lost. + services, expiry_ms = True, 0 # Частоту heartbeat и срок Data lost определяет C, нулевой expiry сохраняет последнее показание. + + def __init__(self, node=11, source_node=1, xdi_net=True, self_start=True, dam_pdo=2, bindings=None, dimmer_group=1): # Сохраняет существующий Python API модели SLAVE. + if any(type(value) is not int for value in (node, source_node, dam_pdo, dimmer_group)): # Порт проверяет типы до преобразования unsigned ABI. + raise ValueError("Параметры профиля DEIF должны быть целыми") # Логические и дробные адреса не превращаются в другой узел. + super().__init__(source="Локальная симуляция", device_node=node, bindings=bindings, source_node=source_node, xdi_net=xdi_net, self_start=self_start, dam_pdo=dam_pdo, dimmer_group=dimmer_group) # Все ограничения и исходные привязки создаются C-ядром. + + def start(self, now=None, reset_values=True): # Явное включение не использует физический транспорт напрямую. + self._sync_bindings() # Последние параметры UI должны соответствовать создаваемому сеансу. + self._api.start(self._context, _milliseconds(monotonic() if now is None else now), bool(reset_values)) # Boot-up и self-start формируются общим автоматом C. + + def stop(self): # Выключение прекращает в том числе невыданные служебные события. + self._api.stop(self._context) # Никакого отдельного Python-таймера heartbeat здесь нет. + + def accept(self, source, identifier, data, extended=False, rtr=False, now=None): # Транспортное имя фильтруется базовым портом, байты принимает C. + return super().accept(source, identifier, data, extended, rtr, monotonic() if now is None else now) # Явные часы позволяют одинаково проверять границы Python и JNI. + + def tick(self, now=None): # Порт регулярно выгружает готовые события и передаёт их выбранному транспорту. + from .can_transport import build_frame # Формат транспорта не смешивается с форматом DEIF. + self._sync_bindings() # Прямое изменение профиля не должно оставлять старые привязки. + output = (Frame * 16)() # Вместимость соответствует ограниченному пакету событий публичного ABI. + count = self._api.poll(self._context, _milliseconds(monotonic() if now is None else now), output, len(output)) # Все сроки и NMT-переходы обслуживаются C. + return [build_frame(frame.can_id, bytes(frame.data[:frame.dlc]), ide=False) for frame in output[:count]] # Python меняет только тип контейнера готовых кадров. + + def lost_fields(self, now=None): # Локализованный список строится поверх готовой маски C. + self._sync_bindings() # Непривязанное поле не должно участвовать в предупреждении. + mask = self._api.lost(self._context, _milliseconds(monotonic() if now is None else now)) # Независимые сроки по 3000 мс определяются общим ядром. + return [key for index, key in enumerate(FIELDS) if mask & (1 << index)] # Соответствие field ID и имени не повторяет алгоритм свежести. + + def _meta(self): # Набор скаляров не раскрывает внутреннюю структуру C. + output = (C.c_int64 * 10)() # Число элементов закреплено в deif_xdi.h. + self._api.meta(self._context, output) # Состояние хранится только в одном месте. + return list(output) # Python-порт не пересчитывает NMT или периоды. + + active = property(lambda self: bool(self._meta()[0])) # Флаг нужен жизненному циклу страницы. + state = property(lambda self: self._meta()[1]) # Готовый CANopen NMT-код используется в локализованной подписи. + started = property(lambda self: self._meta()[2] / 1000) # Секунды сохраняют совместимость прежнего Python API. + last_heartbeat = property(lambda self: self._meta()[3] / 1000) # Преобразуется только единица системного времени. + heartbeat_ms = property(lambda self: self._meta()[4]) # Значение стандартного объекта 0x1017 выдаёт C. + accepted = property(lambda self: self._meta()[5]) # Счётчик принятия не имитируется количеством тиков GUI. + brightness = property(lambda self: self._meta()[7]) # Текущий процент подсветки из C-профиля. + palette = property(lambda self: self._meta()[8]) # Day/Night/Dusk хранится как код общего ядра. + invalid = property(lambda self: {key: STATUS[item[2]] for key, item in self._snapshots().items() if item[2]}) # Только текст статуса локализуется в Python. diff --git a/python/set_devices/deif_xdi.py b/python/set_devices/deif_xdi.py index 97733cd..74bbe56 100644 --- a/python/set_devices/deif_xdi.py +++ b/python/set_devices/deif_xdi.py @@ -1,159 +1,97 @@ -"""DEIF XDi-192 observations from CAN analysis dated 2026-10-03. +"""Локализованное представление DEIF поверх единственного C99-кодека templates.""" # Wire-формат, адресация и диапазоны не реализуются повторно в Python. +import ctypes as C # Преобразуются только структуры публичного ABI. +from protocan import protocan as pc # Существующие CAN-мониторы получают прежний тип Decoded. +from .deif_native import get_deif_native, Frame # C-каталог и готовые метаданные используются без Qt. -Based on DEIF 4189350066D, sections 3.2, 11 and appendix 9. -Angle and AC2 percent use the verified working-library resolution of 0.1. -Absolute AC2 power remains RAW until its physical unit is confirmed. -""" -from protocan import protocan as pc - -SELECTORS = { - bytes.fromhex("01 01 30 07"): "Азимут / руль — задание", - bytes.fromhex("01 81 30 02"): "RPM / обороты", - bytes.fromhex("01 91 30 02"): "Обороты винта, %", - bytes.fromhex("01 21 34 02"): "Электрическая мощность AC2", - bytes.fromhex("01 31 34 02"): "Электрическая мощность AC2, %", -} -MASTER_SELECTORS = tuple(SELECTORS) -SELECTORS[bytes.fromhex("01 01 30 02")] = "Азимут / руль — фактическое значение" - -OBJECTS = { - 0x3000: ("Азимут / руль", "°"), - 0x3080: ("Обороты винта", "rpm"), - 0x3090: ("Обороты винта, %", "%"), - 0x3420: ("Электрическая мощность AC2", None), - 0x3430: ("Электрическая мощность AC2, %", "%"), -} +_API = get_deif_native() # Отсутствие общего DEIF-ядра является явной ошибкой нативной сборки. +_FIELD_NAMES = (("Электрическая мощность AC2", None), ("Обороты винта", "rpm"), ("Азимут / руль", "°"), ("Электрическая мощность AC2, %", "%"), ("Обороты винта, %", "%"), ("Азимут / руль", "°")) # Это только локализованные подписи, числовые объекты определяет C-каталог. +_MASTER_FIELDS = (5, 1, 4, 0, 3) # Соответствие прежнего порядка редакторов ID полей ABI; сборку цикла выполняет C. +MASTER_SELECTORS = tuple(_API.selector_bytes(field, 1) for field in _MASTER_FIELDS) # Селекторы возвращает единый C-каталог. +SELECTORS = dict(zip(MASTER_SELECTORS, ("Азимут / руль — задание", "RPM / обороты", "Обороты винта, %", "Электрическая мощность AC2", "Электрическая мощность AC2, %"))) # Тексты не влияют на проверку или отправку кадра. +SELECTORS[_API.selector_bytes(2, 1)] = "Азимут / руль — фактическое значение" # Фактический угол имеет отдельный объект каталога. +OBJECTS = {} # Сохраняет публичное представление названий семейств для прежних потребителей. +for _field in range(5): # Пять семейств поддерживаемого профиля индикации. + _metadata = (C.c_int32 * 3)() # C возвращает base, subindex и разрешение. + _API.catalog(_field, _metadata) # Базовый индекс не дублируется числовой таблицей Python. + OBJECTS[_metadata[0]] = _FIELD_NAMES[_field] # Имена остаются локализованными данными представления. -def decode(frame): - result = pc.Decoded(pc.ProtoCanId(frame.can_id, 0, 0, 0, 0, 0, 0), - len(frame.data), bytes(frame.data), ide=frame.ide, - rtr=frame.rtr, protocol="DEIF XDi-192") - result.fields = [("Протокол", "DEIF XDi-net / CANopen SAM-MPDO"), - ("CAN ID", "0x%03X" % frame.can_id)] - def reject(message): - result.summary = message - result.warnings.append(message) - return result - if frame.ide or frame.rtr: - return reject("DEIF: ожидается STD data frame; EXT/RTR не интерпретируется") - if 0x701 <= frame.can_id <= 0x77F: - node = frame.can_id - 0x700 - result.device_label = "Node %d" % node - result.message_label = "Heartbeat / Boot-up" - result.fields.append(("Node-ID", str(node))) - if len(frame.data) != 1: - return reject("DEIF: Heartbeat Node %d — ожидается DLC=1" % node) - state = {0: "Boot-up", 5: "Operational"}.get(frame.data[0]) - if state is None: - state = "состояние 0x%02X (не описано в журнале)" % frame.data[0] - result.fields.append(("Состояние", state)) - result.summary = "Node %d: %s" % (node, state) - return result - if not 0x201 <= frame.can_id <= 0x57F: - result.summary = "DEIF: CAN ID не описан в журнале" - return result - node = frame.can_id - 0x200 - result.message_label = "XDi-net broadcast / SAM-MPDO" - result.fields.append(("COB-ID", "0x%03X" % frame.can_id)) - if len(frame.data) != 8: - return reject("DEIF RPDO1: ожидается DLC=8") - selector = frame.data[:4] - result.fields.append(("Селектор Byte0..3", selector.hex(" ").upper())) - source = frame.data[0] & 0x7F - index = int.from_bytes(frame.data[1:3], "little") - subindex = frame.data[3] - result.fields.extend([("Источник Node-ID", str(source)), - ("Индекс объекта", "0x%04X" % index), - ("Подиндекс", "0x%02X" % subindex)]) - dam = bool(frame.data[0] & 0x80) - if dam: - result.fields = [(key, value) for key, value in result.fields if key != "Источник Node-ID"] - result.fields.append(("Получатель Node-ID", str(source))) - result.message_label = "CANopen DAM-MPDO" - if not source or frame.can_id - source not in (0x200, 0x300, 0x400, 0x500): - return reject("DEIF: получатель не соответствует RPDO COB-ID") - elif not source or not 0x201 <= frame.can_id <= 0x27F or (node != 127 and source != node): - return reject("DEIF: источник не соответствует COB-ID") - result.device_label = ("Получатель Node %d" if dam else "Источник Node %d") % source - result.fields[0] = ("Протокол", "CANopen DAM-MPDO" if dam else "DEIF XDi-net / CANopen SAM-MPDO") - family, instance = index & 0xFFF0, index & 0xF - if family not in OBJECTS or not instance or subindex not in (2, 7): - return reject("DEIF: неизвестный объект/подиндекс 0x%04X:%02X" % (index, subindex)) - name, unit = OBJECTS[family] - kind = "фактическое значение" if subindex == 2 else "задание" - name = "%s №%d — %s" % (name, instance, kind) - raw = int.from_bytes(frame.data[4:6], "little", signed=True) - result.fields.extend([("Параметр", name), - ("RAW INT16 LE", str(raw)), - ("Byte6..7", frame.data[6:].hex(" ").upper())]) - result.summary = "Node %d · 0x%04X:%02X · %s: RAW=%d" % (source, index, subindex, name, raw) - if raw in (-32768, 32766, 32767): - status = {-32768: "ниже диапазона", 32766: "выше диапазона", 32767: "ошибка данных"}[raw] - result.fields.append(("Статус", status)) - result.summary += "; " + status - result.warnings.append(status) - elif unit and (subindex == 2 or family == 0x3000): - value = "%.1f %s" % (raw / 10, unit) - result.fields.append(("Значение", value)) - result.fields.append(("Масштаб", "RAW ×0.1")) - result.summary += "; " + value - elif family == 0x3420 and subindex == 2: - result.fields.append(("Масштаб / единицы", "RAW ×1; физическая единица не подтверждена")) - result.summary += "; RAW ×1 (физическая единица не подтверждена)" - else: - result.fields.append(("Масштаб / единицы", "не установлены")) - result.summary += "; масштаб и единицы не установлены" - if frame.data[6:] != b"\x00\x00": - result.warnings.append("Byte6..7: для I16 ожидается нулевое заполнение") - return result +def decode(frame): # Форматирует готовый результат C для обеих таблиц Desktop. + result = pc.Decoded(pc.ProtoCanId(frame.can_id, 0, 0, 0, 0, 0, 0), len(frame.data), bytes(frame.data), ide=frame.ide, rtr=frame.rtr, protocol="DEIF XDi-192") # Контейнер совместим со штатным монитором CAN. + result.fields = [("Протокол", "DEIF XDi-net / CANopen SAM-MPDO"), ("CAN ID", "0x%03X" % frame.can_id)] # Начальные подписи не зависят от структуры wire-полей. + code, metadata = _API.inspect_frame(frame.can_id, frame.data, frame.ide, frame.rtr) # Единственный разбор сообщения выполняется в C. + node, index, subindex, raw, field, instance, status, dam, padding, nmt = metadata[1:11] # Это поля документированного ABI, не смещения CAN-payload. + if code in (2, -2, -3): # C определяет принадлежность к CANopen Heartbeat/Boot-up. + result.device_label, result.message_label = "Node %d" % node, "Heartbeat / Boot-up" # Исходный тип пригоден также для ошибки DLC. + result.fields.append(("Node-ID", str(node))) # Номер уже вычислен общим кодеком. + if code != -2: # При корректной длине сохраняется исходный NMT-код, включая неизвестный. + states = {0: ("Boot-up", "запуск узла"), 4: ("Stopped", "узел остановлен"), 5: ("Operational", "рабочий режим"), 127: ("Pre-operational", "предоперационный режим")} # Локализация кодов состояния; допустимость уже проверена C. + state, meaning = states.get(nmt, ("состояние 0x%02X" % nmt, "неизвестное состояние NMT")) # Неизвестный байт не теряется в представлении. + message = "Boot-up" if nmt == 0 else "Heartbeat" if code == 2 else "Error control" # Подтверждённый тип heartbeat сообщает C-декодер. + result.message_label = "CANopen " + message # Отдельный тип исключает Boot-up из статистики периода HB. + result.fields[0] = ("Протокол", result.message_label) # Служебный кадр не маркируется как SAM-MPDO. + result.fields.extend([("COB-ID", "0x%03X = 0x700 + %d" % (frame.can_id, node)), ("NMT state", "0x%02X (%d)" % (nmt, nmt)), ("Состояние", "%s — %s" % (state, meaning))]) # Формулы выводятся для пояснения пользователю, а не для декодирования. + result.summary = "%s · Node %d: %s — %s" % (message, node, state, meaning) # Сводка остаётся полезной при скрытой колонке типа. + return result # Heartbeat не трактуется как измерение. + errors = {-1: "DEIF: ожидается STD data frame; EXT/RTR не интерпретируется", -2: "DEIF: Heartbeat Node %d — ожидается DLC=1" % node, -4: "DEIF RPDO1: ожидается DLC=8", -5: "DEIF: источник или получатель не соответствует COB-ID", -6: "DEIF: неизвестный объект/подиндекс 0x%04X:%02X" % (index, subindex)} # Причины задаёт C, здесь находятся только русские тексты. + if code in errors: # Неуспешный разбор не получает фиктивных RAW-полей. + result.summary = errors[code] # Понятная причина отказа видна в таблице без открытия подробностей. + result.warnings.append(result.summary) # Сохраняет существующий API диагностики. + return result # Порт не пытается повторно угадать формат байтов. + if code == 0: # COB-ID другого протокола не считается измерением DEIF. + result.summary = "DEIF: CAN ID не описан в журнале" # Исходный кадр сохраняется в raw-колонках монитора. + return result # Неизвестному ID не назначаются физические единицы. + result.message_label = "CANopen DAM-MPDO" if dam else "XDi-net broadcast / SAM-MPDO" # Режим уже определён общей проверкой адресации. + result.fields[0] = ("Протокол", "CANopen DAM-MPDO" if dam else "DEIF XDi-net / CANopen SAM-MPDO") # В подробностях показывается действующий режим. + result.device_label = ("Получатель Node %d" if dam else "Источник Node %d") % node # Node-ID получателя не подписывается источником SAM. + result.fields.extend([("COB-ID", "0x%03X" % frame.can_id), ("Селектор Byte0..3", bytes(frame.data[:4]).hex(" ").upper()), ("Получатель Node-ID" if dam else "Источник Node-ID", str(node)), ("Индекс объекта", "0x%04X" % index), ("Подиндекс", "0x%02X" % subindex)]) # Raw-байты показываются для проверки, алгоритм их не интерпретирует. + if code == 3: # C отделяет U8 диммера от переменных I16. + result.summary = "Node %d · 0x%04X:%02X · %s: %d" % (node, index, subindex, "Яркость, %" if field == 6 else "Color Day/Night/Dusk", raw) # Сохраняет понятную сводку команд подсветки. + return result # Диммер не получает ложный масштаб десятых долей RPM. + name, unit = _FIELD_NAMES[field] # Название выбирается по готовому field ID общего каталога. + kind = "фактическое значение" if subindex == 2 else "задание" # Тип значения показывается пользователю отдельно от физического числа. + parameter = "%s №%d — %s" % (name, instance, kind) # Номер экземпляра поступает из C-декодера. + result.fields.extend([("Параметр", parameter), ("RAW INT16 LE", str(raw)), ("Byte6..7", bytes(frame.data[6:]).hex(" ").upper())]) # Для диагностики сохраняется исходное значение и raw-хвост. + result.summary = "Node %d · 0x%04X:%02X · %s: RAW=%d" % (node, index, subindex, parameter, raw) # Формат совместим с прежними фильтрами и тестами. + if status: # Зарезервированный I16 проверен единственным C-кодеком. + text = {1: "ниже диапазона", 2: "выше диапазона", 3: "ошибка данных"}[status] # Статус локализуется без повторного сравнения raw-кодов. + result.fields.append(("Статус", text)) # Ошибка не выдаётся за огромную физическую величину. + result.warnings.append(text) # Сохраняет предупреждение для таблицы монитора. + result.summary += "; " + text # Причина видна без отдельного окна. + elif unit and (subindex == 2 or field == 2): # Физическое форматирование подтверждено для факта и азимутального задания рабочей библиотеки. + text = "%.1f %s" % (raw / 10, unit) # Это форматирование числа для человека, принятие и масштаб индикаторов выполняет C. + result.fields.extend([("Значение", text), ("Масштаб", "RAW ×0.1")]) # Подпись раскрывает используемое разрешение. + result.summary += "; " + text # Краткая сводка содержит единицу измерения. + elif field == 0 and subindex == 2: # Абсолютная POWER не имеет подтверждённой физической единицы. + result.fields.append(("Масштаб / единицы", "RAW ×1; физическая единица не подтверждена")) # Единица не достраивается по предположению. + result.summary += "; RAW ×1 (физическая единица не подтверждена)" # Сохраняет прежний смысл шкалы мощности. + else: # Разрешение иных set-point зависит от библиотеки прибора. + result.fields.append(("Масштаб / единицы", "не установлены")) # Неизвестный масштаб остаётся явным ограничением. + result.summary += "; масштаб и единицы не установлены" # Не назначает единицы неизвестному объекту. + if not padding: # C уже проверил старшие байты I16. + result.warnings.append("Byte6..7: для I16 ожидается нулевое заполнение") # Ненулевой хвост виден в диагностике и отклоняется моделью приёма. + return result # На выходе находится только представление результата C. -def master_frames(node_id, raw_values, can_id=None, mode="sam", angle_subindex=7): - """Build a five-parameter STD RPDO1 cycle, using signed INT16 LE values.""" - from .can_transport import build_frame - if not isinstance(node_id, int) or not 1 <= node_id <= 127: - raise ValueError("Node-ID должен быть от 1 до 127") - identifier = 0x200 + node_id if can_id is None else can_id - if mode not in ("sam", "dam"): - raise ValueError("Выберите SAM или DAM") - if angle_subindex not in (2, 7): - raise ValueError("Угол: 0x02 — фактический, 0x07 — задание") - if not isinstance(identifier, int) or not 0x201 <= identifier <= 0x57F: - raise ValueError("STD ID вне диапазона RPDO") - if mode == "sam" and identifier not in (0x200 + node_id, 0x27F): - raise ValueError("XDi-net STD ID: 0x200 + источник или 0x27F") - if mode == "dam" and identifier - node_id not in (0x200, 0x300, 0x400, 0x500): - raise ValueError("CANopen DAM: STD ID = база RPDO (200/300/400/500 hex) + получатель") - values = list(raw_values) - if len(values) != len(MASTER_SELECTORS): - raise ValueError("Нужны пять RAW-параметров") - frames = [] - for selector, value in zip(MASTER_SELECTORS, values): - if selector[1:] == bytes.fromhex("01 30 07"): - selector = selector[:3] + bytes([angle_subindex]) - if not isinstance(value, int) or not -32768 <= value <= 32767: - raise ValueError("RAW должен быть INT16: -32768…32767") - data = bytes([node_id | (0x80 if mode == "dam" else 0)]) + selector[1:] + value.to_bytes(2, "little", signed=True) + b"\x00\x00" - frames.append(build_frame(identifier, data, ide=False)) - return frames +def master_frames(node_id, raw_values, can_id=None, mode="sam", angle_subindex=7): # Совместимый API генерирует полный цикл через C. + from .can_transport import build_frame # Транспортный контейнер не задаёт правила DEIF. + values = list(raw_values) # Порт материализует входной iterable для фиксированного ABI. + if mode not in ("sam", "dam") or type(node_id) is not int or not 0 <= node_id <= 127 or len(values) != 5 or any(type(value) is not int or not -2147483648 <= value <= 2147483647 for value in values) or (can_id is not None and (type(can_id) is not int or not 0 < can_id <= 0x1FFFFFFF)): # Проверяются только представимость ABI и число аргументов цикла. + raise ValueError("DEIF: неверный Node-ID, режим, CAN ID или пять RAW-параметров") # Значения не усекаются ctypes незаметно. + source, output = (C.c_int32 * 5)(*values), (Frame * 5)() # Оба буфера имеют явный размер и принадлежат Python. + if _API.master(node_id, source, 0 if can_id is None else can_id, mode == "dam", angle_subindex, output) != 5: # Порядок объектов, I16 и адресацию проверяет общий C-код. + raise ValueError("DEIF: Node-ID, RPDO, подиндекс азимута или RAW вне диапазона") # Частично собранный цикл не передаётся. + return [build_frame(item.can_id, bytes(item.data[:item.dlc]), ide=False) for item in output] # Транспорт получает готовые wire-байты. -# Display binding catalogue. None means library-dependent resolution, not degrees/%. -DISPLAY_PRESETS = { - "power": ("POWER · AC2 (RAW)", 0x3420, 2, None), - "rpm": ("RPM · обороты (×0.1)", 0x3080, 2, 0.1), - "angle": ("Угол · задание (×0.1°)", 0x3000, 7, 0.1), - "angle_actual": ("Угол · фактический (×0.1°)", 0x3000, 2, 0.1), - "power_percent": ("POWER% · AC2 (×0.1%)", 0x3430, 2, 0.1), - "rpm_percent": ("RPM% (×0.1)", 0x3090, 2, 0.1), -} +_PRESET_FIELDS = {"power": 0, "rpm": 1, "angle": 5, "angle_actual": 2, "power_percent": 3, "rpm_percent": 4} # Совместимые имена редакторов связываются с field ID ABI. +_PRESET_NAMES = {"power": "POWER · AC2 (RAW)", "rpm": "RPM · обороты (×0.1)", "angle": "Угол · задание (×0.1°)", "angle_actual": "Угол · фактический (×0.1°)", "power_percent": "POWER% · AC2 (×0.1%)", "rpm_percent": "RPM% (×0.1)"} # Подписи относятся только к представлению. +DISPLAY_PRESETS = {} # Числовые индексы не дублируются таблицей Python. +for _key, _field in _PRESET_FIELDS.items(): # Значения каталога читаются из C при загрузке расширения. + _metadata = (C.c_int32 * 3)() # Публичный ABI выдаёт base/subindex/resolution. + _API.catalog(_field, _metadata) # Общая библиотека является источником объекта и разрешения. + DISPLAY_PRESETS[_key] = (_PRESET_NAMES[_key], _metadata[0], _metadata[1], None if _metadata[2] == 127 else 10.0 ** _metadata[2]) # Конвертация показателя степени нужна только существующему редактору коэффициента. -def display_preset(key, node, instance=1, dam=False): - """Return a catalogue selector and its known scale, or None for RAW.""" - if not 1 <= node <= 127 or not 1 <= instance <= 15: - raise ValueError("Node-ID: 1…127; instance: 1…15") - _, base, subindex, scale = DISPLAY_PRESETS[key] - selector = bytes([node | (0x80 if dam else 0)]) + (base + instance).to_bytes(2, "little") + bytes([subindex]) - return selector.hex(" ").upper(), scale +def display_preset(key, node, instance=1, dam=False): # Сохраняет прежний API выбора библиотечного объекта. + return _API.selector_bytes(_PRESET_FIELDS[key], node, instance, dam).hex(" ").upper(), DISPLAY_PRESETS[key][3] # Ни endian, ни арифметика индексов не повторяются в Python. diff --git a/python/set_devices/emulator_protocol.py b/python/set_devices/emulator_protocol.py new file mode 100644 index 0000000..ec56afb --- /dev/null +++ b/python/set_devices/emulator_protocol.py @@ -0,0 +1,140 @@ +"""SET v2/RTU session for emulator panels; no I/O or Qt dependencies. + +Only discovery may fall back to RTU. An application command is sent once; +failure after choosing v2 must never resend a write using another protocol. +""" +import secrets +import struct + +from setprotocol.core import (Frame, FrameFlag, FrameParser, MessageType, Status, + DeviceInfo, build_frame, decode_response) +from set_devices.tms_terminal import crc16_modbus +from set_devices.mcu_profiles import EMULATOR_PROFILES # Проверка DeviceInfo использует модель конкретной платы из общего аппаратного профиля. + +REG_REQUEST = 0x1000 +TMS_REQUEST = 0x1001 +WAVE_BLOCK = 0x1002 +MODEL = 'STM32F407-EMU-EXPERIMENT' + + +def with_crc(body): + return body + struct.pack('= 9 and raw[1] == 16: + return self._frame(WAVE_BLOCK, raw, raw[0]) + if len(raw) == 8 and raw[1] in (3, 6): + unit, fn, address, value = struct.unpack('>BBHH', raw[:6]) + return self._frame(REG_REQUEST, struct.pack('HH', raw[2:6]): + raise ValueError('SET v2: неверное подтверждение записи') + return raw + return with_crc(bytes([raw[0], 3, count * 2]) + struct.pack(f'>{count}H', *words)) diff --git a/python/set_devices/mcu_profiles.py b/python/set_devices/mcu_profiles.py new file mode 100644 index 0000000..ff7f2d9 --- /dev/null +++ b/python/set_devices/mcu_profiles.py @@ -0,0 +1,46 @@ +TEMPLATE_MCUS = { # Каталог отражает конкретные MCU-порты закреплённого templates; значения загрузчиков совпадают с FirmwareTab. + "stm32": ( # Порядок сохраняет прежние F103/F407 и группирует добавленные варианты по семействам. + ("STM32F103C8T6", "DS18B20, CAN Sensor, RS-485 Boot, Firmware Info и LED Indicator.", 2), # Порты ds18b20/stm32f1 и rs485-boot/stm32f103 используют имеющийся UART ROM Boot, индекс 2. + ("STM32F407", "STM32F407VET6: SETProtocol, DevBoard V1, эмуляторы, генератор ЦАП, Parallel NAND и USB Boot.", 7), # Порты set-protocol/stm32f407-* и parallel-nand/stm32f407ve; индекс 7 относится только к USB-загрузчику F407. + ("STM32F103RCT6", "Parallel NAND через GPIO, SETProtocol, DS18B20, Firmware Info и LED Indicator.", None), # Отдельный порт parallel-nand/stm32f103rc учитывает отсутствие FSMC в корпусе LQFP64; None открывает обзор MCU. + ("STM32F103ZET6", "Parallel NAND через FSMC, SETProtocol, эмуляторы УМП / 2812 / IGBT / ЦАП / DEIF, DS18B20, Firmware Info и LED Indicator.", None), # Порт parallel-nand/stm32f103ze использует аппаратный FSMC, поэтому вариант показан отдельно от RCT6. + ("STM32G431", "DS18B20 / 1-Wire, Firmware Info и LED Indicator.", None), # Поддержка задана конфигурацией ds18b20/stm32g4/onewire_config.g431.template.h и общими портами G4. + ("STM32G474", "SETProtocol, DS18B20 / 1-Wire, Firmware Info, LED Indicator и генератор ЦАП.", None), # Общие порты stm32g4 и set-protocol/stm32g474-wavegen подходят семейству; конкретные платы перечислены ниже. + ("STM32G474CEU6", "Parallel NAND через GPIO, DS18B20 / 1-Wire, Firmware Info и LED Indicator.", None), # Порт parallel-nand/stm32g474ce предназначен для корпуса UFQFPN-48. + ("STM32G474RB", "SETProtocol, периферийный эмулятор, генератор ЦАП и порт USB Boot в templates.", None), # Порты set-protocol/stm32g474-periph и stm32g474-usb-boot описывают RB; USB-клиент F407 не назначается этому MCU. + ("STM32G474VET6", "HOPE: эмуляторы УМП / 2812 / IGBT / ЦАП / DEIF; CANdle CAN / UART BOOT, STM Invertor, DS18B20 / 1-Wire.", None), # CAN_BOOTLOADER_G474.md проекта CANdle подтверждает BOOT именно VET6, отдельно от USB BOOT платы RB. + ), # Завершаем список STM32 без автоматического поиска каталогов, недоступных внутри собранного EXE. + "k1921": ( # К1921 использует те же общие инструменты SETProtocol, что описаны в PROJECT_RELATIONS.md. + ("K1921VK028", "RTC Service, Firmware Info и обмен SETProtocol через UART.", None), # Источники возможностей — rtc-service/k1921vk028, firmware-info/k1921vk028 и транспорт приложения. + ), # Единственный конкретный MCU этого семейства в текущем templates. +} # Чистые данные не создают Qt-виджеты и сохраняют отложенное построение платформ. + +EMULATOR_MODES = { # Номера режимов совпадают с config[3] общего set_emu_server и не зависят от MCU. + 0: ("ump", "УМП — логгер и сигналы", "УМП: запись логгера и управление эмуляцией пуска."), # Режим по умолчанию после сброса MCU. + 1: ("tms2812", "2812 — BALZAM_7 / CMD_STD", "2812: пуск, останов и задание частоты через CMD_STD."), # Обмен 2812 использует собственный адрес, обычно 1. + 2: ("igbt", "IGBT / драйвер — Vin / Vstat", "IGBT: после выбора включите эмуляцию в панели драйвера."), # Физические GPIO зависят от платы, сама модель общая. + 3: ("wavegen", "Генератор сигналов — ЦАП / PA4", "ЦАП: загрузите таблицу, затем отдельно нажмите «Пуск ЦАП»."), # Запись таблицы не запускает аппаратный вывод. + 4: ("deif", "DEIF Master — передача CAN", "DEIF Master: настройте профиль и RAW, затем нажмите «Пуск DEIF»."), # Общая C-модель передаёт SAM/DAM через аппаратный CAN выбранного MCU. + 5: ("deif", "DEIF Slave — приём CAN", "DEIF Slave: настройте Node-ID и привязки, затем нажмите «Пуск DEIF»."), # Роль Slave принимает NMT/MPDO и передаёт Boot-up, heartbeat и EMCY. +} # Режим применяется отдельной подтверждаемой транзакцией STM, запуск выполняется в панели модели. +EMULATOR_TOOLS = { # Один набор инструментов используется каждым реализованным аппаратным портом. + "settings": "Настройки STM", # Адрес, скорость и режим читаются и проверяются общим клиентом. + "ump": "УМП", # Логгер и синтетические сигналы используют set_emu_ump. + "tms2812": "2812", # Обмен BALZAM_7 использует set_emu_tms. + "igbt": "IGBT / драйвер", # Логические входы и выходы используют set_emu_igbt. + "wavegen": "Подключение генератора", # Таблица передаётся общему генератору с таймером выбранной платы. + "deif": "DEIF Master / Slave", # Одна панель управляет обеими ролями set_emu_deif. +} # Приложение предоставляет существующие панели и использует общую панель DEIF из templates. +EMULATOR_MENU_TITLE = "Эмуляторы: УМП / 2812 / IGBT / ЦАП / DEIF" # Обзор MCU и общая навигация используют одинаковый список поддержанных моделей. +EMULATOR_PROFILES = { # Каталог содержит платы с реальной прошивкой stm32-emulator; дополнительные порты подключаются здесь. + "STM32F407": {"title": "STM32F407VET6", "timer_hz": 72_000_000, "sample_max": 1_000_000, "settings": "stm_ump", "model": "STM32F407-EMU-EXPERIMENT", "igbt_inputs": tuple("PB%d" % pin for pin in range(8)), "igbt_gpio": True, "wave_port": "stm32f407-periph", "tools": tuple(EMULATOR_TOOLS)}, # TIM6 72 МГц и прежний ключ stm_ump сохраняют совместимость F407. + "STM32F103ZET6": {"title": "STM32F103ZET6", "timer_hz": 72_000_000, "sample_max": 1_000_000, "settings": "stm_emulator/STM32F103ZET6", "model": "STM32F103ZE-EMU-EXPERIMENT", "igbt_inputs": ("PB0", "PB1", "PB2", "PB10", "PB11", "PB12", "PB13", "PB14"), "igbt_gpio": True, "wave_port": "stm32f103-periph", "tools": tuple(EMULATOR_TOOLS)}, # High-density F103ZE имеет DAC/TIM6 72 МГц, UART/GAS и bxCAN; малые F103 без DAC не объявляются совместимыми автоматически. + "STM32G474RB": {"title": "STM32G474RB", "timer_hz": 160_000_000, "sample_max": 1_000_000, "settings": "stm_emulator/STM32G474RB", "model": "STM32G474RB-EMU-EXPERIMENT", "igbt_inputs": ("PB0", "PB1", "PB2", "PB10", "PB11", "PB12", "PB13", "PB14"), "igbt_gpio": True, "wave_port": "stm32g474-periph", "tools": tuple(EMULATOR_TOOLS)}, # RB использует свой USB BOOT, FDCAN и TIM6 160 МГц. + "STM32G474VET6": {"title": "HOPE STM32G474VET6", "timer_hz": 160_000_000, "sample_max": 1_000_000, "settings": "stm_emulator/STM32G474VET6", "model": "STM32G474VE-HOPE-EMU", "igbt_inputs": ("PB0", "PB1", "PB2", "PB10", "PB11", "PB12", "PB13", "PB14"), "igbt_gpio": False, "wave_port": "stm32g474-periph", "tools": tuple(EMULATOR_TOOLS)}, # HOPE подтверждает собственную USB-модель, USART1 PA9/PA10 и CAN1 PD0/PD1; GPIO IGBT отключены из-за CAN2 на PB12/PB13. +} # Модели общие, а идентичность, GPIO, частоты и сохранённое подключение принадлежат выбранной плате. +CANDLE_G474_BOOT = { # Профиль воспроизводит CAN_BOOTLOADER_G474.md и boot_profile.h проекта CANdle_stm32. + "app_base": 0x08008000, "app_size": 480 * 1024, # Начальные 32 КиБ из 512 КиБ Flash заняты BOOT; приложение занимает оставшиеся 480 КиБ. + "stack_base": 0x20000000, "stack_end": 0x20014000, # Валидатор pcan_boot_stm32g474.c проекта CANdle принимает начальный MSP только внутри используемых linker-скриптом 80 КиБ SRAM. + "device_type": 7, "device": 15, "product": 0, "hardware": 0, # Идентичность ProtoCAN BOOT: Type 7, Device 15, Product 0 и аппаратная ревизия 0. + "can_bitrate": 500_000, "uart_baud": 512_000, # Classic CAN работает на 500 кбит/с, USART1 PA9/PA10 — на 512000 бод, 8N1. +} # Адреса и скорости относятся только к CANdle G474VET6 и не подменяют профиль USB BOOT G474RB. diff --git a/python/set_devices/qt_ports/deif_emulator.py b/python/set_devices/qt_ports/deif_emulator.py new file mode 100644 index 0000000..9077911 --- /dev/null +++ b/python/set_devices/qt_ports/deif_emulator.py @@ -0,0 +1,120 @@ +"""Асинхронное управление общим DEIF через SET v2 или RTU.""" # Обмен не зависит от приложения-потребителя и аппаратного порта MCU. +import struct # FC03 и FC06 используют порядок байтов Modbus big-endian внутри общего транспорта. +from .qt_compat import QObject, QTimer, Signal, QSerialPort # Поддерживает одинаковую машину обмена для Qt5 и Qt6. +from .emulator_serial import EmulatorSerialPort # Автомат обнаруживает протокол и проверяет модель выбранной платы. +from .stm_settings import request, decode_config # Настройки STM читаются тем же кодеком, что явный выбор эмулятора. +from set_devices.tms_terminal import crc16_modbus # CRC проверяется также при транспортировке RTU-команды внутри SET v2. +from set_devices.deif_emulator import DEIF_BASE, DEIF_WORDS, decode_snapshot # Адреса, сигнатура и интерпретация снимка общие для всех MCU. + + +class DeifEmulatorClient(QObject): # Каждая операция заканчивается закрытием COM, не удерживая общий порт между действиями. + finished = Signal(bool, str, object) # Возвращает подтверждённый снимок либо ошибку без ложного успеха записи. + + def __init__(self, parent=None, *, serial_factory=EmulatorSerialPort): # Подмена фабрики предназначена для проверки на реальном C-ядре без оборудования. + super().__init__(parent) # QObject освобождает таймеры вместе с владельцем панели. + self.serial = serial_factory(self) # Один адаптер хранит выбранный MCU и выполняет безопасный handshake. + self.serial.readyRead.connect(self._receive) # Разбор допускает произвольную фрагментацию USB/UART. + self.serial.errorOccurred.connect(self._error) # Ошибка открытия или передачи завершает только текущую операцию. + self.timeout = QTimer(self) # Ограничение ожидания применяется отдельно к каждому запросу. + self.timeout.setSingleShot(True) # Тайм-аут не создаёт повторных записей со сторонними эффектами. + self.timeout.timeout.connect(lambda: self._finish(False, "Тайм-аут DEIF; состояние записи проверьте повторным чтением")) # Потерянный ответ не означает, что команда не исполнилась. + self.delay = QTimer(self) # Следующий запрос отправляется вне стека readyRead. + self.delay.setSingleShot(True) # Каждое подтверждение разрешает ровно один следующий шаг. + self.delay.timeout.connect(self._next) # Последовательные FC06 не перемешиваются с финальной проверкой. + self.active = False # Новая операция разрешена только после полного завершения предыдущей. + + def start(self, port, unit, baud, timeout, writes=()): # Профиль связи передаётся потребителем явно, без глобального COM и адреса. + if self.active: # Повторное нажатие не должно сбросить ожидающую подтверждения запись. + return # Текущая операция сохраняет первоначальный профиль. + self.writes = tuple((int(offset), int(value) & 0xFFFF) for offset, value in writes) # RAW I16 кодируется теми же 16 битами без потери знака. + if any(offset not in (2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) and not 32 <= offset < 44 for offset, _value in self.writes): # Только документированные writable слова версии 1 допускаются в очередь. + self.finished.emit(False, "Регистр DEIF недоступен для записи", None) # Локальная ошибка обнаруживается до открытия физического порта. + return # Чужие регистры и конфигурация STM не могут попасть в эту транзакцию. + self.unit, self.timeout_ms = unit, timeout # Адрес управления STM по умолчанию 16; тайм-аут задаётся в мс. + self.active, self.stage, self.rx = True, "config", bytearray() # Первая команда только читает действующий режим MCU. + self.serial.setPortName(port) # Выбор COM не открывает соединение до настройки формата. + self.serial.setBaudRate(baud) # USART использует выбранную скорость; для CDC это поле совместимости. + self.serial.setDataBits(QSerialPort.DataBits.Data8) # Восемь бит данных соответствуют аппаратным портам STM. + self.serial.setParity(QSerialPort.Parity.NoParity) # Проверку целостности выполняет CRC, формат UART без чётности. + self.serial.setStopBits(QSerialPort.StopBits.OneStop) # Один стоп-бит завершает формат 8N1. + self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) # RTS/CTS прошивкой не используются. + if not self.serial.open(QSerialPort.OpenModeFlag.ReadWrite): # COM может быть занят другой программой. + self._finish(False, self.serial.errorString()) # Ошибка сообщает реальную причину открытия. + return # Ни чтение, ни запись при закрытом порте не выполняются. + self._send(3, 0x1210, 10) # Адрес и длина блока настроек STM взяты из set_emu_server. + + def _send(self, function, address, value): # Запрос отправляется один раз; автоматического повтора записи нет. + self.packet = request(self.unit, function, address, value) # Общий кодек добавляет правильный CRC16. + self.rx.clear() # Байты предыдущего подтверждённого ответа не примешиваются к новому. + self.timeout.start(self.serial.response_timeout(self.timeout_ms)) # Первый обмен учитывает время определения SET v2/RTU. + if self.serial.write(self.packet) != len(self.packet): # Частичная постановка запроса в очередь не считается отправкой. + self._finish(False, "Не удалось передать запрос DEIF") # Операция останавливается без повторной передачи. + + def _receive(self): # Накапливает только ответ текущего FC03 или FC06. + chunk = bytes(self.serial.readAll()) # Готовые байты забираются даже при отменённой операции. + if not self.active or not self.timeout.isActive(): # Поздний ответ не возобновляет отменённую последовательность. + return # Команда остаётся завершённой до нового явного действия пользователя. + self.rx.extend(chunk) # USB может разрезать ответ по любым границам. + function = self.packet[1] # Функция текущего запроса определяет размер ответа. + while len(self.rx) >= 3: # Заголовок содержит unit, function и byte count либо exception code. + if self.rx[0] != self.unit or self.rx[1] not in (function, function | 0x80): # Чужие либо старые кадры не подтверждают текущую операцию. + del self.rx[0] # Сдвиг на байт позволяет восстановиться после шумового префикса. + continue # Повторно ищет подходящий заголовок в оставшихся байтах. + expected = self.packet[5] * 2 if function == 3 else 0 # Все чтения короче 256 слов, старший байт количества равен нулю. + size = 5 if self.rx[1] & 0x80 else expected + 5 if function == 3 else 8 # Exception имеет 5 байт, FC06 echo — 8, FC03 — данные плюс заголовок и CRC. + if len(self.rx) < size: # Незавершённый ответ остаётся в буфере до readyRead или тайм-аута. + return # Никаких записей до полного подтверждения не выполняется. + raw = bytes(self.rx[:size]) # Фиксирует кадр до удаления из транспортного буфера. + del self.rx[:size] # Следующий шаг начнёт новый обмен с чистым буфером. + self.timeout.stop() # Полный ответ больше не ожидает истечения тайм-аута. + try: # Ошибки протокола должны возвращаться через finished, не покидая слот Qt. + if crc16_modbus(raw): # Нулевая CRC полного кадра подтверждает целостность. + raise ValueError("Неверная CRC ответа DEIF") # Повреждённый кадр не подтверждает запись. + if raw[1] & 0x80: # Старая прошивка либо неверный режим возвращает Modbus exception. + raise ValueError("STM отклонила DEIF, код %d; проверьте режим и версию прошивки" % raw[2]) # Показывает реальный код без предположения об успешном исполнении. + if function == 6 and raw != self.packet or function == 3 and raw[2] != expected: # Echo и фактическая длина должны точно совпасть с запросом. + raise ValueError("Неверное подтверждение DEIF") # Ответ другой команды не продвигает очередь. + self._accept(raw) # Только проверенный ответ может разрешить следующий шаг. + except ValueError as error: # Локальная проверка сигнатуры или роли имеет тот же путь завершения. + self._finish(False, str(error)) # Все таймеры и COM закрываются одинаково при любой ошибке. + return # За один сигнал обрабатывается только текущий запрос последовательности. + + def _accept(self, raw): # Порядок: конфигурация, сигнатура DEIF, записи, итоговый снимок. + if self.stage == "config": # Чтение действующего режима предшествует любым командам DEIF. + self.mode = decode_config(raw)[8] # Общий декодер проверяет сигнатуру STM и допустимые режимы. + if self.writes and self.mode not in (4, 5): # Выбор режима выполняется отдельной явной транзакцией STM. + raise ValueError("Выберите DEIF Master или DEIF Slave в настройках STM и примените режим") # Чтение остаётся доступным для диагностики любой прошивки с DEIF. + self.stage = "probe" # Теперь нужно проверить версию и роль отдельного окна DEIF. + self.steps = [(3, DEIF_BASE, DEIF_WORDS)] # Одно чтение возвращает атомарный снимок 52 слов. + elif self.packet[1] == 3: # Окно читается перед записями и после них. + snapshot = decode_snapshot(struct.unpack(">%dH" % DEIF_WORDS, raw[3:-2]), self.mode) # Совместимость подтверждается данными реального MCU. + if self.stage == "probe" and self.writes: # Совместимый сервис разрешает подготовленные пользователем команды. + self.stage = "write" # С этой точки любой отказ может означать частично применённый профиль. + self.steps = [(6, DEIF_BASE + offset, value) for offset, value in self.writes] + [(3, DEIF_BASE, DEIF_WORDS)] # Финальное чтение обязательно даже после успешных echo. + else: # Чтение без записей и финальная проверка завершают транзакцию. + if any(snapshot.words[offset] != value for offset, value in dict(self.writes).items()): # Повторная запись того же регистра проверяется по последнему значению, включая временную сеть и нулевую маску при настройке. + raise ValueError("DEIF не подтвердил новые значения; повторите чтение состояния") # ACK передачи сам по себе не является проверкой применения. + self._finish(True, "Состояние DEIF прочитано и проверено", snapshot) # Только этот путь публикует подтверждённый снимок формы. + return # Завершённая операция не назначает следующий запрос. + self.delay.start(20) # Пауза 20 мс не блокирует GUI и совпадает с шагом клиента настроек STM. + + def _next(self): # Слот таймера продвигает ровно одну команду подтверждённой последовательности. + if self.active and self.steps: # Отмена удаляет разрешение последующих запросов. + self._send(*self.steps.pop(0)) # Следующая запись выполняется только после echo предыдущей. + + def _error(self, error): # Обрабатывает ошибки последовательного порта в том же GUI-потоке. + if self.active and error not in (QSerialPort.SerialPortError.NoError, QSerialPort.SerialPortError.TimeoutError): # Собственный таймер уже отвечает за тайм-аут транзакции. + self._finish(False, self.serial.errorString()) # Не скрывает причину отключения или отказа COM. + + def cancel(self): # Отмена доступна пользователю без смены режима на MCU. + if self.active: # Повторная отмена не создаёт повторных сигналов завершения. + self._finish(False, "Обмен DEIF отменён; проверьте состояние повторным чтением") # Уже подтверждённые записи не откатываются и не повторяются. + + def _finish(self, ok, message, snapshot=None): # Единое завершение используется при успехе, отмене и ошибке. + if not self.active: # Поздняя ошибка Qt после close не должна затронуть новую операцию. + return # finished публикуется ровно один раз за запуск. + self.active = False # Состояние сбрасывается раньше закрытия COM и внешних обработчиков. + self.timeout.stop() # Ожидание текущего ответа больше не действует. + self.delay.stop() # Следующие записи не отправляются после ошибки. + self.serial.close() # Общий COM освобождается для логгера и других панелей. + self.finished.emit(ok, message, snapshot) # UI получает результат после полного освобождения ресурсов. diff --git a/python/set_devices/qt_ports/deif_emulator_panel.py b/python/set_devices/qt_ports/deif_emulator_panel.py new file mode 100644 index 0000000..a2df214 --- /dev/null +++ b/python/set_devices/qt_ports/deif_emulator_panel.py @@ -0,0 +1,189 @@ +"""Общая панель аппаратного DEIF Master/Slave для всех реализованных MCU.""" # Приложение передаёт только параметры подключения и освобождение общего транспорта. +try: # Qt6 используется современными настольными приложениями. + from PySide6.QtWidgets import QWidget, QVBoxLayout, QFormLayout, QHBoxLayout, QLabel, QPushButton, QSpinBox, QComboBox, QCheckBox, QGroupBox, QScrollArea # Панель не импортирует виджеты конкретного приложения. +except ImportError: # На Windows 7 те же элементы предоставляет Qt5. + from PySide2.QtWidgets import QWidget, QVBoxLayout, QFormLayout, QHBoxLayout, QLabel, QPushButton, QSpinBox, QComboBox, QCheckBox, QGroupBox, QScrollArea # Состав и поведение управления сохраняются между версиями Qt. +from .qt_compat import Signal # Совместимый сигнал сообщает потребителю о занятости общего COM. +from .deif_emulator import DeifEmulatorClient # Обмен и проверка ответов принадлежат templates. +from set_devices.deif_emulator import DEIF_FIELDS, DEIF_INDICES # Порядок полей и OD взяты из переносимой C-модели. +from set_devices.mcu_profiles import EMULATOR_PROFILES # Строка модели USB и настройки MCU определяются единым каталогом. + + +class DeifEmulatorPanel(QWidget): # Единственный экземпляр панели обслуживает выбранную плату без дублирования моделей. + busy_changed = Signal(bool) # Приложение блокирует конфликтующие инструменты на время транзакции. + + def __init__(self, connection, prepare, parent=None): # Колбэки позволяют использовать панель вне SETGUI. + super().__init__(parent) # Владелец Qt управляет временем жизни панели и клиента. + self.connection, self.prepare = connection, prepare # connection возвращает COM, unit, baud, timeout; prepare освобождает транспорт либо возвращает причину отказа. + self.client = DeifEmulatorClient(self) # Общий клиент проверяет режим STM и сигнатуру DEIF перед записью. + self.client.finished.connect(self._finished) # Состояние формы обновляется только подтверждённым снимком. + self.snapshot = None # До первого чтения действия записи заблокированы. + outer = QVBoxLayout(self) # Внешняя компоновка оставляет статус доступным при прокрутке длинного профиля. + self.heading = QLabel() # Название платы меняется вместе с общим селектором MCU. + self.heading.setWordWrap(True) # Полное название HOPE не обрезается при узком окне. + outer.addWidget(self.heading) # Текущая аппаратная цель видна до любых команд. + note = QLabel("В «Настройки STM» выберите DEIF Master или DEIF Slave и примените режим. Управление платой идёт через выбранный COM; кадры DEIF передаются её CAN. Для изменения сетевого профиля сначала нажмите «Останов DEIF». Настройки действуют до сброса или смены роли.") # Объясняет пользователю разделение управления MCU и работы CAN без автоматического запуска. + note.setWordWrap(True) # Пояснение занимает доступную ширину. + outer.addWidget(note) # Инструкция остаётся рядом с кнопками даже после прокрутки профиля. + scroll = QScrollArea() # Настройки и шесть привязок должны помещаться при небольшом разрешении экрана. + scroll.setWidgetResizable(True) # Содержимое использует ширину панели. + content = QWidget() # Дочерний контейнер содержит только редактируемые параметры. + layout = QVBoxLayout(content) # Группы расположены последовательно без вложенных вкладок. + network = QGroupBox("Профиль CAN / DEIF") # Параметры соответствуют словам 4…15 окна 0x1600. + form = QFormLayout(network) # Подписи поясняют назначение каждого числа. + self.node = self._spin(1, 127, 1) # XDi-net разрешает узлы 1…126, CANopen дополнительно 127; firmware проверяет выбранную сеть. + self.peer = self._spin(1, 127, 11) # Для Master это DAM-получатель, для Slave — SAM-источник. + self.network = QComboBox() # Сеть задаёт способ адресации и допустимые RPDO. + self.network.addItems(("XDi-net / SAM", "CANopen / DAM")) # Значения индекса 0 и 1 совпадают с set_emu_deif.h. + self.pdo = self._spin(1, 4, 2) # RPDO2 — штатный профиль, RPDO1 разрешён только в чистом CANopen. + self.period = self._spin(1, 65535, 100) # Период цикла в мс: firmware хранит uint16, нулевой период запрещён. + self.period.setSuffix(" мс") # Единица времени указана непосредственно рядом со значением. + self.mask = self._spin(0, 255, 63, hexadecimal=True) # Биты 0…5 выбирают шесть измерений; 6…7 добавляют яркость и палитру Master. + self.group = self._spin(0, 9, 1) # DEIF поддерживает группы диммера 1…9; 0 означает локальный режим. + self.heartbeat = QCheckBox("Передавать heartbeat Master") # Slave передаёт heartbeat независимо от этого флага. + self.self_start = QCheckBox("Автозапуск Slave в CANopen") # В XDi-net самозапуск через 1 с обязателен по профилю. + self.self_start.setChecked(True) # Совпадает с начальной настройкой set_deif_init. + self.service_sam = QCheckBox("Сервисный SAM COB-ID 0x27F") # Специальный идентификатор включается только явным выбором. + for title, widget in (("Собственный Node-ID", self.node), ("DAM получатель / SAM источник", self.peer), ("Сеть", self.network), ("RPDO", self.pdo), ("Период Master", self.period), ("Маска объектов", self.mask), ("Группа диммера", self.group), ("Heartbeat", self.heartbeat), ("Запуск Slave", self.self_start), ("Сервисный SAM", self.service_sam)): # Группа содержит все изменяемые параметры сетевого профиля C-модели. + form.addRow(title, widget) # Числовые поля не скрываются в аппаратных настройках конкретной платы. + layout.addWidget(network) # Сетевые настройки одинаковы для bxCAN и FDCAN. + self.network_group = network # Запущенный эмулятор не разрешает менять сетевой профиль. + values = QGroupBox("Задания Master / показания Slave") # Одни и те же RAW показывают задания передатчика либо принятые значения. + value_form = QFormLayout(values) # Каждое поле сохраняет отдельную подпись и знак. + self.raw = [self._spin(-32768, 32767, 0) for _field in DEIF_FIELDS] # Весь диапазон I16 сохраняет служебные значения и отрицательные задания. + for title, widget in zip(DEIF_FIELDS, self.raw): # Порядок соответствует set_deif_emu.raw[6]. + value_form.addRow(title, widget) # Уставка угла Master отображается отдельно, но зеркалит фактический угол. + self.raw[5].setReadOnly(True) # Master передаёт одинаковый RAW угла для подиндексов 2 и 7; шестой RAW напрямую не записывается. + self.brightness = self._spin(0, 100, 100) # Яркость — U8 в процентах, допустимый диапазон проверяется firmware. + self.brightness.setSuffix(" %") # Единицы яркости не смешиваются с необработанными измерениями. + self.palette = QComboBox() # Палитра соответствует DEIF §11.3.2. + self.palette.addItems(("Day", "Night", "Dusk")) # Номера 0, 1 и 2 передаются без преобразования. + value_form.addRow("Яркость", self.brightness) # Master может изменять яркость во время работы. + value_form.addRow("Палитра", self.palette) # Slave показывает значения, полученные от CAN. + layout.addWidget(values) # Показания остаются доступными при запущенном эмуляторе. + bindings = QGroupBox("Привязки объектов Slave") # Привязки явно задают OD вместо предположений о словаре прибора. + binding_form = QFormLayout(bindings) # Шесть пар индекс/подиндекс соответствуют словам 32…43. + self.indices, self.subindices = [], [] # Пары сохраняются в порядке полей для чтения и записи. + for number, title in enumerate(DEIF_FIELDS): # Все шесть объектов могут настраиваться независимо. + row = QHBoxLayout() # Индекс и подиндекс располагаются рядом с общей подписью поля. + index = self._spin(0, 65535, DEIF_INDICES[number], hexadecimal=True) # Индекс OD хранится как uint16, семейство проверяет MCU. + sub = QComboBox() # Только поддерживаемые I16 подиндексы разрешены моделью. + sub.addItems(("2 — значение", "7 — задание")) # Подиндексы 2 и 7 заданы в set_deif_write. + sub.setCurrentIndex(int(number == 5)) # Последняя привязка соответствует уставке угла. + row.addWidget(index) # Значение OD отображается в привычном шестнадцатеричном виде. + row.addWidget(sub) # Назначение подиндекса видно без отдельной документации. + binding_form.addRow(title, row) # Подпись совпадает с таблицей показаний. + self.indices.append(index) # Сохраняет редактируемый индекс нужного поля. + self.subindices.append(sub) # Сохраняет подиндекс того же поля. + self.bindings_group = bindings # Привязки недоступны при работе и в роли Master. + layout.addWidget(bindings) # Пользователь может настроить словарь перед запуском Slave. + scroll.setWidget(content) # Область прокрутки принимает владение контейнером. + outer.addWidget(scroll) # Высота формы ограничивается размером окна. + buttons = QHBoxLayout() # Основные команды остаются видимыми независимо от прокрутки. + self.read = QPushButton("Прочитать DEIF") # Чтение не запускает передачу CAN. + self.apply = QPushButton("Применить профиль") # Настройки отправляются отдельным явным действием при остановке. + self.send_raw = QPushButton("Записать RAW / подсветку") # Живые задания разрешены только Master. + self.start = QPushButton("Пуск DEIF") # Пуск не совмещается с применением профиля либо выбором роли. + self.stop = QPushButton("Останов DEIF") # Останавливает модель и её фоновые CAN-события. + self.cancel = QPushButton("Отменить обмен") # Отмена относится к транзакции COM, а не к уже работающей модели MCU. + for button, action in ((self.read, lambda: self._request()), (self.apply, self._apply), (self.send_raw, self._write_raw), (self.start, lambda: self._request(((2, 1),))), (self.stop, lambda: self._request(((2, 0),))), (self.cancel, self.client.cancel)): # Кнопки привязаны к единственному клиенту без повторяющихся обработчиков протокола. + button.clicked.connect(action) # Нажатие является единственным источником команд, опрос таймером не запускается. + buttons.addWidget(button) # Все действия имеют видимые названия и одинаковое расположение для любого MCU. + outer.addLayout(buttons) # Команды находятся под прокручиваемым профилем. + self.status = QLabel("Сначала прочитайте состояние DEIF выбранной платы.") # Непрочитанное устройство не выглядит подключённым. + self.status.setWordWrap(True) # Ошибки связи и диагностика не обрезаются. + outer.addWidget(self.status) # После операции результат остаётся видимым. + self.set_mcu("STM32F407") # Начальная модель совпадает с прежней рабочей областью эмуляторов. + + @staticmethod # Создание однотипных числовых полей не зависит от состояния панели. + def _spin(minimum, maximum, value, hexadecimal=False): # Диапазоны берутся из описания регистров C-модели. + spin = QSpinBox() # Qt выполняет базовую проверку числового ввода. + spin.setRange(minimum, maximum) # Допустимый размер поля задаётся до начального значения. + spin.setValue(value) # Штатный профиль можно использовать после явного применения. + if hexadecimal: # Маски и индексы OD удобнее читать как hex. + spin.setDisplayIntegerBase(16) # Меняет только отображение, числовое значение остаётся целым. + spin.setPrefix("0x") # Основание видно рядом с вводимыми цифрами. + return spin # Поле встраивается в нужную группу общей формы. + + def set_mcu(self, key): # Смена цели не открывает порт и не запускает CAN. + self.client.serial.set_mcu(key) # Проверяемая USB-модель выбирается до следующего handshake. + self.snapshot = None # Состояние предыдущего устройства не разрешает запись новому. + self.heading.setText("DEIF Master / Slave · " + EMULATOR_PROFILES[key]["title"]) # Полное название аппаратного профиля всегда видно оператору. + self.status.setText("Выберите режим DEIF в настройках STM, затем прочитайте состояние.") # Явно сбрасывает прежнее подтверждение платы. + self._controls() # Новая цель требует нового чтения перед пуском и настройкой. + + def _controls(self): # Разрешения кнопок зависят только от подтверждённого снимка и текущей транзакции. + busy, snapshot = self.client.active, self.snapshot # Не доверяет вручную отредактированным полям роли. + verified = snapshot is not None and snapshot.mode in (4, 5) # Наличие сервиса не означает, что он выбран активным эмулятором. + self.read.setEnabled(not busy) # Чтение разрешено также до выбора роли для диагностики прошивки. + self.cancel.setEnabled(busy) # Отменять можно только выполняющийся обмен. + self.apply.setEnabled(not busy and verified and not snapshot.running) # Сетевой профиль меняется только у остановленной выбранной модели. + self.start.setEnabled(not busy and verified and not snapshot.running) # Первое чтение и выбор роли обязательны перед пуском. + self.stop.setEnabled(not busy and verified) # Повторная остановка допускается и не запускает новый сценарий. + master = verified and snapshot.role == 1 # Живые задания и подсветка принадлежат только Master. + self.send_raw.setEnabled(not busy and master) # Slave получает эти значения из CAN и не допускает подмены входов. + self.network_group.setEnabled(not busy and verified and not snapshot.running) # Заблокированные поля не создают впечатление применяемого на ходу профиля. + self.bindings_group.setEnabled(not busy and verified and not snapshot.running and not master) # Master использует заданные спецификацией OD, его привязки не изменяют формируемые кадры. + for widget in self.raw[:5] + [self.brightness, self.palette]: # Шестой RAW всегда является показанием либо зеркалом угла. + widget.setEnabled(not busy and master) # Во время операции запрещает редактирование отправляемых значений. + + def _request(self, writes=()): # Все действия используют один путь подготовки, блокировки и проверки. + if self.client.active: # Двойное нажатие не отменяет уже отправленную команду. + return # Дождаться завершения можно по статусу панели. + connection = self.connection() # Параметры берутся из текущей формы STM непосредственно перед действием. + if not connection[0]: # Без выбранного COM невозможно установить физическую цель. + self.status.setText("Выберите COM во вкладке «Настройки STM».") # Сообщает конкретное место выбора. + return # Остальные инструменты не отключаются при пустом профиле. + reason = self.prepare() # Потребитель проверяет конфликтующие операции и освобождает общий порт. + if reason: # Незавершённый обмен другого инструмента сохраняет исходную цель. + self.status.setText(reason) # Причина отказа видна рядом с кнопкой. + return # Конкурирующий COM не открывается. + self.busy_changed.emit(True) # Потребитель блокирует смену STM и конфликтующие панели до открытия COM. + self.status.setText("Обмен с DEIF…") # Начало операции не выдаётся за подтверждение записи. + self.client.start(*connection, writes=writes) # Очередь выполняется последовательно с финальным чтением. + self._controls() # Синхронная ошибка открытия также корректно обновляет состояние кнопок. + + def _apply(self): # Формирует явный набор сетевых настроек остановленной модели. + if self.snapshot is None: # Снимок мог быть сброшен сменой MCU. + return # Без подтверждения роли профиль не отправляется. + if self.network.currentIndex() == 0 and (self.node.value() > 126 or self.pdo.value() < 2): # XDi-net резервирует Node 127 и RPDO1 для служебных функций. + self.status.setText("Для XDi-net выберите Node-ID 1…126 и RPDO2…4.") # Проверка предшествует любой записи. + return # Неверный сетевой профиль не применяется частично. + if self.snapshot.role == 2 and (self.peer.value() > 126 or self.mask.value() > 63) or self.mask.value() & 0xC0 and not self.group.value(): # Источник SAM Slave ограничен 126, Slave использует шесть измерений, сетевой диммер требует группу. + self.status.setText("Проверьте SAM источник, маску объектов Slave и группу диммера.") # Сообщает ограничения переносимой C-модели. + return # Профиль остаётся только в форме до исправления. + writes = [(6, 1), (9, 0), (4, self.node.value()), (5, self.peer.value()), (7, self.pdo.value()), (6, self.network.currentIndex()), (8, self.period.value()), (10, int(self.heartbeat.isChecked())), (11, int(self.self_start.isChecked())), (12, self.group.value()), (15, int(self.service_sam.isChecked())), (9, self.mask.value())] # Временный CANopen и нулевая маска позволяют безопасно менять Node 127, RPDO1 и группу 0 в любом исходном профиле. + if self.snapshot.role == 2: # Привязки используются только принимающей ролью C-модели. + for number, (index, sub) in enumerate(zip(self.indices, self.subindices)): # Пары задают шесть независимых входов Slave. + family = index.value() & 0xFFF0 # Семейство OD исключает младший номер экземпляра. + if not index.value() & 15 or family not in (0x3000, 0x3080, 0x3090, 0x3420, 0x3430): # set_deif_write разрешает экземпляры 1…15 пяти измерительных семейств. + self.status.setText("Привязки DEIF должны использовать измерительные OD 300x/308x/309x/342x/343x, экземпляр 1…15.") # Локальный отказ не меняет часть профиля на MCU. + return # Проверяются все поля до передачи первого запроса. + writes.extend(((32 + 2 * number, index.value()), (33 + 2 * number, 7 if sub.currentIndex() else 2))) # Последовательность совпадает с парами индекс/подиндекс регистров 32…43. + self._request(writes) # Настройка не содержит команды пуска. + + def _write_raw(self): # Живое изменение заданий не меняет сетевой профиль либо состояние запуска. + writes = [(16 + number, widget.value()) for number, widget in enumerate(self.raw[:5])] # Пять RAW Master, угол задания firmware зеркалит автоматически. + self._request(writes + [(13, self.brightness.value()), (14, self.palette.currentIndex())]) # Подсветка и палитра подтверждаются итоговым снимком вместе с RAW. + + def _finished(self, ok, message, snapshot): # Единственный обработчик освобождает блокировки и обновляет форму. + self.snapshot = snapshot if ok else None # После ошибки неизвестно, какие команды уже применены; новое чтение обязательно. + if ok: # Только проверенные данные реального MCU заменяют значения формы. + words = snapshot.words # Все индексы взяты из set_deif_read версии 1. + for widget, offset in ((self.node, 4), (self.peer, 5), (self.pdo, 7), (self.period, 8), (self.mask, 9), (self.group, 12), (self.brightness, 13)): # Обновляет числовые настройки выбранной роли. + widget.setValue(words[offset]) # Снимок сохраняет фактически применённые значения, а не отправленные пожелания. + self.network.setCurrentIndex(words[6]) # Сеть определяет интерпретацию последующего профиля. + self.palette.setCurrentIndex(words[14]) # Slave может получить новую палитру по CAN. + for widget, offset in ((self.heartbeat, 10), (self.self_start, 11), (self.service_sam, 15)): # Флаги читаются независимо от текущего разрешения редактирования. + widget.setChecked(bool(words[offset])) # Правильная роль отображает собственные настройки. + for number, widget in enumerate(self.raw): # Знаковые RAW нужны также для служебных кодов и отрицательной частоты вращения. + widget.setValue(snapshot.raw[number]) # Показывает входы Slave либо задания Master. + self.indices[number].setValue(words[32 + 2 * number]) # Сохраняет явный словарь каждого поля. + self.subindices[number].setCurrentIndex(int(words[33 + 2 * number] == 7)) # Фактическое значение и задание остаются разными привязками. + received = words[26] | words[27] << 16 # RX счётчик состоит из младшего и старшего 16-битных слов. + transmitted = words[28] | words[29] << 16 # TX означает постановку в аппаратную CAN-очередь. + rejected = words[30] | words[31] << 16 # Ошибочные кадры показываются отдельно от успешно принятых. + message += f" · {'Master' if snapshot.role == 1 else 'Slave'} · {'работает' if snapshot.running else 'остановлен'} · NMT {words[22]} · RX {received} / TX {transmitted} / отклонено {rejected} · seen 0x{words[23]:02X} / invalid 0x{words[24]:02X} / lost 0x{words[25]:02X}" # Диагностика позволяет проверить поток без предположения, что ACK COM подтверждает физический CAN. + self.status.setText(message) # Ошибка либо подтверждение остаются видимыми после закрытия порта. + self._controls() # Права редактирования пересчитываются по новому проверенному состоянию. + self.busy_changed.emit(False) # Потребитель освобождает остальные инструменты после завершения клиента. diff --git a/python/set_devices/qt_ports/emulator_serial.py b/python/set_devices/qt_ports/emulator_serial.py new file mode 100644 index 0000000..21fcd61 --- /dev/null +++ b/python/set_devices/qt_ports/emulator_serial.py @@ -0,0 +1,181 @@ +"""Async COM adapter for direct RTU or automatic emulator discovery. + +Panels retain their register/TMS decoders. Only this boundary translates them +to SET v2 on the wire. No protocol cache survives closing/reopening a port. +""" +import time + +from .qt_compat import QObject, QTimer, Signal, QSerialPort # Общий порт не зависит от импортов SETGUI и поддерживает Qt6/Qt5. +from set_devices.emulator_protocol import EmulatorProtocol # Автомат SET v2/RTU находится рядом с остальными переносимыми моделями templates. + + +def response_timeout(port, milliseconds): + return port.response_timeout(milliseconds) if isinstance(port, EmulatorSerialPort) else milliseconds + + +def cancel_pending(port): + if isinstance(port, EmulatorSerialPort): + port.cancel_pending() + + +class EmulatorSerialPort(QObject): + readyRead = Signal() + errorOccurred = Signal(object) + protocolChanged = Signal(str) + wireEvent = Signal(str, bytes) + PROBE_MS = 300 + + def __init__(self, parent=None, port=None, *, protocol_mode='auto', rtu_suffix=b'', mcu="STM32F407"): # Профиль MCU фиксирует проверяемую модель прошивки, сохраняя все старые параметры порта. + super().__init__(parent) + self._port = port if port is not None else QSerialPort(self) + self._port.readyRead.connect(self._receive) + self._port.errorOccurred.connect(self.errorOccurred.emit) + self._probe = QTimer(self) + self._probe.setSingleShot(True) + self._probe.timeout.connect(self._probe_timeout) + self._protocol_mode = protocol_mode + self._rtu_suffix = bytes(rtu_suffix) + self._mcu = mcu # Аппаратная привязка сохраняется между закрытием и повторным открытием того же COM-адаптера. + self.protocol = EmulatorProtocol(protocol_mode, mcu=mcu) # Первичная конфигурация использует ожидаемую модель выбранного MCU. + self._rx = bytearray() + self._error = '' + self._deadline = 0 + + def __getattr__(self, name): + port = self.__dict__.get('_port') + if port is None: + raise AttributeError(name) + return getattr(port, name) + + def open(self, mode): + self._probe.stop() + self._deadline = 0 + self.protocol = EmulatorProtocol(self._protocol_mode, mcu=self._mcu) # Повторное открытие начинает новый handshake с той же аппаратной привязкой. + self._rx.clear() + self._error = '' + return self._port.open(mode) + + def configure_protocol(self, mode, *, rtu_suffix=b''): + """Choose the wire format before opening a new connection.""" + if self.isOpen(): + raise ValueError('Сначала отключите COM-порт для смены протокола') + protocol = EmulatorProtocol(mode, mcu=self._mcu) # Смена wire-формата не должна сбрасывать выбранный MCU на F407. + self.cancel_pending() + self._protocol_mode = mode + self._rtu_suffix = bytes(rtu_suffix) + self.protocol = protocol + self._rx.clear() + + def close(self): + self.cancel_pending() + self._port.close() + self._rx.clear() + + def set_mcu(self, mcu): # Профиль разрешается менять только до открытия физического соединения. + if self.isOpen(): # Открытая транзакция должна завершиться с первоначальной идентичностью устройства. + raise ValueError("Сначала отключите COM-порт для смены MCU") # Предотвращаем подмену цели незавершённого обмена. + protocol = EmulatorProtocol(self._protocol_mode, mcu=mcu) # Неизвестный профиль отвергается до изменения сохранённых параметров. + self._mcu = mcu # Следующее открытие порта проверит DeviceInfo именно этой платы. + self.protocol = protocol # Старое состояние обнаружения не переносится на другой MCU. + + def cancel_pending(self): + self._probe.stop() + self._deadline = 0 + self.protocol.cancel() + + def response_timeout(self, milliseconds): + return milliseconds + (2 * self.PROBE_MS if self.protocol.mode == 'auto' else 0) + + def errorString(self): + return self._error or self._port.errorString() + + def clear(self, *args): + self.cancel_pending() + self._rx.clear() + return self._port.clear(*args) + + def bytesAvailable(self): + return len(self._rx) + + @property + def supports_wave_blocks(self): + return self.protocol.mode == 'v2' + + def readAll(self): + result = bytes(self._rx) + self._rx.clear() + return result + + def write(self, data): + self._error = '' + self._rx.clear() + try: + packet = self.protocol.begin(bytes(data)) + return len(data) if self._send(packet) else -1 + except ValueError as error: + self._fail(str(error)) + return -1 + + def _send(self, packet): + self._probe.stop() + self._deadline = 0 + if self.protocol.phase in ('ping', 'info'): + self._deadline = time.monotonic() + self.PROBE_MS / 1000 + self._probe.start(self.PROBE_MS) + if self.protocol.mode == 'rtu': + packet += self._rtu_suffix + written = self._port.write(packet) + if written > 0: + self.wireEvent.emit('TX', packet[:written]) + if written != len(packet): + self._fail('Не удалось передать запрос целиком') + return False + return True + + def _receive(self): + raw = bytes(self._port.readAll()) + if not raw: + return + self.wireEvent.emit('RX', raw) + try: + old_mode = self.protocol.mode + outgoing, incoming = self.protocol.feed(raw) + if self.protocol.mode != old_mode: + self.protocolChanged.emit('SET v2') + if outgoing and not self._send(outgoing): + return + if incoming: + self._rx.extend(incoming) + self.readyRead.emit() + except ValueError as error: + self._fail(str(error)) + + def _probe_timeout(self): + if not self._deadline: + return + if time.monotonic() < self._deadline: + self._probe.start(max(1, int((self._deadline-time.monotonic())*1000)+1)) + return + try: + packet = self.protocol.fallback() + self.protocolChanged.emit('RTU') + self._send(packet) + except ValueError as error: + self._fail(str(error)) + + def _fail(self, message): + self._error = message + self.cancel_pending() + self.errorOccurred.emit(QSerialPort.SerialPortError.ReadError) + + def waitForReadyRead(self, milliseconds): + # Used only in WaveTransfer's worker. No GUI event loop is blocked. + deadline = time.monotonic() + milliseconds / 1000 + while not self._rx and not self._error and time.monotonic() < deadline: + if self._deadline and time.monotonic() >= self._deadline: + self._probe_timeout() + remaining = max(1, int((deadline-time.monotonic())*1000)) + self._port.waitForReadyRead(min(remaining, 20)) + if self._error: + raise OSError(self._error) + return bool(self._rx) diff --git a/python/set_devices/qt_ports/emulator_workspace.py b/python/set_devices/qt_ports/emulator_workspace.py new file mode 100644 index 0000000..fa8adbe --- /dev/null +++ b/python/set_devices/qt_ports/emulator_workspace.py @@ -0,0 +1,36 @@ +from .qt_compat import Signal # Общий сигнал Qt работает в портах PySide6 и PySide2 без зависимости от SETGUI. +try: # Настольные потребители могут использовать любую из поддерживаемых версий Qt. + from PySide6.QtWidgets import QComboBox, QTabWidget # Qt6 предоставляет контейнер общих панелей и явный выбор MCU. + from PySide6.QtCore import Qt, QSignalBlocker # Блокировка сигнала предотвращает рекурсивное переключение профиля. +except ImportError: # Порт Windows 7 использует тот же API поверх PySide2. + from PySide2.QtWidgets import QComboBox, QTabWidget # Состав и поведение панелей сохраняются в Qt5. + from PySide2.QtCore import Qt, QSignalBlocker # Оба поколения Qt поддерживают блокировку сигналов комбобокса. +from set_devices.mcu_profiles import EMULATOR_PROFILES, EMULATOR_TOOLS # Модели и аппаратные привязки задаёт общая библиотека. + + +class EmulatorWorkspace(QTabWidget): # Потребитель предоставляет UI-панели, а templates выбирает их состав для конкретного MCU. + mcu_changed = Signal(str) # Приложение обновляет локальные настройки и подписи после явного выбора аппаратного профиля. + + def __init__(self, parent=None): # Контейнер не открывает COM/CAN и не создаёт экземпляров аппаратных моделей. + super().__init__(parent) # Qt владеет переданными страницами в течение жизни рабочей области. + self.mcu = "STM32F407" # Сохраняем профиль прежнего эмулятора для существующих пользователей и вызовов. + self.mcu_selector = QComboBox(self) # Общая панель явно показывает MCU, которому предназначены команды. + for key, profile in EMULATOR_PROFILES.items(): # В списке появляются только платы с реализованными портами set_emu_server. + self.mcu_selector.addItem(profile["title"], key) # Стабильный ключ отделяет совместимость устройства от отображаемой подписи. + self.setCornerWidget(self.mcu_selector, Qt.Corner.TopRightCorner) # Выбор MCU доступен рядом с вкладками общих инструментов. + self.mcu_selector.currentIndexChanged.connect(lambda index: self.select_mcu(self.mcu_selector.itemData(index))) # Пользовательский выбор проходит тот же метод, что переход из меню конкретного MCU. + self._emulator_pages = {} # Реестр хранит единственные экземпляры переданных панелей, сохраняя общие транспорты приложения. + + def add_emulator_page(self, key, widget): # Приложение связывает собственный виджет с публичным ключом общего сервиса. + self._emulator_pages[key] = widget # Повторное использование панели не копирует её обработчики или состояние модели. + self.addTab(widget, EMULATOR_TOOLS[key]) # Название инструмента берётся из общего каталога templates. + + def select_mcu(self, key): # Смена профиля меняет только UI и настройки; команды активации остаются явными действиями пользователя. + profile = EMULATOR_PROFILES[key] # Неизвестный MCU отвергается до изменения текущей рабочей области. + self.mcu = key # Текущий ключ используется потребителем для выбора настроек и аппаратных параметров. + with QSignalBlocker(self.mcu_selector): # Программный переход из раздела MCU не вызывает повторный вход через комбобокс. + self.mcu_selector.setCurrentIndex(self.mcu_selector.findData(key)) # Видимый выбор всегда совпадает с применённым профилем. + for name, widget in self._emulator_pages.items(): # Доступность каждого инструмента определяется сервисами выбранного порта. + self.setTabVisible(self.indexOf(widget), name in profile["tools"]) # Новая плата может подключать подмножество тех же панелей без изменения их реализации. + self.mcu_changed.emit(key) # Локальный адаптер обновляет настройки после завершения общего переключения. + return True # Вызывающая навигация может отличить выполненный переход от отказа занятого адаптера. diff --git a/python/set_devices/qt_ports/stm_settings.py b/python/set_devices/qt_ports/stm_settings.py index 3f41bee..bfa0f8b 100644 --- a/python/set_devices/qt_ports/stm_settings.py +++ b/python/set_devices/qt_ports/stm_settings.py @@ -7,6 +7,7 @@ import struct from .qt_compat import QObject, QTimer, Signal from .qt_compat import QSerialPort +from set_devices.mcu_profiles import EMULATOR_MODES # Допустимые номера режимов берутся из общего каталога, включая обе роли DEIF. from set_devices.tms_terminal import crc16_modbus BAUDRATES = (9600, 19200, 38400, 57600, 115200) @@ -24,7 +25,7 @@ def decode_config(raw): if words[0] != 0x5343 or words[1] not in (2, 3): raise ValueError("Прошивка STM не поддерживает настройку связи (нужна новая версия)") address, tms, rate = words[2:5] - if not 1 <= address <= 247 or not 1 <= tms <= 255 or address == tms or rate >= len(BAUDRATES) or words[8] > (3 if words[1] == 3 else 1) or words[9] > (3 if words[1] == 3 else 1): + if not 1 <= address <= 247 or not 1 <= tms <= 255 or address == tms or rate >= len(BAUDRATES) or words[8] not in (EMULATOR_MODES if words[1] == 3 else (0, 1)) or words[9] not in (EMULATOR_MODES if words[1] == 3 else (0, 1)): # Версия 3 C-сервера поддерживает режимы 0…5; версия 2 сохраняет только УМП и 2812. raise ValueError("STM вернула неверные параметры связи") return words @@ -52,7 +53,7 @@ class StmSettingsClient(QObject): return if desired is not None: addr, tms, rate, mode = desired - if not 1 <= addr <= 247 or not 1 <= tms <= 255 or addr == tms or rate not in BAUDRATES or mode not in (0, 1, 2, 3): + if not 1 <= addr <= 247 or not 1 <= tms <= 255 or addr == tms or rate not in BAUDRATES or mode not in EMULATOR_MODES: # Номера 4 и 5 выбирают DEIF Master/Slave того же общего C-сервера. self.finished.emit(False, "Адреса УМП и 2812 должны различаться; проверьте диапазоны и скорость", None) return self.unit, self.baud, self.timeout_ms = unit, baud, timeout @@ -120,7 +121,7 @@ class StmSettingsClient(QObject): return addr, tms, baud, mode = self.desired if mode >= 2 and words[1] < 3: - raise ValueError("Обновите прошивку STM: для отдельных режимов IGBT и ЦАП нужна версия настроек 3") + raise ValueError("Обновите прошивку STM: для IGBT, ЦАП и DEIF нужна версия настроек 3") # Сообщение учитывает обе роли DEIF и возможное применение настроек при потере ответа. self.staged = (addr, tms, BAUDRATES.index(baud), mode) self.steps = [(6, 0x1218, 0), (6, 0x1215, addr), (6, 0x1216, tms), (6, 0x1217, self.staged[2]), (6, 0x1219, mode), (3, 0x1210, 10)] @@ -173,5 +174,5 @@ class StmSettingsClient(QObject): if self.serial.isOpen(): self.serial.close() if not ok and self.committed: - message += " Возможно, STM уже применила новые значения: адрес УМП %d, адрес 2812 %d, %d бод, режим %d (0=УМП, 1=2812, 2=IGBT, 3=ЦАП). Повторите чтение по ним или перезагрузите плату." % self.desired + message += " Возможно, STM уже применила новые значения: адрес УМП %d, адрес 2812 %d, %d бод, режим %d (0=УМП, 1=2812, 2=IGBT, 3=ЦАП, 4=DEIF Master, 5=DEIF Slave). Повторите чтение по ним или перезагрузите плату." % self.desired # Сообщение учитывает обе роли DEIF и возможное применение настроек при потере ответа. self.finished.emit(ok, message, values) diff --git a/python/tests/test_deif_shared.py b/python/tests/test_deif_shared.py new file mode 100644 index 0000000..a859f1c --- /dev/null +++ b/python/tests/test_deif_shared.py @@ -0,0 +1,19 @@ +"""Общие DEIF-векторы проверяют ctypes теми же данными, что C и Android/JVM.""" # Тесты не требуют Qt или структуры Desktop-приложения. +from pathlib import Path # Fixture входит в общий репозиторий templates. +import pytest # Параметризация даёт независимый результат для каждой эталонной строки. +from set_devices.deif_native import get_deif_native # Python не использует альтернативный декодер при отсутствии C. +from set_devices.deif_display import DeifDisplay # Пассивная модель является общим API библиотеки. +FIXTURE = Path(__file__).resolve().parents[2] / 'c/set-protocol/tests/fixtures/deif-xdi-v1.tsv' # Точный общий источник тестов для трёх портов. +CASES = [line.split() for line in FIXTURE.read_text(encoding='utf-8').splitlines() if line and not line.startswith('#')] # Здесь разбирается текст fixture, а не протокол CAN. +@pytest.mark.parametrize('row', CASES) # Каждый ошибочный или валидный wire-кадр проверяется отдельно. +def test_ctypes_matches_c_and_jni_vectors(row): # Числовой результат должен совпасть с независимым описанием ожидаемого кадра. + identifier, payload, flags, result, raw, index, sub = row # Столбцы fixture не зависят от внутреннего layout C. + flags = int(flags) # EXT/RTR являются входными метаданными теста. + code, meta = get_deif_native().inspect_frame(int(identifier, 16), bytes.fromhex(payload), bool(flags & 1), bool(flags & 2)) # Wire-разбор выполняется единственным C-кодеком. + assert (code, meta[4], meta[2], meta[3]) == (int(result), int(raw), int(index, 16), int(sub, 16)) # RAW и адрес объекта совпадают с C- и Kotlin-проверкой. +def test_passive_angle_is_received_without_gui_or_simulation(): # Ошибка со снимка воспроизводится без запуска серверных сервисов. + model = DeifDisplay(source='CAN / мост', device_node=13) # Физический адрес SLAVE не становится источником SAM. + assert model.accept('CAN / мост', 0x201, bytes.fromhex('01 01 30 07 84 03 00 00'), now=10) # Точный кадр пользователя несёт RAW 900. + assert model.angle_selection(now=10) == (90.0, True) # Сетевое задание видно как 90°, с явной пометкой. + assert model.value('angle', now=10) is None # Принятое задание не превращается в фиктивный фактический объект 0x02. + assert model.angle_selection(now=15.001) == (None, False) # При полном молчании наблюдатель уважает прежний срок свежести пять секунд.