5 Commits

16 changed files with 1418 additions and 17 deletions

View File

@@ -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/stm32f407-devboard-v1`](c/set-protocol/ports/stm32f407-devboard-v1) | доступ SETGUI к Modbus-регистрам F407 через CAN485 DevBoard_V1 | `pcan_modbus_server`, STM32 HAL CAN | bxCAN FIFO0 и callbacks карты регистров |
| [`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 в комплекте** |

View File

@@ -26,6 +26,7 @@ set(SETPROTOCOL_LEGACY_SOURCES
src/pcan_gas.c
src/pcan_id.c
src/pcan_link.c
src/pcan_modbus_server.c
src/pcan_ring.c
)
@@ -88,6 +89,10 @@ if(SETP_BUILD_TESTS)
target_link_libraries(test_transport PRIVATE setprotocol_static)
add_test(NAME legacy_transport COMMAND test_transport)
add_executable(test_modbus_server tests/test_modbus_server.c)
target_link_libraries(test_modbus_server PRIVATE setprotocol_static)
add_test(NAME protocan_modbus_server COMMAND test_modbus_server)
add_executable(test_gui tests/test_gui.c)
target_link_libraries(test_gui PRIVATE setprotocol_static)
add_test(NAME legacy_gui COMMAND test_gui)

View File

@@ -0,0 +1,60 @@
/**
* @file pcan_modbus_server.h
* @brief Modbus register windows transported in one classic ProtoCAN frame.
*/
#ifndef PCAN_MODBUS_SERVER_H
#define PCAN_MODBUS_SERVER_H
#include <stdbool.h>
#include <stdint.h>
#include "pcan_frame.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
PCAN_MODBUS_COIL = 0x4U,
PCAN_MODBUS_DISCRETE = 0x5U,
PCAN_MODBUS_HOLDING = 0x6U,
PCAN_MODBUS_INPUT = 0x7U
} pcan_modbus_bank_t;
typedef bool (*pcan_modbus_read_fn)(void *user, pcan_modbus_bank_t bank,
uint16_t address, uint16_t *value);
typedef bool (*pcan_modbus_write_fn)(void *user, pcan_modbus_bank_t bank,
uint16_t address, uint16_t value);
typedef struct {
uint8_t device_type;
uint8_t device_id;
pcan_modbus_read_fn read;
pcan_modbus_write_fn write;
void *user;
uint32_t requests;
uint32_t responses;
uint32_t rejected;
} pcan_modbus_server_t;
typedef enum {
PCAN_MODBUS_NOT_FOR_US = 0,
PCAN_MODBUS_HANDLED_NO_RESPONSE,
PCAN_MODBUS_RESPONSE
} pcan_modbus_result_t;
bool pcan_modbus_server_init(pcan_modbus_server_t *server,
uint8_t device_type, uint8_t device_id,
pcan_modbus_read_fn read,
pcan_modbus_write_fn write, void *user);
/** Zero DLC reads; data writes COIL/HOLDING. Register words are little-endian. */
pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
const pcan_frame_t *request,
pcan_frame_t *response);
#ifdef __cplusplus
}
#endif
#endif /* PCAN_MODBUS_SERVER_H */

View File

@@ -23,6 +23,7 @@
#include "pcan_gas.h"
#include "pcan_id.h"
#include "pcan_link.h"
#include "pcan_modbus_server.h"
#include "pcan_ring.h"
#endif /* PROTOCAN_TRANSPORT_H */

View File

@@ -0,0 +1,22 @@
# STM32F407 + CAN485 DevBoard_V1
Порт даёт SETGUI доступ к существующим Modbus-регистрам STM32F407 через
классический CAN. ПК подключён к USB-UART платы CAN485 DevBoard_V1, команда
`T,E` платы отправляет ProtoCAN-кадр, а этот порт отвечает тем же типом кадра.
При маршруте `Q3` плата одновременно ретранслирует поток по RS-485.
Порт не настраивает GPIO, bitrate, фильтры и не запускает CAN: это остаётся за
CubeMX-проектом. Необработанные кадры передаются callback-у приложения, поэтому
загрузчик ProtoCAN и регистровый сервис могут делить FIFO0.
Слой приложения предоставляет callback-и чтения и записи. Нулевой DLC означает
чтение; данные в кадре `HOLDING`/`COIL` — запись. За кадр передаются до четырёх
16-битных регистров или до пятнадцати coils (предел 4-битного `RegCount`).
```c
static set_devboard_v1_stm32f407_t gui_can;
set_devboard_v1_stm32f407_init(&gui_can, &hcan, 7, 3,
app_read_register, app_write_register, NULL,
app_handle_boot_frame, NULL);
for (;;) set_devboard_v1_stm32f407_poll(&gui_can);
```

View File

@@ -0,0 +1,83 @@
#include "set_devboard_v1_stm32f407.h"
static bool transmit(set_devboard_v1_stm32f407_t *port,
const pcan_frame_t *frame)
{
CAN_TxHeaderTypeDef header = {0};
uint32_t mailbox = 0U;
uint8_t data[8] = {0U};
header.ExtId = frame->id & 0x1FFFFFFFUL;
header.IDE = CAN_ID_EXT;
header.RTR = ((frame->flags & PCAN_FLAG_RTR) != 0U) ?
CAN_RTR_REMOTE : CAN_RTR_DATA;
header.DLC = frame->dlc;
header.TransmitGlobalTime = DISABLE;
for (uint8_t index = 0U; index < frame->dlc; ++index) {
data[index] = frame->data[index];
}
if (HAL_CAN_AddTxMessage(port->can, &header, data, &mailbox) != HAL_OK) {
port->tx_errors++;
return false;
}
port->tx_frames++;
return true;
}
bool set_devboard_v1_stm32f407_init(
set_devboard_v1_stm32f407_t *port, CAN_HandleTypeDef *can,
uint8_t device_type, uint8_t device_id,
pcan_modbus_read_fn read, pcan_modbus_write_fn write, void *register_user,
set_devboard_v1_unhandled_fn unhandled, void *unhandled_user)
{
if ((port == NULL) || (can == NULL)) {
return false;
}
port->can = can;
port->unhandled = unhandled;
port->unhandled_user = unhandled_user;
port->rx_frames = 0U;
port->tx_frames = 0U;
port->tx_errors = 0U;
return pcan_modbus_server_init(&port->server, device_type, device_id,
read, write, register_user);
}
size_t set_devboard_v1_stm32f407_poll(set_devboard_v1_stm32f407_t *port)
{
size_t handled = 0U;
if ((port == NULL) || (port->can == NULL)) {
return 0U;
}
while (HAL_CAN_GetRxFifoFillLevel(port->can, CAN_RX_FIFO0) != 0U) {
CAN_RxHeaderTypeDef header;
uint8_t data[8] = {0U};
pcan_frame_t request = {0};
pcan_frame_t response;
pcan_modbus_result_t result;
if (HAL_CAN_GetRxMessage(port->can, CAN_RX_FIFO0, &header, data) != HAL_OK) {
break;
}
port->rx_frames++;
request.id = (header.IDE == CAN_ID_EXT) ? header.ExtId : header.StdId;
request.flags = (header.IDE == CAN_ID_EXT) ? PCAN_FLAG_IDE : 0U;
if (header.RTR == CAN_RTR_REMOTE) {
request.flags |= PCAN_FLAG_RTR;
}
request.dlc = (header.DLC > 8U) ? 8U : (uint8_t)header.DLC;
for (uint8_t index = 0U; index < request.dlc; ++index) {
request.data[index] = data[index];
}
result = pcan_modbus_server_handle(&port->server, &request, &response);
if (result == PCAN_MODBUS_RESPONSE) {
(void)transmit(port, &response);
handled++;
} else if ((result == PCAN_MODBUS_NOT_FOR_US) &&
(port->unhandled != NULL)) {
port->unhandled(port->unhandled_user, &header, data);
}
}
return handled;
}

View File

@@ -0,0 +1,38 @@
/** STM32F407 bxCAN port for SETGUI through CAN485 DevBoard_V1. */
#ifndef SET_DEVBOARD_V1_STM32F407_H
#define SET_DEVBOARD_V1_STM32F407_H
#include "stm32f4xx_hal.h"
#include "pcan_modbus_server.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef void (*set_devboard_v1_unhandled_fn)(void *user,
const CAN_RxHeaderTypeDef *header,
const uint8_t data[8]);
typedef struct {
CAN_HandleTypeDef *can;
pcan_modbus_server_t server;
set_devboard_v1_unhandled_fn unhandled;
void *unhandled_user;
uint32_t rx_frames;
uint32_t tx_frames;
uint32_t tx_errors;
} set_devboard_v1_stm32f407_t;
bool set_devboard_v1_stm32f407_init(
set_devboard_v1_stm32f407_t *port, CAN_HandleTypeDef *can,
uint8_t device_type, uint8_t device_id,
pcan_modbus_read_fn read, pcan_modbus_write_fn write, void *register_user,
set_devboard_v1_unhandled_fn unhandled, void *unhandled_user);
size_t set_devboard_v1_stm32f407_poll(set_devboard_v1_stm32f407_t *port);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,144 @@
#include "pcan_modbus_server.h"
#include <stddef.h>
#include "pcan_id.h"
static bool is_bank(uint8_t value)
{
return (value >= (uint8_t)PCAN_MODBUS_COIL) &&
(value <= (uint8_t)PCAN_MODBUS_INPUT);
}
bool pcan_modbus_server_init(pcan_modbus_server_t *server,
uint8_t device_type, uint8_t device_id,
pcan_modbus_read_fn read,
pcan_modbus_write_fn write, void *user)
{
if ((server == NULL) || (read == NULL) || (device_type > 7U) ||
(device_id > 15U)) {
return false;
}
server->device_type = device_type;
server->device_id = device_id;
server->read = read;
server->write = write;
server->user = user;
server->requests = 0U;
server->responses = 0U;
server->rejected = 0U;
return true;
}
static pcan_modbus_result_t reject(pcan_modbus_server_t *server)
{
server->rejected++;
return PCAN_MODBUS_HANDLED_NO_RESPONSE;
}
pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
const pcan_frame_t *request,
pcan_frame_t *response)
{
pcan_id_t id;
uint16_t address;
uint8_t count;
pcan_modbus_bank_t bank;
if ((server == NULL) || (request == NULL) || (response == NULL) ||
((request->flags & PCAN_FLAG_IDE) == 0U)) {
return PCAN_MODBUS_NOT_FOR_US;
}
pcan_id_unpack(request->id, &id);
if ((id.route != PCAN_ROUTE_FROM_PM) ||
(id.device_type != server->device_type) ||
(id.device_id != server->device_id) || !is_bank(id.msg_type)) {
return PCAN_MODBUS_NOT_FOR_US;
}
server->requests++;
address = (uint16_t)(id.msg_body >> 4U);
count = (uint8_t)(id.msg_body & 0x0FU);
bank = (pcan_modbus_bank_t)id.msg_type;
if (count == 0U) {
return reject(server);
}
*response = *request;
id.route = PCAN_ROUTE_FROM_DEVICE;
response->id = pcan_id_pack(&id);
response->flags = PCAN_FLAG_IDE;
response->seq = 0U;
if (request->dlc == 0U) {
uint16_t packed = 0U;
if ((bank == PCAN_MODBUS_COIL) || (bank == PCAN_MODBUS_DISCRETE)) {
if (count > 16U) {
return reject(server);
}
for (uint8_t index = 0U; index < count; ++index) {
uint16_t value = 0U;
if (!server->read(server->user, bank,
(uint16_t)(address + index), &value)) {
return reject(server);
}
if (value != 0U) {
packed |= (uint16_t)(1U << index);
}
}
response->dlc = 2U;
response->data[0] = (uint8_t)packed;
response->data[1] = (uint8_t)(packed >> 8U);
} else {
if (count > 4U) {
return reject(server);
}
response->dlc = (uint8_t)(count * 2U);
for (uint8_t index = 0U; index < count; ++index) {
uint16_t value = 0U;
if (!server->read(server->user, bank,
(uint16_t)(address + index), &value)) {
return reject(server);
}
response->data[index * 2U] = (uint8_t)value;
response->data[(index * 2U) + 1U] = (uint8_t)(value >> 8U);
}
}
} else {
if ((bank == PCAN_MODBUS_INPUT) || (bank == PCAN_MODBUS_DISCRETE) ||
(server->write == NULL)) {
return reject(server);
}
if (bank == PCAN_MODBUS_COIL) {
uint16_t bits;
if ((count > 16U) || (request->dlc != 2U)) {
return reject(server);
}
bits = (uint16_t)((uint16_t)request->data[0] |
((uint16_t)request->data[1] << 8U));
for (uint8_t index = 0U; index < count; ++index) {
if (!server->write(server->user, bank,
(uint16_t)(address + index),
(uint16_t)((bits >> index) & 1U))) {
return reject(server);
}
}
} else {
if ((count > 4U) || (request->dlc != (uint8_t)(count * 2U))) {
return reject(server);
}
for (uint8_t index = 0U; index < count; ++index) {
uint16_t value = (uint16_t)(request->data[index * 2U] |
((uint16_t)request->data[(index * 2U) + 1U] << 8U));
if (!server->write(server->user, bank,
(uint16_t)(address + index), value)) {
return reject(server);
}
}
}
response->dlc = request->dlc;
}
server->responses++;
return PCAN_MODBUS_RESPONSE;
}

View File

@@ -0,0 +1,90 @@
#include <stdio.h>
#include <string.h>
#include "pcan_id.h"
#include "pcan_modbus_server.h"
static uint16_t holding[32];
static uint16_t input[32];
static uint16_t coils;
static bool read_word(void *user, pcan_modbus_bank_t bank,
uint16_t address, uint16_t *value)
{
(void)user;
if (address >= 32U) return false;
if (bank == PCAN_MODBUS_HOLDING) *value = holding[address];
else if (bank == PCAN_MODBUS_INPUT) *value = input[address];
else if (bank == PCAN_MODBUS_COIL) *value = (uint16_t)((coils >> address) & 1U);
else return false;
return true;
}
static bool write_word(void *user, pcan_modbus_bank_t bank,
uint16_t address, uint16_t value)
{
(void)user;
if (address >= 32U) return false;
if (bank == PCAN_MODBUS_HOLDING) holding[address] = value;
else if (bank == PCAN_MODBUS_COIL && address < 16U) {
if (value) coils |= (uint16_t)(1U << address);
else coils &= (uint16_t)~(1U << address);
} else return false;
return true;
}
static pcan_frame_t request(uint8_t type, uint16_t address, uint8_t count)
{
pcan_id_t id = {0};
pcan_frame_t frame = {0};
id.priority = PCAN_PRIORITY_STANDARD;
id.route = PCAN_ROUTE_FROM_PM;
id.device_type = 7U;
id.device_id = 3U;
id.msg_type = type;
id.msg_body = pcan_body_modbus(address, count);
frame.id = pcan_id_pack(&id);
frame.flags = PCAN_FLAG_IDE;
return frame;
}
#define CHECK(x) do { if (!(x)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); return 1; \
} } while (0)
int main(void)
{
pcan_modbus_server_t server;
pcan_frame_t req;
pcan_frame_t rsp;
pcan_id_t id;
memset(holding, 0, sizeof(holding));
memset(input, 0, sizeof(input));
input[10] = 0x1234U;
input[11] = 0xABCDU;
CHECK(pcan_modbus_server_init(&server, 7U, 3U, read_word, write_word, NULL));
req = request(PCAN_MODBUS_INPUT, 10U, 2U);
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
CHECK(rsp.dlc == 4U && rsp.data[0] == 0x34U && rsp.data[1] == 0x12U);
CHECK(rsp.data[2] == 0xCDU && rsp.data[3] == 0xABU);
pcan_id_unpack(rsp.id, &id);
CHECK(id.route == PCAN_ROUTE_FROM_DEVICE);
req = request(PCAN_MODBUS_HOLDING, 3U, 2U);
req.dlc = 4U;
req.data[0] = 0x22U; req.data[1] = 0x11U;
req.data[2] = 0x44U; req.data[3] = 0x33U;
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
CHECK(holding[3] == 0x1122U && holding[4] == 0x3344U);
req = request(PCAN_MODBUS_COIL, 2U, 3U);
req.dlc = 2U; req.data[0] = 0x05U; req.data[1] = 0U;
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
CHECK(coils == 0x0014U);
req = request(PCAN_MODBUS_INPUT, 0U, 5U);
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE);
CHECK(server.requests == 4U && server.responses == 3U && server.rejected == 1U);
return 0;
}

View File

@@ -23,7 +23,7 @@ SOURCES = [
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c",
"balsam_can.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
"pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c",
"pcan_gas.c",
"pcan_gas.c", "pcan_modbus_server.c",
)
]

View File

@@ -0,0 +1,379 @@
# Разбор CAN-кадров ProtoCAN Boot v1 и SETProtocol v2
Документ описывает wire-форматы двух протоколов обновления прошивки:
- **v1** — `templates/c/protocan-boot`, одна команда или 8 байт образа в одном
Extended CAN-кадре;
- **v2** — `templates/c/set-protocol`, полный кадр SETProtocol разбивается на
несколько Extended CAN-кадров.
Все многобайтные поля payload передаются **little-endian**. CAN ID — 29-битный.
Для рабочего кода нужно использовать канонические реализации из `templates`,
а приведённый ниже Python-парсер удобен для анализатора, логов и отладки.
## 1. ProtoCAN Boot v1
### 1.1. Разметка Extended CAN ID
```text
bits size field
28 1 Priority
27 1 Route: 0 = host -> device, 1 = device -> host
26..24 3 Device Type
23..20 4 Device ID
19..16 4 Message Type
15..0 16 Message Body
```
Формула:
```text
ID = Priority << 28 |
Route << 27 |
DeviceType << 24 |
DeviceID << 20 |
MessageType << 16 |
MessageBody
```
Типы загрузочных сообщений:
| Message Type | Имя | Message Body | CAN payload |
|---:|---|---|---|
| `0x9` | `BOOT_CONTROL` | `SessionID << 8 \| Command` | параметры команды |
| `0xA` | `BOOT_DATA_A` | индекс блока | 8 байт слота A |
| `0xB` | `BOOT_DATA_B` | индекс блока | 8 байт слота B |
| `0xC` | `BOOT_STATUS` | `SessionID << 8 \| Command` | статус и прогресс |
| `0xD` | `BOOT_DISCOVERY` | подтип | информация об устройстве |
Команды `BOOT_CONTROL`:
| Код | Команда | Payload |
|---:|---|---|
| `0x01` | `IDENTIFY` | пустой |
| `0x02` | `ENTER_BOOT` | пустой |
| `0x03` | `BEGIN_IMAGE` | `image_size u32`, `image_crc32 u32` |
| `0x04` | `BEGIN_COMPAT` | `product u16`, `hw_min u8`, `hw_max u8`, `version u32` |
| `0x05` | `ERASE` | пустой |
| `0x06` | `VERIFY` | пустой |
| `0x07` | `COMMIT` | пустой |
| `0x08` | `CONFIRM` | пустой |
| `0x09` | `REBOOT` | пустой |
| `0x0A` | `ABORT` | пустой |
| `0x0B` | `QUERY_PROGRESS` | пустой |
`BOOT_STATUS` всегда содержит 8 байт:
```text
offset size field
0 1 status
1 1 target_slot
2 2 next_block u16 LE
4 4 running_crc32 u32 LE
```
`BOOT_DISCOVERY` с body `1` содержит:
```text
offset size field
0 2 product_type u16 LE
2 1 hardware_revision
3 1 protocol_version = 1
4 4 firmware_version u32 LE
```
Пример запроса `IDENTIFY` для `DeviceType=7`, `DeviceID=13`:
```text
CAN ID: 17D90001
DLC: 0
```
### 1.2. Python-парсер v1
```python
def parse_v1(can_id: int, data: bytes) -> dict:
if not 0 <= can_id <= 0x1FFFFFFF:
raise ValueError("неверный Extended CAN ID")
if len(data) > 8:
raise ValueError("DLC больше 8")
result = {
"version": 1,
"priority": (can_id >> 28) & 0x01,
"route": (can_id >> 27) & 0x01,
"device_type": (can_id >> 24) & 0x07,
"device_id": (can_id >> 20) & 0x0F,
"message_type": (can_id >> 16) & 0x0F,
"message_body": can_id & 0xFFFF,
"data": bytes(data),
}
msg_type = result["message_type"]
body = result["message_body"]
if msg_type in (0x9, 0xC):
result["session_id"] = (body >> 8) & 0xFF
result["command"] = body & 0xFF
elif msg_type in (0xA, 0xB):
result["slot"] = msg_type - 0xA
result["block_index"] = body
if msg_type == 0xC:
if len(data) != 8:
raise ValueError("BOOT_STATUS должен содержать 8 байт")
result.update({
"status": data[0],
"target_slot": data[1],
"next_block": int.from_bytes(data[2:4], "little"),
"running_crc32": int.from_bytes(data[4:8], "little"),
})
elif msg_type == 0xD and body == 1:
if len(data) != 8:
raise ValueError("BOOT_DISCOVERY должен содержать 8 байт")
result.update({
"product_type": int.from_bytes(data[0:2], "little"),
"hardware_revision": data[2],
"protocol_version": data[3],
"firmware_version": int.from_bytes(data[4:8], "little"),
})
return result
```
## 2. SETProtocol v2 поверх classic CAN
В v2 CAN-кадр является только транспортным сегментом. Сначала нужно собрать
полный SETP-пакет, и только затем разбирать его заголовок, payload и CRC32.
### 2.1. Разметка Extended CAN ID
```text
bits size field
28..24 5 Prefix = 0x12
23..16 8 Destination node
15..8 8 Source node
7 1 Priority
6..0 7 Channel
```
Формула:
```text
ID = 0x12 << 24 |
Destination << 16 |
Source << 8 |
Priority << 7 |
Channel
```
### 2.2. CAN-сегменты
Первый байт CAN payload — PCI:
| PCI | Назначение | Формат CAN payload |
|---:|---|---|
| `0x10` | первый сегмент | `10`, `total_length u16 LE`, первые 5 байт SETP |
| `0x20..0x2F` | продолжение | `2N`, следующие 17 байт SETP |
| `0x30..0x32` | flow control | `3S`, `block_size`, `st_min_ms` |
`N` — циклический номер сегмента `1..15,0..`; следующий сегмент обязан иметь
ожидаемый номер, тот же CAN ID и прийти до тайм-аута сборки 500 мс.
### 2.3. Внутренний кадр SETProtocol v2
```text
offset size field
0 2 SOF = A5 5A
2 1 version = 02
3 1 flags
4 2 message_type u16 LE
6 2 source u16 LE
8 2 destination u16 LE
10 2 sequence u16 LE
12 2 payload_length u16 LE
14 N payload
14+N 4 CRC32 IEEE u32 LE
```
CRC32 считается по байтам от `version` на offset 2 до конца payload. Поля
`source`, `destination` и `priority` внутреннего заголовка должны совпадать с
CAN ID.
Флаги:
| Бит | Значение |
|---:|---|
| `0x01` | RESPONSE |
| `0x02` | EVENT |
| `0x04` | ERROR |
| `0x08` | ACK_REQUIRED |
| `0x10` | MORE |
| `0x20` | PRIORITY |
Каждый response начинается с `status u16 LE`. Основные firmware message types:
`FW_BEGIN=0x0100`, `FW_DATA=0x0101`, `FW_END=0x0102`, `FW_ABORT=0x0103`,
`FW_STATUS=0x0104`, `FW_ACTIVATE=0x0105`.
Пример `PING` к BALZAM node `13`, source `0`, sequence `1`, priority `1`,
channel `1`:
```text
Полный SETP:
A5 5A 02 28 01 00 00 00 0D 00 01 00 00 00 E7 29 51 40
CAN ID 120D0081, сегменты:
10 12 00 A5 5A 02 28 01
21 00 00 00 0D 00 01 00
22 00 00 E7 29 51 40
```
### 2.4. Python-парсер и сборщик v2
```python
import binascii
def parse_v2_can_id(can_id: int) -> dict:
if not 0 <= can_id <= 0x1FFFFFFF:
raise ValueError("неверный Extended CAN ID")
if (can_id >> 24) & 0x1F != 0x12:
raise ValueError("не SETProtocol v2 CAN ID")
return {
"destination": (can_id >> 16) & 0xFF,
"source": (can_id >> 8) & 0xFF,
"priority": (can_id >> 7) & 0x01,
"channel": can_id & 0x7F,
}
def parse_setp(packet: bytes, can_id: int) -> dict:
if len(packet) < 18 or packet[:2] != b"\xA5\x5A":
raise ValueError("нет полного SETP-кадра")
if packet[2] != 2:
raise ValueError("неподдерживаемая версия SETP")
flags = packet[3]
if flags & 0xC0:
raise ValueError("установлены зарезервированные флаги")
payload_length = int.from_bytes(packet[12:14], "little")
if len(packet) != 14 + payload_length + 4:
raise ValueError("не совпадает payload_length")
expected_crc = int.from_bytes(packet[-4:], "little")
actual_crc = binascii.crc32(packet[2:-4]) & 0xFFFFFFFF
if actual_crc != expected_crc:
raise ValueError("ошибка CRC32 SETP")
address = parse_v2_can_id(can_id)
source = int.from_bytes(packet[6:8], "little")
destination = int.from_bytes(packet[8:10], "little")
priority = int(bool(flags & 0x20))
if (source, destination, priority) != (
address["source"], address["destination"], address["priority"]
):
raise ValueError("SETP header не совпадает с CAN ID")
payload = packet[14:-4]
result = {
"version": 2,
"flags": flags,
"message_type": int.from_bytes(packet[4:6], "little"),
"source": source,
"destination": destination,
"sequence": int.from_bytes(packet[10:12], "little"),
"payload": payload,
"can": address,
}
if flags & 0x01:
if len(payload) < 2:
raise ValueError("response не содержит status")
result["status"] = int.from_bytes(payload[:2], "little")
result["body"] = payload[2:]
return result
class V2CanReassembler:
def __init__(self, timeout_ms: int = 500):
self.timeout_ms = timeout_ms
self.reset()
def reset(self):
self.can_id = None
self.total = 0
self.data = bytearray()
self.next_sequence = 1
self.deadline_ms = 0
def feed(self, can_id: int, data: bytes, now_ms: int):
parse_v2_can_id(can_id)
if not 1 <= len(data) <= 8:
raise ValueError("DLC вне диапазона 1..8")
if self.can_id is not None and now_ms >= self.deadline_ms:
self.reset()
raise ValueError("тайм-аут сборки SETP")
pci_type = data[0] & 0xF0
if pci_type == 0x10:
if len(data) != 8:
raise ValueError("первый сегмент должен иметь DLC 8")
total = int.from_bytes(data[1:3], "little")
if not 18 <= total <= 530:
raise ValueError("неверный размер SETP")
self.can_id = can_id
self.total = total
self.data = bytearray(data[3:])
self.next_sequence = 1
self.deadline_ms = now_ms + self.timeout_ms
return None
if pci_type == 0x20:
sequence = data[0] & 0x0F
if (
self.can_id is None
or can_id != self.can_id
or sequence != self.next_sequence
or len(data) < 2
):
self.reset()
raise ValueError("ошибка последовательности CAN-сегментов")
if len(data) - 1 > self.total - len(self.data):
self.reset()
raise ValueError("лишние байты CAN-сегмента")
self.data.extend(data[1:])
self.next_sequence = (self.next_sequence + 1) & 0x0F
self.deadline_ms = now_ms + self.timeout_ms
if len(self.data) == self.total:
packet = bytes(self.data)
packet_can_id = self.can_id
self.reset()
return parse_setp(packet, packet_can_id)
return None
if pci_type == 0x30:
return {"flow_control": data[0] & 0x0F, "data": data[1:]}
raise ValueError("неизвестный PCI")
```
В SETGUI эти операции уже реализованы в
`third_party/templates/python/setprotocol/can.py`; собственный parser нужен
только внешнему анализатору или диагностическому скрипту.
## 3. Как отличать v1 от v2
Для используемых сейчас адресов достаточно следующих признаков:
- v2: верхние пять бит CAN ID равны `0x12`, PCI начинается с `0x10`, `0x2N`
или `0x3S`, после reassembly присутствует `A5 5A 02`;
- v1: `MessageType` в битах `19..16` равен `0x9..0xD`, каждый кадр разбирается
самостоятельно.
Однако универсальное автоопределение только по одному CAN ID невозможно:
комбинация `Priority/Route/DeviceType` v1 теоретически тоже может дать верхнее
поле `0x12`, а первый байт firmware data v1 может случайно совпасть с PCI.
Надёжный анализатор должен учитывать настроенный режим узла либо подтвердить v2
только после сборки кадра с корректными `A5 5A 02`, длиной и CRC32.
## 4. Канонические исходники
- v1 ID и state machine: `third_party/templates/c/protocan-boot/src/pcan_boot.c`;
- v2 CAN transport: `third_party/templates/c/set-protocol/src/set_can.c`;
- v2 frame/CRC: `third_party/templates/c/set-protocol/src/set_protocol.c`;
- v2 firmware payload: `third_party/templates/c/set-protocol/src/set_firmware.c`;
- Python v2 CAN: `third_party/templates/python/setprotocol/can.py`.

View File

@@ -4,9 +4,22 @@ param()
$ErrorActionPreference = 'Stop'
$outputPath = Join-Path $PSScriptRoot 'setprotocol.html'
$sourcePath = Join-Path $PSScriptRoot '..\c\set-protocol\docs\SETPROTOCOL.md'
$canFrameSourcePath = Join-Path $PSScriptRoot 'CAN_FRAME_PARSE_V1_V2.md'
$markdown = Get-Content -Raw -LiteralPath $sourcePath -Encoding UTF8
$html = (ConvertFrom-Markdown -InputObject $markdown).Html
function Convert-DocumentationMarkdown {
param([string]$Path)
$markdown = Get-Content -Raw -LiteralPath $Path -Encoding UTF8
$html = (ConvertFrom-Markdown -InputObject $markdown).Html
# Wide protocol tables must scroll horizontally instead of squeezing their
# contents into unreadable one-character columns on a narrow viewport.
$html = $html -replace '<table>', '<div class="table-wrap"><table>'
$html = $html -replace '</table>', '</table></div>'
return $html
}
$html = Convert-DocumentationMarkdown -Path $sourcePath
$generatedBlock = @"
<!-- SETPROTOCOL:START -->
<article class="card full-doc" data-source="c/set-protocol/docs/SETPROTOCOL.md">
@@ -15,6 +28,15 @@ $html
<!-- SETPROTOCOL:END -->
"@
$canFrameHtml = Convert-DocumentationMarkdown -Path $canFrameSourcePath
$canFrameBlock = @"
<!-- CAN-FRAME-PARSE:START -->
<article class="card full-doc" data-source="doc/CAN_FRAME_PARSE_V1_V2.md">
$canFrameHtml
</article>
<!-- CAN-FRAME-PARSE:END -->
"@
$page = Get-Content -Raw -LiteralPath $outputPath -Encoding UTF8
$pattern = '(?s)<!-- SETPROTOCOL:START -->.*?<!-- SETPROTOCOL:END -->'
if ($page -notmatch $pattern) {
@@ -26,6 +48,16 @@ $page = [regex]::Replace($page, $pattern, [System.Text.RegularExpressions.MatchE
$generatedBlock
}, 1)
$canFramePattern = '(?s)<!-- CAN-FRAME-PARSE:START -->.*?<!-- CAN-FRAME-PARSE:END -->'
if ($page -notmatch $canFramePattern) {
throw 'Не найдены маркеры CAN-FRAME-PARSE:START/END в doc/setprotocol.html.'
}
$page = [regex]::Replace($page, $canFramePattern, [System.Text.RegularExpressions.MatchEvaluator]{
param($match)
$canFrameBlock
}, 1)
$page = $page.TrimEnd("`r", "`n") + [Environment]::NewLine
$utf8WithoutBom = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText($outputPath, $page, $utf8WithoutBom)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,60 @@
"""CAN485 DevBoard_V1 USB command and frame helpers (no GUI/serial dependency)."""
from __future__ import annotations
import re
from dataclasses import dataclass
FRAME_RE = re.compile(
r"^\[\s*(\d+)\.(\d{3})\]\s+(?:(RS485)\s+)?"
r"(EXT|STD)\s+0x([0-9A-Fa-f]+)\s+"
r"(RTR\s+)?DLC=(\d)(?:\s+DATA=([0-9A-Fa-f ]*))?"
)
@dataclass(frozen=True, slots=True)
class BoardFrame:
timestamp: float
extended: bool
can_id: int
data: bytes
rtr: bool = False
source: str = "CAN"
def parse_frame_line(line: str) -> BoardFrame | None:
match = FRAME_RE.match(line.strip())
if match is None:
return None
seconds, millis, source, frame_type, raw_id, raw_rtr, raw_dlc, raw_data = match.groups()
dlc = int(raw_dlc)
data = bytes.fromhex(raw_data or "")
rtr = bool(raw_rtr)
if (not rtr and len(data) != dlc) or (rtr and data):
return None
extended = frame_type == "EXT"
can_id = int(raw_id, 16)
if can_id > (0x1FFFFFFF if extended else 0x7FF):
return None
return BoardFrame(int(seconds) + int(millis) / 1000.0, extended, can_id,
data, rtr, source or "CAN")
def command_transmit(frame: BoardFrame) -> bytes:
if len(frame.data) > 8:
raise ValueError("DLC cannot exceed 8")
kind = "E" if frame.extended else "S"
return f"T,{kind},{frame.can_id:X},{len(frame.data)},{frame.data.hex().upper()}\n".encode("ascii")
def command_setup(*, can_bitrate: int = 500, rs485_baud: int = 512000,
route: int = 3) -> tuple[bytes, ...]:
if can_bitrate not in (25, 50, 100, 125, 250, 500, 800, 1000):
raise ValueError("unsupported CAN bitrate")
if not 1200 <= rs485_baud <= 4_000_000:
raise ValueError("unsupported RS485 baud")
if route not in range(4):
raise ValueError("route must be 0..3")
return (f"S{can_bitrate}\n".encode("ascii"), b"M0\n",
f"B{rs485_baud}\n".encode("ascii"), f"Q{route}\n".encode("ascii"))

View File

@@ -0,0 +1,14 @@
from protocan.devboard_v1 import BoardFrame, command_setup, command_transmit, parse_frame_line
def test_parse_can_and_rs485_lines():
can = parse_frame_line("[ 12.345] EXT 0x073700A2 DLC=2 DATA=34 12 |4.|")
rs = parse_frame_line("[ 12.346] RS485 STD 0x123 RTR DLC=4")
assert can and can.can_id == 0x073700A2 and can.data == b"\x34\x12"
assert rs and rs.source == "RS485" and rs.rtr and rs.data == b""
def test_commands_are_firmware_compatible_and_lf_terminated():
frame = BoardFrame(0.0, True, 0x073700A2, b"\x34\x12")
assert command_transmit(frame) == b"T,E,73700A2,2,3412\n"
assert command_setup() == (b"S500\n", b"M0\n", b"B512000\n", b"Q3\n")