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:
@@ -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. */
|
||||
@@ -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) # Отдельный тест защищает восстановленный регистровый контракт.
|
||||
|
||||
@@ -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
@@ -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-модуле. */
|
||||
@@ -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 /* Конец контракта двух представлений одних данных инвертора. */
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
@@ -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. */
|
||||
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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 без платформенных зависимостей. */
|
||||
@@ -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 = ®ion; /* Единственный регион не пересекается с картами других CAN-узлов. */
|
||||
map.count = 1U; /* Карта содержит ровно один подключённый диапазон значений. */
|
||||
if (request->dlc == 0U) { /* Чтение использует стандартное правило ответа до четырёх последовательных регистров. */
|
||||
if (response == NULL || !pcan_gas_handle(&map, request, &out)) { return -1; } /* Пустой или недоступный диапазон не превращается в фиктивный ответ. */
|
||||
*response = out; return 1; /* Публикуем полностью сформированный существующим кодеком ответ. */
|
||||
} /* Непустой payload далее является только запросом записи. */
|
||||
if (request->dlc > PCAN_DATA_MAX || (request->dlc & 1U) != 0U) { return -1; } /* Отбрасываем нечётную длину и превышение восьми октетов до подготовки слов. */
|
||||
(void)pcan_gas_handle(&map, request, &out); /* Весь wire-пакет разбирает тот же кодек, который используется другими устройствами. */
|
||||
if (port->write == NULL || transaction.count != request->dlc / 2U) { return -1; } /* Частично попавший в карту блок не меняет даже свои первые корректные адреса. */
|
||||
return port->write(port->context, transaction.addresses, transaction.values, transaction.count) == 0 ? 0 : -1; /* Provider проверяет весь набор прав/значений и применяет его одним вызовом без скрытого ACK. */
|
||||
} /* Конец регистрового пути с единым состоянием приложения. */
|
||||
static int ReadSlot(void *context, uint16_t slot, uint16_t words[4]) /* Масочный ответ проецирует те же значения в Word1…Word3. */
|
||||
{ /* Word0 формируется только из реально прочитанных значений. */
|
||||
const set_invertor_gas_port_t *port = (const set_invertor_gas_port_t *)context; /* Указатель относится к тому же provider, что используется обычным GAS. */
|
||||
unsigned word; /* Индекс перебирает три полезных позиции, исключая маску. */
|
||||
if (slot >= SET_INVERTOR_GAS_SLOT_COUNT || port->read == NULL) { return -1; } /* Слот вне выделенного блока либо отсутствующий источник не порождает нулевую телеметрию. */
|
||||
words[0] = 0U; /* До успешного чтения ни одно поле не отмечено достоверным. */
|
||||
for (word = 1U; word <= SET_INVERTOR_GAS_WORDS_PER_SLOT; ++word) { /* Каждое полезное слово получает независимую проверку доступности. */
|
||||
uint16_t value = 0U; /* Неиспользуемое место кадра заполняется нулём без утверждения его достоверности. */
|
||||
const uint16_t address = (uint16_t)set_invertor_gas_register(slot, word); /* Проверенные координаты отображаются единственной общей формулой. */
|
||||
words[word] = 0U; /* Не допускаем утечки неинициализированного значения в ответ CAN. */
|
||||
if (port->read(port->context, address, &value) == 0) { /* Только положительный результат provider разрешает соответствующий бит. */
|
||||
words[0] |= (uint16_t)(1U << word); /* Биты 1…3 имеют смысл, заданный новым контрактом GAS SLOT 0x4. */
|
||||
words[word] = value; /* Сохраняем достоверное значение, в том числе корректный числовой ноль. */
|
||||
} /* Недоступный сосед не делает доступные значения ошибочными. */
|
||||
} /* Полностью неподключённый слот имеет пустую маску и не публикуется. */
|
||||
return words[0] != 0U ? 0 : -1; /* Общий кодек отправляет только ответ с непустой разрешённой маской. */
|
||||
} /* Чтение слота не создаёт второе хранилище значений. */
|
||||
static int WriteSlot(void *context, uint16_t slot, const uint16_t words[4]) /* Запись передаёт provider только выбранные маской позиции. */
|
||||
{ /* Предварительное чтение соседних readonly-полей не требуется. */
|
||||
const set_invertor_gas_port_t *port = (const set_invertor_gas_port_t *)context; /* Используем единственный источник состояния обоих типов GAS. */
|
||||
uint16_t addresses[PCAN_GAS_SLOT_DATA_WORDS], values[PCAN_GAS_SLOT_DATA_WORDS]; /* Один слот содержит максимум три изменяемых значения. */
|
||||
unsigned count = 0U, word; /* Подготавливаем компактный список только отмеченных слов. */
|
||||
if (slot >= SET_INVERTOR_GAS_SLOT_COUNT || port->write == NULL) { return -1; } /* Слот вне диапазона и отсутствие права записи не вызывают приложение. */
|
||||
for (word = 1U; word <= SET_INVERTOR_GAS_WORDS_PER_SLOT; ++word) { /* Маска уже полностью проверена общим кодеком 0x4. */
|
||||
if ((words[0] & (1U << word)) != 0U) { /* Неотмеченные поля сохраняются и не участвуют в проверке прав записи. */
|
||||
addresses[count] = (uint16_t)set_invertor_gas_register(slot, word); /* Переводим полезное слово в общий регистровый адрес. */
|
||||
values[count] = words[word]; /* Значение передаётся без изменения знака или масштаба поля. */
|
||||
count++; /* Число выбранных позиций ограничено тремя размерами маски. */
|
||||
} /* readonly-сосед не запрещает запись другой отмеченной позиции слота. */
|
||||
} /* До этого места ни одно значение приложения не изменялось. */
|
||||
return port->write(port->context, addresses, values, count) == 0 ? 0 : -1; /* Provider целиком проверяет выбранную транзакцию перед применением. */
|
||||
} /* Успех записи не создаёт не предусмотренный базовым протоколом ответ ACK. */
|
||||
int set_invertor_gas_handle(const set_invertor_gas_port_t *port, const pcan_frame_t *request, pcan_frame_t *response) /* Публичный диспетчер выбирает формат исключительно по MsgType. */
|
||||
{ /* Все проверки адреса узла выполняются раньше callbacks приложения. */
|
||||
pcan_id_t id; /* Поля извлекает существующая общая реализация ProtoCAN. */
|
||||
pcan_gas_slot_frame_t slot_request = {0}, slot_response = {0}; /* Масочный путь переиспользует готовый тип независимого Classic CAN-кодека. */
|
||||
pcan_gas_slot_port_t slot_port; /* Локальный адаптер соединяет три значения слота с единой картой. */
|
||||
pcan_frame_t out = {0}; /* Ошибка оставляет выходной кадр вызывающего без изменений. */
|
||||
unsigned i; int result; /* Размеры всех копируемых буферов заранее ограничены восемью октетами. */
|
||||
if (port == NULL || request == NULL || request->dlc > PCAN_DATA_MAX || (request->id & ~PCAN_ID_MASK) != 0U || (request->flags & PCAN_FLAG_IDE) == 0U || (request->flags & (PCAN_FLAG_RTR | PCAN_FLAG_ERR)) != 0U) { return -1; } /* Отвергаем нецелевые типы физических кадров и неверный размер до разбора данных. */
|
||||
pcan_id_unpack(request->id, &id); /* Не дублируем битовые сдвиги адресации в адаптере изделия. */
|
||||
if (id.device_type != SET_INVERTOR_DEVICE_TYPE || id.device_id != SET_INVERTOR_DEVICE_ID || id.route != PCAN_ROUTE_FROM_PM) { return -1; } /* Ответы других приборов и запросы к соседям не могут менять состояние инвертора. */
|
||||
if (id.msg_type == PCAN_MSG_GAS) { return HandleRegisters(port, request, response); } /* Тип 0x3 интерпретирует MsgBody как отдельный регистр. */
|
||||
if (id.msg_type != PCAN_MSG_GAS_SLOT || id.msg_body >= SET_INVERTOR_GAS_SLOT_COUNT || (request->dlc == 0U && response == NULL)) { return -1; } /* Тип 0x4 использует только выделенные 250 слотов; других форматов или fallback нет. */
|
||||
slot_port.context = (void *)port; /* Callback не изменяет структуру порта и не сохраняет указатель после возврата. */
|
||||
slot_port.read = ReadSlot; /* Общий слотный кодек получает маску достоверности от нашего отображения данных. */
|
||||
slot_port.write = WriteSlot; /* Общая проверка wire-маски предшествует вызову прикладной записи. */
|
||||
slot_request.id = request->id; /* Адрес узла и MsgType сохраняются без повторной упаковки. */
|
||||
slot_request.flags = request->flags; /* Служебные признаки уже проверены публичной границей. */
|
||||
slot_request.dlc = request->dlc; /* Длина остаётся количеством октетов физического кадра. */
|
||||
for (i = 0U; i < request->dlc; ++i) { slot_request.data[i] = request->data[i]; } /* Копируем готовые октеты, не разбирая их вторым endian-кодеком. */
|
||||
result = pcan_gas_slot_handle_address(SET_INVERTOR_DEVICE_TYPE, SET_INVERTOR_DEVICE_ID, &slot_port, &slot_request, &slot_response); /* Существующий C-кодек проверяет маску и строит ответ 0x4. */
|
||||
if (result == 1) { /* Только завершённое чтение публикует CAN-ответ. */
|
||||
out.id = slot_response.id; out.flags = (uint8_t)slot_response.flags; out.dlc = (uint8_t)slot_response.dlc; /* Переносим проверенный транспортный результат без изменения его смысла. */
|
||||
for (i = 0U; i < out.dlc; ++i) { out.data[i] = (uint8_t)slot_response.data[i]; } /* Восемь готовых октетов копируются в обычный кадр проекта STM32/host. */
|
||||
*response = out; /* Выход изменяется только после успешного выполнения всех общих проверок. */
|
||||
} /* Запись и ошибка сохраняют предыдущий выход вызывающего кода. */
|
||||
return result; /* 1 означает ответ, 0 — запись, -1 — отказ без изменения соседнего протокольного профиля. */
|
||||
} /* Конец общего адаптера двух GAS-интерфейсов одного инвертора. */
|
||||
unsigned set_invertor_gas_limit(unsigned message_type) /* Единый C-источник границ не допускает расхождения Python, прошивки и другого GUI. */
|
||||
{ /* Значение ноль обозначает неизвестный тип, а не пустой действующий блок. */
|
||||
if (message_type == PCAN_MSG_GAS) { return SET_INVERTOR_GAS_REGISTER_COUNT; } /* Обычный GAS адресует каждое из 750 значений отдельно. */
|
||||
return message_type == PCAN_MSG_GAS_SLOT ? SET_INVERTOR_GAS_SLOT_COUNT : 0U; /* Масочный GAS имеет 250 слотов, остальные типы не поддерживаются этим профилем. */
|
||||
} /* Завершаем получение размера карты без аппаратных зависимостей. */
|
||||
int set_invertor_gas_build(unsigned message_type, unsigned address, const uint16_t *words, unsigned count, uint32_t *identifier, uint8_t data[8]) /* Все endian-преобразования остаются внутри общего C-кодека. */
|
||||
{ /* При отказе не меняем ни идентификатор, ни выходные октеты вызывающего кода. */
|
||||
pcan_frame_t frame = {0}; uint_least8_t slot_data[8] = {0}; unsigned i; int length; /* Временный результат ограничен одним восьмиоктетным кадром. */
|
||||
const uint32_t id = set_invertor_gas_request_id(message_type, address, PCAN_ROUTE_FROM_PM); /* Проверяем тип, узел и соответствующую единицу адресации централизованно. */
|
||||
if (id == 0U || identifier == NULL || data == NULL || count > 4U || (count != 0U && words == NULL)) { return -1; } /* Проверяем буферы и число слов до обращения к существующим builders. */
|
||||
if (message_type == PCAN_MSG_GAS) { /* У типа 0x3 допустима запись от одного до четырёх последовательных значений. */
|
||||
if (address + count > SET_INVERTOR_GAS_REGISTER_COUNT) { return -1; } /* Даже начало последнего регистра не позволяет записать несуществующего соседа. */
|
||||
pcan_gas_regs_to_frame(&frame, id, (uint16_t)address, words, (uint16_t)count); length = frame.dlc; /* Полностью переиспользуем существующую сериализацию обычного GAS. */
|
||||
} else { /* Оставшийся проверенный тип равен GAS SLOT 0x4. */
|
||||
length = pcan_gas_slot_encode(words, count, slot_data); if (length < 0) { return -1; } /* Общий кодек требует четыре wire-слова с корректной маской либо пустой запрос. */
|
||||
for (i = 0U; i < (unsigned)length; ++i) { frame.data[i] = (uint8_t)slot_data[i]; } /* Копируем готовые октеты без повторного разбора расположения слов. */
|
||||
} /* После успешного кодирования можно публиковать результат вызывающему. */
|
||||
*identifier = id; for (i = 0U; i < 8U; ++i) { data[i] = frame.data[i]; } /* Выход всегда полностью инициализирован, включая неиспользуемый хвост. */
|
||||
return length; /* Ноль обозначает корректное чтение с DLC=0, отрицательное значение — отказ. */
|
||||
} /* Конец builder для тонких ctypes/JNI-портов. */
|
||||
int set_invertor_gas_decode(uint32_t identifier, unsigned flags, const uint8_t *data, unsigned length, unsigned metadata[2], uint16_t words[4]) /* Клиент получает уже проверенные значения без собственной битовой и endian-логики. */
|
||||
{ /* Временные выходы защищают данные вызывающего при некорректном ответе. */
|
||||
pcan_frame_t frame = {0}; pcan_id_t id; uint16_t decoded[4] = {0}; uint_least8_t slot_data[8]; unsigned i; int count; /* Все структуры локальны и имеют фиксированную вместимость. */
|
||||
if (metadata == NULL || words == NULL || data == NULL || length == 0U || length > 8U || flags > UINT8_MAX || (identifier & ~PCAN_ID_MASK) != 0U || (flags & PCAN_FLAG_IDE) == 0U || (flags & (PCAN_FLAG_RTR | PCAN_FLAG_ERR)) != 0U) { return -1; } /* Ответ должен быть непустым допустимым Extended data frame. */
|
||||
pcan_id_unpack(identifier, &id); /* Существующий кодек раскрывает поля идентификатора. */
|
||||
if (id.device_type != SET_INVERTOR_DEVICE_TYPE || id.device_id != SET_INVERTOR_DEVICE_ID || id.route != PCAN_ROUTE_FROM_DEVICE) { return -1; } /* Эхо запроса или ответ другого узла не обновляет телеметрию инвертора. */
|
||||
if (id.msg_type == PCAN_MSG_GAS) { /* Обычный GAS допускает один, два, три или четыре значения. */
|
||||
frame.id = identifier; frame.flags = (uint8_t)flags; frame.dlc = (uint8_t)length; /* Устанавливаем проверенные транспортные метаданные существующего frame-кодека. */
|
||||
for (i = 0U; i < length; ++i) { frame.data[i] = data[i]; } /* Исходные октеты не разбираются повторно на языковой стороне. */
|
||||
count = (int)pcan_gas_frame_to_regs(&frame, decoded, 4U); /* Длину и little-endian проверяет существующая реализация GAS. */
|
||||
if (count == 0 || (unsigned)id.msg_body + (unsigned)count > SET_INVERTOR_GAS_REGISTER_COUNT) { return -1; } /* Не публикуем нечётный payload либо блок, пересекающий конец 750 регистров. */
|
||||
} else if (id.msg_type == PCAN_MSG_GAS_SLOT && id.msg_body < SET_INVERTOR_GAS_SLOT_COUNT) { /* Масочный ответ проверяется по собственной карте 250 слотов. */
|
||||
for (i = 0U; i < length; ++i) { slot_data[i] = data[i]; } /* Передаём готовые октеты общему слотному декодеру. */
|
||||
count = pcan_gas_slot_decode_words(identifier, flags, slot_data, length, decoded); if (count != 4) { return -1; } /* Проверяем DLC=8 и допустимую непустую маску Word0. */
|
||||
} else { return -1; } /* Другой MsgType или несуществующий слот не вызывают fallback к обычным регистрам. */
|
||||
metadata[0] = id.msg_type; metadata[1] = id.msg_body; /* Публикуем тип и адрес только после проверки всего ответа. */
|
||||
for (i = 0U; i < (unsigned)count; ++i) { words[i] = decoded[i]; } /* Передаём только реально декодированные wire-слова. */
|
||||
return count; /* GUI работает со словами и типом, а не повторяет wire-протокол. */
|
||||
} /* Завершаем единый клиентский декодер нового профиля. */
|
||||
+15
@@ -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
|
||||
|
@@ -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 и физического оборудования. */
|
||||
@@ -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-кодеке без оборудования. */
|
||||
} /* Конец сценариев согласованности и защиты границ новой карты инвертора. */
|
||||
@@ -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.
|
||||
])
|
||||
|
||||
@@ -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 общего интерфейса дисплея. */
|
||||
@@ -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 # Недопустимый кадр не изменяет отображённые значения.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 и тестов.
|
||||
@@ -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-код.
|
||||
@@ -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.
|
||||
+88
-150
File diff suppressed because one or more lines are too long
@@ -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 # Вызывающая навигация может отличить выполненный переход от отказа занятого адаптера.
|
||||
@@ -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)
|
||||
|
||||
@@ -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) # При полном молчании наблюдатель уважает прежний срок свежести пять секунд.
|
||||
Reference in New Issue
Block a user