# Протокол телеметрии

Документ описывает текущий бинарный пакет телеметрии из прошивки
`AD_Keil_Project`.

Основные файлы реализации:

| Файл | Назначение |
|---|---|
| `Core/Inc/simulink_interface.h` | Формат пакета, версия, размеры, API |
| `Core/Src/simulink_interface.c` | Заполнение пакета и CRC16 |
| `Core/Inc/ad_binary_transport.h` | Выбор протокола и транспорта |
| `Core/Src/ad_binary_transport.c` | Отправка готового пакета |
| `Core/Src/ad_usb_cdc.c` | USB FS CDC ACM транспорт |
| `Core/Src/ad_can_telemetry.c` | FDCAN1 classic CAN транспорт |
| `Core/Src/ad_modbus.c` | Modbus-регистры включения бинарной телеметрии |

## Кратко

Прошивка формирует один бинарный пакет `SimulinkTelemetryPacket_t`.
Пакет можно отдавать через USB CDC, через FDCAN1 classic CAN или одновременно
через оба транспорта.

По умолчанию бинарный поток выключен:

```c
AD_HOST_PROTOCOL_DEFAULT = 0
AD_BINARY_TELEMETRY_TRANSPORT_DEFAULT = 0
```

То есть после старта активен режим `0` - Modbus-контроль без бинарной
телеметрии. Чтобы включить поток, нужно через Modbus записать:

```text
0x001D / 29 = транспорт
0x001C / 28 = 1
```

Где транспорт:

| Значение | Транспорт |
|---:|---|
| `0` | USB CDC |
| `1` | CAN/FDCAN1 |
| `2` | USB CDC и CAN/FDCAN1 одновременно |

Чтобы выключить бинарный поток:

```text
0x001C / 28 = 0
```

## Частота

Пакет обновляется в `SimulinkInterface_StepSlow()`, который вызывается из
`AD_Board_Loop()` раз в `AD_BOARD_SLOW_PERIOD_MS = 10` мс.

Отправка из `AD_BinaryTransport_Loop()` также ограничена периодом:

```c
AD_BINARY_TELEMETRY_PERIOD_MS = 10
```

Итого штатная частота телеметрии - около `100 Гц`, если главный цикл успевает
обслуживать транспорт.

## Кодирование данных

| Параметр | Значение |
|---|---|
| Порядок байт | little-endian, как у STM32 Cortex-M |
| `float` | IEEE-754 binary32, 4 байта |
| Выравнивание | обычное C-выравнивание ARMCLANG, без `packed` |
| Текущий размер заголовка | `16` байт |
| Текущий размер payload | `136` байт |
| Текущий размер полного пакета | `152` байта |
| Максимум, разрешенный кодом | `SIMULINK_TELEMETRY_MAX_BYTES = 256` |

Если структура в C будет изменена, PC-парсер должен проверять
`header.version` и `header.payload_size`.

## Заголовок пакета

Тип: `SimulinkTelemetryHeader_t`.

Смещения указаны от начала пакета.

| Offset | Размер | Тип | Поле | Значение / описание |
|---:|---:|---|---|---|
| `0` | `4` | `uint32_t` | `magic` | `0x41444944` |
| `4` | `2` | `uint16_t` | `version` | `3` |
| `6` | `2` | `uint16_t` | `payload_size` | `136` для текущей версии |
| `8` | `4` | `uint32_t` | `sequence` | Увеличивается при каждой упаковке |
| `12` | `2` | `uint16_t` | `crc16` | CRC16-CCITT по payload |
| `14` | `2` | `uint16_t` | `reserved` | `0` |

`magic` сравнивать как `uint32_t` little-endian. На проводе байты идут так:

```text
44 49 44 41
```

## CRC16

CRC считается только по payload, без заголовка.

Параметры:

| Параметр | Значение |
|---|---|
| Алгоритм | CRC16-CCITT |
| Начальное значение | `0xFFFF` |
| Полином | `0x1021` |
| Reflection | нет |
| Final XOR | нет |

Проверка на стороне ПК:

```text
crc16_ccitt(packet[16 : 16 + payload_size]) == header.crc16
```

## Payload

Тип: `SimulinkTelemetryPayload_t`.

Смещения в таблице указаны от начала payload. Чтобы получить смещение от начала
пакета, прибавь `16`.

### Измерения

Блок `measurements`, тип `AD_Measurements_t`, размер `64` байта.

| Payload offset | Тип | Поле | Единицы |
|---:|---|---|---|
| `0` | `float` | `measurements.ia_A` | A |
| `4` | `float` | `measurements.ib_A` | A |
| `8` | `float` | `measurements.ic_A` | A |
| `12` | `float` | `measurements.ia_rms_A` | A RMS |
| `16` | `float` | `measurements.ib_rms_A` | A RMS |
| `20` | `float` | `measurements.ic_rms_A` | A RMS |
| `24` | `float` | `measurements.vdc_V` | V |
| `28` | `float` | `measurements.va_V` | V |
| `32` | `float` | `measurements.vb_V` | V |
| `36` | `float` | `measurements.vc_V` | V |
| `40` | `float` | `measurements.speed_rpm` | rpm |
| `44` | `float` | `measurements.torque_Nm` | N*m |
| `48` | `float` | `measurements.temperature_C` | град C |
| `52` | `uint32_t` | `measurements.timestamp_us` | us |
| `56` | `uint32_t` | `measurements.status_flags` | `AD_MEAS_STATUS_*` |
| `60` | `float` | `measurements.slip_percent` | % |

`measurements.status_flags`:

| Бит | Маска | Флаг |
|---:|---:|---|
| `0` | `0x00000001` | `AD_MEAS_STATUS_OVERCURRENT` |
| `1` | `0x00000002` | `AD_MEAS_STATUS_OVERVOLTAGE` |
| `2` | `0x00000004` | `AD_MEAS_STATUS_UNDERVOLTAGE` |
| `3` | `0x00000008` | `AD_MEAS_STATUS_OVERTEMPERATURE` |
| `4` | `0x00000010` | `AD_MEAS_STATUS_DRIVER_FAULT` |
| `5` | `0x00000020` | `AD_MEAS_STATUS_EMERGENCY_STOP` |

### Параметры двигателя

Блок `motor_parameters`, тип `AD_MotorParameters_t`, размер `60` байт.

| Payload offset | Тип | Поле | Единицы |
|---:|---|---|---|
| `64` | `float` | `motor_parameters.Rs_ohm` | Ohm |
| `68` | `float` | `motor_parameters.Rr_ohm` | Ohm |
| `72` | `float` | `motor_parameters.Ls_H` | H |
| `76` | `float` | `motor_parameters.Lr_H` | H |
| `80` | `float` | `motor_parameters.Lm_H` | H |
| `84` | `float` | `motor_parameters.Ll_H` | H |
| `88` | `float` | `motor_parameters.J_kg_m2` | kg*m2 |
| `92` | `float` | `motor_parameters.B_Nm_s` | N*m*s |
| `96` | `float` | `motor_parameters.pole_pairs` | пары полюсов |
| `100` | `float` | `motor_parameters.nominal_voltage_V` | V |
| `104` | `float` | `motor_parameters.nominal_current_A` | A |
| `108` | `float` | `motor_parameters.nominal_frequency_Hz` | Hz |
| `112` | `float` | `motor_parameters.nominal_power_W` | W |
| `116` | `float` | `motor_parameters.nominal_speed_rpm` | rpm |
| `120` | `uint32_t` | `motor_parameters.valid_mask` | `AD_MOTOR_PARAM_VALID_*` |

`valid_mask`:

| Бит | Маска | Флаг | Поле |
|---:|---:|---|---|
| `0` | `0x00000001` | `AD_MOTOR_PARAM_VALID_RS` | `Rs_ohm` |
| `1` | `0x00000002` | `AD_MOTOR_PARAM_VALID_RR` | `Rr_ohm` |
| `2` | `0x00000004` | `AD_MOTOR_PARAM_VALID_LS` | `Ls_H` |
| `3` | `0x00000008` | `AD_MOTOR_PARAM_VALID_LR` | `Lr_H` |
| `4` | `0x00000010` | `AD_MOTOR_PARAM_VALID_LM` | `Lm_H` |
| `5` | `0x00000020` | `AD_MOTOR_PARAM_VALID_LL` | `Ll_H` |
| `6` | `0x00000040` | `AD_MOTOR_PARAM_VALID_J` | `J_kg_m2` |
| `7` | `0x00000080` | `AD_MOTOR_PARAM_VALID_B` | `B_Nm_s` |
| `8` | `0x00000100` | `AD_MOTOR_PARAM_VALID_NOMINALS` | nominal fields |
| `9` | `0x00000200` | `AD_MOTOR_PARAM_VALID_POLE_PAIRS` | `pole_pairs` |

Перед использованием результата идентификации обязательно проверять
соответствующий бит `valid_mask`.

### Статус идентификации и команда

| Payload offset | Тип | Поле | Описание |
|---:|---|---|---|
| `124` | `uint32_t` | `param_id_status` | `AD_PARAM_ID_STATUS_*` |
| `128` | `uint32_t` | `param_id_faults` | `AD_PARAM_ID_FAULT_*` |
| `132` | `uint8_t` | `param_id_mode` | Текущий режим `AD_PARAM_ID_MODE_*` |
| `133` | `uint8_t` | `command_enable` | Последняя команда enable |
| `134` | `uint8_t` | `command_test_mode` | Последняя команда test mode |
| `135` | `uint8_t` | `reserved` | `0` |

`param_id_status`:

| Бит | Маска | Флаг |
|---:|---:|---|
| `0` | `0x00000001` | `AD_PARAM_ID_STATUS_ACTIVE` |
| `1` | `0x00000002` | `AD_PARAM_ID_STATUS_POWER_TEST_BLOCKED` |
| `2` | `0x00000004` | `AD_PARAM_ID_STATUS_POWER_STAGE_ARMED` |
| `3` | `0x00000008` | `AD_PARAM_ID_STATUS_FAULT_LATCHED` |
| `4` | `0x00000010` | `AD_PARAM_ID_STATUS_TIMEOUT` |
| `5` | `0x00000020` | `AD_PARAM_ID_STATUS_LOCKED_ROTOR_BLOCKED` |
| `6` | `0x00000040` | `AD_PARAM_ID_STATUS_SAFETY_LIMITS_UNKNOWN` |
| `7` | `0x00000080` | `AD_PARAM_ID_STATUS_DATA_VALID` |
| `8` | `0x00000100` | `AD_PARAM_ID_STATUS_COMPLETE` |

`param_id_faults`:

| Бит | Маска | Флаг |
|---:|---:|---|
| `0` | `0x00000001` | `AD_PARAM_ID_FAULT_OVERCURRENT` |
| `1` | `0x00000002` | `AD_PARAM_ID_FAULT_OVERVOLTAGE` |
| `2` | `0x00000004` | `AD_PARAM_ID_FAULT_UNDERVOLTAGE` |
| `3` | `0x00000008` | `AD_PARAM_ID_FAULT_OVERTEMPERATURE` |
| `4` | `0x00000010` | `AD_PARAM_ID_FAULT_DRIVER` |
| `5` | `0x00000020` | `AD_PARAM_ID_FAULT_EMERGENCY_STOP` |
| `6` | `0x00000040` | `AD_PARAM_ID_FAULT_TIMEOUT` |
| `7` | `0x00000080` | `AD_PARAM_ID_FAULT_NULL_INPUT` |

Режимы `param_id_mode` и `command_test_mode`:

| Значение | Режим |
|---:|---|
| `0` | `AD_PARAM_ID_MODE_IDLE` |
| `1` | `AD_PARAM_ID_MODE_STATOR_RESISTANCE` |
| `2` | `AD_PARAM_ID_MODE_NO_LOAD_MAGNETIZING` |
| `3` | `AD_PARAM_ID_MODE_LOCKED_ROTOR_LEAKAGE` |
| `4` | `AD_PARAM_ID_MODE_INERTIA_FRICTION` |
| `5` | `AD_PARAM_ID_MODE_DATA_LOGGING` |
| `6` | `AD_PARAM_ID_MODE_PWM_TEST_UH` |
| `7` | `AD_PARAM_ID_MODE_PWM_TEST_UL` |
| `8` | `AD_PARAM_ID_MODE_PWM_TEST_VH` |
| `9` | `AD_PARAM_ID_MODE_PWM_TEST_VL` |
| `10` | `AD_PARAM_ID_MODE_PWM_TEST_WH` |
| `11` | `AD_PARAM_ID_MODE_PWM_TEST_WL` |
| `12` | `AD_PARAM_ID_MODE_PWM_TEST_ALL` |
| `13` | `AD_PARAM_ID_MODE_AUTO_IDENTIFICATION` |
| `14` | `AD_PARAM_ID_MODE_ROTATION_3HZ` |

## USB CDC транспорт

USB реализован как CDC ACM устройство на USB FS:

| Параметр | Значение |
|---|---|
| Линии | `PA11 USB_DM`, `PA12 USB_DP` |
| Тактирование | `HSI48` |
| VID/PID | `0x0483 / 0x5740` |
| Product string | `AD Telemetry CDC` |
| Data IN endpoint | `0x81`, bulk, 64 байта |
| Data OUT endpoint | `0x01`, bulk, 64 байта |
| CMD endpoint | `0x82`, interrupt, 8 байт |

Код вызывает `HAL_PCD_EP_Transmit()` на весь пакет сразу, но CDC на стороне ПК
нужно считать потоком байт. Один `read()` на ПК не обязан совпадать с одним
пакетом прошивки.

Рекомендуемый парсер:

1. Искать в потоке magic bytes `44 49 44 41`.
2. Дочитать 16-байтный заголовок.
3. Проверить `version == 3`.
4. Проверить `payload_size == 136` или обработать совместимую новую версию.
5. Дочитать `payload_size` байт.
6. Проверить CRC16.
7. Передать payload в декодер.

## CAN/FDCAN транспорт

CAN реализован через FDCAN1 в режиме classic CAN, кадры по 8 байт.

| Параметр | Значение |
|---|---|
| Периферия | `FDCAN1` |
| RX/TX | `PB8 / PB9` |
| Frame format | Classic CAN |
| Bit rate switch | Off |
| Default base standard ID | `0x5A0` |
| Payload одного CAN-кадра | до 8 байт |

Базовый ID задается:

```c
AD_CAN_TELEMETRY_STD_ID_BASE = 0x5A0
```

Полный бинарный пакет режется последовательно:

| CAN ID | Данные |
|---:|---|
| `0x5A0` | bytes `0..7` |
| `0x5A1` | bytes `8..15` |
| `0x5A2` | bytes `16..23` |
| `...` | `...` |

Для текущего пакета `152` байта нужно `19` CAN-кадров:

```text
ceil(152 / 8) = 19
IDs: 0x5A0..0x5B2
```

Последний кадр несет полный 8-байтный блок.

Рекомендация для приемника CAN:

1. Считать кадр с `base_id`.
2. Начать буфер нового пакета.
3. Добавлять кадры `base_id + index`.
4. После первых 16 байт прочитать `payload_size`.
5. Ждать `16 + payload_size` байт.
6. Проверить `magic`, `version`, `sequence`, `crc16`.
7. При пропуске ID или смене `sequence` сбросить сборку пакета.

## Включение через Modbus

Modbus RTU остается каналом управления.

| Параметр | Значение |
|---|---|
| UART | `USART2` |
| TX/RX | `PA2 / PA3` |
| Скорость | `115200` |
| Формат | `8N1` |
| Slave address | `1` |

Регистры:

| Адрес hex / dec | Имя | Доступ | Значение |
|---:|---|---|---|
| `0x001C` / `28` | `control_host_protocol` | R/W | `0` = Modbus, `1` = binary telemetry |
| `0x001D` / `29` | `control_binary_transport` | R/W | `0` = USB, `1` = CAN, `2` = USB + CAN |

Пример включения USB CDC:

```text
write 0x001D / 29 = 0
write 0x001C / 28 = 1
```

Пример включения CAN:

```text
write 0x001D / 29 = 1
write 0x001C / 28 = 1
```

Пример одновременной отправки USB и CAN:

```text
write 0x001D / 29 = 2
write 0x001C / 28 = 1
```

Остановка бинарной телеметрии:

```text
write 0x001C / 28 = 0
```

## Состояние транспорта в отладчике

Основной debug-view:

```c
g_ad_debug.binary_transport
g_ad_debug.modbus
```

`g_ad_debug.binary_transport.status_flags`:

| Бит | Маска | Флаг |
|---:|---:|---|
| `0` | `0x00000001` | `AD_BINARY_TRANSPORT_STATUS_INITIALIZED` |
| `1` | `0x00000002` | `AD_BINARY_TRANSPORT_STATUS_USB_READY` |
| `2` | `0x00000004` | `AD_BINARY_TRANSPORT_STATUS_CAN_READY` |
| `3` | `0x00000008` | `AD_BINARY_TRANSPORT_STATUS_USB_CONFIGURED` |
| `4` | `0x00000010` | `AD_BINARY_TRANSPORT_STATUS_USB_TX_BUSY` |
| `5` | `0x00000020` | `AD_BINARY_TRANSPORT_STATUS_CAN_TX_BUSY` |
| `6` | `0x00000040` | `AD_BINARY_TRANSPORT_STATUS_LAST_DROPPED` |

Полезные счетчики:

| Поле | Значение |
|---|---|
| `tx_packets` | Сколько пакетов успешно поставлено хотя бы в один транспорт |
| `usb_packets` | Сколько пакетов принято USB TX |
| `can_packets` | Сколько пакетов принято CAN TX |
| `dropped_packets` | Сколько пакетов сброшено из-за busy/error/size |
| `last_size_bytes` | Последний размер пакета, сейчас должен быть `152` |
| `last_tx_tick_ms` | `HAL_GetTick()` последней успешной постановки |

## API прошивки

```c
void SimulinkInterface_PackTelemetry(void);
const SimulinkTelemetryPacket_t* SimulinkInterface_GetTelemetryPacket(void);
const uint8_t* SimulinkInterface_GetTelemetryBytes(void);
size_t SimulinkInterface_GetTelemetrySize(void);

void AD_BinaryTransport_Init(void);
void AD_BinaryTransport_Loop(void);
uint8_t AD_BinaryTransport_SetProtocol(uint16_t protocol);
uint8_t AD_BinaryTransport_SetTransport(uint16_t transport);
const AD_BinaryTransportState_t* AD_BinaryTransport_GetState(void);
```

## Минимальный Python-декодер CRC

```python
def crc16_ccitt(data: bytes) -> int:
    crc = 0xFFFF
    for value in data:
        crc ^= value << 8
        for _ in range(8):
            if crc & 0x8000:
                crc = ((crc << 1) ^ 0x1021) & 0xFFFF
            else:
                crc = (crc << 1) & 0xFFFF
    return crc
```

Поля unpack для текущей версии:

```python
HEADER_FMT = "<IHHIHH"
PAYLOAD_SIZE_V3 = 136
PACKET_SIZE_V3 = 16 + PAYLOAD_SIZE_V3
MAGIC = 0x41444944
VERSION = 3
```
