feat(set-protocol): добавь порт F407 для DevBoard_V1
This commit is contained in:
@@ -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 в комплекте** |
|
||||
|
||||
@@ -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)
|
||||
|
||||
60
c/set-protocol/include/pcan_modbus_server.h
Normal file
60
c/set-protocol/include/pcan_modbus_server.h
Normal 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 */
|
||||
@@ -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 */
|
||||
|
||||
22
c/set-protocol/ports/stm32f407-devboard-v1/README.md
Normal file
22
c/set-protocol/ports/stm32f407-devboard-v1/README.md
Normal 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.
|
||||
|
||||
```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);
|
||||
```
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "set_devboard_v1_stm32f407.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static bool transmit(set_devboard_v1_stm32f407_t *port,
|
||||
const pcan_frame_t *frame)
|
||||
{
|
||||
CAN_TxHeaderTypeDef header;
|
||||
uint32_t mailbox = 0U;
|
||||
uint8_t data[8] = {0U};
|
||||
|
||||
memset(&header, 0, sizeof(header));
|
||||
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;
|
||||
memcpy(data, frame->data, frame->dlc);
|
||||
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;
|
||||
}
|
||||
memset(port, 0, sizeof(*port));
|
||||
port->can = can;
|
||||
port->unhandled = unhandled;
|
||||
port->unhandled_user = unhandled_user;
|
||||
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;
|
||||
memcpy(request.data, data, request.dlc);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
144
c/set-protocol/src/pcan_modbus_server.c
Normal file
144
c/set-protocol/src/pcan_modbus_server.c
Normal 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;
|
||||
}
|
||||
90
c/set-protocol/tests/test_modbus_server.c
Normal file
90
c/set-protocol/tests/test_modbus_server.c
Normal 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;
|
||||
}
|
||||
@@ -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",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
60
python/protocan/devboard_v1.py
Normal file
60
python/protocan/devboard_v1.py
Normal 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"))
|
||||
14
python/tests/test_devboard_v1.py
Normal file
14
python/tests/test_devboard_v1.py
Normal 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")
|
||||
Reference in New Issue
Block a user