feat(set-protocol): Добавь DEIF XDi, STM Invertor и порт HOPE G474

Объедини реализацию DEIF в C99-ядре, чтобы Python и Android
использовали один кодек и общие эталонные кадры.
Добавь две GAS-проекции единой карты STM Invertor и аппаратные
порты эмулятора и загрузчика для платы HOPE STM32G474VET6.

Проверки: сборка MSVC Debug, 24 теста CTest, 30 тестов Python
и 29 дополнительных проверок, синтаксис 16 файлов Python.
This commit is contained in:
2026-10-24 15:39:59 +03:00
parent 83fb9b6fe8
commit e52ba797e4
46 changed files with 2482 additions and 157 deletions
+5
View File
@@ -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
@@ -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 КиБ.
@@ -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. */
@@ -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 /* Завершает интерфейс платы без копирования загрузочного протокола. */
@@ -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. */
+8
View File
@@ -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) # Отдельный тест защищает восстановленный регистровый контракт.
+104
View File
@@ -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.
File diff suppressed because one or more lines are too long
+41
View File
@@ -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-модуле. */
+32
View File
@@ -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 /* Конец контракта двух представлений одних данных инвертора. */
+1
View File
@@ -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"
+2
View File
@@ -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)
@@ -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<Double?>, val statuses: List<Int>, val lostFields: Set<Int>, // Порядок полей задаёт общий 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<DeifFrame> { // События выдаются один раз, даже если экран журнала стоит на паузе.
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 после закрытия владельца.
@@ -0,0 +1,32 @@
#include <jni.h> /* 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;i<n;i++) { values[i*10]=(jint)frames[i].can_id; values[i*10+1]=(jint)frames[i].dlc; for(j=0;j<8;j++) values[i*10+2+j]=frames[i].data[j]; } /* Границы Java/C представлены id, dlc и восемью готовыми байтами. */
result=(*e)->NewIntArray(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. */
@@ -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.
@@ -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.
+121
View File
@@ -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 /* Завершает опциональное обновление физических ответов. */
} /* Завершает один миллисекундный шаг без обработки моделей из прерываний. */
@@ -0,0 +1,10 @@
#ifndef HOPE_BOARD_H /* Защищает интерфейс платы от повторных объявлений в общей композиции. */
#define HOPE_BOARD_H /* Отделяет аппаратную реализацию HOPE от моделей и транспортных кодеков. */
#include <stdint.h> /* Предоставляет фиксированные типы времени и диагностических кодов. */
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. */
@@ -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. */
@@ -0,0 +1,6 @@
#ifndef HOPE_BOOT_PORT_H /* Защищает интерфейс входа в существующий CANdle boot от повторного включения. */
#define HOPE_BOOT_PORT_H /* Общий USB SET-адаптер использует этот профиль вместо несовместимого RB USB boot. */
#include <stdint.h> /* Определяет тип миллисекундного времени отложенного 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. */
@@ -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. */
@@ -0,0 +1,9 @@
#ifndef HOPE_TRANSPORT_H /* Исключает повторное объявление транспорта HOPE при включении общего UART. */
#define HOPE_TRANSPORT_H /* Сохраняет одинаковый poll API у всех аппаратных портов эмулятора. */
#include <stdint.h> /* Объявляет миллисекундное время и диагностические счётчики фиксированной ширины. */
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. */
@@ -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)
+185
View File
@@ -0,0 +1,185 @@
#include "deif_xdi.h" /* Публичный ABI не зависит от Python, Kotlin, Qt или Android. */
#include <string.h> /* Фиксированные структуры копируются без динамической памяти. */
#include <math.h> /* Проверка конечности пользовательского масштаба предотвращает недопустимые показания. */
#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<sizeof(*s) || source<1U || source>(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;f++) { setp_deif_selector(f,source,1,0,selector); setp_deif_bind(s,f,selector,f==0U?1.0:0.1); } return 1; /* Исходный профиль включает все пять измерений и независимое задание азимута. */
} /* Запуск сервера выполняется отдельным явным действием. */
int setp_deif_bind(void *p,unsigned f,const uint8_t *selector,double scale) { /* Обновление привязки не затрагивает другие поля. */
deif_state *s=context(p); deif_binding *b; unsigned bound; if(!s || 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;f<DEIF_FIELDS;f++) if(s->field[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;f<DEIF_FIELDS;f++) { s->field[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<<f; } return mask; /* До первого фактического угла допускается явно помеченное сетевое задание; оно не обновляет другие поля. */
} /* Последнее валидное число сохраняется в словаре независимо от тайм-аута. */
static void store(deif_binding *b,int32_t raw,uint64_t now) { b->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;f<DEIF_FIELDS;f++) if(s->field[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;f<DEIF_FIELDS;f++) if(s->field[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->queued<cap?s->queued: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 без платформенных зависимостей. */
+175
View File
@@ -0,0 +1,175 @@
#include "set_invertor_gas.h" /* Переиспользуем общий регистровый и масочный GAS вместо нового wire-кодека. */
#include "pcan_id.h" /* Поля идентификатора упаковываются и проверяются существующей реализацией ProtoCAN. */
#include <stddef.h> /* 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 = &region; /* Единственный регион не пересекается с картами других 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-протокол. */
} /* Завершаем единый клиентский декодер нового профиля. */
+15
View File
@@ -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
1 # COB-ID payload flags result RAW index subindex
2 201 0101300784030000 0 1 900 3001 7
3 201 0101300284030000 0 1 900 3001 2
4 201 0181300239FC0000 0 1 -967 3081 2
5 201 0191300203FE0000 0 1 -509 3091 2
6 201 01213402CEF70000 0 1 -2098 3421 2
7 201 01313402D4FE0000 0 1 -300 3431 2
8 30D 8D01300784030000 0 1 900 3001 7
9 201 0101300784030000 1 -1 0 0 0
10 201 0101300784030000 2 -1 0 0 0
11 202 0101300784030000 0 -5 900 3001 7
12 201 0181 0 -4 0 0 0
13 701 05 0 2 0 0 0
14 70D 00 0 2 0 0 0
15 70D 7F 0 2 0 0 0
+26
View File
@@ -0,0 +1,26 @@
#include "deif_xdi.h" /* Эталонные векторы проверяют непосредственно единственное C-ядро. */
#include <assert.h> /* Любое расхождение байтов или сроков делает тест неуспешным. */
#include <stdio.h> /* Файловый ввод нужен только хостовому тесту, не переносимому ядру. */
#include <stdlib.h> /* Выравнивание контекста обеспечивает стандартный хостовый allocator. */
#include <string.h> /* Сравниваются точные 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<n;i++) { assert(sscanf(hex+2U*i,"%2x",&byte)==1); data[i]=(uint8_t)byte; } /* Тестовые HEX-октеты преобразуются в входной массив. */
assert(setp_deif_inspect(id,data,n,flags,meta)==expected); assert(meta[4]==raw && meta[2]==(int32_t)index && meta[3]==(int32_t)sub); /* Поля сверяются с независимым опубликованным примером. */
} /* Неудачный assert завершает процесс ненулевым кодом. */
fclose(input); state=calloc(1,setp_deif_state_size()); assert(state); /* Хостовый тест владеет буфером; ядро не выделяет память. */
assert(setp_deif_init(state,setp_deif_state_size(),13,1,1,1,2,1,0,0)); /* Пассивный SLAVE Node 13 слушает MASTER Node 1. */
memcpy(data,(uint8_t[]){1,1,0x30,7,0x84,3,0,0},8); assert(setp_deif_accept(state,0x201,data,8,0,10000)); /* Точный пользовательский кадр задаёт 90°. */
assert(setp_deif_angle(state,10000,5000,&value)==2 && value==90.0); assert(setp_deif_poll(state,10000,out,16)==0); /* Угол виден с пометкой задания, при выключенной симуляции TX отсутствует. */
data[3]=2; data[4]=0xE5; data[5]=1; assert(setp_deif_accept(state,0x201,data,8,0,10100)); /* Приходят фактические 48,5°, отличающиеся от задания 90°. */
assert(setp_deif_angle(state,10100,5000,&value)==1 && value==48.5); /* Фактическое положение имеет приоритет и не подменяется заданием. */
assert(setp_deif_init(state,setp_deif_state_size(),13,1,1,1,2,1,1,0)); assert(setp_deif_start(state,0,1)); /* Теперь тот же профиль работает активным CAN-сервером. */
assert(setp_deif_poll(state,0,out,16)==1 && out[0].can_id==0x70D && out[0].data[0]==0); /* Boot-up объявляет собственный Node-ID ровно один раз. */
assert(setp_deif_poll(state,500,out,16)==1 && out[0].data[0]==127); /* До self-start heartbeat сообщает Pre-operational. */
assert(setp_deif_poll(state,1000,out,16)==1 && out[0].data[0]==5); /* На границе 1000 мс узел переходит в Operational. */
assert(setp_deif_validate(out[0].can_id,out[0].data,out[0].dlc,0,13)); assert(!setp_deif_validate(out[0].can_id,out[0].data,out[0].dlc,0,12)); /* Исходящий кадр не может принадлежать соседнему SLAVE. */
setp_deif_stop(state); assert(setp_deif_poll(state,2000,out,16)==0); free(state); return 0; /* Выключение завершает протокольные события без утечки хостовой памяти. */
} /* Проверки выполняются без Qt, Android и физического оборудования. */
+103
View File
@@ -0,0 +1,103 @@
#include "set_invertor_gas.h" /* Проверяем два публичных представления одной карты инвертора. */
#include "pcan_id.h" /* Некорректные адреса узлов строятся тем же существующим кодеком ProtoCAN. */
#include <stdio.h> /* Ошибка теста выводит строку и возвращает ненулевой код также в Release. */
#include <string.h> /* Сравниваем исходные данные и неизменность выходного кадра при отказе. */
#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-кодеке без оборудования. */
} /* Конец сценариев согласованности и защиты границ новой карты инвертора. */
+3
View File
@@ -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.
])
+6
View File
@@ -0,0 +1,6 @@
#ifndef ST7789_DISABLED_H /* Защищает отключённый аппаратный интерфейс от повторного включения. */
#define ST7789_DISABLED_H /* Позволяет общему main обслуживать одинаковый API на плате без подключённого дисплея. */
#include <stdint.h> /* Время общего опроса передаётся как 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 общего интерфейса дисплея. */
+28
View File
@@ -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 # Недопустимый кадр не изменяет отображённые значения.
+52
View File
@@ -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.
+129
View File
@@ -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.
+36
View File
@@ -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 и тестов.
+75
View File
@@ -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-код.
+56
View File
@@ -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.
File diff suppressed because one or more lines are too long
+140
View File
@@ -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('<H', crc16_modbus(body))
class EmulatorProtocol:
def __init__(self, mode='auto', *, mcu="STM32F407"): # Необязательный MCU сохраняет старый API и задаёт ожидаемую идентичность до прикладных команд.
if mode not in ('auto', 'rtu'):
raise ValueError('Неизвестный режим протокола: ' + str(mode))
self.mode = mode
self.model = EMULATOR_PROFILES[mcu]["model"] # F407 и G474RB должны подтвердить собственную прошивку, а не только общий формат SET v2.
self.phase = None
self.parser = FrameParser()
self.sequence = secrets.randbelow(65535)
self.pending = b''
self.node = None
self.expected = None
def _frame(self, kind, payload=b'', destination=0):
self.sequence = (self.sequence + 1) & 0xFFFF
self.expected = (self.sequence, kind, destination)
return build_frame(Frame(kind, self.sequence, payload,
FrameFlag.ACK_REQUIRED, 0, destination))
def begin(self, raw):
self.pending = bytes(raw)
if self.mode == 'rtu':
return self.pending
if self.mode == 'v2':
return self._application()
self.parser.reset()
self.phase = 'ping'
return self._frame(MessageType.PING)
def fallback(self):
if self.phase != 'ping' or self.mode != 'auto':
raise ValueError('SET v2 обнаружен, но устройство не подтвердило совместимость')
self.mode, self.phase = 'rtu', None
self.parser.reset()
return self.pending
def cancel(self):
self.pending = b''
self.phase = None
self.expected = None
self.parser.reset()
def _application(self):
raw = self.pending
if not raw or crc16_modbus(raw):
raise ValueError('Неверный исходный запрос эмулятора')
self.phase = 'reply'
if len(raw) >= 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('<BHH', fn, address, value), unit)
if len(raw) == 18 and raw[1] == 65:
return self._frame(TMS_REQUEST, raw, self.node)
raise ValueError('Эта команда пока не поддерживается эмулятором SET v2')
def feed(self, chunk):
if self.mode == 'rtu':
return b'', bytes(chunk)
for frame in self.parser.feed(chunk):
if self.expected is None:
continue
seq, kind, destination = self.expected
if (frame.sequence != seq or frame.message_type != kind or
frame.destination != 0 or not frame.flags & FrameFlag.RESPONSE or
frame.flags & (FrameFlag.EVENT | FrameFlag.MORE) or
frame.source in (0, 65535) or
(destination and frame.source != destination)):
continue
status, body = decode_response(frame)
if bool(frame.flags & FrameFlag.ERROR) != bool(status):
raise ValueError('Несогласованные status/ERROR в ответе SET v2')
if self.phase == 'ping':
if status or len(body) != 4:
raise ValueError('Неверный PING SET v2')
self.node, self.phase = frame.source, 'info'
return self._frame(MessageType.DEVICE_INFO, destination=self.node), b''
if self.phase == 'info':
if status or DeviceInfo.decode(body).model != self.model: # Запись разрешается только после подтверждения аппаратного профиля выбранного MCU.
raise ValueError('SET v2: подключено другое устройство, команды эмулятора не отправлены')
self.mode = 'v2'
return self._application(), b''
if self.phase == 'reply':
response = self._reply(status, body)
self.phase, self.expected = None, None
return b'', response
return b'', b''
def _reply(self, status, body):
raw = self.pending
if raw[1] == 16:
if status:
raise ValueError(f'SET v2 отклонил блок генератора: status={status}')
# The shared C waveform codec validates CRC, address, count and echo.
return body
if raw[1] == 65:
if status == Status.UNSUPPORTED:
raise ValueError('Обновите прошивку STM: требуется CMD_STD через SET v2')
if status:
raise ValueError(f'SET v2 отклонил CMD_STD: status={status}')
if len(body) != 101 or body[:2] != raw[:2] or crc16_modbus(body[:-1]):
raise ValueError('Неверный ответ CMD_STD внутри SET v2')
return body
if status:
error = {Status.UNSUPPORTED: 1, Status.NOT_FOUND: 2,
Status.INVALID_ARGUMENT: 3, Status.INVALID_LENGTH: 3,
Status.BUSY: 6}.get(status, 4)
return with_crc(bytes([raw[0], raw[1] | 128, error]))
count = int.from_bytes(raw[4:6], 'big') if raw[1] == 3 else 2
if len(body) != 2 + 2 * count or struct.unpack_from('<H', body)[0] != count:
raise ValueError('SET v2: неверная длина ответа регистров')
words = struct.unpack_from(f'<{count}H', body, 2)
if raw[1] == 6:
if words != struct.unpack('>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))
File diff suppressed because one or more lines are too long
@@ -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 получает результат после полного освобождения ресурсов.
File diff suppressed because one or more lines are too long
@@ -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)
@@ -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 # Вызывающая навигация может отличить выполненный переход от отказа занятого адаптера.
+5 -4
View File
@@ -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)
+19
View File
@@ -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) # При полном молчании наблюдатель уважает прежний срок свежести пять секунд.