diff --git a/README.md b/README.md index d7e75a6..9f548cc 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ templates/ | [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 | | [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** | | [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android и STM32F4-порты в комплекте** | +| [`c/set-protocol/ports/stm32-bxcan`](c/set-protocol/ports/stm32-bxcan) | порт прикладного ProtoCAN для STM32, бывший SETCAN; сохранён API `PROTOCAN_*` | STM32 HAL CAN/RTC/TIM + общее ядро `pcan_id` | classic bxCAN; настройки платы предоставляет прошивка | | [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot | | [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** | | [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** | @@ -98,7 +99,6 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git | Код | Почему не переносится | Что можно вытащить | |---|---|---| -| `SETCAN/Src/protocan.c` | 59 вызовов `HAL_*`, привязка к `CAN_HandleTypeDef`, `RTC_HandleTypeDef`, `NVIC_SystemReset()` | разбиение длинных посылок на кадры и проверка даты — чистая логика, нужны два интерфейса: «отправить кадр» и «часы». Раскладка идентификатора уже вынесена в `pcan_id` | | `1921vk028/drivers/user/src/one_wire_user.c` | аппаратный 1-Wire `OWI_*` из `plib028`, мигание светодиодом прямо внутри опроса датчика | логика DS18B20 уже есть в `c/ds18b20` — переносится порт, а не алгоритм | | `1921vk028/drivers/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу | | `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK | diff --git a/c/can-sensor/CMakeLists.txt b/c/can-sensor/CMakeLists.txt new file mode 100644 index 0000000..9171761 --- /dev/null +++ b/c/can-sensor/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.13) +project(can_sensor C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +add_library(can_sensor STATIC can_sensor.c) +target_include_directories(can_sensor PUBLIC .) + +if(MSVC) + target_compile_options(can_sensor PRIVATE /W4) +else() + target_compile_options(can_sensor PRIVATE -Wall -Wextra -Wpedantic) +endif() + +option(CAN_SENSOR_BUILD_TESTS "Собирать тесты транспорта CAN sensor" ON) +if(CAN_SENSOR_BUILD_TESTS) + enable_testing() + add_executable(test_can_sensor tests/test_can_sensor.c) + target_link_libraries(test_can_sensor PRIVATE can_sensor) + add_test(NAME can_sensor COMMAND test_can_sensor) +endif() diff --git a/c/can-sensor/README.md b/c/can-sensor/README.md index 4fbbe07..59c109e 100644 --- a/c/can-sensor/README.md +++ b/c/can-sensor/README.md @@ -19,7 +19,7 @@ | Файл | Что делает | Зависимости | |---|---|---| -| `can_sensor.h`, `can_sensor.c` | сборка и разбор SETTINGS, повторы передачи, счётчики обмена | `stdint.h` | +| `can_sensor.h`, `can_sensor.c` | сборка и разбор SETTINGS, планировщик PULSE, повторы передачи, счётчики обмена | `stdint.h` | | `ports/stm32f1/` | опросный порт CAN1: GPIO, BTR, фильтр, mailbox/FIFO, тайм-аут ACK | CMSIS `stm32f10x.h` | `ZZ` — номер сборки, `YY` — позиция. Нулевой ROM с DLC=8 очищает локацию. @@ -40,14 +40,20 @@ CanSensor link; CanSensor_Io io = { .send = bxcan_send, .receive = bxcan_receive, .context = &board }; CanSensor_Config config; CanSensor_ConfigDefault(&config); /* TX 0x1FFE0000, RX 0x17FE0000 */ +config.pulse_period_ms = 1000U; /* 0 — PULSE выключен */ CanSensor_Init(&link, &io, &config); CanSensor_SendId(&link, position, assembly_serial, rom); +CanSensor_Task(&link, now_ms); CanSensor_Message message; if (CanSensor_Poll(&link, &message)) { /* принят идентификатор */ } ``` +Период можно менять во время работы через `CanSensor_SetPulsePeriod()`. Значение +`0` именно выключает heartbeat: кадры PULSE больше не ставятся в очередь. При +повторном включении первый кадр отправляется через полный заданный период. + ## Порт STM32F1 Порт не использует STM32 HAL и не занимает прерывания. Скопируйте diff --git a/c/can-sensor/can_sensor.c b/c/can-sensor/can_sensor.c index e1b6fd4..681b811 100644 --- a/c/can-sensor/can_sensor.c +++ b/c/can-sensor/can_sensor.c @@ -40,6 +40,7 @@ void CanSensor_ConfigDefault(CanSensor_Config *config) } config->tx_id = CAN_SENSOR_DEFAULT_TX_ID; config->rx_id = CAN_SENSOR_DEFAULT_RX_ID; + config->pulse_period_ms = 0U; config->extended = 1U; config->retries = CAN_SENSOR_DEFAULT_RETRIES; } @@ -72,6 +73,9 @@ uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io, link->send_errors = 0U; link->received_messages = 0U; link->dropped_frames = 0U; + link->pulse_last_ms = 0U; + link->pulse_counter = 0U; + link->pulse_armed = 0U; return 1U; } @@ -201,3 +205,51 @@ uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out) } return 0U; } + +void CanSensor_SetPulsePeriod(CanSensor *link, uint32_t period_ms) +{ + if (link == 0) { + return; + } + if (link->config.pulse_period_ms != period_ms) { + link->config.pulse_period_ms = period_ms; + /* После включения или смены периода отсчитываем полный новый период. */ + link->pulse_armed = 0U; + } +} + +uint32_t CanSensor_PulsePeriod(const CanSensor *link) +{ + return (link != 0) ? link->config.pulse_period_ms : 0U; +} + +void CanSensor_Task(CanSensor *link, uint32_t now_ms) +{ + CanSensor_Frame frame; + const uint32_t period_ms = (link != 0) ? link->config.pulse_period_ms : 0U; + + if ((link == 0) || (link->io.send == 0) || (period_ms == 0U)) { + return; + } + if (link->pulse_armed == 0U) { + link->pulse_last_ms = now_ms; + link->pulse_armed = 1U; + return; + } + if ((now_ms - link->pulse_last_ms) < period_ms) { + return; + } + + frame.id = (link->config.tx_id & CAN_SENSOR_ADDRESS_MASK) + | ((uint32_t)CAN_SENSOR_MSGTYPE_PULSE << 16U); + frame.extended = 1U; + frame.length = 1U; + link->pulse_counter++; + frame.data[0] = link->pulse_counter; + if (can_sensor_send_frame(link, &frame) != 0U) { + link->pulse_last_ms = now_ms; + } else { + /* Счётчик меняется только для реально поставленных в очередь кадров. */ + link->pulse_counter--; + } +} diff --git a/c/can-sensor/can_sensor.h b/c/can-sensor/can_sensor.h index d6a5ebf..06f4f46 100644 --- a/c/can-sensor/can_sensor.h +++ b/c/can-sensor/can_sensor.h @@ -27,9 +27,11 @@ /** Назначенный проекту тип сообщения SETCAN SETTINGS. */ #define CAN_SENSOR_MSGTYPE_SETTINGS 0xEU +#define CAN_SENSOR_MSGTYPE_PULSE 0xFU /** Маска полей SETCAN от Priority до MsgType; Body под маской не находится. */ #define CAN_SENSOR_HEADER_MASK 0x1FFF0000UL +#define CAN_SENSOR_ADDRESS_MASK 0x1FF00000UL #define CAN_SENSOR_BODY_MASK 0x0000FFFFUL /** Базовые ID SETTINGS для выбранных DeviceType=0x7 и DeviceID=0xF. */ @@ -88,6 +90,7 @@ typedef struct { uint32_t rx_id; /**< База запроса 0x17FE0000, Body должен быть нулевым. */ uint8_t extended; /**< 1 — Extended CAN ID. */ uint8_t retries; /**< Число попыток передачи. */ + uint32_t pulse_period_ms; /**< Период PULSE; 0 полностью отключает PULSE. */ } CanSensor_Config; /** Состояние транспорта и диагностические счётчики. */ @@ -98,6 +101,9 @@ typedef struct { uint32_t send_errors; uint32_t received_messages; uint32_t dropped_frames; + uint32_t pulse_last_ms; + uint8_t pulse_counter; + uint8_t pulse_armed; } CanSensor; void CanSensor_ConfigDefault(CanSensor_Config *config); @@ -124,4 +130,17 @@ uint8_t CanSensor_HandleFrame(CanSensor *link, const CanSensor_Frame *frame, uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out); +/** + * @brief Меняет период PULSE во время работы. + * + * @param period_ms Период в миллисекундах; 0 выключает PULSE. + */ +void CanSensor_SetPulsePeriod(CanSensor *link, uint32_t period_ms); + +/** Возвращает действующий период PULSE; 0 означает «выключено». */ +uint32_t CanSensor_PulsePeriod(const CanSensor *link); + +/** Вызывает планировщик PULSE из главного цикла. */ +void CanSensor_Task(CanSensor *link, uint32_t now_ms); + #endif /* CAN_SENSOR_H */ diff --git a/c/can-sensor/tests/test_can_sensor.c b/c/can-sensor/tests/test_can_sensor.c new file mode 100644 index 0000000..5d0a010 --- /dev/null +++ b/c/can-sensor/tests/test_can_sensor.c @@ -0,0 +1,79 @@ +#include "can_sensor.h" + +#include + +typedef struct { + CanSensor_Frame last_frame; + uint32_t send_count; + uint8_t accept; +} FakeCan; + +static uint8_t fake_send(void *context, const CanSensor_Frame *frame) +{ + FakeCan *fake = (FakeCan *)context; + + fake->send_count++; + fake->last_frame = *frame; + return fake->accept; +} + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "check failed at line %d: %s\n", __LINE__, \ + #condition); \ + return 1; \ + } \ + } while (0) + +int main(void) +{ + CanSensor link; + CanSensor_Config config; + CanSensor_Io io; + FakeCan fake = {0}; + + fake.accept = 1U; + io.send = fake_send; + io.receive = 0; + io.context = &fake; + CanSensor_ConfigDefault(&config); + + CHECK(config.pulse_period_ms == 0U); + CHECK(CanSensor_Init(&link, &io, &config) != 0U); + + /* Нулевой период означает выключенный PULSE при любых метках времени. */ + CanSensor_Task(&link, 0U); + CanSensor_Task(&link, 0xFFFFFFFFUL); + CHECK(fake.send_count == 0U); + + CanSensor_SetPulsePeriod(&link, 1000U); + CHECK(CanSensor_PulsePeriod(&link) == 1000U); + CanSensor_Task(&link, 500U); + CanSensor_Task(&link, 1499U); + CHECK(fake.send_count == 0U); + CanSensor_Task(&link, 1500U); + CHECK(fake.send_count == 1U); + CHECK(fake.last_frame.id == 0x1FFF0000UL); + CHECK(fake.last_frame.extended == 1U); + CHECK(fake.last_frame.length == 1U); + CHECK(fake.last_frame.data[0] == 1U); + + /* После period=0 дальнейшие вызовы не отправляют heartbeat. */ + CanSensor_SetPulsePeriod(&link, 0U); + CHECK(CanSensor_PulsePeriod(&link) == 0U); + CanSensor_Task(&link, 0xFFFFFFFFUL); + CHECK(fake.send_count == 1U); + + /* Повторное включение начинает новый полный период. */ + CanSensor_SetPulsePeriod(&link, 10U); + CanSensor_Task(&link, 2000U); + CanSensor_Task(&link, 2009U); + CHECK(fake.send_count == 1U); + CanSensor_Task(&link, 2010U); + CHECK(fake.send_count == 2U); + CHECK(fake.last_frame.data[0] == 2U); + + puts("can_sensor pulse tests passed"); + return 0; +} diff --git a/c/set-protocol/PORTING.md b/c/set-protocol/PORTING.md index 14d6d4c..6373c96 100644 --- a/c/set-protocol/PORTING.md +++ b/c/set-protocol/PORTING.md @@ -36,6 +36,13 @@ multicast запрещены WRITE, FW_BEGIN и FW_ACTIVATE. ## CAN +Для существующего прикладного ProtoCAN доступен +[порт STM32 bxCAN](ports/stm32-bxcan/README.md), перенесённый из SETCAN. +Он сохраняет API `PROTOCAN_*`, использует HAL CAN/RTC/TIM и общий `pcan_id`. +Порт рассчитан на classic bxCAN; он не реализует сегментацию SETProtocol v2. +В проект устройства добавляются `ports/stm32-bxcan/protocan.c` и `src/pcan_id.c`, +а в include paths — `ports/stm32-bxcan` и `include`. + Не вызывайте прикладной router из ISR. ISR принимает CAN-кадры в очередь, сегментация собирается в главном цикле, и только полный SETP-кадр передаётся parser/router. Сборка должна иметь timeout, контроль номера сегмента и одного diff --git a/c/set-protocol/README.md b/c/set-protocol/README.md index ace75b2..6bd2b4d 100644 --- a/c/set-protocol/README.md +++ b/c/set-protocol/README.md @@ -32,6 +32,7 @@ TCP, UART/DMA и аппаратный CAN подключаются портам | `src/` | общая реализация всех протокольных модулей | | `ports/android` | JNI и Kotlin facade | | `ports/stm32f4` | UART/DMA port для legacy byte stream | +| [`ports/stm32-bxcan`](ports/stm32-bxcan/README.md) | STM32 HAL bxCAN/RTC/TIM, прикладной ProtoCAN; бывший отдельный SETCAN | | `docs/SETPROTOCOL.md` | архитектура, ABI, память и переносимость | | `docs/legacy` | нормативные документы ProtoCAN/SETGUI v1 | | `PROTOCOL.md` | нормативный wire contract SET protocol v2 | diff --git a/c/set-protocol/docs/legacy/GAS.md b/c/set-protocol/docs/legacy/GAS.md index b0f11bd..51207fc 100644 --- a/c/set-protocol/docs/legacy/GAS.md +++ b/c/set-protocol/docs/legacy/GAS.md @@ -48,7 +48,7 @@ uint16_t n = pcan_gas_read_block(&map, 0x0000, block, 4); Тип сообщения `PCAN_MSG_GAS` (`0b0011`). Адрес первого регистра лежит в `MsgBody`, данные — до 4 регистров подряд, младшим байтом вперёд. Это ровно то, что делает `PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()` -в `SETCAN/Src/protocan.c`, поэтому обмен совместим с существующими +в `ports/stm32-bxcan/protocan.c` (бывший SETCAN), поэтому обмен совместим с существующими устройствами. | Кадр | DLC | Смысл | diff --git a/c/set-protocol/include/pcan_gas.h b/c/set-protocol/include/pcan_gas.h index aa59398..4f97dd8 100644 --- a/c/set-protocol/include/pcan_gas.h +++ b/c/set-protocol/include/pcan_gas.h @@ -10,7 +10,7 @@ * Транспорт ProtoCAN (MsgType = 0b0011) кладёт в MsgBody адрес первого * регистра, а в данные - до 4 регистров подряд, младшим байтом вперёд. * Это ровно то, что делает PROTOCAN_SEND_GENERAL_ADDRESS_SPACE() - * в SETCAN/Src/protocan.c, поэтому обмен совместим с существующими + * в ports/stm32-bxcan/protocan.c, поэтому обмен совместим с существующими * устройствами. * * Запрос на чтение кодируется кадром GAS с DLC = 0: в исходном коде diff --git a/c/set-protocol/include/pcan_id.h b/c/set-protocol/include/pcan_id.h index c52749f..29f5b10 100644 --- a/c/set-protocol/include/pcan_id.h +++ b/c/set-protocol/include/pcan_id.h @@ -2,7 +2,7 @@ * @file pcan_id.h * @brief Упаковка и разбор 29-битного идентификатора ProtoCAN. * - * Раскладка (соответствует ProtoCanId_t из SETCAN/Inc/protocan.h): + * Раскладка (ProtoCanId_t из ports/stm32-bxcan/protocan.h, бывший SETCAN): * * биты 28 Priority * 27 Route diff --git a/c/set-protocol/ports/stm32-bxcan/README.md b/c/set-protocol/ports/stm32-bxcan/README.md new file mode 100644 index 0000000..5f1dd42 --- /dev/null +++ b/c/set-protocol/ports/stm32-bxcan/README.md @@ -0,0 +1,602 @@ +# ProtoCAN: порт STM32 bxCAN (STM32 HAL) + +`stm32-bxcan` — порт прикладного ProtoCAN для STM32 HAL, перенесённый из +SETCAN. Модуль принимает +кадры, управляет HAL CAN/RTC/TIM и вызывает обработчики приложения. Формат +29-битного идентификатора и его переносимая реализация принадлежат общему +ядру [`c/set-protocol`](../../README.md) в этом же репозитории `templates`. + +Порт использует classic bxCAN (`HAL_CAN_*`), RTC и TIM. Для FDCAN на +STM32G4/H7 требуется другой аппаратный порт. Здесь нет `.ioc`, startup-файлов, +linker script или настроек тактирования: их предоставляет проект STM32CubeIDE. + +Схема подключения: приложение → `stm32-bxcan` → STM32 HAL → bxCAN/RTC/TIM. +Для упаковки и разбора CAN ID порт вызывает общее ядро `pcan_id`. +Старый API `PROTOCAN_*` и имена `protocan.c/.h` сохранены для существующих +прошивок. Отдельный репозиторий и вложенный сабмодуль SETCAN больше не нужны. + +Исходная версия: SETCAN `c8eec559785ab809469483dc58d3976ec9db340a`. +История SETCAN включена в историю `templates` отдельным родителем коммита +переноса; прежние пути можно посмотреть через +`git show c8eec55:Inc/protocan.h` и `git log c8eec55`. + +## Состав + +```text +c/set-protocol/ +├── include/pcan_id.h +├── src/pcan_id.c +└── ports/stm32-bxcan/ + ├── protocan.h # типы протокола, настройки устройства и публичный API + ├── protocan.c # приём, буферизация, разбор и отправка CAN-кадров + └── README.md +``` + +Модуль зависит от файлов `main.h` и `can.h`, сгенерированных STM32CubeMX, а также +от HAL-драйверов CAN, RTC и TIM. В сборку добавляются +`c/set-protocol/ports/stm32-bxcan/protocan.c` и `c/set-protocol/src/pcan_id.c`. +В include paths добавляются `c/set-protocol/ports/stm32-bxcan`, +`c/set-protocol/include` и каталог CubeMX с `main.h`/`can.h`. +Если прошивка уже собирает общее ядро SETProtocol, второй раз добавлять +`pcan_id.c` не нужно. HAL-порт не входит в host-сборку CMake общего ядра. + +| Файл | Зависимости | +|---|---| +| `protocan.h` | `main.h`, `can.h`, общий `pcan_id.h` | +| `protocan.c` | `protocan.h`, STM32 HAL CAN/RTC/TIM, CMSIS | +| `../../src/pcan_id.c` | переносимое ядро C99, `../../include` | + +## Документация протокола + +Каноническое описание теперь находится рядом с переносимой реализацией: + +- [Структура ProtoCAN](../../docs/legacy/PROTOCOL.md); +- [Прошивка по CAN](../../../protocan-boot/docs/BOOTLOADER.md); +- [Правила ведения ОАП](../../docs/legacy/OAP.md); +- [Машинные эталоны](../../tests/vectors/test-vectors.json); +- [Историческая HTML-документация SETCAN](../../../../doc/setcan/index.html). + +Файлы в `doc/setcan/protocan` сохранены как исторический снимок SETCAN и больше не +являются источником истины. + +## Формат расширенного CAN ID + +Протокол размещает служебные поля в 29-битном Extended ID: + +| Биты | Поле | Размер | Назначение | +|---:|---|---:|---| +| 28 | `Priority` | 1 | `0` — критический, `1` — стандартный приоритет | +| 27 | `Route` | 1 | `0` — от управляющего модуля, `1` — от устройства | +| 26..24 | `DeviceType` | 3 | тип устройства, `0..7` | +| 23..20 | `DeviceID` | 4 | номер устройства, `0..15` | +| 19..16 | `MsgType` | 4 | тип сообщения | +| 15..0 | `MsgBody` | 16 | тип команды, адрес, ID датчика или код ошибки | + +Типы сообщений: + +| Значение | Тип | +|---:|---| +| `0x0` | широковещательное (`BROADCAST`) | +| `0x1` | дискретное (`DISCRETE`) | +| `0x2` | аналоговое (`ANALOG`) | +| `0x3` | общее адресное пространство | +| `0x4` | Modbus Coil | +| `0x5` | Modbus Discrete | +| `0x6` | Modbus Holding | +| `0x7` | Modbus Input | +| `0x8` | ошибка | +| `0x9` | управление загрузчиком (`BOOT_CONTROL`, зарезервировано) | +| `0xA` | данные прошивки, слот A (`BOOT_DATA_A`, зарезервировано) | +| `0xB` | данные прошивки, слот B (`BOOT_DATA_B`, зарезервировано) | +| `0xC` | состояние загрузчика (`BOOT_STATUS`, зарезервировано) | +| `0xD` | обнаружение загрузчиков (`BOOT_DISCOVERY`, зарезервировано) | +| `0xE` | настройка привязки датчика (`SETTINGS`) | +| `0xF` | пульс присутствия устройства | + +Разметка `MsgBody` зависит от типа сообщения: + +- broadcast: младшие 4 бита — дополнительное поле, старшие 12 бит — команда; +- discrete: младшие 12 бит — данные/адрес, старшие 4 бита — подтип; +- analog: младшие 12 бит — ID датчика, старшие 4 бита — тип величины; +- Modbus: младшие 4 бита — количество регистров, старшие 12 бит — начальный адрес; +- SETTINGS: старший байт — номер сборки `Z`, младший байт — позиция `Y`; +- error: младший байт — код ошибки, старший байт — дополнительная информация. + +Для регистров `uint16_t` в CAN payload используется порядок байтов little-endian: сначала младший байт, затем старший. + +Пара `DeviceType/DeviceID` образует уникальный адрес прибора. Три бита +`DeviceType` задают 8 типов, четыре бита `DeviceID` — 16 экземпляров каждого +типа; всего на одной шине можно адресовать до 128 приборов. Загрузочные кадры +так же адресуются этой парой и не требуют изменения 29-битного CAN ID. + +> Разметка ID реализована C-битовыми полями. Она соответствует используемому STM32 GCC ABI, но не является переносимой между произвольными компиляторами без проверки фактического расположения битов. + +## Подключение к STM32-проекту + +### 1. Добавить файлы + +Скопировать или подключить к сборке: + +- `c/set-protocol/ports/stm32-bxcan/protocan.h`; +- `c/set-protocol/ports/stm32-bxcan/protocan.c`; +- `c/set-protocol/src/pcan_id.c` (если ещё не собирается в составе ядра). + +Каталоги `c/set-protocol/ports/stm32-bxcan` и `c/set-protocol/include` +добавить в include paths компилятора. + +### 2. Настроить устройство + +В `protocan.h` для прибора привязки DS18B20 заданы выбранные значения: + +```c +#define CURRENT_TYPE_DEVICE 0b111 /* DeviceType = 0x7 */ +#define CURRENT_ID_DEVICE 0b1111 /* DeviceID = 0xF */ +``` + +При необходимости изменить размер программного RX-буфера: + +```c +#define PROTOCAN_RX_BUFFER_SIZE 128 +``` + +Полезная ёмкость кольцевого буфера на один элемент меньше заданного размера, то есть при значении `128` в нём помещается 127 ожидающих кадров. + +### 3. Настроить CubeMX + +В проекте должны быть инициализированы: + +- CAN с поддержкой Extended ID; +- RTC; +- базовый таймер, задающий период отправки пульса. + +Модуль использует CAN FIFO0 и фильтры с номерами `0`, `1`, `2`. Адресованный фильтр принимает Route=0 (запрос от ПМ) для текущих `DeviceType/DeviceID`; отдельные фильтры принимают broadcast и pulse. В `PROTOCAN_CONFIG_FILTER()` также жёстко задано `SlaveStartFilterBank = 14`. Если приложение уже использует фильтры, номера и границу банков нужно согласовать. + +### 4. Инициализировать и запустить периферию + +Пример для `main.c`: + +```c +#include "protocan.h" + +int main(void) +{ + HAL_Init(); + SystemClock_Config(); + + MX_GPIO_Init(); + MX_CAN_Init(); + MX_RTC_Init(); + MX_TIM2_Init(); + + PROTOCAN_INIT_StatusTypeDef init_status = + PROTOCAN_INIT(&hcan, &hrtc, &htim2); + + if (init_status != PROTOCAN_INIT_OK) { + Error_Handler(); + } + + /* PROTOCAN_INIT() этого не делает. */ + if (HAL_CAN_Start(&hcan) != HAL_OK) { + Error_Handler(); + } + if (HAL_CAN_ActivateNotification( + &hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK) { + Error_Handler(); + } + if (HAL_TIM_Base_Start_IT(&htim2) != HAL_OK) { + Error_Handler(); + } + + while (1) { + (void)PROTOCAN_ProcessAllRxMsgs(); + } +} +``` + +`PROTOCAN_INIT()` требует ненулевые указатели на CAN, RTC и TIM, настраивает CAN-фильтры и включает отправку пульса. Функцию следует вызвать после `MX_CAN_Init()`, `MX_RTC_Init()` и `MX_TIMx_Init()`, но до запуска CAN. + +### 5. Подключить callback’и HAL + +Если в HAL включены `USE_HAL_CAN_REGISTER_CALLBACKS == 1` и `USE_HAL_TIM_REGISTER_CALLBACKS == 1`, `PROTOCAN_INIT()` зарегистрирует callback’и автоматически. + +Если регистрация callback’ов отключена, добавить перенаправление из стандартных HAL-callback’ов: + +```c +void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan_ptr) +{ + if (hcan_ptr == &hcan) { + ProtoCanRxFifo0MsgPendingCallback(hcan_ptr); + } +} + +void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim_ptr) +{ + if (htim_ptr == &htim2) { + ProtoCanPulseCallback(htim_ptr); + } +} +``` + +Не следует одновременно регистрировать callback через HAL и вручную вызывать его из стандартного callback — иначе один кадр или событие таймера может быть обработано дважды. + +## Приём сообщений + +В прерывании `ProtoCanRxFifo0MsgPendingCallback()` модуль вычитывает все кадры из FIFO0: + +- стандартные CAN ID игнорируются; +- `PULSE` не занимает место в RX-буфере, а сразу обновляет внутреннюю таблицу присутствующих устройств; +- остальные Extended-кадры помещаются в кольцевой буфер; +- при переполнении новые кадры молча отбрасываются. + +Обработку прикладной логики следует выполнять вне прерывания одним из трёх способов: + +```c +PROTOCAN_ProcessSingleRxMsg(); /* обработать не более одного кадра */ +PROTOCAN_ProcessAllRxMsgs(); /* обработать все накопленные кадры */ +PROTOCAN_LoopProcessRxMsgs(); /* бесконечный блокирующий цикл */ +``` + +Для обычного `while (1)` удобнее `PROTOCAN_ProcessAllRxMsgs()`. Она возвращает `PROTOCAN_TIMEOUT`, если очередь была пуста, `PROTOCAN_OK` после успешной обработки или код первой ошибки. + +## Свои обработчики входящих команд + +Большинство обработчиков объявлены как `__weak`. Приложение может определить функцию с тем же именем и заменить демонстрационную реализацию своей логикой. + +Пример обработки запроса температуры: + +```c +PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogTSens(struct RXMsg msg) +{ + msgBodyAnalogType body = {0}; + body.Body = msg.eID.Fields.MsgBody; + + uint16_t sensor_id = body.Fields.SensorID; + /* Прочитать датчик sensor_id и сформировать нужный ответ. */ + + return PROTOCAN_OK; +} +``` + +Доступные точки переопределения находятся в `protocan.h`: + +- `ProtoCanMsgToBroadcast...()` — статус, включение/выключение, restart, RTC; +- `ProtoCanMsgToDiscrete...()` — аварии, предупреждения, флаги и команды; +- `ProtoCanMsgToAnalog...()` — универсальные данные, настройки, U/I/T; +- `ProtoCanMsgToSettings()` — GET/WRITE/CLEAR привязки ROM к локации; +- `ProtoCanMsgToGeneralAddressSpace()`; +- `ProtoCanMsgToModbus...()`; +- `PROTOCAN_RequestError()`. + +Стандартные слабые обработчики в основном являются демонстрационными: часть возвращает `PROTOCAN_OK` без действий, часть отправляет текстовые ответы вроде `TS0001` или `GAS-0001`. Для рабочего изделия их обычно нужно переопределить. + +## SETTINGS: привязка DS18B20 к локации + +Для прибора `DeviceType=0x7`, `DeviceID=0xF` используется `MsgType=0xE`: + +```text +Body = (Z << 8) | Y + +запрос от ПМ = 0x17FEZZYY +ответ прибора = 0x1FFEZZYY +``` + +| Запрос | Операция | +|---|---| +| `DLC=0` | `GET`: прочитать ROM локации | +| `DLC=8`, ROM не нулевой | `WRITE/REPLACE`: записать или заменить датчик | +| `DLC=8`, восемь нулей | `CLEAR`: очистить локацию | +| другой DLC или RTR | автоматический ответ `INVALID_DLC` | + +Успешный ответ имеет DLC=8 и содержит текущее состояние локации: ROM после +GET/WRITE либо восемь нулей после CLEAR. Ошибка сохраняет тот же Body, имеет +DLC=1 и передаёт `ProtoCanSettingsResultType` в `Data[0]`. + +`PROTOCAN_SettingsProcessing()` выполняет транспортный разбор и вызывает +слабую функцию `ProtoCanMsgToSettings()`. Приложение переопределяет её и +выполняет проверку family code/CRC8, поиск ROM на 1-Wire и запись EEPROM: + +```c +PROTOCAN_StatusTypeDef ProtoCanMsgToSettings( + const ProtoCanSettingsMsg_t *message) +{ + uint8_t current_rom[PROTOCAN_SETTINGS_ROM_SIZE] = {0}; + + switch (message->Operation) { + case PROTOCAN_SETTINGS_GET: + /* Загрузить ROM локации в current_rom; свободная локация = нули. */ + return PROTOCAN_SEND_SETTINGS_RESPONSE( + message->AssemblySerial, message->Position, current_rom); + + case PROTOCAN_SETTINGS_WRITE: + /* Проверить ROM и атомарно записать его в каталог/EEPROM. */ + return PROTOCAN_SEND_SETTINGS_RESPONSE( + message->AssemblySerial, message->Position, message->Rom); + + case PROTOCAN_SETTINGS_CLEAR: + /* Удалить локацию из каталога/EEPROM. */ + return PROTOCAN_SEND_SETTINGS_RESPONSE( + message->AssemblySerial, message->Position, current_rom); + + default: + return PROTOCAN_SEND_SETTINGS_ERROR( + message->AssemblySerial, message->Position, + PROTOCAN_SETTINGS_RESULT_INVALID_DLC); + } +} +``` + +Слабая реализация специально не подтверждает операцию: без прикладного +каталога библиотека не должна сообщать, что ROM был сохранён. + +## Отправка + +Публичная точка отправки — `PROTOCAN_SEND(id, data)`. Сначала заполняется общий Extended ID, затем соответствующая часть `ProtoCanData_t`. + +Пример отправки диапазона 16-битных регистров общего адресного пространства: + +```c +uint16_t registers[] = { 0x1234, 0x5678, 0x9ABC }; + +ProtoCanId_t id = {0}; +id.Fields.Priority = PROTOCAN_PRIORITY_STANDARD; +id.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; +id.Fields.DeviceType = CURRENT_TYPE_DEVICE; +id.Fields.DeviceID = CURRENT_ID_DEVICE; +id.Fields.MsgType = PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE; + +ProtoCanData_t tx = {0}; +tx.GeneralAddressSpaceData.RegStartAdr = 100; +tx.GeneralAddressSpaceData.Data = registers; +tx.GeneralAddressSpaceData.RegCount = 3; + +if (PROTOCAN_SEND(id, tx) != PROTOCAN_OK) { + /* Очередь CAN занята или произошла ошибка HAL. */ +} +``` + +Массив автоматически разбивается на кадры максимум по четыре регистра (8 байт) с увеличением адреса в `MsgBody`. + +Пример сообщения об ошибке без payload: + +```c +ProtoCanId_t id = {0}; +id.Fields.Priority = PROTOCAN_PRIORITY_CRITICAL; +id.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; +id.Fields.DeviceType = CURRENT_TYPE_DEVICE; +id.Fields.DeviceID = CURRENT_ID_DEVICE; +id.Fields.MsgType = PROTOCAN_MSGTYPE_ERROR; + +ProtoCanData_t tx = {0}; +tx.ErrorData.Code = 0x12; +tx.ErrorData.Info = 0x34; + +(void)PROTOCAN_SEND(id, tx); +``` + +Для `BROADCAST`, `DISCRETE` и `ANALOG` используется `tx.CoreData`; для Modbus-типов — `tx.ModbusData`, для `SETTINGS` — `tx.SettingsData`. Для успешных ответов и ошибок SETTINGS удобнее использовать готовые функции `PROTOCAN_SEND_SETTINGS_RESPONSE()` и `PROTOCAN_SEND_SETTINGS_ERROR()`. Если количество передаваемых элементов больше нуля, соответствующий указатель `Data` должен быть валиден до завершения вызова. + +## Пульс устройства + +Каждое прерывание переданного в `PROTOCAN_INIT()` таймера вызывает `ProtoCanPulseCallback()` и отправляет кадр: + +- `MsgType = PROTOCAN_MSGTYPE_PULSE`; +- `DLC = 1`; +- `Data[0]` — циклический счётчик `0..255`. + +Период пульса полностью определяется настройками таймера. Broadcast-команда `PROTOCAN_BROADCAST_ONOFF` в стандартной реализации переключает его отправку. + +Полученный пульс помечает удалённое устройство как активное и обнуляет `TimeFromLastPulse`. Увеличение этого времени и перевод устройства в offline в текущем модуле не реализованы — если это требуется, контроль таймаута нужно добавить в приложение. + +## Синхронизация RTC + +Broadcast-команда `PROTOCAN_BROADCAST_RTCSETUP` ожидает ровно 7 байт: + +| Индекс | Значение | +|---:|---| +| 0 | часы, `0..23` | +| 1 | минуты, `0..59` | +| 2 | секунды, `0..59` | +| 3 | год как смещение от 2000, `0..99` | +| 4 | месяц, `1..12` | +| 5 | число месяца | +| 6 | день недели в формате, ожидаемом данной прошивкой | + +Перед записью проверяются диапазоны и количество дней с учётом високосного года. + +## Прошивка приборов по CAN + +Ниже зафиксирован формат планируемого загрузочного сервиса. Значения +`MsgType=0x9..0xD` зарезервированы в протоколе, но обработчики загрузчика в +текущих `protocan.c/.h` ещё не реализованы. + +Загрузчик работает поверх classic CAN 2.0B с Extended ID. Управляющий модуль +использует `Route=0`, прибор отвечает с `Route=1`. Команды стирания и записи +всегда должны быть адресованы конкретной паре `DeviceType/DeviceID`; +широковещательный режим допустим только для обнаружения. + +### Карта загрузочных сообщений + +| `MsgType` | Имя | Назначение `MsgBody` | CAN payload | +|---:|---|---|---| +| `0x9` | `BOOT_CONTROL` | `SessionID:8 \| Command:8` | параметры команды | +| `0xA` | `BOOT_DATA_A` | номер 8-байтового блока слота A | 8 байт образа | +| `0xB` | `BOOT_DATA_B` | номер 8-байтового блока слота B | 8 байт образа | +| `0xC` | `BOOT_STATUS` | `SessionID:8 \| Command:8` | статус и прогресс | +| `0xD` | `BOOT_DISCOVERY` | подтип запроса/ответа | идентификация прибора | + +`MsgBody` в кадрах данных является не байтовым адресом, а номером блока: + +```c +block_offset = (uint32_t)MsgBody * 8U; + +if (MsgType == PROTOCAN_MSGTYPE_BOOT_DATA_A) { + address = SLOT_A_BASE + block_offset; +} else if (MsgType == PROTOCAN_MSGTYPE_BOOT_DATA_B) { + address = SLOT_B_BASE + block_offset; +} +``` + +Диапазон `MsgBody=0x0000..0xFFFF` адресует 65536 блоков: + +```text +65536 блоков * 8 байт = 524288 байт = 512 КиБ на слот +``` + +Таким образом, `BOOT_DATA_A` и `BOOT_DATA_B` адресуют два логических слота +по 512 КиБ, всего 1 МиБ пространства образов. Физические `SLOT_A_BASE` и +`SLOT_B_BASE` задаёт конкретный загрузчик. Если внутренняя Flash имеет ровно +1 МиБ, два полных слота в ней не поместятся вместе с загрузчиком и метаданными: +нужно уменьшить слоты, выбрать MCU с большей Flash или хранить staging-образ +во внешней памяти. + +### Кадр данных + +```text +Extended CAN ID + Priority = STANDARD + Route = FROM_PM + DeviceType = тип целевого прибора + DeviceID = экземпляр целевого прибора + MsgType = 0xA (слот A) или 0xB (слот B) + MsgBody = BlockIndex, 0x0000..0xFFFF + +DATA[0..7] = очередные 8 байт образа +``` + +Например, `MsgBody=0x0123` задаёт смещение `0x0123 * 8 = 0x0918` от +начала выбранного слота. Последний неполный блок дополняется значениями +`0xFF`; фактический размер передаётся командой `BEGIN_UPDATE`, поэтому CRC32 +считается только по байтам образа. + +### Управляющие команды + +В `BOOT_CONTROL` поле `MsgBody` имеет формат: + +```text +15........8 7.........0 + SessionID Command +``` + +Рекомендуемые команды: + +| Код | Команда | Назначение | +|---:|---|---| +| `0x01` | `IDENTIFY` | прочитать тип, аппаратную и программную версии | +| `0x02` | `ENTER_BOOT` | перейти из приложения в загрузчик | +| `0x03` | `BEGIN_IMAGE` | передать размер и CRC32 образа | +| `0x04` | `BEGIN_COMPAT` | передать тип, аппаратную и программную версии | +| `0x05` | `ERASE` | подготовить неактивный слот | +| `0x06` | `VERIFY` | проверить размер, CRC32 и подпись | +| `0x07` | `COMMIT` | назначить проверенный слот кандидатом на запуск | +| `0x08` | `CONFIRM` | подтвердить успешный запуск новой программы | +| `0x09` | `REBOOT` | перезагрузить прибор | +| `0x0A` | `ABORT` | отменить текущую сессию | +| `0x0B` | `QUERY_PROGRESS` | запросить слот и следующий ожидаемый блок | + +`BEGIN_IMAGE` содержит размер и CRC образа: + +```text +DATA[0..3] ImageSize, uint32 little-endian +DATA[4..7] ImageCRC32, uint32 little-endian +``` + +`BEGIN_COMPAT` содержит совместимость и версию: + +```text +DATA[0..1] ProductType, uint16 little-endian +DATA[2] минимальная HardwareRevision +DATA[3] максимальная HardwareRevision +DATA[4..7] FirmwareVersion, uint32 little-endian +``` + +Обе команды передаются с одним `SessionID`. До стирания Flash загрузчик обязан +получить обе части метаданных и проверить `ProductType`, аппаратную ревизию, +размер образа, границы выбранного слота и допустимость версии. + +### Ответ состояния + +`BOOT_STATUS` возвращает результат команды и точку продолжения: + +```text +MsgBody[15..8] = SessionID +MsgBody[7..0] = команда, на которую дан ответ + +DATA[0] Status +DATA[1] TargetSlot: 0 = A, 1 = B +DATA[2..3] NextBlock, uint16 little-endian +DATA[4..7] RunningCRC32, uint32 little-endian +``` + +Минимальный набор статусов: + +| Код | Статус | +|---:|---| +| `0x00` | `OK` | +| `0x01` | `BUSY` | +| `0x02` | `INVALID_COMMAND` | +| `0x03` | `WRONG_DEVICE` | +| `0x04` | `WRONG_HARDWARE` | +| `0x05` | `INVALID_SIZE` | +| `0x06` | `CRC_ERROR` | +| `0x07` | `FLASH_ERROR` | +| `0x08` | `SEQUENCE_ERROR` | +| `0x09` | `SIGNATURE_ERROR` | +| `0x0A` | `SESSION_ERROR` | +| `0x0B` | `VOLTAGE_ERROR` | + +`NextBlock` позволяет возобновить загрузку после разрыва связи. Для первой +реализации допустимо подтверждать каждый блок. Для рабочей скорости лучше +передавать окна по 16 кадров и подтверждать окно одним `BOOT_STATUS`; при +необходимости протокол статуса можно расширить битовой картой потерянных +блоков. + +### Выбор слота и безопасное обновление + +GUI не должен самостоятельно перезаписывать активный слот. После получения +`BEGIN_IMAGE` и `BEGIN_COMPAT` загрузчик выбирает неактивный слот и сообщает его в +`BOOT_STATUS`: + +```text +активен A -> принимать BOOT_DATA_B +активен B -> принимать BOOT_DATA_A +``` + +Рекомендуемый цикл обновления: + +1. Обнаружить прибор и сверить `DeviceType/DeviceID`, `ProductType`, UID и версии. +2. Выполнить адресную команду `ENTER_BOOT` и получить новый `SessionID`. +3. Передать `BEGIN_IMAGE` и `BEGIN_COMPAT`; загрузчик выберет неактивный слот. +4. Стереть выбранный слот и передать блоки `BOOT_DATA_A` или `BOOT_DATA_B`. +5. Выполнить `VERIFY`: проверить размер, CRC32 и цифровую подпись образа. +6. Выполнить `COMMIT` и перезагрузить устройство. +7. Новое приложение вызывает `CONFIRM` после успешной самопроверки. +8. При отсутствии подтверждения загрузчик возвращается к предыдущему слоту. + +CRC32 обнаруживает случайное повреждение, но не защищает от подмены. Для +серийных изделий образ следует подписывать, а открытый ключ проверки хранить +в неизменяемой части загрузчика. Сам загрузчик не должен обновляться обычными +командами `BOOT_DATA_A/B`. + +## Важные ограничения текущей реализации + +- Модуль рассчитан на classic CAN с payload до 8 байт, не на CAN FD. +- `PROTOCAN_INIT()` не запускает CAN/таймер и не включает CAN notification. +- Все три аппаратных фильтра и номера банков заданы внутри библиотеки. +- Фильтр адресованных сообщений рассчитан на устройство: он принимает Route=0 от ПМ. Для использования библиотеки на стороне ПМ потребуется отдельная конфигурация фильтров Route=1. +- Проверку ROM, физического датчика и EEPROM выполняет приложение в переопределённом `ProtoCanMsgToSettings()`. +- Таблица присутствующих устройств обновляется при пульсе, но автоматического offline-таймаута нет. +- Буфер переполнения не ведёт счётчик потерь и не сообщает ошибку приложению. +- У порта нет полного примера прошивки STM32; его сборку и проверку на плате выполняет проект устройства. Host-тесты общего ядра находятся в `../../tests`. +- Несколько внутренних функций отправки реализованы только в `protocan.c` и не входят в публичный заголовок; для прикладного кода следует использовать `PROTOCAN_SEND()`. + +## Краткий порядок запуска + +1. Подключить `templates` к проекту устройства. +2. Добавить `protocan.c/.h` и общий `pcan_id.c` в STM32-проект. +3. Задать `CURRENT_TYPE_DEVICE` и `CURRENT_ID_DEVICE`. +4. Настроить CAN, RTC и периодический TIM в CubeMX. +5. Вызвать `PROTOCAN_INIT()` после `MX_..._Init()`. +6. Запустить CAN, активировать `CAN_IT_RX_FIFO0_MSG_PENDING` и запустить таймер с прерыванием. +7. При необходимости подключить HAL-callback’и вручную. +8. Переопределить нужные `__weak`-обработчики. +9. Вызывать `PROTOCAN_ProcessAllRxMsgs()` в основном цикле. diff --git a/c/set-protocol/ports/stm32-bxcan/protocan.c b/c/set-protocol/ports/stm32-bxcan/protocan.c new file mode 100644 index 0000000..153320d --- /dev/null +++ b/c/set-protocol/ports/stm32-bxcan/protocan.c @@ -0,0 +1,1667 @@ +#include "protocan.h" + +ProtoCanDevice_t CurrentDevice; +ProtoCanDevice_t Device_on_the_Network[8][16]; +struct controlflags ControlFlags; + +CAN_HandleTypeDef *_HCAN = 0; +RTC_HandleTypeDef *_HRTC = 0; +TIM_HandleTypeDef *_HTIM = 0; + +/* CurrentStep - индекс следующего сообщения на обработку (пишет основной код). + LastStep - индекс последнего записанного сообщения (пишет ISR). + Оба индекса volatile: без этого компилятор вправе закэшировать значение в + регистре, т.к. в его модели исполнения прерывание в код не входит. + Сам массив rxMsg volatile НЕ объявлен - это ухудшило бы кодогенерацию и + мешало бы копированию структуры по значению. Порядок "заполнить слот, затем + опубликовать индекс" обеспечивается барьерами __DMB() в точках публикации и + чтения. */ +volatile uint16_t CurrentStep = 1; +volatile uint16_t LastStep = 0; +struct RXMsg rxMsg[PROTOCAN_RX_BUFFER_SIZE]; + +/** +* @brief Проверяет, является ли год високосным. +* В системе год хранится как смещение от 2000. Например, значение 24 соответствует 2024. +* Эта функция добавляет 2000 к значению и проверяет, високосный ли получившийся год. +* @param year Смещение от 2000, тип uint8_t (от 0 до 255). +* @return _Bool true, если год високосный; false — иначе. +*/ +_Bool IsLeapYear(uint8_t year) +{ + year += 2000; + return (year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0)); +} + +/** +* @brief Возвращает количество доступных сообщений в буфере приёма CAN. +* Используется кольцевой буфер. +* Возвращаемое значение — количество элементов между указателями LastStep и CurrentStep. +* @return uint16_t Количество доступных сообщений, ожидающих обработки, в буфере. +*/ +uint16_t AvailableCanRxMsg(void) +{ + /* Однократное чтение индекса, обновляемого из ISR. */ + uint16_t last = LastStep; + uint16_t current = CurrentStep; + /* Индекс прочитан до обращения к данным слота. */ + __DMB(); + return (uint16_t)(((unsigned)PROTOCAN_RX_BUFFER_SIZE + last - current + 1u) % PROTOCAN_RX_BUFFER_SIZE); +} + +void PROTOCAN_DEINIT(uint8_t stage) +{ + switch(stage) + { + case 3: + #if (USE_HAL_TIM_REGISTER_CALLBACKS == 1) + HAL_TIM_UnRegisterCallback(_HTIM, HAL_TIM_PERIOD_ELAPSED_CB_ID); + #endif + case 2: + case 1: + #if (USE_HAL_CAN_REGISTER_CALLBACKS == 1) + HAL_CAN_UnRegisterCallback(_HCAN, HAL_CAN_RX_FIFO0_MSG_PENDING_CB_ID); + #endif + } + return; +} + +/** +* @brief Инициализация переферии +* @details Инициализация указателей на HCAN, HRTC, установка фильтров CAN. +* @note Фильтры CAN описаны в разделе PROTOCAN_CAN_FILTERS(). +*/ +PROTOCAN_INIT_StatusTypeDef PROTOCAN_INIT(CAN_HandleTypeDef *tmp_hcan, RTC_HandleTypeDef *tmp_hrtc, TIM_HandleTypeDef *tmp_tim) +{ + unsigned initStage = 0; + if(tmp_hcan) + { + _HCAN = tmp_hcan; + #if (USE_HAL_CAN_REGISTER_CALLBACKS == 1) + HAL_StatusTypeDef CAN_RC_RESULT = HAL_CAN_RegisterCallback(_HCAN, HAL_CAN_RX_FIFO0_MSG_PENDING_CB_ID, ProtoCanRxFifo0MsgPendingCallback); + if(CAN_RC_RESULT != HAL_OK) + { + PROTOCAN_DEINIT(initStage); + return PROTOCAN_INIT_HCAN_ERROR; + } + #endif + } else { + PROTOCAN_DEINIT(initStage); + return PROTOCAN_INIT_HCAN_ERROR; + } + initStage++; + if(tmp_hrtc) + { + _HRTC = tmp_hrtc; + } + else + { + PROTOCAN_DEINIT(initStage); + return PROTOCAN_INIT_HRTC_ERROR; + } + initStage++; + if(tmp_tim) + { + _HTIM = tmp_tim; + #if (USE_HAL_TIM_REGISTER_CALLBACKS == 1) + HAL_StatusTypeDef TIM_RC_RESULT = HAL_TIM_RegisterCallback(_HTIM, HAL_TIM_PERIOD_ELAPSED_CB_ID, ProtoCanPulseCallback); + if(TIM_RC_RESULT != HAL_OK) + { + PROTOCAN_DEINIT(initStage); + return PROTOCAN_INIT_TIM_ERROR; + } + #endif + } + else + { + PROTOCAN_DEINIT(initStage); + return PROTOCAN_INIT_TIM_ERROR; + } + initStage++; + PROTOCAN_FILTERS(); + ControlFlags.IsPulse = 1; + return PROTOCAN_INIT_OK; +} + +/** + * @brief Бесконечный цикл обработки входящих CAN-сообщений. + * + * Эта функция постоянно проверяет наличие доступных сообщений в буфере CAN и + * вызывает соответствующие обработчики в зависимости от типа полученного сообщения. + * Обрабатываемые типы сообщений: + * - Аналоговые данные + * - Широковещательные сообщения + * - Дискретные данные + * - Общее адресное пространство + * - Модбас сообщения + * - Ошибки + * + * Обработка сообщений осуществляется внутри бесконечного цикла с использованием + * функции AvailableCanRxMsg() для определения наличия новых сообщений. + * + * В конце каждого обработанного сообщения индекс текущего элемента буфера инкрементируется + * с учетом размера буфера через операцию по модулю. + */ +void PROTOCAN_LoopProcessRxMsgs(void) +{ + while(1) + { + if(AvailableCanRxMsg()) + { + if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_ANALOG) + { + PROTOCAN_AnalogProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_BROADCAST) + { + PROTOCAN_BroadcastProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_DISCRETE) + { + PROTOCAN_DiscreticProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE) + { + ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS) + { + PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL || + rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || + rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || + rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT) + { + PROTOCAN_ModbusProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_ERROR) + { + PROTOCAN_RequestError(rxMsg[CurrentStep]); + } + // Обновление индекса текущего сообщения с циклическим масштабированием + CurrentStep = (uint16_t)(CurrentStep + 1) % PROTOCAN_RX_BUFFER_SIZE; + } + } +} + +/** + * @brief Обрабатывает все доступные входящие CAN-сообщения. + * + * Эта функция последовательно обрабатывает все сообщения, доступные в буфере, + * вызывая соответствующие функции обработчики в зависимости от типа сообщения. + * После обработки каждого сообщения индекс текущего элемента буфера обновляется + * циклически. + * + * @return PROTOCAN_OK, если все сообщения были обработаны успешно. + * В противном случае возвращается код ошибки, полученный при обработке сообщения. + * + * Обработка происходит до тех пор, пока есть доступные сообщения, полученные функцией + * AvailableCanRxMsg(). + */ +PROTOCAN_StatusTypeDef PROTOCAN_ProcessAllRxMsgs(void) +{ + PROTOCAN_StatusTypeDef result = PROTOCAN_TIMEOUT; + while(AvailableCanRxMsg()) + { + if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_ANALOG) + { + result = PROTOCAN_AnalogProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_BROADCAST) + { + result = PROTOCAN_BroadcastProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_DISCRETE) + { + result = PROTOCAN_DiscreticProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE) + { + result = ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS) + { + result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL || + rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || + rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || + rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT) + { + result = PROTOCAN_ModbusProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_ERROR) + { + result = PROTOCAN_RequestError(rxMsg[CurrentStep]); + } + CurrentStep = (uint16_t)(CurrentStep + 1) % PROTOCAN_RX_BUFFER_SIZE; + if(result != PROTOCAN_OK) + { + return result; + } + } + return result; +} + +/** + * @brief Обрабатывает одно доступное CAN-сообщение. + * + * Эта функция проверяет наличие доступных сообщений в буфере и, если таковое есть, + * выполняет обработку текущего сообщения в зависимости от его типа. После обработки + * индекс текущего сообщения обновляется циклически. + * + * @return Прошедший статус обработки: + * - PROTOCAN_OK, если сообщение успешно обработано, + * - PROTOCAN_TIMEOUT, если сообщений не было, + * - или код ошибки, возвращенный функцией обработки. + */ +PROTOCAN_StatusTypeDef PROTOCAN_ProcessSingleRxMsg(void) +{ + PROTOCAN_StatusTypeDef result = PROTOCAN_TIMEOUT; + if(AvailableCanRxMsg()) + { + if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_ANALOG) + { + result = PROTOCAN_AnalogProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_BROADCAST) + { + result = PROTOCAN_BroadcastProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_DISCRETE) + { + result = PROTOCAN_DiscreticProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE) + { + result = ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS) + { + result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL || + rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || + rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || + rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT) + { + result = PROTOCAN_ModbusProcessing(rxMsg[CurrentStep]); + } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_ERROR) + { + result = PROTOCAN_RequestError(rxMsg[CurrentStep]); + } + CurrentStep = (uint16_t)(CurrentStep + 1) % PROTOCAN_RX_BUFFER_SIZE; + } + return result; +} + +/** +* @brief Функция обработки широковещательных запросов. +* @param struct RXMsg _rxMsg - структура для полученного сообщения. +* @details Функция, выполняющая команды, переданные в широковещательном формате с головного (master) устройства. Типы команд: Запрос статуса, запрос на включение или выключение, рестарт устройств, установка времени. +* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка). +*/ +PROTOCAN_StatusTypeDef PROTOCAN_BroadcastProcessing(struct RXMsg _rxMsg) +{ + msgBodyBroadcastType msg; + msg.Body = _rxMsg.eID.Fields.MsgBody; + switch(msg.Fields.Type) + { + case PROTOCAN_BROADCAST_STATUS: + { + //Обработка запроса статуса устройства + return ProtoCanMsgToBroadcastStatus(_rxMsg); + } + case PROTOCAN_BROADCAST_ONOFF: + { + //Обработка запроса на вкл/выкл + ProtoCanMsgToBroadcastOnOff(_rxMsg); + break; + } + case PROTOCAN_BROADCAST_RESTARTDEVICE: + { + ProtoCanMsgToBroadcastRestart(_rxMsg); + break; + } + case PROTOCAN_BROADCAST_RTCSETUP: + { + //Обработка запроса на синхронизацию времени + //С головным устройством + ProtoCanMsgToBroadcastRtcSetup(_rxMsg); + break;; + } + default: + //RESERVE SENSOR TYPE. + return PROTOCAN_ERROR; + break; + } + return PROTOCAN_OK; +} + +/** +* @brief Формирует и отправляет широковещательное CAN-сообщение для ответа на запрос статуса. +* Устанавливает идентификатор, собирает текущие параметры времени и даты, и отправляет сообщение. +* @param _rxMsg Структура полученного по CAN запроса. +* @return HAL_StatusTypeDef Статус операции HAL, например HAL_OK при успехе. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastStatus(struct RXMsg _rxMsg) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.DLC = 7; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = _rxMsg.eID.BitAll; + tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + tmp_eID.Fields.DeviceType = CURRENT_TYPE_DEVICE; + tmp_eID.Fields.DeviceID = CURRENT_ID_DEVICE; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + RTC_TimeTypeDef sTime = {0}; + HAL_RTC_GetTime(_HRTC, &sTime, RTC_FORMAT_BIN); + data[0] = sTime.Hours; + data[1] = sTime.Minutes; + data[2] = sTime.Seconds; + RTC_DateTypeDef DateToUpdate = {0}; + HAL_RTC_GetDate(_HRTC, &DateToUpdate, RTC_FORMAT_BIN); + data[3] = DateToUpdate.Year; + data[4] = DateToUpdate.Month; + data[5] = DateToUpdate.Date; + data[6] = DateToUpdate.WeekDay; + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); +} + +/** +* @brief Виртуальная слабая функция для переключения режима пульсации ON/OFF. +* Меняет состояние флага IsPulse в структуре ControlFlags. +* В данном случае — просто инвертирует его логическое значение. +* @param _rxMsg Структура полученного CAN-сообщения (Пока не используется в функции). +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastOnOff(struct RXMsg _rxMsg) +{ + ControlFlags.IsPulse = !ControlFlags.IsPulse; + return PROTOCAN_OK; +} + +/** +* @brief __weak функция для обработки широковещательного сообщения перезапуска устройства. +* Проверяет длину сообщения, извлекает ID из данных и, если она совпадает с текущим устройством, +* вызывает системный перезапуск. +* @param _rxMsg Структура полученного CAN-сообщения с командой перезапуска. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastRestart(struct RXMsg _rxMsg) +{ + if(_rxMsg.DLC == 0) + { + return PROTOCAN_ERROR; + } + msgBodyBroadcastType msg; + msg.Body = _rxMsg.eID.Fields.MsgBody; + if(msg.Fields.Body == (CURRENT_ID_DEVICE / (_rxMsg.DLC*8))) + { + uint64_t page = 0; + for(uint32_t i = 0; i < _rxMsg.DLC; i++) + { + /* Приведение обязательно: Data[i] продвигается до int, и сдвиг на + 32..56 бит без него - неопределённое поведение. */ + page |= (uint64_t)_rxMsg.Data[i] << (i * 8u); + } + if((page >> CURRENT_ID_DEVICE) & 1u) + { + NVIC_SystemReset(); + } + } + return PROTOCAN_OK; +} + +/** +* @brief __weak функция для обработки запроса на синхронизацию RTC через широковещательное сообщение. +* Проверяет длину сообщения и корректность данных (время и дата). Если все проверки пройдены, +* вызывает функцию синхронизации RTC. +* @param _rxMsg Структура полученного CAN-сообщения с данными для установки времени и даты. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastRtcSetup(struct RXMsg _rxMsg) +{ + /* Нужны все 7 полей: чч, мм, сс, год, месяц, число, день недели. + Прежняя проверка (DLC > 7) пропускала в том числе пустой кадр, и время + устанавливалось из неинициализированных байт. */ + if(_rxMsg.DLC != 7) + { + return PROTOCAN_ERROR; + } + else + { + static const uint8_t DaysCount_Normal[2][12] = {{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, + {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}; + /* Месяц нумеруется с 1, индекс массива - с 0. */ + if( _rxMsg.Data[0] > 23 || + _rxMsg.Data[1] > 59 || + _rxMsg.Data[2] > 59 || + _rxMsg.Data[3] > 99 || + _rxMsg.Data[4] == 0 || _rxMsg.Data[4] > 12 || + _rxMsg.Data[5] == 0 || + _rxMsg.Data[5] > DaysCount_Normal[IsLeapYear(_rxMsg.Data[3])][_rxMsg.Data[4] - 1] || + _rxMsg.Data[6] > 6) + { + return PROTOCAN_ERROR; + } + else + { + PROTOCAN_RTC_SYNC(_rxMsg.Data); + } + } + return PROTOCAN_OK; +} + +/** +* @brief Функция обработки дискретных запросов. +* @param struct RXMsg _rxMsg - структура для полученного сообщения. +* @details Функция, формирующая и отправляющая ответ на запросы. Типы запросов: Аварии, Предупреждения, Управляющие сигналы, Флаги, Рестарт устройства, Изменение режима работы устройства, Запрос на устройство. +* @note Запрос на устройство. Головное (master) устройство запрашивает некоторое колличество параметров. В Data - 64 битовых адресса параметров, тип которых задаётся в Sensor ID. Имеется возможность запрашивать непоследовательные параметры. +* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка). +*/ +PROTOCAN_StatusTypeDef PROTOCAN_DiscreticProcessing(struct RXMsg _rxMsg) +{ + msgBodyDiscreteType msg; + msg.Body = _rxMsg.eID.Fields.MsgBody; + switch(msg.Fields.Type) + { + case PROTOCAN_DISCRETE_ACCIDENT: + { + ProtoCanMsgToDiscreteAccident(_rxMsg); + break; + } + case PROTOCAN_DISCRETE_WARNING: + { + ProtoCanMsgToDiscreteWarning(_rxMsg); + break; + } + case PROTOCAN_DISCRETE_CONTROL_SIGNALS: + { + ProtoCanMsgToDiscreteControlSignals(_rxMsg); + break; + } + case PROTOCAN_DISCRETE_FLAGS: + { + ProtoCanMsgToDiscreteFlags(_rxMsg); + break; + } + case PROTOCAN_DISCRETE_RESET: + { + ProtoCanMsgToDiscreteReset(_rxMsg); + break; + } + case PROTOCAN_DISCRETE_CHANGE_MODE: + { + ProtoCanMsgToDiscreteChangeMode(_rxMsg); + break; + } + case PROTOCAN_DISCRETE_REQUEST_LIST_OF_PARAMETERS: + { + ProtoCanMsgToDiscreteRequestListOfParameters(_rxMsg); + break; + } + default: + //RESERVE SENSOR TYPE. + return PROTOCAN_ERROR; + break; + } + return PROTOCAN_OK; +} + +/** +* @brief Обработка запроса аварийных ситуаций. +* В текущей реализации — заглушка, ничего не выполняет. +* Можно реализовать отправку или обработку аварийных данных. +* @param _rxMsg Входящее сообщение с данными запроса. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteAccident(struct RXMsg _rxMsg) +{ + return PROTOCAN_OK; +} + +/** +* @brief Обработка запроса предупреждений. +* В текущей реализации — заглушка. +* Можно реализовать обработку или ответ на предупреждения. +* @param _rxMsg Входящее сообщение. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteWarning(struct RXMsg _rxMsg) +{ + return PROTOCAN_OK; +} + +/** +* @brief Обработка запроса управляющих сигналов. +* В текущей реализации — заглушка. +* Можно реализовать управление сигналами. +* @param _rxMsg Входящее сообщение. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteControlSignals(struct RXMsg _rxMsg) +{ + return PROTOCAN_OK; +} + +/** +* @brief Обработка запроса флагов. +* В текущей реализации — заглушка. +* Можно реализовать работу с флагами. +* @param _rxMsg Входящее сообщение. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteFlags(struct RXMsg _rxMsg) +{ + return PROTOCAN_OK; +} + +/** +* @brief Обработка рестарта системы по CAN-запросу. +* Выполняет программный рестарт системы. +* @param _rxMsg Входящее сообщение. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteReset(struct RXMsg _rxMsg) +{ + NVIC_SystemReset(); + return PROTOCAN_ERROR; +} + +/** +* @brief Обработка запроса на изменение режима. +* В текущей реализации — заглушка. +* Можно реализовать изменение режима работы. +* @param _rxMsg Входящее сообщение. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteChangeMode(struct RXMsg _rxMsg) +{ + return PROTOCAN_OK; +} + +/** +* @brief Обработка запроса на получение списка параметров. +* В текущей реализации — заглушка. +* Можно реализовать отправку списка параметров. +* @param _rxMsg Входящее сообщение. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteRequestListOfParameters(struct RXMsg _rxMsg) +{ + return PROTOCAN_OK; +} + +/** +* @brief Функция обработки аналоговых запросов. +* @param struct RXMsg _rxMsg - структура для полученного сообщения. +* @details Функция, сортирующая запросы по типам и вызывающая соответствующие функции. Типы запросов: Универсальный, Уставки, Напряжение, Ток, Температура. +* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка). +*/ +PROTOCAN_StatusTypeDef PROTOCAN_AnalogProcessing(struct RXMsg _rxMsg) +{ + msgBodyAnalogType msg; + msg.Body = _rxMsg.eID.Fields.MsgBody; + switch (msg.Fields.Type) + { + case PROTOCAN_ANALOG_UNIVERSAL: + { + return ProtoCanMsgToAnalogUniversal(_rxMsg); + break; + } + case PROTOCAN_ANALOG_SETTINGS: + { + return ProtoCanMsgToAnalogSettings(_rxMsg); + break; + } + case PROTOCAN_ANALOG_U: + { + return ProtoCanMsgToAnalogUSens(_rxMsg); + break; + } + case PROTOCAN_ANALOG_I: + { + return ProtoCanMsgToAnalogISens(_rxMsg); + break; + } + case PROTOCAN_ANALOG_T: + { + return ProtoCanMsgToAnalogTSens(_rxMsg); + break; + } + default: + //RESERVE SENSOR TYPE + return PROTOCAN_ERROR; + break; + } + return PROTOCAN_ERROR; +} + +/** +* @brief Функция ответа на запрос данных со всех датчиков. +* Создает сообщение с определенными данными и отправляет его через CAN-шину. +* @param _rxMsg Структура входящего сообщения, используется для формирования ID. +* @return HAL_StatusTypeDef Статус выполнения операции HAL, например HAL_OK. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogUniversal(struct RXMsg _rxMsg) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = _rxMsg.eID.BitAll; + tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + TxHeader.DLC = 6; + data[0] = 'U'; + data[1] = 'N'; + data[2] = 'I'; + data[3] = 'V'; + data[4] = 'E'; + data[5] = 'R'; + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); +} + +/** +* @brief Функция ответа на запрос уставок по CAN. +* Создает сообщение с определенными данными и отправляет его через CAN-шину. +* @param _rxMsg Структура входящего сообщения, используется только для формирования ID. +* @return HAL_StatusTypeDef Статус выполнения операции HAL, например HAL_OK. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogSettings(struct RXMsg _rxMsg) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = _rxMsg.eID.BitAll; + tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + TxHeader.DLC = 7; + data[0] = 'U'; + data[1] = 'S'; + data[2] = 'T'; + data[3] = 'A'; + data[4] = 'V'; + data[5] = 'K'; + data[6] = 'I'; + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); +} + +/** +* @brief Функция ответа на запрос данных с датчиков напряжения. +* Создает сообщение с указанными параметрами и отправляет его через CAN-шину. +* @param _rxMsg Структура входящего сообщения, используемая для формирования идентификатора и данных. +* @return HAL_StatusTypeDef Статус выполнения операции HAL, например HAL_OK при успешной отправке. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogUSens(struct RXMsg _rxMsg) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = _rxMsg.eID.BitAll; + tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + TxHeader.DLC = 6; + msgBodyAnalogType msg; + msg.Body = _rxMsg.eID.Fields.MsgBody; + data[0] = 'U'; + data[1] = 'S'; + data[2] = '0' + msg.Fields.SensorID / 1000; + data[3] = '0' + msg.Fields.SensorID / 100 % 10; + data[4] = '0' + msg.Fields.SensorID / 10 % 10; + data[5] = '0' + msg.Fields.SensorID % 10; + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); +} + +/** +* @brief Функция ответа на запрос данных с датчиков тока. +* Создает сообщение с определенными параметрами и отправляет его через CAN-шину. +* @param _rxMsg Структура входящего сообщения, используется для формирования идентификатора и данных. +* @return HAL_StatusTypeDef Статус операции HAL, например HAL_OK при успешной отправке. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogISens(struct RXMsg _rxMsg) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = _rxMsg.eID.BitAll; + tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + TxHeader.DLC = 6; + msgBodyAnalogType msg; + msg.Body = _rxMsg.eID.Fields.MsgBody; + data[0] = 'I'; + data[1] = 'S'; + data[2] = '0' + msg.Fields.SensorID / 1000; + data[3] = '0' + msg.Fields.SensorID / 100 % 10; + data[4] = '0' + msg.Fields.SensorID / 10 % 10; + data[5] = '0' + msg.Fields.SensorID % 10; + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); +} + +/** +* @brief Функция ответа на запрос данных с датчиков температуры. +* Создает сообщение с определенным заголовком, формирует данные по ID сенсора и отправляет его через CAN. +* @param _rxMsg Структура полученного сообщения, которая используется для формирования идентификатора. +* @return HAL_StatusTypeDef Статус выполнения функции HAL, например HAL_OK при успехе. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogTSens(struct RXMsg _rxMsg) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = _rxMsg.eID.BitAll; + tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + TxHeader.DLC = 6; + msgBodyAnalogType msg; + msg.Body = _rxMsg.eID.Fields.MsgBody; + data[0] = 'T'; + data[1] = 'S'; + data[2] = '0' + msg.Fields.SensorID / 1000; + data[3] = '0' + msg.Fields.SensorID / 100 % 10; + data[4] = '0' + msg.Fields.SensorID / 10 % 10; + data[5] = '0' + msg.Fields.SensorID % 10; + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); +} + +/** + * @brief Отправляет сообщение по CAN-шине в соответствии с типом протокола. + * Эта функция формирует и отправляет сообщение через CAN-шину, основываясь на типе + * переданного ID и данных. Поддерживаются типы сообщений: широковещательные, дискретные + * и аналоговые. + * @param id Идентификатор сообщения, содержащий тип сообщения и другую информацию. + * @param coreData Структура данных, включающая тип, тело и данные для передачи. + * @return PROTOCAN_StatusTypeDef Статус операции. + * @details + * В зависимости от типа сообщения (панель broadcast, discrete или analog), + * формируется соответствующий пакет данных и отправляется на шину CAN. + * Для сообщений, превышающих длину 8 байт, передача выполняется пакетами. + * В случае ошибок при добавлении сообщения в очередь передачи, возвращается ошибка. + */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_CORE(const ProtoCanId_t id, const struct ProtoCanCoreData coreData) +{ + ProtoCanId_t localId = id; + CAN_TxHeaderTypeDef TxHeader; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.RTR = CAN_RTR_DATA; + TxHeader.TransmitGlobalTime = DISABLE; + uint32_t TxMailBox = 0; + uint8_t canData[8]; + switch(id.Fields.MsgType) + { + case PROTOCAN_MSGTYPE_BROADCAST: + { + msgBodyBroadcastType body; + body.Fields.Type = coreData.Type; + body.Fields.Body = coreData.Body; + localId.Fields.MsgBody = body.Body; + TxHeader.ExtId = PROTOCAN_IdPack(&localId); + TxHeader.DLC = 0; + if(HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox) != HAL_OK) + { + return PROTOCAN_ERROR; + } + break; + } + case PROTOCAN_MSGTYPE_DISCRETE: + { + msgBodyDiscreteType body; + body.Fields.Type = coreData.Type; + uint8_t regsRemaining = coreData.DataCount; + uint8_t currentIndex = 0; + while(regsRemaining > 0) + { + uint8_t regsInPacket = (regsRemaining > 8) ? 8 : regsRemaining; + body.Fields.Body = coreData.Body + currentIndex; + localId.Fields.MsgBody = body.Body; + TxHeader.ExtId = PROTOCAN_IdPack(&localId); + TxHeader.DLC = regsInPacket; + for(int i = 0; i < regsInPacket; i++) + { + canData[i] = coreData.Data[currentIndex + i]; + } + HAL_StatusTypeDef status = HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox); + if(status != HAL_OK) + { + return (PROTOCAN_StatusTypeDef)status; + } + regsRemaining -= regsInPacket; + currentIndex += regsInPacket; + } + break; + } + case PROTOCAN_MSGTYPE_ANALOG: + { + msgBodyAnalogType body; + body.Fields.Type = coreData.Type; + uint8_t regsRemaining = coreData.DataCount; + uint8_t currentIndex = 0; + while(regsRemaining > 0) + { + uint8_t regsInPacket = (regsRemaining > 8) ? 8 : regsRemaining; + body.Fields.SensorID = coreData.Body + currentIndex; + localId.Fields.MsgBody = body.Body; + TxHeader.ExtId = PROTOCAN_IdPack(&localId); + TxHeader.DLC = regsInPacket; + for(int i = 0; i < regsInPacket; i++) + { + canData[i] = coreData.Data[currentIndex + i]; + } + HAL_StatusTypeDef status = HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox); + if(status != HAL_OK) + { + return (PROTOCAN_StatusTypeDef)status; + } + regsRemaining -= regsInPacket; + currentIndex += regsInPacket; + } + break; + } + default: + { + return PROTOCAN_ERROR; + } + } + return PROTOCAN_OK; +} + +/** + * @brief Разбирает однокадровый запрос SETTINGS для текущего устройства. + * + * Body[15:8] содержит номер сборки, Body[7:0] — позицию. DLC=0 означает + * GET, DLC=8 с ненулевым ROM — WRITE/REPLACE, DLC=8 с нулями — CLEAR. + * Проверка физического датчика и сохранение EEPROM выполняются приложением в + * переопределённой функции ProtoCanMsgToSettings(). + */ +PROTOCAN_StatusTypeDef PROTOCAN_SettingsProcessing(struct RXMsg _rxMsg) +{ + ProtoCanSettingsMsg_t message; + uint8_t index; + uint8_t romIsZero = 1U; + + if((_rxMsg.eID.BitAll & PROTOCAN_SETTINGS_HEADER_MASK) + != PROTOCAN_SETTINGS_REQUEST_BASE_ID) + { + return PROTOCAN_ERROR; + } + + message.AssemblySerial = (uint8_t)((_rxMsg.eID.Fields.MsgBody >> 8U) & 0xFFU); + message.Position = (uint8_t)(_rxMsg.eID.Fields.MsgBody & 0xFFU); + message.Result = PROTOCAN_SETTINGS_RESULT_OK; + for(index = 0U; index < PROTOCAN_SETTINGS_ROM_SIZE; index++) + { + message.Rom[index] = 0U; + } + + if((_rxMsg.info.RTR != 0U) + || ((_rxMsg.DLC != 0U) && (_rxMsg.DLC != PROTOCAN_SETTINGS_ROM_SIZE))) + { + message.Operation = PROTOCAN_SETTINGS_INVALID; + message.Result = PROTOCAN_SETTINGS_RESULT_INVALID_DLC; + return PROTOCAN_SEND_SETTINGS_ERROR(message.AssemblySerial, message.Position, + message.Result); + } + if(_rxMsg.DLC == 0U) + { + message.Operation = PROTOCAN_SETTINGS_GET; + return ProtoCanMsgToSettings(&message); + } + + for(index = 0U; index < PROTOCAN_SETTINGS_ROM_SIZE; index++) + { + message.Rom[index] = _rxMsg.Data[index]; + if(message.Rom[index] != 0U) + { + romIsZero = 0U; + } + } + message.Operation = (romIsZero != 0U) ? PROTOCAN_SETTINGS_CLEAR + : PROTOCAN_SETTINGS_WRITE; + return ProtoCanMsgToSettings(&message); +} + +/** + * @brief Слабая точка подключения каталога датчиков приложения. + */ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToSettings( + const ProtoCanSettingsMsg_t *message) +{ + (void)message; + return PROTOCAN_ERROR; +} + +/** Отправляет один SETTINGS-кадр с Body из id и payload длиной 0..8. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS( + ProtoCanId_t id, const struct ProtoCanSettingsData data) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0U; + uint8_t canData[PROTOCAN_SETTINGS_ROM_SIZE] = {0U}; + uint8_t index; + + if((id.Fields.MsgType != PROTOCAN_MSGTYPE_SETTINGS) + || (data.DataCount > PROTOCAN_SETTINGS_ROM_SIZE) + || ((data.DataCount != 0U) && (data.Data == 0))) + { + return PROTOCAN_ERROR; + } + for(index = 0U; index < data.DataCount; index++) + { + canData[index] = data.Data[index]; + } + TxHeader.ExtId = PROTOCAN_IdPack(&id); + TxHeader.IDE = CAN_ID_EXT; + TxHeader.RTR = CAN_RTR_DATA; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.DLC = data.DataCount; + if(HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox) != HAL_OK) + { + return PROTOCAN_ERROR; + } + return PROTOCAN_OK; +} + +/** Отправляет успешный ответ SETTINGS с текущим ROM целевой локации. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS_RESPONSE(uint8_t assemblySerial, + uint8_t position, const uint8_t *rom) +{ + ProtoCanId_t id; + ProtoCanData_t data; + + if(rom == 0) + { + return PROTOCAN_ERROR; + } + id.BitAll = PROTOCAN_SETTINGS_RESPONSE_BASE_ID + | ((uint32_t)assemblySerial << 8U) | position; + data.SettingsData.Data = rom; + data.SettingsData.DataCount = PROTOCAN_SETTINGS_ROM_SIZE; + return PROTOCAN_SEND_SETTINGS(id, data.SettingsData); +} + +/** Отправляет однобайтовую ошибку SETTINGS с сохранением Body запроса. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS_ERROR(uint8_t assemblySerial, + uint8_t position, ProtoCanSettingsResultType result) +{ + ProtoCanId_t id; + ProtoCanData_t data; + const uint8_t error = (uint8_t)result; + + if(result == PROTOCAN_SETTINGS_RESULT_OK) + { + return PROTOCAN_ERROR; + } + id.BitAll = PROTOCAN_SETTINGS_RESPONSE_BASE_ID + | ((uint32_t)assemblySerial << 8U) | position; + data.SettingsData.Data = &error; + data.SettingsData.DataCount = 1U; + return PROTOCAN_SEND_SETTINGS(id, data.SettingsData); +} + +/** +* @brief Обработка и ответ на запрос общего адресного пространства. +* Формирует и передает сообщение с данными о статусе "GAS-XXXX", где XXXX — значение из сообщения _rxMsg. +* @param _rxMsg Структура входящего сообщения, содержит идентификатор и тело сообщения. +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToGeneralAddressSpace(struct RXMsg _rxMsg) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = _rxMsg.eID.BitAll; + tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + TxHeader.DLC = 8; + data[0] = 'G'; + data[1] = 'A'; + data[2] = 'S'; + data[3] = '-'; + for(int i = 0; i < 4; i++) + { + unsigned sym = (_rxMsg.eID.Fields.MsgBody >> (12 - (i * 4))) & 0xF; + if(sym >= 10) + { + data[4 + i] = sym % 10 + 'A'; + } + else + { + data[4 + i] = sym + '0'; + } + } + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); +} + +/** + * @brief Отправляет произвольный диапазон регистров в пакетах по, максимум, 4 регистра за раз через CAN. + * Функция разбивает передаваемый массив регистров на пакеты по 4 или меньше и отправляет их по отдельности. + * Для каждого пакета формируется уникальный MsgBody, равный адресу первого регистра в пакете. + * @param priority Приоритет сообщения CAN. ProtoCan_Priority_TypeDef + * @param regStartAdr Адрес первого регистра из всего массива. + * @param data Указатель на массив регистров для отправки. + * @param regCount Общее количество регистров для отправки. + * @return Возвращает статус отправки: HAL_OK при успехе, или код ошибки HAL_StatusTypeDef при сбое. + */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_GENERAL_ADDRESS_SPACE(ProtoCanId_t id, uint16_t regStartAdr, uint16_t *data, uint8_t regCount) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t canData[8]; + + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = id.BitAll; + + uint8_t regsRemaining = regCount; + uint8_t currentIndex = 0; + + while(regsRemaining > 0) + { + uint8_t regsInPacket = (regsRemaining > 4) ? 4 : regsRemaining; + + tmp_eID.Fields.MsgBody = regStartAdr + currentIndex; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + TxHeader.DLC = regsInPacket*2; + + for(int i = 0; i < regsInPacket; i++) + { + canData[(i * 2)] = LowByteOfWord(data[currentIndex + i]); + canData[(i * 2) + 1] = HighByteOfWord(data[currentIndex + i]); + } + + HAL_StatusTypeDef status = HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox); + + if(status != HAL_OK) + { + return (PROTOCAN_StatusTypeDef)status; + } + + regsRemaining -= regsInPacket; + currentIndex += regsInPacket; + } + return (PROTOCAN_StatusTypeDef)HAL_OK; +} + +/** +* @brief Обработка запроса на основе типа Modbus. +* В зависимости от типа сообщения, вызывает соответствующую функцию +* для отправки Modbus-запроса по CAN-шине. +* @param _rxMsg Структура входящего сообщения RXMsg. +* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка). +*/ +PROTOCAN_StatusTypeDef PROTOCAN_ModbusProcessing(struct RXMsg _rxMsg) +{ + switch(_rxMsg.eID.Fields.MsgType) + { + case PROTOCAN_MSGTYPE_MODBUS_COIL: + { + return ProtoCanMsgToModbusCoil(_rxMsg); + break; + } + case PROTOCAN_MSGTYPE_MODBUS_DISCRETE: + { + return ProtoCanMsgToModbusDiscrete(_rxMsg); + break; + } + case PROTOCAN_MSGTYPE_MODBUS_HOLDING: + { + return ProtoCanMsgToModbusHolding(_rxMsg); + break; + } + case PROTOCAN_MSGTYPE_MODBUS_INPUT: + { + return ProtoCanMsgToModbusInput(_rxMsg); + break; + } + default: + //ERROR + return PROTOCAN_ERROR; + break; + } + return PROTOCAN_ERROR; +} + +/** +* @brief __weak Функция отправки ответа на запрос Coil Modbus по CAN. +* Формирует CAN ответ на запрос Coil Modbus и отправляет его. +* @param _rxMsg Структура входящего сообщения RXMsg. +* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка). +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusCoil(struct RXMsg _rxMsg) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = _rxMsg.eID.BitAll; + tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + TxHeader.DLC = 8; + msgBodyModbusType msg; + msg.Body = _rxMsg.eID.Fields.MsgBody; + data[0] = 'M'; + data[1] = 'C'; + data[2] = ' '; + data[3] = 'S'; + data[4] = msg.Fields.StrAdr; + data[5] = ' '; + data[6] = 'C'; + data[7] = msg.Fields.RegCount; + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); +} + +/** +* @brief __weak Функция отправки ответа на запрос Discrete Modbus по CAN. +* Формирует CAN ответ на запрос Discrete Modbus и отправляет его. +* @param _rxMsg Структура входящего сообщения RXMsg. +* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка). +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusDiscrete(struct RXMsg _rxMsg) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = _rxMsg.eID.BitAll; + tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + TxHeader.DLC = 8; + msgBodyModbusType msg; + msg.Body = _rxMsg.eID.Fields.MsgBody; + data[0] = 'M'; + data[1] = 'D'; + data[2] = ' '; + data[3] = 'S'; + data[4] = msg.Fields.StrAdr; + data[5] = ' '; + data[6] = 'C'; + data[7] = msg.Fields.RegCount; + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); +} + +/** +* @brief __weak Функция отправки ответа на запрос Holding Modbus по CAN. +* Формирует CAN ответ на запрос Holding Modbus и отправляет его. +* @param _rxMsg Структура входящего сообщения RXMsg. +* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка). +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusHolding(struct RXMsg _rxMsg) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = _rxMsg.eID.BitAll; + tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + TxHeader.DLC = 8; + msgBodyModbusType msg; + msg.Body = _rxMsg.eID.Fields.MsgBody; + data[0] = 'M'; + data[1] = 'H'; + data[2] = ' '; + data[3] = 'S'; + data[4] = msg.Fields.StrAdr; + data[5] = ' '; + data[6] = 'C'; + data[7] = msg.Fields.RegCount; + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); +} + +/** +* @brief __weak Функция отправки ответа на запрос Input Modbus по CAN. +* Формирует CAN ответ на запрос Input Modbus и отправляет его. +* @param _rxMsg Структура входящего сообщения RXMsg. +* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка). +*/ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusInput(struct RXMsg _rxMsg) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = _rxMsg.eID.BitAll; + tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + TxHeader.DLC = 8; + msgBodyModbusType msg; + msg.Body = _rxMsg.eID.Fields.MsgBody; + data[0] = 'M'; + data[1] = 'I'; + data[2] = ' '; + data[3] = 'S'; + data[4] = msg.Fields.StrAdr; + data[5] = ' '; + data[6] = 'C'; + data[7] = msg.Fields.RegCount; + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); +} + +/** + * @brief Отправляет данные Modbus через CAN-шину в соответствии с типом сообщения. + * Эта функция формирует и отправляет Modbus-данные по CAN-шине, поддерживая + * различные типы сообщений: Coil, Discrete, Holding Register и Input Register. + * Для сообщений, превышающих 4 регистров, данные отправляются пакетами. + * @param id Идентификатор сообщения, содержащий тип сообщения и другую информацию. + * @param modbusData Структура с данными Modbus: адрес, количество регистров и сами данные. + * @return PROTOCAN_StatusTypeDef Статус операции. + * @details + * В зависимости от типа Modbus (coil/discrete или holding/input), формируется + * соответствующий CAN-пакет. Для coil/discrete отправляется один пакет, для holding/input — + * несколько, по 4 регистра в пакете. В случае ошибок при добавлении сообщения API HAL возвращается ошибка. + */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_MODBUS(const ProtoCanId_t id, const struct ProtoCanModbusData modbusData) +{ + ProtoCanId_t localId = id; + uint8_t msgType = localId.Fields.MsgType; + // Структура для отправки + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t canData[8]; + if(msgType == PROTOCAN_MSGTYPE_MODBUS_COIL || msgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE) + { + // Формируем body + msgBodyModbusType body; + body.Fields.StrAdr = modbusData.StrAdr; + body.Fields.RegCount = modbusData.RegCount; + // Устанавливаем коллизию ExtId с body.Body + localId.Fields.MsgBody = (body.Body & 0xFFFF); + // Обновляем ExtId + TxHeader.ExtId = PROTOCAN_IdPack(&localId); + // Остальные настройки + TxHeader.IDE = CAN_ID_EXT; + TxHeader.RTR = CAN_RTR_DATA; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.DLC = modbusData.RegCount % 8 + 1; + // Формируем CAN данные + canData[0] = LowByteOfWord(modbusData.Data[0]); + canData[1] = HighByteOfWord(modbusData.Data[0]); + if (HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox) != HAL_OK) + { + return PROTOCAN_ERROR; + } + } + else if(msgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || msgType == PROTOCAN_MSGTYPE_MODBUS_INPUT) + { + uint16_t* dataPtr = modbusData.Data; + unsigned totalRegs = modbusData.RegCount; + unsigned regsProcessed = 0; + unsigned startAddress = modbusData.StrAdr; + while (regsProcessed < totalRegs) + { + uint8_t regsInPacket = (totalRegs - regsProcessed) > 4 ? 4 : (totalRegs - regsProcessed); + // Формируем body + msgBodyModbusType body; + body.Fields.StrAdr = startAddress; + body.Fields.RegCount = regsInPacket; + // Обновляем ExtId + localId.Fields.MsgBody = (body.Body & 0xFFFF); + TxHeader.ExtId = PROTOCAN_IdPack(&localId); + TxHeader.IDE = CAN_ID_EXT; + TxHeader.RTR = CAN_RTR_DATA; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.DLC = regsInPacket * 2; + // Добавляем регистры + for (int i = 0; i < regsInPacket; i++) + { + canData[i * 2] = LowByteOfWord(dataPtr[regsProcessed + i]); + canData[i * 2 + 1] = HighByteOfWord(dataPtr[regsProcessed + i]); + } + // Отправляем сообщение + if(HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox) != HAL_OK) + { + return PROTOCAN_ERROR; + } + regsProcessed += regsInPacket; + startAddress += regsInPacket; + } + } + else + { + return PROTOCAN_ERROR; // Неверный тип сообщения + } + return PROTOCAN_OK; +} + +/** +* @brief __weak Функция отправки сообщения об ошибке. +* Посылает CAN сообщение с кодом ошибки. Используется, когда необходимо оповестить о неуспешной операции. +* @param _rxMsg Структура входящего сообщения RXMsg, данными которого формируется ответ-ошибка. +* @return HAL_StatusTypeDef Статус выполнения команды CAN. +*/ +__weak PROTOCAN_StatusTypeDef PROTOCAN_RequestError(struct RXMsg _rxMsg) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + ProtoCanId_t tmp_eID; + tmp_eID.BitAll = _rxMsg.eID.BitAll; + tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + tmp_eID.Fields.MsgType = PROTOCAN_MSGTYPE_ERROR; + msgBodyErrorType msg; + msg.Fields.ErrorCode = 0xFF; + msg.Fields.Info = 0; + tmp_eID.Fields.MsgBody = msg.Body; + TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID); + TxHeader.DLC = 0; + return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); +} + +/** + * @brief Отправляет сообщение об ошибке по CAN-шине. + * Эта функция формирует и отправляет сообщение об ошибке с кодом и информацией, + * объединёнными в поле MsgBody идентификатора сообщения, и передаёт его через CAN. + * @param id Идентификатор сообщения, содержащий базовую информацию. + * @param errorData Структура с данными ошибки: код и дополнительная информация. + * @return PROTOCAN_StatusTypeDef Статус операции. + * @details + * В качестве данных передаётся 2 байта: старшие биты — информация об ошибке, младшие — код ошибки. + * Размер данных в CANFrames устанавливается нулевым, так как все данные инкапсулированы в MsgBody. + */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_ERROR(const ProtoCanId_t id, const struct ProtoCanErrorData errorData) +{ + ProtoCanId_t localId = id; + // Структура для отправки + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + uint8_t canData[8]; + localId.Fields.MsgBody = ((errorData.Info << 8) | errorData.Code); + // Обновляем ExtId + TxHeader.ExtId = PROTOCAN_IdPack(&localId); + // Остальные настройки + TxHeader.IDE = CAN_ID_EXT; + TxHeader.RTR = CAN_RTR_DATA; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.DLC = 0; + if(HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox) != HAL_OK) + { + return PROTOCAN_ERROR; + } + return PROTOCAN_OK; +} + +/** +* @brief Записывает полученное сообщение CAN в буфер rxMsg. +* Копирует параметры расширенного ID, типы, длину данных и сам данные в указанный элемент буфера. +* @param tmp_eID Расширенный ID сообщения. +* @param tmp_IDE Тип идентификатора (например, CAN_ID_EXT). +* @param tmp_RTR Тип запроса (например, CAN_RTR_DATA). +* @param tmp_DLC Длина данных в сообщении. +* @param tmp_DATA Указатель на массив данных, полученных из CAN. +* @param tmp_LastStep Индекс позиции в буфере, куда записывать сообщение. +*/ +void TakeRxMsgToBuffer(ProtoCanId_t tmp_eID, uint32_t tmp_IDE, uint32_t tmp_RTR, uint32_t tmp_DLC, uint8_t *tmp_DATA, uint16_t tmp_LastStep) +{ + if(tmp_DLC > 8u) + { + tmp_DLC = 8u; + } + rxMsg[tmp_LastStep].eID.BitAll = tmp_eID.BitAll; + rxMsg[tmp_LastStep].info.EXT = (tmp_IDE == CAN_ID_EXT) ? 1U : 0U; + rxMsg[tmp_LastStep].info.RTR = (tmp_RTR == CAN_RTR_REMOTE) ? 1U : 0U; + rxMsg[tmp_LastStep].DLC = tmp_DLC; + for(uint32_t i = 0; i < tmp_DLC; i++) + { + rxMsg[tmp_LastStep].Data[i] = tmp_DATA[i]; + } + /* Слот заполнен - только теперь публикуем его. Без барьера компилятору + разрешено поднять запись LastStep выше записей в rxMsg, и обработчик + увидит наполовину заполненное сообщение. */ + __DMB(); + LastStep = tmp_LastStep; +} + +/** +* @brief Обработка сообщений, ожидающих в FIFO, по прерыванию HAL_CAN_RxFifo0MsgPendingCallback. +* Читает все сообщения из FIFO, проверяет их тип, обновляет статус устройств сети или сохраняет сообщение в буфер. +* @param hcan Указатель на структуру управления CAN-Hardware. +*/ +void ProtoCanRxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) +{ + //Обработка всех сообщений в FIFO + CAN_RxHeaderTypeDef RxHeader; + uint8_t RCAN_Data[8]; + while(HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RCAN_Data) == HAL_OK) + { + //Расширенный ID + if(RxHeader.IDE == CAN_ID_EXT) + { + ProtoCanId_t ExtID_Of_RX_MSG; + PROTOCAN_IdUnpack(RxHeader.ExtId, &ExtID_Of_RX_MSG); + //Полученное сообщение - пульс устройств в сети. + //Место в буфере не занимает, поэтому проверяется до проверки переполнения. + if(ExtID_Of_RX_MSG.Fields.MsgType == PROTOCAN_MSGTYPE_PULSE) + { + Device_on_the_Network[ExtID_Of_RX_MSG.Fields.DeviceType][ExtID_Of_RX_MSG.Fields.DeviceID].Status = 1u; + Device_on_the_Network[ExtID_Of_RX_MSG.Fields.DeviceType][ExtID_Of_RX_MSG.Fields.DeviceID].TimeFromLastPulse = 0; + //continue, а не return: остаток FIFO нужно вычитать в этом же вызове, + //иначе прерывание по непустому FIFO сработает повторно. + continue; + } + //Полезная ёмкость буфера - (PROTOCAN_RX_BUFFER_SIZE - 1): при большем + //заполнении счётчик AvailableCanRxMsg() переполняется и даёт 0 ("пусто"). + if(AvailableCanRxMsg() >= (PROTOCAN_RX_BUFFER_SIZE - 1u)) + { + //Буфер переполнен, кадр отбрасывается. Кадр уже извлечён из + //аппаратного FIFO, поэтому продолжаем вычитывать остаток. + continue; + } + uint16_t tmp_LastStep = (uint16_t)((LastStep + 1u) % PROTOCAN_RX_BUFFER_SIZE); + TakeRxMsgToBuffer(ExtID_Of_RX_MSG, RxHeader.IDE, RxHeader.RTR, RxHeader.DLC, RCAN_Data, tmp_LastStep); + } + } +} + +/** +* @brief Функция отправки пульса устройства. +* @details Пульс устройства. Есть возможность отключить пульс по запросу. +*/ +void ProtoCanPulseCallback(TIM_HandleTypeDef *htim) +{ + if(ControlFlags.IsPulse) + { + static unsigned PulseStage = 0; + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0; + ProtoCanId_t currentID; + currentID.BitAll = 0; + currentID.Fields.Priority = PROTOCAN_PRIORITY_STANDARD; + currentID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; + currentID.Fields.DeviceType = CURRENT_TYPE_DEVICE; + currentID.Fields.DeviceID = CURRENT_ID_DEVICE; + currentID.Fields.MsgType = PROTOCAN_MSGTYPE_PULSE; + currentID.Fields.MsgBody = 0; + TxHeader.ExtId = PROTOCAN_IdPack(¤tID); + uint8_t data[8]; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.RTR = CAN_RTR_DATA; + TxHeader.DLC = 1; + PulseStage++; + if(PulseStage > 0xFF) + { + PulseStage = 0; + } + data[0] = PulseStage; + HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox); + } +} + +/** +* @brief Функция установки в RTC полученной даты/время из запроса. +* @param uint8_t *data - Байтовый массив, 7 элементов. [0] - Часы. [1] - Минуты. [2] - Секунды. [3] - Год. [4] - Месяц. [5] - Дата. [6] - День недели. +*/ +void PROTOCAN_RTC_SYNC(uint8_t *data) +{ + __HAL_RTC_WRITEPROTECTION_DISABLE(_HRTC); + RTC_TimeTypeDef sTime = {0}; + RTC_DateTypeDef DateToUpdate = {0}; + sTime.Hours = data[0]; + sTime.Minutes = data[1]; + sTime.Seconds = data[2]; + if(HAL_RTC_SetTime(_HRTC, &sTime, RTC_FORMAT_BIN) != HAL_OK) + { + Error_Handler(); + } + DateToUpdate.Year = data[3]; + DateToUpdate.Month = data[4]; + DateToUpdate.Date = data[5]; + DateToUpdate.WeekDay = data[6]; + if(HAL_RTC_SetDate(_HRTC, &DateToUpdate, RTC_FORMAT_BIN) != HAL_OK) + { + Error_Handler(); + } + __HAL_RTC_WRITEPROTECTION_ENABLE(_HRTC); +} + +/** +* @brief Настройка CAN-Фильтра. +* На основе переданных ID и маски настраивает фильтр CAN для приема только определённых сообщений. +* @param filterBank Номер фильтра (обычно 0..27). +* @param idFilter 32-битный идентификатор, используемый для фильтрации. +* @param idMask 32-битная маска, определяющая биты, проверяемые фильтром. +*/ +void PROTOCAN_CONFIG_FILTER(uint8_t filterBank, uint32_t idFilter, uint32_t idMask) +{ + CAN_FilterTypeDef canFilterConfig; + canFilterConfig.FilterBank = filterBank; + canFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; + canFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; + canFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; + canFilterConfig.FilterActivation = ENABLE; + canFilterConfig.SlaveStartFilterBank = 14; + #define CAN_IDE_32 0b00000100 // Для 32-х битного масштаба + // Разбиваем 32-битный ID и маску для фильтрации на High и Low 16 бит + canFilterConfig.FilterIdHigh = (uint16_t)(((idFilter) >> 13)); // верхние 16 бит + canFilterConfig.FilterIdLow = (uint16_t)((((idFilter) << 3)) | CAN_IDE_32); // низкие 16 бит, + canFilterConfig.FilterMaskIdHigh = (uint16_t)(((idMask) >> 13)); + canFilterConfig.FilterMaskIdLow = (uint16_t)((((idMask) << 3)) | CAN_IDE_32); + if(HAL_CAN_ConfigFilter(_HCAN, &canFilterConfig) != HAL_OK) + { + Error_Handler(); + } +} + +/** +* @brief Конфигурирует три фильтра CAN для различных типов сообщений. +*/ +void PROTOCAN_FILTERS() +{ + #define BIT27 (1UL << 27) + #define BITS_20_23_MASK (0xFUL << 20) // 4 бита (20..23) + #define BITS_24_26_MASK (0x7UL << 24) // 3 бита (24..26) + + /* Адресованный запрос приходит от ПМ, поэтому Route (бит 27) равен нулю. */ + uint32_t filter1_id = (CURRENT_TYPE_DEVICE << 24) | (CURRENT_ID_DEVICE << 20); + uint32_t filter1_mask = BIT27 | BITS_24_26_MASK | BITS_20_23_MASK; // все проверяемые биты + + #define BITS_16_19_MASK (0xFUL << 16) + + uint32_t filter2_id = PROTOCAN_MSGTYPE_BROADCAST << 16; + uint32_t filter2_mask = BITS_16_19_MASK; + + uint32_t filter3_id = PROTOCAN_MSGTYPE_PULSE << 16; + uint32_t filter3_mask = BITS_16_19_MASK; + + // Первый фильтр: Route=0 (от ПМ), DeviceType и DeviceID текущего устройства. + PROTOCAN_CONFIG_FILTER(0, filter1_id, filter1_mask); + + // Второй фильтр: проверяем 16-19 биты, равны PROTOCAN_MSGTYPE_BROADCASTE + PROTOCAN_CONFIG_FILTER(1, filter2_id, filter2_mask); + + // Третий фильтр: проверяем 16-19 биты, равны PROTOCAN_MSGTYPE_PULSE + PROTOCAN_CONFIG_FILTER(2, filter3_id, filter3_mask); +} + +/** + * @brief Отправляет CAN-сообщение в зависимости от типа сообщения. + * Эта функция анализирует тип сообщения в идентификаторе и вызывает + * соответствующую функцию отправки. В текущей реализации поддерживается только + * тип PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE. + * @param id Идентификатор CAN-сообщения, содержащий информацию о типе. + * @param data Структура с данными, которые необходимо отправить. Зависит от типа сообщения. + * @return Статус выполнения операции. + */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND(ProtoCanId_t id, ProtoCanData_t data) +{ + switch(id.Fields.MsgType) + { + case PROTOCAN_MSGTYPE_BROADCAST: + case PROTOCAN_MSGTYPE_DISCRETE: + case PROTOCAN_MSGTYPE_ANALOG: + { + return PROTOCAN_SEND_CORE(id, data.CoreData); + break; + } + case PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE: + { + /* Если есть регистры для отправки, указатель data должен быть валиден. */ + if((data.GeneralAddressSpaceData.RegCount > 0U) && data.GeneralAddressSpaceData.Data == 0) + { + return PROTOCAN_ERROR; + } + return PROTOCAN_SEND_GENERAL_ADDRESS_SPACE(id, data.GeneralAddressSpaceData.RegStartAdr, + data.GeneralAddressSpaceData.Data, + data.GeneralAddressSpaceData.RegCount); + } + case PROTOCAN_MSGTYPE_MODBUS_COIL: + case PROTOCAN_MSGTYPE_MODBUS_DISCRETE: + case PROTOCAN_MSGTYPE_MODBUS_HOLDING: + case PROTOCAN_MSGTYPE_MODBUS_INPUT: + { + return PROTOCAN_SEND_MODBUS(id, data.ModbusData); + break; + } + case PROTOCAN_MSGTYPE_ERROR: + { + return PROTOCAN_SEND_ERROR(id, data.ErrorData); + break; + } + case PROTOCAN_MSGTYPE_SETTINGS: + { + return PROTOCAN_SEND_SETTINGS(id, data.SettingsData); + } + default: + return PROTOCAN_ERROR; + } + return PROTOCAN_ERROR; +} diff --git a/c/set-protocol/ports/stm32-bxcan/protocan.h b/c/set-protocol/ports/stm32-bxcan/protocan.h new file mode 100644 index 0000000..ea07e45 --- /dev/null +++ b/c/set-protocol/ports/stm32-bxcan/protocan.h @@ -0,0 +1,427 @@ +//тест + +#ifndef _protocan +#define _protocan +#include "main.h" +#include "can.h" +#include "pcan_id.h" + +#define _DEMO + +#define ONLINE 1; +#define OFFLINE 0; + +#define ID_MAIN_DEVICE 0x000 +#define CURRENT_TYPE_DEVICE 0b111 +#define CURRENT_ID_DEVICE 0b1111 + +#define PROTOCAN_RX_BUFFER_SIZE 128 + +#define SensorToModbusRegister(SensorType, SensorID) SensorType << 11 | SensorID +#define HighByteOfWord(WORD) (WORD>>8)&0xFF +#define LowByteOfWord(WORD) WORD&0xFF + +/** + * @brief Статус инициализации протокола CAN. + * + * Это перечисление описывает возможные статусы после попытки инициализации. + */ +typedef enum +{ + PROTOCAN_INIT_OK = 0x00U, /**< Инициализация прошла успешно. */ + PROTOCAN_INIT_HCAN_ERROR = 0x01U, /**< Ошибка при инициализации модуля CAN. */ + PROTOCAN_INIT_HRTC_ERROR = 0x02U, /**< Ошибка при инициализации модуля RTC. */ + PROTOCAN_INIT_TIM_ERROR = 0x04U /**< Ошибка при инициализации таймера. */ +} PROTOCAN_INIT_StatusTypeDef; + +/** + * @brief Статус выполнения операции протокола CAN. + * + * Это перечисление описывает возможные результаты выполнения функций протокола. + */ +typedef enum +{ + PROTOCAN_OK = 0x00U, /**< Операция прошла успешно. */ + PROTOCAN_ERROR = 0x01U, /**< Обнаружена ошибка во время выполнения. */ + PROTOCAN_BUSY = 0x02U, /**< Канал занят, выполнение невозможно в данный момент. */ + PROTOCAN_TIMEOUT = 0x03U /**< Истек таймаут ожидания. */ +} PROTOCAN_StatusTypeDef; + +/** + * @brief Уровень приоритета CAN-сообщения. + * + * Это перечисление определяет важность и приоритет обработки сообщения. + */ +typedef enum { + PROTOCAN_PRIORITY_CRITICAL = 0, /**< Критический приоритет, рекомендуется обработка без задержек. */ + PROTOCAN_PRIORITY_STANDARD = 1 /**< Стандартный приоритет, менее важный. */ +} ProtoCanPriorityType; + +/** + * @brief Маршрут CAN-сообщения. + * + * Это перечисление задает направление маршрута отправки сообщения. + */ +typedef enum { + PROTOCAN_ROUTE_FROM_PM = 0, /**< Сообщение отправляется от управляющего модуля (PM). */ + PROTOCAN_ROUTE_FROM_DEVICE = 1 /**< Сообщение отправляется от устройства (Device). */ +} ProtoCanRouteType; + +/** + * @brief Типы сообщений CAN. + * + * Перечисление определяет различные типы сообщений, используемые в протоколе. + */ +typedef enum { + PROTOCAN_MSGTYPE_BROADCAST = 0b0000, /**< Широковещательное сообщение. */ + PROTOCAN_MSGTYPE_DISCRETE = 0b0001, /**< Дискретное цифровое сообщение. */ + PROTOCAN_MSGTYPE_ANALOG = 0b0010, /**< Аналоговое сообщение. */ + PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE = 0b0011, /**< Сообщение общего адресного пространства. */ + PROTOCAN_MSGTYPE_MODBUS_COIL = 0b0100, /**< Сообщение Modbus - Coil. */ + PROTOCAN_MSGTYPE_MODBUS_DISCRETE = 0b0101, /**< Сообщение Modbus - Discrete. */ + PROTOCAN_MSGTYPE_MODBUS_HOLDING = 0b0110, /**< Сообщение Modbus - Holding. */ + PROTOCAN_MSGTYPE_MODBUS_INPUT = 0b0111, /**< Сообщение Modbus - Input. */ + PROTOCAN_MSGTYPE_ERROR = 0b1000, /**< Сообщение об ошибке. */ + PROTOCAN_MSGTYPE_BOOT_CONTROL = 0b1001, /**< Команды ProtoCAN Boot. */ + PROTOCAN_MSGTYPE_BOOT_DATA_A = 0b1010, /**< Блок образа для слота A. */ + PROTOCAN_MSGTYPE_BOOT_DATA_B = 0b1011, /**< Блок образа для слота B. */ + PROTOCAN_MSGTYPE_BOOT_STATUS = 0b1100, /**< Статус команды загрузчика. */ + PROTOCAN_MSGTYPE_BOOT_DISCOVERY = 0b1101, /**< Идентификация загрузчика/приложения. */ + PROTOCAN_MSGTYPE_SETTINGS = 0b1110, /**< Привязка датчика к локации. */ + PROTOCAN_MSGTYPE_PULSE = 0b1111, /**< Импульсное сообщение. */ +}ProtoCanMsgType; + +/** Длина ROM-кода 1-Wire и payload успешного ответа SETTINGS. */ +#define PROTOCAN_SETTINGS_ROM_SIZE 8U + +/** Фиксированные базы выбранного DeviceType=0x7, DeviceID=0xF, MsgType=0xE. */ +#define PROTOCAN_SETTINGS_REQUEST_BASE_ID 0x17FE0000UL +#define PROTOCAN_SETTINGS_RESPONSE_BASE_ID 0x1FFE0000UL +#define PROTOCAN_SETTINGS_HEADER_MASK 0x1FFF0000UL + +/** Операции SETTINGS, определяемые по направлению, DLC и payload. */ +typedef enum { + PROTOCAN_SETTINGS_GET = 0, + PROTOCAN_SETTINGS_WRITE = 1, + PROTOCAN_SETTINGS_CLEAR = 2, + PROTOCAN_SETTINGS_RESPONSE = 3, + PROTOCAN_SETTINGS_ERROR = 4, + PROTOCAN_SETTINGS_INVALID = 5 +} ProtoCanSettingsOperationType; + +/** Коды результата SETTINGS в ответе с DLC=1. */ +typedef enum { + PROTOCAN_SETTINGS_RESULT_OK = 0x00U, + PROTOCAN_SETTINGS_RESULT_INVALID_DLC = 0x01U, + PROTOCAN_SETTINGS_RESULT_INVALID_ROM_CRC = 0x02U, + PROTOCAN_SETTINGS_RESULT_NOT_FOUND = 0x03U, + PROTOCAN_SETTINGS_RESULT_ALREADY_ASSIGNED = 0x04U, + PROTOCAN_SETTINGS_RESULT_EEPROM_ERROR = 0x05U, + PROTOCAN_SETTINGS_RESULT_INVALID_LOCATION = 0x06U, + PROTOCAN_SETTINGS_RESULT_BUSY = 0x07U +} ProtoCanSettingsResultType; + +/** Разобранная команда SETTINGS с локацией и ROM. */ +typedef struct { + ProtoCanSettingsOperationType Operation; + uint8_t AssemblySerial; /**< Body[15:8], номер сборки Z. */ + uint8_t Position; /**< Body[7:0], позиция Y. */ + ProtoCanSettingsResultType Result; + uint8_t Rom[PROTOCAN_SETTINGS_ROM_SIZE]; +} ProtoCanSettingsMsg_t; + +/** + * @brief Типы широковещательных сообщений CAN. + * + * Перечисление задает различные типы широковещательных команд, используемых в протоколе. + */ +typedef enum { + PROTOCAN_BROADCAST_STATUS = 0b000000000000, /**< Статус устройства. */ + PROTOCAN_BROADCAST_ONOFF = 0b000000000001, /**< Включение/выключение устройства. */ + PROTOCAN_BROADCAST_RESTARTDEVICE = 0b000000000010, /**< Перезагрузка устройства. */ + PROTOCAN_BROADCAST_RTCSETUP = 0b000000000011, /**< Настройка RTC (часы реального времени). */ + PROTOCAN_BROADCAST_END = 0b111111111111 /**< Конец диапазона широковещательных сообщений. */ +}ProtoCanBroadcastType; + +/** + * @brief Структура и представление широковещательного сообщения CAN. + * + * Объединение позволяет обращаться к данным сообщения либо как к отдельным полям, либо как к целому 16-битному значению. + */ +typedef union{ + struct msgBody_Broadcast{ + unsigned Body:4; /**< Поле для дополнительных данных или кода задачи (4 бита). */ + ProtoCanBroadcastType Type:12; /**< Тип широковещательного сообщения (12 бит). */ + } Fields; /**< Структура с разбивкой по полям. */ + unsigned Body; /**< Общее 16-битное представление сообщения. */ +} msgBodyBroadcastType; + +/** + * @brief Типы дискретных сообщений CAN. + * + * Перечисление задает различные виды дискретных команд или событий. + */ +typedef enum { + PROTOCAN_DISCRETE_ACCIDENT = 0b0000, /**< Аварийное состояние. */ + PROTOCAN_DISCRETE_WARNING = 0b0001, /**< Предупредительный сигнал. */ + PROTOCAN_DISCRETE_CONTROL_SIGNALS = 0b0010, /**< Управляющие сигналы. */ + PROTOCAN_DISCRETE_FLAGS = 0b0011, /**< Флаги состояния. */ + PROTOCAN_DISCRETE_RESET = 0b0100, /**< Сброс устройства. */ + PROTOCAN_DISCRETE_CHANGE_MODE = 0b0101, /**< Смена режима работы. */ + PROTOCAN_DISCRETE_REQUEST_LIST_OF_PARAMETERS = 0b0110, /**< Запрос списка параметров. */ + PROTOCAN_DISCRETE_END = 0b1111 /**< Конец диапазона дискретных сообщений. */ +}ProtoCanDiscreteType; + +/** + * @brief Структура и представление дискретного сообщения CAN. + * + * Объединение позволяет обращаться к данным сообщения как к отдельным полям или как к целому 16-битному значению. + */ +typedef union{ + struct msgBody_Discrete{ + unsigned Body:12; /**< Поле данных или кода (12 бит). */ + ProtoCanDiscreteType Type:4; /**< Тип дискретного сообщения (4 бита). */ + } Fields; /**< Структура с разбивкой по полям. */ + unsigned Body; /**< Общее 16-битное представление сообщения. */ +} msgBodyDiscreteType; + +/** + * @brief Типы аналоговых сообщений CAN. + * + * Перечисление определяет различные типы аналоговых данных или команд. + */ +typedef enum { + PROTOCAN_ANALOG_UNIVERSAL = 0b0000, /**< Универсальный тип. */ + PROTOCAN_ANALOG_SETTINGS = 0b0001, /**< Настройки. */ + PROTOCAN_ANALOG_U = 0b0010, /**< Напряжение (U). */ + PROTOCAN_ANALOG_I = 0b0011, /**< Ток (I). */ + PROTOCAN_ANALOG_T = 0b0100, /**< Температура (T). */ + PROTOCAN_ANALOG_END = 0b1111 /**< Конец диапазона типов. */ +}ProtoCanAnalogType; + +/** + * @brief Структура и представление аналогового сообщения CAN. + * + * Объединение позволяет обращаться к данным сообщения как к отдельным полям или как к целому 16-битному значению. + */ +typedef union{ + struct msgBody_Analog{ + unsigned SensorID:12; /**< Идентификатор датчика (12 бит). */ + ProtoCanAnalogType Type:4; /**< Тип аналоговых данных (4 бита). */ + } Fields; /**< Структура с разбивкой по полям. */ + unsigned Body; /**< Общее 16-битное представление сообщения. */ +}msgBodyAnalogType; + +/** + * @brief Структура и представление сообщения Modbus CAN. + * + * Объединение позволяет обращаться к данным как к отдельным полям или как к целому 16-битному значению. + */ +typedef union{ + struct msgBody_Modbus{ + unsigned RegCount:4; /**< Количество регистров (4 бита). */ + unsigned StrAdr:12; /**< Адрес строки или регистра (12 бит). */ + } Fields; /**< Поля структуры сообщения Modbus. */ + unsigned Body; /**< Общее 16-битное представление сообщения. */ +} msgBodyModbusType; + +/** + * @brief Структура и представление сообщения об ошибке CAN. + * + * Объединение позволяет обращаться к данным как к отдельным полям или как к целому 16-битному значению. + */ +typedef union{ + struct msgBody_Error{ + unsigned ErrorCode:8; /**< Код ошибки (8 бит). */ + unsigned Info:8; /**< Дополнительная информация (8 бит). */ + } Fields; /**< Поля ошибки. */ + unsigned Body; /**< Общее 16-битное представление сообщения об ошибке. */ +} msgBodyErrorType; + +struct controlflags{ + unsigned IsPulse:1; + unsigned IsRtrMode:1; +}; + +/** + * @brief Поля идентификатора протокола CAN. + * + * Объединение позволяет обращаться к различным полям идентификатора, а также как к 29-битному целому числу. + */ +typedef union{ + struct protoCanIdFields{ + unsigned MsgBody:16; /**< Поле сообщения (16 бит). */ + ProtoCanMsgType MsgType:4; /**< Тип сообщения (4 бита). */ + unsigned DeviceID:4; /**< Идентификатор устройства (4 бита). */ + unsigned DeviceType:3; /**< Тип устройства (3 бита). */ + ProtoCanRouteType Route:1; /**< Маршрут (1 бит). */ + ProtoCanPriorityType Priority:1; /**< Приоритет (1 бит). */ + } Fields; /**< Структура с разбивкой по полям. */ + unsigned int BitAll:29; /**< Общее представление 29-битного идентификатора. */ +} ProtoCanId_t; + +/** + * Переходная граница legacy API -> переносимый формат линии. + * + * Прикладной код SETCAN пока может заполнять исторические Fields, но CAN ID + * всегда собирается и разбирается общим pcan_id без зависимости от порядка + * битовых полей конкретного компилятора. + */ +static inline uint32_t PROTOCAN_IdPack(const ProtoCanId_t *legacy) +{ + pcan_id_t shared; + shared.msg_body = (uint16_t)legacy->Fields.MsgBody; + shared.msg_type = (uint8_t)legacy->Fields.MsgType; + shared.device_id = (uint8_t)legacy->Fields.DeviceID; + shared.device_type = (uint8_t)legacy->Fields.DeviceType; + shared.route = (uint8_t)legacy->Fields.Route; + shared.priority = (uint8_t)legacy->Fields.Priority; + return pcan_id_pack(&shared); +} + +static inline void PROTOCAN_IdUnpack(uint32_t raw, ProtoCanId_t *legacy) +{ + pcan_id_t shared; + pcan_id_unpack(raw, &shared); + legacy->BitAll = 0U; + legacy->Fields.MsgBody = shared.msg_body; + legacy->Fields.MsgType = (ProtoCanMsgType)shared.msg_type; + legacy->Fields.DeviceID = shared.device_id; + legacy->Fields.DeviceType = shared.device_type; + legacy->Fields.Route = (ProtoCanRouteType)shared.route; + legacy->Fields.Priority = (ProtoCanPriorityType)shared.priority; +} + +/** + * @brief Структура для общего адресного пространства CAN. + * + * Используется для хранения данных о регистре и его содержимом. + */ +typedef struct{ + struct ProtoCanCoreData{ + uint16_t Type; + uint16_t Body; + uint8_t *Data; + unsigned DataCount; + } CoreData; + struct ProtoCanGeneralAddressSpaceData{ + uint16_t RegStartAdr; /**< Начальный адрес регистров. */ + uint16_t *Data; /**< Указатель на массив данных. */ + unsigned RegCount; /**< Количество регистров. */ + } GeneralAddressSpaceData; + struct ProtoCanModbusData{ + uint16_t StrAdr; + uint16_t *Data; + unsigned RegCount:4; + } ModbusData; + struct ProtoCanErrorData{ + uint16_t Info; + uint16_t Code; + } ErrorData; + struct ProtoCanSettingsData{ + const uint8_t *Data; + uint8_t DataCount; + } SettingsData; +} ProtoCanData_t; + +/** + * @brief Структура данных устройства CAN. + * + * Включает статус, идентификатор, таймер последнего импульса и шаг последнего импульса. + */ +typedef struct{ + unsigned Status:1; /**< Статус устройства (флаг). */ + ProtoCanId_t Id; /**< Идентификатор CAN. */ + unsigned TimeFromLastPulse; /**< Время с последнего импульса (мс). */ + unsigned LastPulseStep; /**< Шаг последнего импульса. */ +} ProtoCanDevice_t; + +/** + * @brief Структура для хранения сообщения RXCAN. + */ +struct RXMsg { + /** + * @brief Структура, содержащая флаги информации. + */ + struct INFO { + unsigned EXT : 1; /**< Бит расширенного идентификатора (EXT). */ + unsigned RTR : 1; /**< Бит запроса на передачу (RTR). */ + } info; /**< Информационные флаги сообщения. */ + ProtoCanId_t eID; /**< Идентификатор CAN-сообщения. */ + uint16_t DLC; /**< Длина данных (Data Length Code). */ + uint8_t Data[8]; /**< Массив данных (до 8 байт). */ +}; + +uint16_t AvailableCanRxMsg(void); + +PROTOCAN_INIT_StatusTypeDef PROTOCAN_INIT(CAN_HandleTypeDef *tmp_hcan, + RTC_HandleTypeDef *tmp_hrtc, + TIM_HandleTypeDef *tmp_tim); +void PROTOCAN_DEINIT(uint8_t stage); +void PROTOCAN_FILTERS(void); +void PROTOCAN_LoopProcessRxMsgs(void); +PROTOCAN_StatusTypeDef PROTOCAN_ProcessAllRxMsgs(void); +PROTOCAN_StatusTypeDef PROTOCAN_ProcessSingleRxMsg(void); +PROTOCAN_StatusTypeDef PROTOCAN_SEND(ProtoCanId_t id, ProtoCanData_t data); + +void ProtoCanPulseCallback(TIM_HandleTypeDef *htim); +void ProtoCanRxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan); + +PROTOCAN_StatusTypeDef PROTOCAN_BroadcastProcessing(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastStatus(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastOnOff(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastRestart(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastRtcSetup(struct RXMsg _rxMsg); + +PROTOCAN_StatusTypeDef PROTOCAN_DiscreticProcessing(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteAccident(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteWarning(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteControlSignals(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteFlags(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteReset(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteChangeMode(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteRequestListOfParameters(struct RXMsg _rxMsg); + +PROTOCAN_StatusTypeDef PROTOCAN_AnalogProcessing(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogUniversal(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogSettings(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogUSens(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogISens(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogTSens(struct RXMsg _rxMsg); + +/** Разбирает запрос SETTINGS и вызывает прикладной обработчик. */ +PROTOCAN_StatusTypeDef PROTOCAN_SettingsProcessing(struct RXMsg _rxMsg); + +/** + * Прикладной обработчик GET/WRITE/CLEAR. Приложение должно переопределить + * слабую реализацию и после проверки датчика/EEPROM отправить ответ или ошибку. + */ +PROTOCAN_StatusTypeDef ProtoCanMsgToSettings(const ProtoCanSettingsMsg_t *message); + +/** Отправляет произвольный SETTINGS-кадр с уже заполненным Body. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS(ProtoCanId_t id, + const struct ProtoCanSettingsData data); + +/** Отправляет успешный ответ 0x1FFEZZYY, DLC=8, с текущим ROM локации. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS_RESPONSE(uint8_t assemblySerial, + uint8_t position, + const uint8_t *rom); + +/** Отправляет ошибку 0x1FFEZZYY, DLC=1, Data[0]=result. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS_ERROR(uint8_t assemblySerial, + uint8_t position, + ProtoCanSettingsResultType result); + +PROTOCAN_StatusTypeDef ProtoCanMsgToGeneralAddressSpace(struct RXMsg _rxMsg); + +PROTOCAN_StatusTypeDef PROTOCAN_RequestError(struct RXMsg _rxMsg); + +PROTOCAN_StatusTypeDef PROTOCAN_ModbusProcessing(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToModbusCoil(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToModbusDiscrete(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToModbusHolding(struct RXMsg _rxMsg); +PROTOCAN_StatusTypeDef ProtoCanMsgToModbusInput(struct RXMsg _rxMsg); + +void PROTOCAN_RTC_SYNC(uint8_t *data); + +#endif diff --git a/doc/setcan/README.md b/doc/setcan/README.md index 0864e80..b50387b 100644 --- a/doc/setcan/README.md +++ b/doc/setcan/README.md @@ -10,12 +10,16 @@ |---|---| | Однокадровые SETTINGS и bxCAN STM32F1 | [`c/can-sensor`](../../c/can-sensor) | | Единый SET protocol v2 | [`c/set-protocol`](../../c/set-protocol) | -| Транспорт ProtoCAN | [`c/protocan-transport`](../../c/protocan-transport) | +| Транспорт ProtoCAN | [`c/set-protocol`](../../c/set-protocol) | +| STM32 HAL bxCAN/RTC/TIM, API `PROTOCAN_*` | [`c/set-protocol/ports/stm32-bxcan`](../../c/set-protocol/ports/stm32-bxcan/README.md) | | CAN-загрузчик | [`c/protocan-boot`](../../c/protocan-boot) | Файлы `Inc/protocan.h` и `Src/protocan.c`, упомянутые в историческом -[руководстве](index.html), больше не являются подключаемым исходным кодом. -В новых проектах используются нормализованные модули из таблицы выше. +[руководстве](index.html), перенесены в `c/set-protocol/ports/stm32-bxcan` +с сохранением имён и API. Общий `pcan_id` подключается из этого же `templates`. +Отдельный репозиторий SETCAN больше не требуется. Его история до `c8eec55` +включена в Git-историю `templates`; документация здесь сохранена без +перезаписи старым снимком. ## Документы diff --git a/python/protocan/protocan.py b/python/protocan/protocan.py index 94a7264..8a2789e 100644 --- a/python/protocan/protocan.py +++ b/python/protocan/protocan.py @@ -66,6 +66,7 @@ class SettingsResult(IntEnum): EEPROM_ERROR = 0x05 INVALID_LOCATION = 0x06 BUSY = 0x07 + INVALID_VALUE = 0x08 class BroadcastType(IntEnum): @@ -145,6 +146,7 @@ SETTINGS_RESULT_RU = { SettingsResult.EEPROM_ERROR: "ошибка EEPROM", SettingsResult.INVALID_LOCATION: "неверная локация", SettingsResult.BUSY: "устройство занято", + SettingsResult.INVALID_VALUE: "значение настройки вне диапазона", } BROADCAST_RU = { @@ -680,6 +682,30 @@ def _decode_settings(res: Decoded) -> None: (" SETTINGS.Position Y", "%d (0x%02X)" % (position, position)), ] + # Зарезервированная локация прибора KONOR: runtime-настройки не являются + # ROM датчика, хотя используют тот же однокадровый SETTINGS-транспорт. + if assembly == 0xFF and position == 0x01: + if res.dlc == 0 and res.id.pm == Route.FROM_PM: + res.summary = "SETTINGS RUNTIME GET" + return + if res.dlc == 8: + pulse_seconds = res.data[0] + led_hz = int.from_bytes(res.data[1:3], "little") + res.fields += [ + (" SETTINGS.Pulse period", + "%d s" % pulse_seconds if pulse_seconds else "выключен"), + (" SETTINGS.LED frequency", "%d Hz" % led_hz), + ] + if led_hz > 500: + res.warnings.append("Частота LED должна быть 0..500 Гц") + if any(res.data[3:]): + res.warnings.append("Резерв SETTINGS Data[3:8] должен быть нулевым") + direction = "WRITE" if res.id.pm == Route.FROM_PM else "RESPONSE" + period_text = "%d s" % pulse_seconds if pulse_seconds else "выключен" + res.summary = ("SETTINGS RUNTIME %s: PULSE %s, LED %d Hz" + % (direction, period_text, led_hz)) + return + if res.id.pm == Route.FROM_PM: if res.dlc == 0: operation = "GET"