diff --git a/README.md b/README.md index c6268bd..b8eb5f8 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/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 в комплекте** | diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index 30dc23a..40dafeb 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -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) diff --git a/c/set-protocol/include/pcan_modbus_server.h b/c/set-protocol/include/pcan_modbus_server.h new file mode 100644 index 0000000..a8e4c01 --- /dev/null +++ b/c/set-protocol/include/pcan_modbus_server.h @@ -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 +#include + +#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 */ diff --git a/c/set-protocol/include/protocan_transport.h b/c/set-protocol/include/protocan_transport.h index 379a9cb..bd9090c 100644 --- a/c/set-protocol/include/protocan_transport.h +++ b/c/set-protocol/include/protocan_transport.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 */ diff --git a/c/set-protocol/ports/stm32f407-devboard-v1/README.md b/c/set-protocol/ports/stm32f407-devboard-v1/README.md new file mode 100644 index 0000000..83f56ef --- /dev/null +++ b/c/set-protocol/ports/stm32f407-devboard-v1/README.md @@ -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); +``` diff --git a/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c b/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c new file mode 100644 index 0000000..9f4a98d --- /dev/null +++ b/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.c @@ -0,0 +1,80 @@ +#include "set_devboard_v1_stm32f407.h" + +#include + +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; +} diff --git a/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.h b/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.h new file mode 100644 index 0000000..862154d --- /dev/null +++ b/c/set-protocol/ports/stm32f407-devboard-v1/set_devboard_v1_stm32f407.h @@ -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 diff --git a/c/set-protocol/src/pcan_modbus_server.c b/c/set-protocol/src/pcan_modbus_server.c new file mode 100644 index 0000000..7b304e6 --- /dev/null +++ b/c/set-protocol/src/pcan_modbus_server.c @@ -0,0 +1,144 @@ +#include "pcan_modbus_server.h" + +#include + +#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; +} diff --git a/c/set-protocol/tests/test_modbus_server.c b/c/set-protocol/tests/test_modbus_server.c new file mode 100644 index 0000000..7b6f89b --- /dev/null +++ b/c/set-protocol/tests/test_modbus_server.c @@ -0,0 +1,90 @@ +#include +#include + +#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; +} diff --git a/c/set-protocol/tools/build_host.py b/c/set-protocol/tools/build_host.py index 2f54914..e208eb3 100644 --- a/c/set-protocol/tools/build_host.py +++ b/c/set-protocol/tools/build_host.py @@ -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", ) ] diff --git a/python/protocan/devboard_v1.py b/python/protocan/devboard_v1.py new file mode 100644 index 0000000..6d3116a --- /dev/null +++ b/python/protocan/devboard_v1.py @@ -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")) diff --git a/python/tests/test_devboard_v1.py b/python/tests/test_devboard_v1.py new file mode 100644 index 0000000..c9ff90b --- /dev/null +++ b/python/tests/test_devboard_v1.py @@ -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")