Объедини Modbus в один тип с функциями и количеством 1–4
This commit is contained in:
@@ -47,9 +47,7 @@ typedef enum {
|
|||||||
PCAN_MSG_ANALOG = 0x2U,
|
PCAN_MSG_ANALOG = 0x2U,
|
||||||
PCAN_MSG_GAS = 0x3U, /**< общее адресное пространство */
|
PCAN_MSG_GAS = 0x3U, /**< общее адресное пространство */
|
||||||
PCAN_MSG_GAS_SLOT = 0x4U, /**< Слотный GAS с маской слов; код 0x4 освобождён от Modbus Coil. */
|
PCAN_MSG_GAS_SLOT = 0x4U, /**< Слотный GAS с маской слов; код 0x4 освобождён от Modbus Coil. */
|
||||||
PCAN_MSG_MODBUS_DISCR = 0x5U,
|
PCAN_MSG_MODBUS = 0x5U, /**< Единый Modbus; банк выбирается функцией 1…4 в MsgBody, коды 0x6/0x7 свободны. */
|
||||||
PCAN_MSG_MODBUS_HOLDING = 0x6U,
|
|
||||||
PCAN_MSG_MODBUS_INPUT = 0x7U,
|
|
||||||
PCAN_MSG_ERROR = 0x8U,
|
PCAN_MSG_ERROR = 0x8U,
|
||||||
PCAN_MSG_PULSE = 0xFU
|
PCAN_MSG_PULSE = 0xFU
|
||||||
} pcan_msgtype_t;
|
} pcan_msgtype_t;
|
||||||
@@ -81,11 +79,21 @@ static inline uint16_t pcan_body_typed(uint_least8_t type, uint16_t body) /* Н
|
|||||||
return (uint16_t)(((uint16_t)(type & 0x0FU) << 12) | (body & 0x0FFFU));
|
return (uint16_t)(((uint16_t)(type & 0x0FU) << 12) | (body & 0x0FFFU));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** MODBUS: StrAdr[15:4], RegCount[3:0]. */
|
/** MODBUS: StrAdr[15:4], Function-1[3:2], RegCount-1[1:0]; вызывающий проверяет адрес 0…4095 и значения 1…4. */
|
||||||
static inline uint16_t pcan_body_modbus(uint16_t addr, uint_least8_t count) /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */
|
static inline uint16_t pcan_body_modbus(uint16_t addr, uint_least8_t count, uint_least8_t function) /* Два двухбитных поля кодируют четыре функции и четыре количества без потери 12-битного адреса. */
|
||||||
{
|
{ /* Упаковка применяется после проверки диапазонов параметров на стороне клиента. */
|
||||||
return (uint16_t)(((addr & 0x0FFFU) << 4) | (count & 0x0FU));
|
return (uint16_t)(((addr & 0x0FFFU) << 4) | (((function - 1U) & 3U) << 2) | ((count - 1U) & 3U)); /* 0xFFF ограничивает адрес 12 битами; смещение на единицу переводит значения 1…4 в wire-коды 0…3. */
|
||||||
}
|
} /* Конец упаковки единого Modbus типа 0x5. */
|
||||||
|
|
||||||
|
static inline uint_least8_t pcan_modbus_count(uint16_t body) /* Возвращает физическое количество регистров или битов из двух младших битов Body. */
|
||||||
|
{ /* Каждый двухбитный wire-код имеет допустимое значение. */
|
||||||
|
return (uint_least8_t)((body & 3U) + 1U); /* Маска 3 выделяет биты 1:0; прибавление единицы восстанавливает диапазон 1…4. */
|
||||||
|
} /* Конец извлечения количества. */
|
||||||
|
|
||||||
|
static inline uint_least8_t pcan_modbus_function(uint16_t body) /* Возвращает номер функции: Coil=1, Discrete=2, Holding=3, Input=4. */
|
||||||
|
{ /* Функция занимает два бита между адресом и количеством. */
|
||||||
|
return (uint_least8_t)(((body >> 2) & 3U) + 1U); /* Сдвиг выделяет биты 3:2; wire-коды 0…3 соответствуют функциям 1…4. */
|
||||||
|
} /* Конец извлечения функции. */
|
||||||
|
|
||||||
/** ERROR: Info[15:8], Code[7:0]. */
|
/** ERROR: Info[15:8], Code[7:0]. */
|
||||||
static inline uint16_t pcan_body_error(uint_least8_t info, uint_least8_t code) /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */
|
static inline uint16_t pcan_body_error(uint_least8_t info, uint_least8_t code) /* На C28x хранит октет в доступном 16-битном типе; допустимые значения поля и host ABI остаются прежними. */
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ extern "C" {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
PCAN_MODBUS_DISCRETE = 0x5U,
|
PCAN_MODBUS_COIL = 1U, /* Функция 1 выбирает банк дискретных выходов. */
|
||||||
PCAN_MODBUS_HOLDING = 0x6U,
|
PCAN_MODBUS_DISCRETE = 2U, /* Функция 2 выбирает банк дискретных входов. */
|
||||||
PCAN_MODBUS_INPUT = 0x7U
|
PCAN_MODBUS_HOLDING = 3U, /* Функция 3 выбирает банк изменяемых 16-битных регистров. */
|
||||||
|
PCAN_MODBUS_INPUT = 4U /* Функция 4 выбирает банк входных 16-битных регистров. */
|
||||||
} pcan_modbus_bank_t;
|
} pcan_modbus_bank_t;
|
||||||
|
|
||||||
typedef bool (*pcan_modbus_read_fn)(void *user, pcan_modbus_bank_t bank,
|
typedef bool (*pcan_modbus_read_fn)(void *user, pcan_modbus_bank_t bank,
|
||||||
@@ -53,7 +54,7 @@ bool pcan_modbus_server_init(pcan_modbus_server_t *server,
|
|||||||
pcan_modbus_read_fn read,
|
pcan_modbus_read_fn read,
|
||||||
pcan_modbus_write_fn write, void *user);
|
pcan_modbus_write_fn write, void *user);
|
||||||
|
|
||||||
/** DLC=0 читает DISCRETE/HOLDING/INPUT; данные записывают только HOLDING. Тип 0x4 принадлежит GAS SLOT. */
|
/** MsgType=0x5, функция/количество 1…4: DLC=0 читает банк; данные записывают COIL/HOLDING по правилам ProtoCAN. */
|
||||||
pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
|
pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
|
||||||
const pcan_frame_t *request,
|
const pcan_frame_t *request,
|
||||||
pcan_frame_t *response);
|
pcan_frame_t *response);
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ enum class ProtoCanMessageType(val code: Int, val title: String) {
|
|||||||
ANALOG(0x2, "Analog"),
|
ANALOG(0x2, "Analog"),
|
||||||
GENERAL_ADDRESS_SPACE(0x3, "General Address Space"),
|
GENERAL_ADDRESS_SPACE(0x3, "General Address Space"),
|
||||||
GAS_SLOT(0x4, "GAS Slot"), // Код 0x4 выбирает слотный GAS с маской, регистровый GAS сохраняет собственный код 0x3.
|
GAS_SLOT(0x4, "GAS Slot"), // Код 0x4 выбирает слотный GAS с маской, регистровый GAS сохраняет собственный код 0x3.
|
||||||
MODBUS_DISCRETE(0x5, "Modbus Discrete"),
|
MODBUS(0x5, "Modbus"), // Функции 1…4 в Body выбирают банк; номера типов 0x6 и 0x7 освобождены.
|
||||||
MODBUS_HOLDING(0x6, "Modbus Holding"),
|
|
||||||
MODBUS_INPUT(0x7, "Modbus Input"),
|
|
||||||
ERROR(0x8, "Error"),
|
ERROR(0x8, "Error"),
|
||||||
BOOT_CONTROL(0x9, "Boot Control"),
|
BOOT_CONTROL(0x9, "Boot Control"),
|
||||||
BOOT_DATA_A(0xA, "Boot Data A"),
|
BOOT_DATA_A(0xA, "Boot Data A"),
|
||||||
@@ -63,8 +61,10 @@ object ProtoCanBody {
|
|||||||
|
|
||||||
fun typed(type: Int, body: Int): Int = ((type and 0xF) shl 12) or (body and 0xFFF)
|
fun typed(type: Int, body: Int): Int = ((type and 0xF) shl 12) or (body and 0xFFF)
|
||||||
|
|
||||||
fun modbus(address: Int, registerCount: Int): Int =
|
fun modbus(address: Int, registerCount: Int, function: Int): Int { // Явная функция 1…4 выбирает Coil, Discrete, Holding или Input.
|
||||||
((address and 0xFFF) shl 4) or (registerCount and 0xF)
|
require(address in 0..0xFFF && registerCount in 1..4 && function in 1..4 && address + registerCount <= 0x1000) // Проверяем 12-битное окно и два поля без молчаливого усечения.
|
||||||
|
return (address shl 4) or ((function - 1) shl 2) or (registerCount - 1) // Биты 3:2 содержат функцию минус один, биты 1:0 — количество минус один.
|
||||||
|
} // Конец упаковки единого Modbus типа 0x5.
|
||||||
|
|
||||||
fun error(info: Int, code: Int): Int = ((info and 0xFF) shl 8) or (code and 0xFF)
|
fun error(info: Int, code: Int): Int = ((info and 0xFF) shl 8) or (code and 0xFF)
|
||||||
|
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ class ProtoCanCatalogTest {
|
|||||||
fun bodyLayoutsMatchSetGuiBuilder() {
|
fun bodyLayoutsMatchSetGuiBuilder() {
|
||||||
assertEquals(0x1234, ProtoCanBody.broadcast(0x123, 0x4))
|
assertEquals(0x1234, ProtoCanBody.broadcast(0x123, 0x4))
|
||||||
assertEquals(0x2001, ProtoCanBody.typed(ProtoCanAnalogType.U.code, 1))
|
assertEquals(0x2001, ProtoCanBody.typed(ProtoCanAnalogType.U.code, 1))
|
||||||
assertEquals(0x1234, ProtoCanBody.modbus(0x123, 4))
|
assertEquals(0x123B, ProtoCanBody.modbus(0x123, 4, 3)) // Holding=3 и четыре регистра кодируются младшим полубайтом 1011.
|
||||||
assertEquals(0xABCD, ProtoCanBody.error(0xAB, 0xCD))
|
assertEquals(0xABCD, ProtoCanBody.error(0xAB, 0xCD))
|
||||||
assertEquals(0xFF01, ProtoCanBody.settings(0xFF, 0x01))
|
assertEquals(0xFF01, ProtoCanBody.settings(0xFF, 0x01))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun messageRegistryCoversAllFourBitValues() {
|
fun messageRegistryReservesFormerModbusTypes() { // После объединения Modbus два кода остаются свободными, остальные сохраняют назначение.
|
||||||
assertEquals((0..0xF).toList(), ProtoCanMessageType.entries.map { it.code })
|
assertEquals((0..0xF).filter { it != 6 && it != 7 }, ProtoCanMessageType.entries.map { it.code }) // Освобождённые MsgType 0x6/0x7 отсутствуют в каталоге после объединения Modbus.
|
||||||
assertEquals("Settings", ProtoCanId.parse(0x17FE1234).messageTypeName)
|
assertEquals("Settings", ProtoCanId.parse(0x17FE1234).messageTypeName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,9 +178,7 @@ void PROTOCAN_LoopProcessRxMsgs(void)
|
|||||||
{
|
{
|
||||||
PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]);
|
PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]);
|
||||||
}
|
}
|
||||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || /* Тип 0x4 передаётся отдельному слотному обработчику, Modbus начинается с 0x5. */
|
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS) /* Единый MsgType 0x5 передаёт функцию 1…4 в Body; 0x6/0x7 больше не относятся к Modbus. */
|
||||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING ||
|
|
||||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT)
|
|
||||||
{
|
{
|
||||||
PROTOCAN_ModbusProcessing(rxMsg[CurrentStep]);
|
PROTOCAN_ModbusProcessing(rxMsg[CurrentStep]);
|
||||||
}
|
}
|
||||||
@@ -237,9 +235,7 @@ PROTOCAN_StatusTypeDef PROTOCAN_ProcessAllRxMsgs(void)
|
|||||||
{
|
{
|
||||||
result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]);
|
result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]);
|
||||||
}
|
}
|
||||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || /* Тип 0x4 передаётся отдельному слотному обработчику, Modbus начинается с 0x5. */
|
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS) /* Единый MsgType 0x5 передаёт функцию 1…4 в Body; 0x6/0x7 больше не относятся к Modbus. */
|
||||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING ||
|
|
||||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT)
|
|
||||||
{
|
{
|
||||||
result = PROTOCAN_ModbusProcessing(rxMsg[CurrentStep]);
|
result = PROTOCAN_ModbusProcessing(rxMsg[CurrentStep]);
|
||||||
}
|
}
|
||||||
@@ -297,9 +293,7 @@ PROTOCAN_StatusTypeDef PROTOCAN_ProcessSingleRxMsg(void)
|
|||||||
{
|
{
|
||||||
result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]);
|
result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]);
|
||||||
}
|
}
|
||||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || /* Тип 0x4 передаётся отдельному слотному обработчику, Modbus начинается с 0x5. */
|
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS) /* Единый MsgType 0x5 передаёт функцию 1…4 в Body; 0x6/0x7 больше не относятся к Modbus. */
|
||||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING ||
|
|
||||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT)
|
|
||||||
{
|
{
|
||||||
result = PROTOCAN_ModbusProcessing(rxMsg[CurrentStep]);
|
result = PROTOCAN_ModbusProcessing(rxMsg[CurrentStep]);
|
||||||
}
|
}
|
||||||
@@ -1121,39 +1115,16 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND_GENERAL_ADDRESS_SPACE(ProtoCanId_t id, uint
|
|||||||
return (PROTOCAN_StatusTypeDef)HAL_OK;
|
return (PROTOCAN_StatusTypeDef)HAL_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
PROTOCAN_StatusTypeDef PROTOCAN_ModbusProcessing(struct RXMsg message) /* Проверяем общий контракт перед передачей единому обработчику приложения. */
|
||||||
* @brief Обработка запроса на основе типа Modbus.
|
{ /* Битовая функция и количество извлекаются переносимыми помощниками, без зависимости от C bitfields. */
|
||||||
* В зависимости от типа сообщения, вызывает соответствующую функцию
|
unsigned count = pcan_modbus_count((uint16_t)message.eID.Fields.MsgBody); /* Wire-код 0…3 преобразуется в физическое количество 1…4. */
|
||||||
* для отправки Modbus-запроса по CAN-шине.
|
unsigned function = pcan_modbus_function((uint16_t)message.eID.Fields.MsgBody); /* Функции 1/2 выбирают биты, 3/4 — 16-битные регистры. */
|
||||||
* @param _rxMsg Структура входящего сообщения RXMsg.
|
unsigned expected = function <= 2U ? 1U : count * 2U; /* Битовому банку нужен один октет, регистровому — два октета на слово. */
|
||||||
* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка).
|
if (message.eID.Fields.MsgType != PROTOCAN_MSGTYPE_MODBUS || !message.info.EXT || message.info.RTR || (message.eID.Fields.MsgBody >> 4U) + count > 0x1000U) return PROTOCAN_ERROR; /* Отвергаем чужой формат и выход окна за 12-битное адресное пространство. */
|
||||||
*/
|
if (message.DLC != 0U && message.DLC != expected) return PROTOCAN_ERROR; /* Непустой payload должен точно соответствовать функции и количеству. */
|
||||||
PROTOCAN_StatusTypeDef PROTOCAN_ModbusProcessing(struct RXMsg _rxMsg)
|
if (message.DLC != 0U && function <= 2U && (message.Data[0] >> count) != 0U) return PROTOCAN_ERROR; /* За пределами запрошенных 1…4 битов нет значащих данных. */
|
||||||
{
|
return ProtoCanMsgToModbus(message); /* Приложение получает единый тип сообщения и само выбирает карту по функции. */
|
||||||
switch(_rxMsg.eID.Fields.MsgType)
|
} /* Конец проверки и диспетчеризации единого Modbus. */
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const pcan_gas_slot_port_t *gas_slot_port; /* Provider принадлежит приложению; NULL означает, что слотный сервис ещё не подключён. */
|
static const pcan_gas_slot_port_t *gas_slot_port; /* Provider принадлежит приложению; NULL означает, что слотный сервис ещё не подключён. */
|
||||||
void PROTOCAN_GAS_SLOT_BIND(const pcan_gas_slot_port_t *port) /* Вызывать при инициализации до обработки очереди CAN. */
|
void PROTOCAN_GAS_SLOT_BIND(const pcan_gas_slot_port_t *port) /* Вызывать при инициализации до обработки очереди CAN. */
|
||||||
@@ -1187,187 +1158,35 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToGasSlot(struct RXMsg message) /* Пр
|
|||||||
} /* Завершаем отправку сформированного ответа. */
|
} /* Завершаем отправку сформированного ответа. */
|
||||||
} /* Конец стандартного адаптера слотного GAS. */
|
} /* Конец стандартного адаптера слотного GAS. */
|
||||||
|
|
||||||
/**
|
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbus(struct RXMsg message) /* Приложение переопределяет одну точку для функций 1…4. */
|
||||||
* @brief __weak Функция отправки ответа на запрос Discrete Modbus по CAN.
|
{ /* Без прикладной карты регистров возвращаем штатную ошибку необработанного запроса. */
|
||||||
* Формирует CAN ответ на запрос Discrete Modbus и отправляет его.
|
return PROTOCAN_RequestError(message); /* Не публикуем демонстрационные ASCII-байты как реальные значения 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
PROTOCAN_StatusTypeDef PROTOCAN_SEND_MODBUS(const ProtoCanId_t id, const struct ProtoCanModbusData data) /* Отправляет одно окно из 1…4 битов или регистров. */
|
||||||
* @brief __weak Функция отправки ответа на запрос Holding Modbus по CAN.
|
{ /* Вся полезная нагрузка помещается в один classic CAN-кадр с DLC не более восьми. */
|
||||||
* Формирует CAN ответ на запрос Holding Modbus и отправляет его.
|
ProtoCanId_t local = id; /* Сохраняем исходный адрес узла, приоритет и направление. */
|
||||||
* @param _rxMsg Структура входящего сообщения RXMsg.
|
CAN_TxHeaderTypeDef header = {0}; /* Нулевая инициализация исключает неопределённые поля HAL-заголовка. */
|
||||||
* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка).
|
uint32_t mailbox = 0U; /* HAL запишет номер использованного аппаратного mailbox. */
|
||||||
*/
|
uint8_t bytes[8] = {0}; /* Максимум четыре 16-битных регистра занимают восемь октетов. */
|
||||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusHolding(struct RXMsg _rxMsg)
|
unsigned index; /* Индекс ограничен проверенным количеством регистров 1…4. */
|
||||||
{
|
if (_HCAN == NULL || local.Fields.MsgType != PROTOCAN_MSGTYPE_MODBUS || data.Function < 1U || data.Function > 4U || data.RegCount < 1U || data.RegCount > 4U || data.StrAdr > 0xFFFU || data.StrAdr + data.RegCount > 0x1000U) return PROTOCAN_ERROR; /* Не отправляем неинициализированный CAN или значения за пределами wire-полей. */
|
||||||
CAN_TxHeaderTypeDef TxHeader;
|
if (data.Data == NULL && local.Fields.Route == PROTOCAN_ROUTE_FROM_DEVICE) return PROTOCAN_ERROR; /* Устройство не отправляет пустые ответы вместо запрошенных значений. */
|
||||||
uint32_t TxMailBox = 0;
|
local.Fields.MsgBody = pcan_body_modbus(data.StrAdr, (uint_least8_t)data.RegCount, (uint_least8_t)data.Function); /* Общий упаковщик помещает функцию минус один в 3:2, количество минус один в 1:0. */
|
||||||
uint8_t data[8];
|
header.ExtId = PROTOCAN_IdPack(&local); /* Используем явную переносимую упаковку всех полей CAN ID. */
|
||||||
TxHeader.IDE = CAN_ID_EXT;
|
header.IDE = CAN_ID_EXT; header.RTR = CAN_RTR_DATA; header.TransmitGlobalTime = DISABLE; /* ProtoCAN передаётся расширенным data frame без RTR. */
|
||||||
TxHeader.TransmitGlobalTime = DISABLE;
|
header.DLC = data.Data == NULL ? 0U : (data.Function <= 2U ? 1U : data.RegCount * 2U); /* NULL запрашивает чтение; непустой буфер содержит один октет битов либо 2…8 октетов регистров. */
|
||||||
TxHeader.RTR = CAN_RTR_DATA;
|
if (data.Data != NULL && data.Function <= 2U) { /* Битовые банки используют младшие биты первого слова, как и переносимый сервер. */
|
||||||
ProtoCanId_t tmp_eID;
|
if ((data.Data[0] >> data.RegCount) != 0U) return PROTOCAN_ERROR; /* Не усекаем установленные биты за пределами запрошенного окна. */
|
||||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
bytes[0] = (uint8_t)data.Data[0]; /* Для 1…4 битов достаточно одного wire-октета. */
|
||||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
} else if (data.Data != NULL) { /* Слова Holding/Input кодируются little-endian. */
|
||||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
for (index = 0U; index < data.RegCount; ++index) { /* Каждому адресу соответствует ровно одно 16-битное слово. */
|
||||||
TxHeader.DLC = 8;
|
bytes[index * 2U] = (uint8_t)data.Data[index]; /* Младший октет располагается первым в payload. */
|
||||||
msgBodyModbusType msg;
|
bytes[index * 2U + 1U] = (uint8_t)(data.Data[index] >> 8U); /* Сдвиг на восемь бит извлекает старший октет регистра. */
|
||||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
} /* Заканчиваем упаковку проверенного числа регистров. */
|
||||||
data[0] = 'M';
|
} /* Для запроса чтения массив остаётся пустым, поскольку DLC равен нулю. */
|
||||||
data[1] = 'H';
|
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &header, bytes, &mailbox); /* Возвращаем фактический результат постановки одного кадра в очередь HAL. */
|
||||||
data[2] = ' ';
|
} /* Конец отправки единого Modbus типа 0x5. */
|
||||||
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 (discrete или holding/input), формируется
|
|
||||||
* соответствующий CAN-пакет. Для 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_DISCRETE) /* Битовая ветвь Modbus поддерживает только входы 0x5; 0x4 зарезервирован слотному GAS. */
|
|
||||||
{
|
|
||||||
// Формируем 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 Функция отправки сообщения об ошибке.
|
* @brief __weak Функция отправки сообщения об ошибке.
|
||||||
@@ -1655,9 +1474,7 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND(ProtoCanId_t id, ProtoCanData_t data)
|
|||||||
}
|
}
|
||||||
case PROTOCAN_MSGTYPE_GAS_SLOT: /* Отправка 0x4 использует служебную маску, а не структуру Modbus. */
|
case PROTOCAN_MSGTYPE_GAS_SLOT: /* Отправка 0x4 использует служебную маску, а не структуру Modbus. */
|
||||||
return PROTOCAN_SEND_GAS_SLOT(id, data.GasSlotData.Address, data.GasSlotData.Words, data.GasSlotData.Count); /* Count=0 запрашивает чтение; Count=4 передаёт маску и три значения. */
|
return PROTOCAN_SEND_GAS_SLOT(id, data.GasSlotData.Address, data.GasSlotData.Words, data.GasSlotData.Count); /* Count=0 запрашивает чтение; Count=4 передаёт маску и три значения. */
|
||||||
case PROTOCAN_MSGTYPE_MODBUS_DISCRETE:
|
case PROTOCAN_MSGTYPE_MODBUS: /* Все четыре банка отправляются одним типом, функция задаётся в ModbusData. */
|
||||||
case PROTOCAN_MSGTYPE_MODBUS_HOLDING:
|
|
||||||
case PROTOCAN_MSGTYPE_MODBUS_INPUT:
|
|
||||||
{
|
{
|
||||||
return PROTOCAN_SEND_MODBUS(id, data.ModbusData);
|
return PROTOCAN_SEND_MODBUS(id, data.ModbusData);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -84,9 +84,7 @@ typedef enum {
|
|||||||
PROTOCAN_MSGTYPE_ANALOG = 0b0010, /**< Аналоговое сообщение. */
|
PROTOCAN_MSGTYPE_ANALOG = 0b0010, /**< Аналоговое сообщение. */
|
||||||
PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE = 0b0011, /**< Сообщение общего адресного пространства. */
|
PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE = 0b0011, /**< Сообщение общего адресного пространства. */
|
||||||
PROTOCAN_MSGTYPE_GAS_SLOT = 0b0100, /**< Слотный GAS: Word0 — маска, Word1…Word3 — значения; прежний код Coil заменён. */
|
PROTOCAN_MSGTYPE_GAS_SLOT = 0b0100, /**< Слотный GAS: Word0 — маска, Word1…Word3 — значения; прежний код Coil заменён. */
|
||||||
PROTOCAN_MSGTYPE_MODBUS_DISCRETE = 0b0101, /**< Сообщение Modbus - Discrete. */
|
PROTOCAN_MSGTYPE_MODBUS = 0b0101, /**< Единый Modbus с функцией 1…4 в Body; номера 0x6/0x7 свободны. */
|
||||||
PROTOCAN_MSGTYPE_MODBUS_HOLDING = 0b0110, /**< Сообщение Modbus - Holding. */
|
|
||||||
PROTOCAN_MSGTYPE_MODBUS_INPUT = 0b0111, /**< Сообщение Modbus - Input. */
|
|
||||||
PROTOCAN_MSGTYPE_ERROR = 0b1000, /**< Сообщение об ошибке. */
|
PROTOCAN_MSGTYPE_ERROR = 0b1000, /**< Сообщение об ошибке. */
|
||||||
PROTOCAN_MSGTYPE_BOOT_CONTROL = 0b1001, /**< Команды ProtoCAN Boot. */
|
PROTOCAN_MSGTYPE_BOOT_CONTROL = 0b1001, /**< Команды ProtoCAN Boot. */
|
||||||
PROTOCAN_MSGTYPE_BOOT_DATA_A = 0b1010, /**< Блок образа для слота A. */
|
PROTOCAN_MSGTYPE_BOOT_DATA_A = 0b1010, /**< Блок образа для слота A. */
|
||||||
@@ -225,7 +223,8 @@ typedef union{
|
|||||||
*/
|
*/
|
||||||
typedef union{
|
typedef union{
|
||||||
struct msgBody_Modbus{
|
struct msgBody_Modbus{
|
||||||
unsigned RegCount:4; /**< Количество регистров (4 бита). */
|
unsigned RegCountMinus1:2; /**< Биты 1:0 хранят количество регистров или битов минус один, wire-коды 0…3. */
|
||||||
|
unsigned FunctionMinus1:2; /**< Биты 3:2 хранят функцию Coil=1, Discrete=2, Holding=3, Input=4 минус один. */
|
||||||
unsigned StrAdr:12; /**< Адрес строки или регистра (12 бит). */
|
unsigned StrAdr:12; /**< Адрес строки или регистра (12 бит). */
|
||||||
} Fields; /**< Поля структуры сообщения Modbus. */
|
} Fields; /**< Поля структуры сообщения Modbus. */
|
||||||
unsigned Body; /**< Общее 16-битное представление сообщения. */
|
unsigned Body; /**< Общее 16-битное представление сообщения. */
|
||||||
@@ -321,9 +320,10 @@ typedef struct{
|
|||||||
unsigned Count; /* Ноль означает чтение, четыре — маску и три значения; другие размеры отвергает C-кодек. */
|
unsigned Count; /* Ноль означает чтение, четыре — маску и три значения; другие размеры отвергает C-кодек. */
|
||||||
} GasSlotData; /* Член структуры используется только для MsgType=0x4. */
|
} GasSlotData; /* Член структуры используется только для MsgType=0x4. */
|
||||||
struct ProtoCanModbusData{
|
struct ProtoCanModbusData{
|
||||||
uint16_t StrAdr;
|
uint16_t StrAdr; /* Адрес первого регистра или бита 0…4095; всё окно должно помещаться в этот диапазон. */
|
||||||
uint16_t *Data;
|
uint16_t *Data; /* NULL запрашивает чтение; битовые банки используют упакованную маску в Data[0], регистровые — массив слов. */
|
||||||
unsigned RegCount:4;
|
unsigned RegCount; /* Физическое количество 1…4; обычное поле позволяет отклонить неверный ввод до упаковки. */
|
||||||
|
unsigned Function; /* Физический номер функции 1…4, независимый от единого MsgType=0x5. */
|
||||||
} ModbusData;
|
} ModbusData;
|
||||||
struct ProtoCanErrorData{
|
struct ProtoCanErrorData{
|
||||||
uint16_t Info;
|
uint16_t Info;
|
||||||
@@ -431,9 +431,7 @@ PROTOCAN_StatusTypeDef PROTOCAN_ModbusProcessing(struct RXMsg _rxMsg);
|
|||||||
void PROTOCAN_GAS_SLOT_BIND(const pcan_gas_slot_port_t *port); /* Подключает долгоживущий provider слотов до обработки очереди; NULL отключает сервис. */
|
void PROTOCAN_GAS_SLOT_BIND(const pcan_gas_slot_port_t *port); /* Подключает долгоживущий provider слотов до обработки очереди; NULL отключает сервис. */
|
||||||
PROTOCAN_StatusTypeDef ProtoCanMsgToGasSlot(struct RXMsg message); /* Передаёт тип 0x4 общему обработчику с проверкой адреса узла и маски Word0. */
|
PROTOCAN_StatusTypeDef ProtoCanMsgToGasSlot(struct RXMsg message); /* Передаёт тип 0x4 общему обработчику с проверкой адреса узла и маски Word0. */
|
||||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND_GAS_SLOT(ProtoCanId_t id, uint16_t address, const uint16_t *words, unsigned count); /* Отправляет один слот через общий C-кодек, без Modbus-упаковки адреса. */
|
PROTOCAN_StatusTypeDef PROTOCAN_SEND_GAS_SLOT(ProtoCanId_t id, uint16_t address, const uint16_t *words, unsigned count); /* Отправляет один слот через общий C-кодек, без Modbus-упаковки адреса. */
|
||||||
PROTOCAN_StatusTypeDef ProtoCanMsgToModbusDiscrete(struct RXMsg _rxMsg);
|
PROTOCAN_StatusTypeDef ProtoCanMsgToModbus(struct RXMsg _rxMsg); /* Единая weak-точка приложения обрабатывает все четыре банка по полю функции Body. */
|
||||||
PROTOCAN_StatusTypeDef ProtoCanMsgToModbusHolding(struct RXMsg _rxMsg);
|
|
||||||
PROTOCAN_StatusTypeDef ProtoCanMsgToModbusInput(struct RXMsg _rxMsg);
|
|
||||||
|
|
||||||
void PROTOCAN_RTC_SYNC(uint8_t *data);
|
void PROTOCAN_RTC_SYNC(uint8_t *data);
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,6 @@
|
|||||||
|
|
||||||
#include "pcan_id.h"
|
#include "pcan_id.h"
|
||||||
|
|
||||||
static bool is_bank(uint8_t value)
|
|
||||||
{
|
|
||||||
return (value >= (uint8_t)PCAN_MODBUS_DISCRETE) && /* Диапазон Modbus начинается с 0x5: тип 0x4 занят слотным GAS и не должен вызывать Modbus callbacks. */
|
|
||||||
(value <= (uint8_t)PCAN_MODBUS_INPUT); /* Сохраняем существующие номера HOLDING=0x6 и INPUT=0x7 без сдвига адресации. */
|
|
||||||
}
|
|
||||||
|
|
||||||
bool pcan_modbus_server_init(pcan_modbus_server_t *server,
|
bool pcan_modbus_server_init(pcan_modbus_server_t *server,
|
||||||
uint8_t device_type, uint8_t device_id,
|
uint8_t device_type, uint8_t device_id,
|
||||||
pcan_modbus_read_fn read,
|
pcan_modbus_read_fn read,
|
||||||
@@ -58,15 +52,15 @@ pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
|
|||||||
pcan_id_unpack(request->id, &id);
|
pcan_id_unpack(request->id, &id);
|
||||||
if ((id.route != PCAN_ROUTE_FROM_PM) ||
|
if ((id.route != PCAN_ROUTE_FROM_PM) ||
|
||||||
(id.device_type != server->device_type) ||
|
(id.device_type != server->device_type) ||
|
||||||
(id.device_id != server->device_id) || !is_bank(id.msg_type)) {
|
(id.device_id != server->device_id) || (id.msg_type != PCAN_MSG_MODBUS)) { /* Только единый тип 0x5 адресует Modbus; 0x4, 0x6 и 0x7 не вызывают provider. */
|
||||||
return PCAN_MODBUS_NOT_FOR_US;
|
return PCAN_MODBUS_NOT_FOR_US;
|
||||||
}
|
}
|
||||||
|
|
||||||
server->requests++;
|
server->requests++;
|
||||||
address = (uint16_t)(id.msg_body >> 4U);
|
address = (uint16_t)(id.msg_body >> 4U);
|
||||||
count = (uint8_t)(id.msg_body & 0x0FU);
|
count = (uint8_t)pcan_modbus_count(id.msg_body); /* Два младших бита хранят количество минус один, поэтому нулевого запроса больше нет. */
|
||||||
bank = (pcan_modbus_bank_t)id.msg_type;
|
bank = (pcan_modbus_bank_t)pcan_modbus_function(id.msg_body); /* Биты 3:2 выбирают банк Coil/Discrete/Holding/Input через функцию 1…4. */
|
||||||
if (count == 0U) {
|
if ((address + count > 0x1000U) || (request->dlc > 8U) || ((request->flags & PCAN_FLAG_RTR) != 0U)) { /* Диапазон не пересекает границу 4096 адресов; поддерживаются только classic data frames. */
|
||||||
return reject(server);
|
return reject(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,10 +72,7 @@ pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
|
|||||||
|
|
||||||
if (request->dlc == 0U) {
|
if (request->dlc == 0U) {
|
||||||
uint16_t packed = 0U;
|
uint16_t packed = 0U;
|
||||||
if (bank == PCAN_MODBUS_DISCRETE) { /* Только тип 0x5 передаёт битовые входы; 0x4 обрабатывается отдельным GAS-кодеком. */
|
if ((bank == PCAN_MODBUS_COIL) || (bank == PCAN_MODBUS_DISCRETE)) { /* Функции 1 и 2 возвращают до четырёх последовательных битов в одном октете. */
|
||||||
if (count > 16U) {
|
|
||||||
return reject(server);
|
|
||||||
}
|
|
||||||
for (uint8_t index = 0U; index < count; ++index) {
|
for (uint8_t index = 0U; index < count; ++index) {
|
||||||
uint16_t value = 0U;
|
uint16_t value = 0U;
|
||||||
if (!server->read(server->user, bank,
|
if (!server->read(server->user, bank,
|
||||||
@@ -92,9 +83,8 @@ pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
|
|||||||
packed |= (uint16_t)(1U << index);
|
packed |= (uint16_t)(1U << index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response->dlc = 2U;
|
response->dlc = 1U; /* Для 1…4 битов достаточно одного октета; старшие биты остаются нулевыми. */
|
||||||
response->data[0] = (uint8_t)packed;
|
response->data[0] = (uint8_t)packed;
|
||||||
response->data[1] = (uint8_t)(packed >> 8U);
|
|
||||||
} else {
|
} else {
|
||||||
if (count > 4U) {
|
if (count > 4U) {
|
||||||
return reject(server);
|
return reject(server);
|
||||||
@@ -111,11 +101,16 @@ pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ((bank == PCAN_MODBUS_INPUT) || (bank == PCAN_MODBUS_DISCRETE) ||
|
if ((bank == PCAN_MODBUS_INPUT) || (bank == PCAN_MODBUS_DISCRETE) || /* Входные банки функций 2 и 4 сохраняют запрет записи. */
|
||||||
(server->write == NULL)) {
|
(server->write == NULL)) {
|
||||||
return reject(server);
|
return reject(server);
|
||||||
}
|
}
|
||||||
{ /* После проверки readonly-банков запись возможна только в HOLDING; битовая запись бывшего 0x4 удалена. */
|
if (bank == PCAN_MODBUS_COIL) { /* ProtoCAN выбирает запись по непустому payload, сохраняя номер функции как селектор банка. */
|
||||||
|
if ((request->dlc != 1U) || (request->data[0] >> count) != 0U) return reject(server); /* Ровно один октет хранит маску выбранного числа выходов без лишних установленных битов. */
|
||||||
|
for (uint8_t index = 0U; index < count; ++index) { /* Выходы записываются по возрастанию адреса начиная с младшего бита маски. */
|
||||||
|
if (!server->write(server->user, bank, (uint16_t)(address + index), (uint16_t)((request->data[0] >> index) & 1U))) return reject(server); /* Передаём provider значение 0 или 1 каждого выхода. */
|
||||||
|
} /* Все запрошенные выходы обработаны. */
|
||||||
|
} else { /* Для Holding сохраняется запись 16-битных значений little-endian. */
|
||||||
if ((count > 4U) || (request->dlc != (uint8_t)(count * 2U))) {
|
if ((count > 4U) || (request->dlc != (uint8_t)(count * 2U))) {
|
||||||
return reject(server);
|
return reject(server);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,18 +10,20 @@
|
|||||||
#include "pcan_id.h"
|
#include "pcan_id.h"
|
||||||
#include "pcan_modbus_server.h"
|
#include "pcan_modbus_server.h"
|
||||||
|
|
||||||
static uint16_t holding[32];
|
static uint16_t holding[4096]; /* Полная 12-битная карта позволяет проверить последний допустимый адрес 0xFFF. */
|
||||||
static uint16_t input[32];
|
static uint16_t input[4096]; /* Входной банк имеет тот же размер 4096 адресов, что поле StrAdr. */
|
||||||
|
static uint16_t coils = 0x000AU; /* Чередующиеся выходы 1 и 3 отличают функцию Coil=1 от Discrete=2. */
|
||||||
static uint16_t discretes = 0x0014U; /* Входы 2 и 4 установлены для проверки сохранённого битового чтения типа 0x5. */
|
static uint16_t discretes = 0x0014U; /* Входы 2 и 4 установлены для проверки сохранённого битового чтения типа 0x5. */
|
||||||
|
|
||||||
static bool read_word(void *user, pcan_modbus_bank_t bank,
|
static bool read_word(void *user, pcan_modbus_bank_t bank,
|
||||||
uint16_t address, uint16_t *value)
|
uint16_t address, uint16_t *value)
|
||||||
{
|
{
|
||||||
(void)user;
|
(void)user;
|
||||||
if (address >= 32U) return false;
|
if (address >= 4096U) return false; /* Provider не принимает адреса вне 12-битной карты. */
|
||||||
if (bank == PCAN_MODBUS_HOLDING) *value = holding[address];
|
if (bank == PCAN_MODBUS_HOLDING) *value = holding[address];
|
||||||
else if (bank == PCAN_MODBUS_INPUT) *value = input[address];
|
else if (bank == PCAN_MODBUS_INPUT) *value = input[address];
|
||||||
else if (bank == PCAN_MODBUS_DISCRETE && address < 16U) *value = (uint16_t)((discretes >> address) & 1U); /* Не сдвигаем 16-битное поле на недопустимое число позиций. */
|
else if (bank == PCAN_MODBUS_DISCRETE && address < 16U) *value = (uint16_t)((discretes >> address) & 1U); /* Не сдвигаем 16-битное поле на недопустимое число позиций. */
|
||||||
|
else if (bank == PCAN_MODBUS_COIL && address < 16U) *value = (uint16_t)((coils >> address) & 1U); /* Функция 1 читает независимую маску выходов. */
|
||||||
else return false;
|
else return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -30,13 +32,14 @@ static bool write_word(void *user, pcan_modbus_bank_t bank,
|
|||||||
uint16_t address, uint16_t value)
|
uint16_t address, uint16_t value)
|
||||||
{
|
{
|
||||||
(void)user;
|
(void)user;
|
||||||
if (address >= 32U) return false;
|
if (address >= 4096U) return false; /* Запись ограничена тем же адресным пространством, что чтение. */
|
||||||
if (bank == PCAN_MODBUS_HOLDING) holding[address] = value;
|
if (bank == PCAN_MODBUS_HOLDING) holding[address] = value;
|
||||||
else return false; /* Битовые входы и новый слотный GAS не записываются через Modbus provider. */
|
else if (bank == PCAN_MODBUS_COIL && address < 16U) coils = (uint16_t)((coils & ~(1U << address)) | ((value & 1U) << address)); /* Один callback изменяет ровно выбранный выход. */
|
||||||
|
else return false; /* Входные банки Discrete/Input недоступны для записи. */
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static pcan_frame_t request(uint8_t type, uint16_t address, uint8_t count)
|
static pcan_frame_t request(uint8_t function, uint16_t address, uint8_t count) /* Функция задаёт банк независимо от единого MsgType=0x5. */
|
||||||
{
|
{
|
||||||
pcan_id_t id = {0};
|
pcan_id_t id = {0};
|
||||||
pcan_frame_t frame = {0};
|
pcan_frame_t frame = {0};
|
||||||
@@ -44,8 +47,8 @@ static pcan_frame_t request(uint8_t type, uint16_t address, uint8_t count)
|
|||||||
id.route = PCAN_ROUTE_FROM_PM;
|
id.route = PCAN_ROUTE_FROM_PM;
|
||||||
id.device_type = 7U;
|
id.device_type = 7U;
|
||||||
id.device_id = 3U;
|
id.device_id = 3U;
|
||||||
id.msg_type = type;
|
id.msg_type = PCAN_MSG_MODBUS; /* Все четыре функции используют один код сообщения 0x5. */
|
||||||
id.msg_body = pcan_body_modbus(address, count);
|
id.msg_body = pcan_body_modbus(address, count, function); /* Передаём физические значения функции и количества 1…4 общему упаковщику. */
|
||||||
frame.id = pcan_id_pack(&id);
|
frame.id = pcan_id_pack(&id);
|
||||||
frame.flags = PCAN_FLAG_IDE;
|
frame.flags = PCAN_FLAG_IDE;
|
||||||
return frame;
|
return frame;
|
||||||
@@ -83,12 +86,45 @@ int main(void)
|
|||||||
|
|
||||||
req = request(PCAN_MODBUS_DISCRETE, 2U, 3U); /* Читаем три входа 2…4 через сохранённый тип 0x5. */
|
req = request(PCAN_MODBUS_DISCRETE, 2U, 3U); /* Читаем три входа 2…4 через сохранённый тип 0x5. */
|
||||||
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
|
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
|
||||||
CHECK(rsp.dlc == 2U && rsp.data[0] == 0x05U && rsp.data[1] == 0U); /* Маска прочитанных входов должна сохранять порядок младших битов. */
|
CHECK(rsp.dlc == 1U && rsp.data[0] == 0x05U); /* Три входа передаются младшими битами одного октета. */
|
||||||
req = request(PCAN_MSG_GAS_SLOT, 2U, 3U); /* Передаём бывший номер Coil, теперь занятый независимым GAS SLOT. */
|
req.id = (req.id & ~0xF0000UL) | ((uint32_t)PCAN_MSG_GAS_SLOT << 16U); /* Подменяем MsgType на независимый GAS SLOT 0x4, сохраняя остальные поля. */
|
||||||
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_NOT_FOR_US); /* Слотный кадр не должен вызывать Modbus provider и менять его счётчики. */
|
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_NOT_FOR_US); /* Слотный кадр не должен вызывать Modbus provider и менять его счётчики. */
|
||||||
|
|
||||||
req = request(PCAN_MODBUS_INPUT, 0U, 5U);
|
req = request(PCAN_MODBUS_INPUT, 0xFFFU, 2U); /* Двухрегистровое окно с последнего адреса должно быть отвергнуто до обращения к карте. */
|
||||||
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE);
|
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE);
|
||||||
CHECK(server.requests == 4U && server.responses == 3U && server.rejected == 1U);
|
CHECK(server.requests == 4U && server.responses == 3U && server.rejected == 1U);
|
||||||
|
for (uint8_t function = 1U; function <= 4U; ++function) { /* Покрываем все четыре банка единого сообщения. */
|
||||||
|
for (uint8_t count = 1U; count <= 4U; ++count) { /* Каждая функция проверяется с каждым допустимым количеством. */
|
||||||
|
uint16_t expected = (uint16_t)(0x20U | ((function - 1U) << 2U) | (count - 1U)); /* Адрес 2 остаётся в старших 12 битах, функция и количество независимо занимают по два бита. */
|
||||||
|
CHECK(pcan_body_modbus(2U, count, function) == expected); /* Проверяем эталонную битовую раскладку всех 16 сочетаний. */
|
||||||
|
CHECK(pcan_modbus_count(expected) == count && pcan_modbus_function(expected) == function); /* Декодирование возвращает исходные физические значения 1…4. */
|
||||||
|
req = request(function, 2U, count); /* Запрашиваем окно в пределах тестовых битовых и регистровых банков. */
|
||||||
|
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE); /* Все допустимые сочетания обслуживаются сервером. */
|
||||||
|
CHECK(rsp.dlc == (function <= 2U ? 1U : count * 2U)); /* Функции 1/2 возвращают октет, 3/4 — два октета на слово. */
|
||||||
|
pcan_id_unpack(rsp.id, &id); /* Ответ должен сохранять банк и количество исходного запроса. */
|
||||||
|
CHECK(id.msg_type == PCAN_MSG_MODBUS && id.msg_body == expected && id.route == PCAN_ROUTE_FROM_DEVICE); /* Меняется только направление сообщения. */
|
||||||
|
if (function <= 2U) CHECK(rsp.data[0] == (((function == 1U ? coils : discretes) >> 2U) & ((1U << count) - 1U))); /* Сверяем каждый бит с независимым источником банка. */
|
||||||
|
} /* Завершаем перебор количеств для текущей функции. */
|
||||||
|
} /* Проверены все шестнадцать вариантов. */
|
||||||
|
input[4095] = 0xFEDCU; /* Последний адрес карты содержит отличимое 16-битное значение. */
|
||||||
|
req = request(PCAN_MODBUS_INPUT, 4095U, 1U); /* Окно из одного регистра на границе допустимо. */
|
||||||
|
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE && rsp.data[0] == 0xDCU && rsp.data[1] == 0xFEU); /* Верхняя граница и little-endian проверяются одновременно. */
|
||||||
|
req = request(PCAN_MODBUS_COIL, 2U, 4U); /* Записываем четыре последовательных выхода отдельными битами. */
|
||||||
|
req.dlc = 1U; req.data[0] = 0x09U; /* Выходы addr+0 и addr+3 включены, промежуточные выключены. */
|
||||||
|
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE && ((coils >> 2U) & 15U) == 9U); /* Provider получает правильные адреса и значения каждого выхода. */
|
||||||
|
req.data[0] = 0x10U; /* Пятый бит не входит в заявленное четырёхбитное окно. */
|
||||||
|
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE && ((coils >> 2U) & 15U) == 9U); /* Неверная маска отклоняется до изменения выходов. */
|
||||||
|
req = request(PCAN_MODBUS_HOLDING, 3U, 2U); req.dlc = 3U; /* Двум регистрам нужны четыре октета; три являются неполным payload. */
|
||||||
|
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE && holding[3] == 0x1122U); /* Неверная длина не меняет уже записанные значения. */
|
||||||
|
for (uint8_t function = 2U; function <= 4U; function += 2U) { /* Функции входных банков 2 и 4 доступны только для чтения. */
|
||||||
|
req = request(function, 2U, 1U); req.dlc = function == 2U ? 1U : 2U; /* Подготавливаем формально правильный payload записи readonly-банка. */
|
||||||
|
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE); /* Сервер отвергает запись независимо от длины данных. */
|
||||||
|
} /* Оба входных банка защищены. */
|
||||||
|
req = request(PCAN_MODBUS_HOLDING, 0U, 1U); req.flags |= PCAN_FLAG_RTR; /* Remote frame не несёт разрешённого data payload ProtoCAN. */
|
||||||
|
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE); /* RTR отвергается до чтения банка. */
|
||||||
|
for (uint8_t type = 6U; type <= 7U; ++type) { /* Старые отдельные типы Holding/Input освобождены. */
|
||||||
|
req = request(PCAN_MODBUS_HOLDING, 0U, 1U); req.id = (req.id & ~0xF0000UL) | ((uint32_t)type << 16U); /* Сохраняем адрес и Body, изменяя только номер сервиса. */
|
||||||
|
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_NOT_FOR_US); /* Освобождённые типы не маршрутизируются в Modbus. */
|
||||||
|
} /* Проверены оба освобождённых номера. */
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ static void test_id(void)
|
|||||||
/* Раскладки MsgBody из protocan.h */
|
/* Раскладки MsgBody из protocan.h */
|
||||||
CHECK(pcan_body_broadcast(3U, 0U) == 0x0030U);
|
CHECK(pcan_body_broadcast(3U, 0U) == 0x0030U);
|
||||||
CHECK(pcan_body_typed(4U, 17U) == 0x4011U);
|
CHECK(pcan_body_typed(4U, 17U) == 0x4011U);
|
||||||
CHECK(pcan_body_modbus(0x100U, 3U) == 0x1003U);
|
CHECK(pcan_body_modbus(0x100U, 3U, 4U) == 0x100EU); /* Адрес 0x100, функция Input=4 и три регистра дают wire-коды функции 3 и количества 2. */
|
||||||
CHECK(pcan_body_error(0U, 0xFFU) == 0x00FFU);
|
CHECK(pcan_body_error(0U, 0xFFU) == 0x00FFU);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
from .set_uart import number, parse_frame
|
from .set_uart import number, parse_frame
|
||||||
|
|
||||||
TYPES = {0: 'BROADCAST', 1: 'DISCRETE', 2: 'ANALOG', 3: 'GAS',
|
TYPES = {0: 'BROADCAST', 1: 'DISCRETE', 2: 'ANALOG', 3: 'GAS',
|
||||||
4: 'GAS_SLOT', 5: 'MODBUS_DISCRETE', 6: 'MODBUS_HOLDING', # Тип 0x4 теперь означает масочный слотный GAS, номера остальных Modbus-типов сохранены.
|
4: 'GAS_SLOT', 5: 'MODBUS', # Единый Modbus занимает 0x5, прежние коды 0x6 и 0x7 освобождены.
|
||||||
7: 'MODBUS_INPUT', 8: 'ERROR', 9: 'BOOT_CONTROL', 10: 'BOOT_DATA_A',
|
8: 'ERROR', 9: 'BOOT_CONTROL', 10: 'BOOT_DATA_A', # Остальные сервисы сохраняют числовые коды.
|
||||||
11: 'BOOT_DATA_B', 12: 'BOOT_STATUS', 13: 'BOOT_DISCOVERY', 15: 'PULSE'}
|
11: 'BOOT_DATA_B', 12: 'BOOT_STATUS', 13: 'BOOT_DISCOVERY', 15: 'PULSE'}
|
||||||
BOOT = dict(enumerate(('IDENTIFY ENTER_BOOT BEGIN_IMAGE BEGIN_COMPAT ERASE VERIFY '
|
BOOT = dict(enumerate(('IDENTIFY ENTER_BOOT BEGIN_IMAGE BEGIN_COMPAT ERASE VERIFY '
|
||||||
'COMMIT CONFIRM REBOOT ABORT QUERY_PROGRESS').split(), 1))
|
'COMMIT CONFIRM REBOOT ABORT QUERY_PROGRESS').split(), 1))
|
||||||
@@ -49,8 +49,14 @@ def legacy(ident, data, profile='protocan'):
|
|||||||
result['mask'] = values[0] if values else None # Пустой запрос чтения не содержит фиктивной маски.
|
result['mask'] = values[0] if values else None # Пустой запрос чтения не содержит фиктивной маски.
|
||||||
result['slot_words'] = [(index, values[index]) for index in range(1, 4) if values and values[0] & (1 << index)] # Публикуем только выбранные или достоверные значения, без служебного Word0.
|
result['slot_words'] = [(index, values[index]) for index in range(1, 4) if values and values[0] & (1 << index)] # Публикуем только выбранные или достоверные значения, без служебного Word0.
|
||||||
text += ' slot=%04X' % body + (' READ' if not values else ' mask=%04X' % values[0]) # В логе ясно различаются адрес и маска частичной записи.
|
text += ' slot=%04X' % body + (' READ' if not values else ' mask=%04X' % values[0]) # В логе ясно различаются адрес и маска частичной записи.
|
||||||
elif 5 <= msg <= 7: # Диапазон Modbus больше не включает бывший Coil 0x4.
|
elif msg == 5: # Modbus использует один тип сообщения и поле функции внутри Body.
|
||||||
text += ' address=%03X count=%d' % (body >> 4, body & 15)
|
from protocan.protocan import decode # Один подробный декодер проверяет длину, диапазон адресов и битовые маски.
|
||||||
|
decoded = decode(ident, bytes(data)) # Передаём исходный payload без повторной реализации протокольных проверок.
|
||||||
|
if decoded.warnings: # Некорректные значения должны быть видны вызывающему анализатору.
|
||||||
|
raise ValueError('; '.join(decoded.warnings)) # Сохраняем подробные сообщения общей реализации.
|
||||||
|
result['address'], result['count'], result['function'] = body >> 4, (body & 3) + 1, ((body >> 2) & 3) + 1 # Внешний результат содержит физические значения полей 1…4.
|
||||||
|
result['registers'] = decoded.registers # Для Coil/Discrete публикуются отдельные биты, для Holding/Input — 16-битные слова.
|
||||||
|
text += ' FC%d address=%03X count=%d' % (result['function'], result['address'], result['count']) # Журнал явно различает четыре банка единого Modbus.
|
||||||
elif msg == 8:
|
elif msg == 8:
|
||||||
text += ' info=%02X code=%02X' % (body >> 8, body & 255)
|
text += ' info=%02X code=%02X' % (body >> 8, body & 255)
|
||||||
elif msg in (9, 12):
|
elif msg in (9, 12):
|
||||||
|
|||||||
+50
-55
@@ -47,9 +47,7 @@ class MsgType(IntEnum):
|
|||||||
ANALOG = 0b0010
|
ANALOG = 0b0010
|
||||||
GENERAL_ADDRESS_SPACE = 0b0011
|
GENERAL_ADDRESS_SPACE = 0b0011
|
||||||
GAS_SLOT = 0b0100 # Тип 0x4 однозначно выбирает слотный GAS с маской вместо прежнего Modbus Coil.
|
GAS_SLOT = 0b0100 # Тип 0x4 однозначно выбирает слотный GAS с маской вместо прежнего Modbus Coil.
|
||||||
MODBUS_DISCRETE = 0b0101
|
MODBUS = 0b0101 # Единый тип 0x5 выбирает банк функцией 1…4 в Body; номера 0x6 и 0x7 освобождены.
|
||||||
MODBUS_HOLDING = 0b0110
|
|
||||||
MODBUS_INPUT = 0b0111
|
|
||||||
ERROR = 0b1000
|
ERROR = 0b1000
|
||||||
BOOT_CONTROL = 0b1001
|
BOOT_CONTROL = 0b1001
|
||||||
BOOT_DATA_A = 0b1010
|
BOOT_DATA_A = 0b1010
|
||||||
@@ -60,6 +58,13 @@ class MsgType(IntEnum):
|
|||||||
PULSE = 0b1111
|
PULSE = 0b1111
|
||||||
|
|
||||||
|
|
||||||
|
class ModbusFunction(IntEnum): # Функция единого типа Modbus выбирает один из четырёх банков.
|
||||||
|
COIL = 1 # Дискретные выходы соответствуют функции 1.
|
||||||
|
DISCRETE = 2 # Дискретные входы соответствуют функции 2.
|
||||||
|
HOLDING = 3 # Изменяемые 16-битные регистры соответствуют функции 3.
|
||||||
|
INPUT = 4 # Входные 16-битные регистры соответствуют функции 4.
|
||||||
|
|
||||||
|
|
||||||
class SettingsResult(IntEnum):
|
class SettingsResult(IntEnum):
|
||||||
OK = 0x00
|
OK = 0x00
|
||||||
INVALID_DLC = 0x01
|
INVALID_DLC = 0x01
|
||||||
@@ -116,9 +121,7 @@ MSGTYPE_RU = {
|
|||||||
MsgType.ANALOG: "Analog",
|
MsgType.ANALOG: "Analog",
|
||||||
MsgType.GENERAL_ADDRESS_SPACE: "General Address Space",
|
MsgType.GENERAL_ADDRESS_SPACE: "General Address Space",
|
||||||
MsgType.GAS_SLOT: "GAS Slot", # Отдельное имя показывает, что адрес в Body относится к слоту, а не к Modbus-регистру.
|
MsgType.GAS_SLOT: "GAS Slot", # Отдельное имя показывает, что адрес в Body относится к слоту, а не к Modbus-регистру.
|
||||||
MsgType.MODBUS_DISCRETE: "Modbus Discrete",
|
MsgType.MODBUS: "Modbus", # Один тип охватывает Coil, Discrete, Holding и Input через поле функции.
|
||||||
MsgType.MODBUS_HOLDING: "Modbus Holding",
|
|
||||||
MsgType.MODBUS_INPUT: "Modbus Input",
|
|
||||||
MsgType.ERROR: "Error",
|
MsgType.ERROR: "Error",
|
||||||
MsgType.BOOT_CONTROL: "Boot Control",
|
MsgType.BOOT_CONTROL: "Boot Control",
|
||||||
MsgType.BOOT_DATA_A: "Boot Data A",
|
MsgType.BOOT_DATA_A: "Boot Data A",
|
||||||
@@ -348,9 +351,8 @@ def split_analog(body: int):
|
|||||||
return body & 0xFFF, (body >> 12) & 0xF
|
return body & 0xFFF, (body >> 12) & 0xF
|
||||||
|
|
||||||
|
|
||||||
def split_modbus(body: int):
|
def split_modbus(body: int): # Возвращает адрес, количество и функцию в тех же единицах, что принимает упаковщик.
|
||||||
"""MODBUS: RegCount[3:0], StrAdr[15:4]."""
|
return (body >> 4) & 0xFFF, (body & 3) + 1, ((body >> 2) & 3) + 1 # Адрес занимает 12 бит, два остальных поля кодируются значением минус один.
|
||||||
return (body >> 4) & 0xFFF, body & 0xF
|
|
||||||
|
|
||||||
|
|
||||||
def split_error(body: int):
|
def split_error(body: int):
|
||||||
@@ -370,8 +372,10 @@ def merge_analog(an_type: int, sensor_id: int) -> int:
|
|||||||
return ((an_type & 0xF) << 12) | (sensor_id & 0xFFF)
|
return ((an_type & 0xF) << 12) | (sensor_id & 0xFFF)
|
||||||
|
|
||||||
|
|
||||||
def merge_modbus(str_adr: int, reg_count: int) -> int:
|
def merge_modbus(str_adr: int, reg_count: int, function: int) -> int: # Функция 1…4 явно задаёт банк единого сообщения Modbus.
|
||||||
return ((str_adr & 0xFFF) << 4) | (reg_count & 0xF)
|
if not 0 <= str_adr <= 0xFFF or not 1 <= reg_count <= 4 or not 1 <= function <= 4 or str_adr + reg_count > 0x1000: # Проверяем окно целиком, чтобы не усекать адреса и значения двухбитных полей.
|
||||||
|
raise ValueError("Modbus: адрес 0…4095, количество и функция 1…4; окно не должно выходить за 4095") # Ошибочный ввод не превращается в допустимый кадр другого банка.
|
||||||
|
return (str_adr << 4) | ((function - 1) << 2) | (reg_count - 1) # Функция занимает биты 3:2, количество — 1:0; оба wire-кода лежат в диапазоне 0…3.
|
||||||
|
|
||||||
|
|
||||||
def merge_error(info: int, code: int) -> int:
|
def merge_error(info: int, code: int) -> int:
|
||||||
@@ -460,14 +464,16 @@ def decode(raw_id: int, data: bytes, ide: bool = True, rtr: bool = False) -> Dec
|
|||||||
res.fields.append(("Слот 0x%04X, Word%d" % (pid.body, index), "0x%04X (%d)" % (values[index], values[index]))) # Полная координата сохраняет адрес слота и выбранную позицию.
|
res.fields.append(("Слот 0x%04X, Word%d" % (pid.body, index), "0x%04X (%d)" % (values[index], values[index]))) # Полная координата сохраняет адрес слота и выбранную позицию.
|
||||||
res.summary = "GAS SLOT slot=0x%04X%s" % (pid.body, " mask=0x%04X" % values[0] if values else " (запрос)") # CAN-журнал явно показывает маску частичного изменения.
|
res.summary = "GAS SLOT slot=0x%04X%s" % (pid.body, " mask=0x%04X" % values[0] if values else " (запрос)") # CAN-журнал явно показывает маску частичного изменения.
|
||||||
return res # Не передаём четыре слова старому обработчику адреса первого регистра.
|
return res # Не передаём четыре слова старому обработчику адреса первого регистра.
|
||||||
|
if pid.msg_type == MsgType.MODBUS and len(source_data) > 8: # Проверяем реальную длину до усечения отображаемого classic CAN payload.
|
||||||
|
res.summary = "MODBUS: неверная длина кадра" # Нельзя выдавать девять и более октетов за допустимые восемь.
|
||||||
|
res.warnings.append("Modbus: classic CAN допускает не более 8 байт") # Сохраняем причину отказа для журнала и анализатора захвата.
|
||||||
|
return res # Не публикуем регистры из частично обрезанного кадра.
|
||||||
handler = {
|
handler = {
|
||||||
MsgType.BROADCAST: _decode_broadcast,
|
MsgType.BROADCAST: _decode_broadcast,
|
||||||
MsgType.DISCRETE: _decode_discrete,
|
MsgType.DISCRETE: _decode_discrete,
|
||||||
MsgType.ANALOG: _decode_analog,
|
MsgType.ANALOG: _decode_analog,
|
||||||
MsgType.GENERAL_ADDRESS_SPACE: _decode_gas,
|
MsgType.GENERAL_ADDRESS_SPACE: _decode_gas,
|
||||||
MsgType.MODBUS_DISCRETE: _decode_modbus_bit,
|
MsgType.MODBUS: _decode_modbus, # Банк выбирается функцией из Body внутри общего обработчика.
|
||||||
MsgType.MODBUS_HOLDING: _decode_modbus_reg,
|
|
||||||
MsgType.MODBUS_INPUT: _decode_modbus_reg,
|
|
||||||
MsgType.ERROR: _decode_error,
|
MsgType.ERROR: _decode_error,
|
||||||
MsgType.BOOT_CONTROL: _decode_boot,
|
MsgType.BOOT_CONTROL: _decode_boot,
|
||||||
MsgType.BOOT_DATA_A: _decode_boot,
|
MsgType.BOOT_DATA_A: _decode_boot,
|
||||||
@@ -663,47 +669,36 @@ def _decode_gas(res: Decoded) -> None:
|
|||||||
res.summary = "GAS: запрос по адресу 0x%04X" % start
|
res.summary = "GAS: запрос по адресу 0x%04X" % start
|
||||||
|
|
||||||
|
|
||||||
def _decode_modbus_bit(res: Decoded) -> None:
|
def _decode_modbus(res: Decoded) -> None: # Единый тип 0x5 разбирается по номеру функции, а не по прежним трём MsgType.
|
||||||
str_adr, reg_count = split_modbus(res.id.body)
|
if not res.ide or res.rtr: # Modbus поверх ProtoCAN принимает только расширенные data frames.
|
||||||
kind = "DISCRETE" # После назначения 0x4 слотному GAS битовым Modbus-сообщением остаётся только тип 0x5.
|
return # Общий декодер уже добавил предупреждение о неверном формате; данные такого кадра не публикуются.
|
||||||
res.fields.append((" Modbus.StrAdr", "0x%03X (%d)" % (str_adr, str_adr)))
|
str_adr, reg_count, function = split_modbus(res.id.body) # Извлекаем физические значения функции и количества 1…4.
|
||||||
res.fields.append((" Modbus.RegCount", "%d" % reg_count))
|
kind = ("COIL", "DISCRETE", "HOLDING", "INPUT")[function - 1] # Порядок банков соответствует функциям Modbus 1, 2, 3 и 4.
|
||||||
expected = reg_count % 8 + 1
|
res.fields.extend([(" Modbus.Function", "%d (%s)" % (function, kind)), (" Modbus.StrAdr", "0x%03X (%d)" % (str_adr, str_adr)), (" Modbus.RegCount", str(reg_count))]) # Журнал показывает функцию отдельно от единого типа сообщения.
|
||||||
if res.dlc and res.dlc != expected:
|
res.summary = "MODBUS FC%d %s @0x%03X x%d" % (function, kind, str_adr, reg_count) # Общая подпись сохраняется также для ошибочного payload.
|
||||||
res.warnings.append(
|
if str_adr + reg_count > 0x1000: # Проверяем конец окна относительно 12-битного адресного пространства.
|
||||||
"Для %s ожидается DLC = RegCount %% 8 + 1 = %d, получено %d"
|
res.warnings.append("Modbus: окно выходит за адрес 0xFFF") # Не отображаем выход за границу как корректные регистры.
|
||||||
% (kind, expected, res.dlc))
|
return # Неверное окно не интерпретируется дальше.
|
||||||
if len(res.data) >= 2:
|
expected = 1 if function <= 2 else reg_count * 2 # До четырёх битов помещаются в октет; каждому 16-битному регистру нужны два октета.
|
||||||
val = res.data[0] | (res.data[1] << 8)
|
if not res.dlc: # Пустой payload обозначает запрос чтения от ПМ.
|
||||||
bits = ["%d" % ((val >> i) & 1) for i in range(16)]
|
if res.id.route == Route.FROM_DEVICE: # Ответ устройства обязан содержать запрошенные значения.
|
||||||
res.registers = [(str_adr, val)]
|
res.warnings.append("Modbus: пустой ответ устройства") # Пустой ответ не маскируется под запрос.
|
||||||
res.fields.append((" Значение", "0x%04X" % val))
|
res.summary += " (запрос)" # Запрос отображается без вымышленных нулевых значений.
|
||||||
res.fields.append((" Биты 0..15", " ".join(bits)))
|
return # Чтение не содержит регистров для декодирования.
|
||||||
res.summary = "MODBUS %s @0x%03X x%d = 0x%04X" % (kind, str_adr, reg_count, val)
|
if res.dlc != expected: # Длина должна точно соответствовать функции и количеству в Body.
|
||||||
else:
|
res.warnings.append("Modbus FC%d: ожидается DLC=%d, получено %d" % (function, expected, res.dlc)) # Сообщаем ожидаемую длину без частичного разбора.
|
||||||
res.summary = "MODBUS %s @0x%03X x%d (запрос)" % (kind, str_adr, reg_count)
|
return # Некорректный payload не выдаётся за действительные данные.
|
||||||
|
if function <= 2: # Coil и Discrete передают значения младшими битами одного октета.
|
||||||
|
if res.data[0] >> reg_count: # Биты вне запрошенного количества должны оставаться нулевыми.
|
||||||
def _decode_modbus_reg(res: Decoded) -> None:
|
res.warnings.append("Modbus: установлены биты вне запрошенного количества") # Выявляем неверную битовую маску.
|
||||||
str_adr, reg_count = split_modbus(res.id.body)
|
return # Не публикуем неверные значения.
|
||||||
kind = "HOLDING" if res.id.msg_type == MsgType.MODBUS_HOLDING else "INPUT"
|
values = [(res.data[0] >> index) & 1 for index in range(reg_count)] # Каждый следующий адрес соответствует следующему младшему биту.
|
||||||
res.fields.append((" Modbus.StrAdr", "0x%03X (%d)" % (str_adr, str_adr)))
|
else: # Holding и Input сохраняют порядок little-endian внутри каждого регистра.
|
||||||
res.fields.append((" Modbus.RegCount", "%d" % reg_count))
|
values = regs_le(res.data) # Общий помощник извлекает проверенное число 16-битных значений.
|
||||||
if res.dlc and res.dlc != reg_count * 2:
|
res.registers = [(str_adr + index, value) for index, value in enumerate(values)] # Возвращаем отдельную координату для каждого бита или регистра.
|
||||||
res.warnings.append(
|
for address, value in res.registers: # Детальная панель показывает все значения выбранного банка.
|
||||||
"Для %s ожидается DLC = 2 * RegCount = %d, получено %d"
|
res.fields.append((" Reg 0x%04X" % address, "0x%04X (%d)" % (value, value))) # Одновременно доступны HEX и десятичное представление.
|
||||||
% (kind, reg_count * 2, res.dlc))
|
res.summary += ": " + " ".join("%04X" % value for value in values) # Краткая строка журнала включает полученные значения.
|
||||||
if reg_count > 4:
|
|
||||||
res.warnings.append("В одном кадре не более 4 регистров, в ID указано %d" % reg_count)
|
|
||||||
vals = regs_le(res.data)
|
|
||||||
res.registers = [(str_adr + i, v) for i, v in enumerate(vals)]
|
|
||||||
for adr, v in res.registers:
|
|
||||||
res.fields.append((" Reg 0x%04X" % adr, "0x%04X (%d)" % (v, v)))
|
|
||||||
if vals:
|
|
||||||
res.summary = "MODBUS %s @0x%03X: %s" % (
|
|
||||||
kind, str_adr, " ".join("%04X" % v for v in vals))
|
|
||||||
else:
|
|
||||||
res.summary = "MODBUS %s @0x%03X x%d (запрос)" % (kind, str_adr, reg_count)
|
|
||||||
|
|
||||||
|
|
||||||
def _decode_error(res: Decoded) -> None:
|
def _decode_error(res: Decoded) -> None:
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ class BusDemoSource:
|
|||||||
data = tag + ("%04d" % sid).encode()
|
data = tag + ("%04d" % sid).encode()
|
||||||
elif kind == "modbus":
|
elif kind == "modbus":
|
||||||
adr, cnt = self._rng.randint(0, 0x200), self._rng.randint(1, 4)
|
adr, cnt = self._rng.randint(0, 0x200), self._rng.randint(1, 4)
|
||||||
cid = pc.ProtoCanId.build(0, 1, dev_type, dev_id, pc.MsgType.MODBUS_HOLDING,
|
cid = pc.ProtoCanId.build(0, 1, dev_type, dev_id, pc.MsgType.MODBUS, # Демонстрация использует единый тип 0x5 вместо прежнего Holding 0x6.
|
||||||
pc.merge_modbus(adr, cnt))
|
pc.merge_modbus(adr, cnt, pc.ModbusFunction.HOLDING)) # Функция 3 обозначает банк Holding, количество по-прежнему находится в диапазоне 1…4.
|
||||||
data = b"".join(self._rng.randint(0, 0xFFFF).to_bytes(2, "little")
|
data = b"".join(self._rng.randint(0, 0xFFFF).to_bytes(2, "little")
|
||||||
for _ in range(cnt))
|
for _ in range(cnt))
|
||||||
elif kind == "gas":
|
elif kind == "gas":
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class CanFrameFields(QWidget): # Компонент содержит тольк
|
|||||||
arg_layout.addWidget(self.settings_body) # Два байта SETTINGS заменяют единое поле аргумента.
|
arg_layout.addWidget(self.settings_body) # Два байта SETTINGS заменяют единое поле аргумента.
|
||||||
arg_layout.addStretch(1) # Свободная ширина остаётся справа от редакторов.
|
arg_layout.addStretch(1) # Свободная ширина остаётся справа от редакторов.
|
||||||
self.count = QSpinBox() # RegCount используется протокольными типами Modbus.
|
self.count = QSpinBox() # RegCount используется протокольными типами Modbus.
|
||||||
self.count.setRange(0, 15) # Поле количества занимает четыре бита Body Modbus.
|
self.count.setRange(1, 4) # Два младших бита Body кодируют количество минус один, допустимы 1…4 регистра или бита.
|
||||||
self.count.setValue(1) # Исходный запрос относится к одному регистру.
|
self.count.setValue(1) # Исходный запрос относится к одному регистру.
|
||||||
self.sub_label = QLabel("Подтип") # Подпись уточняется при выборе типа сообщения.
|
self.sub_label = QLabel("Подтип") # Подпись уточняется при выборе типа сообщения.
|
||||||
self.arg_label = QLabel("SensorID / адрес") # Назначение аргумента зависит от MsgType.
|
self.arg_label = QLabel("SensorID / адрес") # Назначение аргумента зависит от MsgType.
|
||||||
@@ -116,10 +116,11 @@ class CanFrameFields(QWidget): # Компонент содержит тольк
|
|||||||
settings = msg_type == pc.MsgType.SETTINGS # Только SETTINGS редактируется отдельными байтами Z и Y.
|
settings = msg_type == pc.MsgType.SETTINGS # Только SETTINGS редактируется отдельными байтами Z и Y.
|
||||||
self.arg.setVisible(not settings) # Единый числовой аргумент скрывается для SETTINGS.
|
self.arg.setVisible(not settings) # Единый числовой аргумент скрывается для SETTINGS.
|
||||||
self.settings_body.setVisible(settings) # Двухбайтовый редактор появляется вместо числового аргумента.
|
self.settings_body.setVisible(settings) # Двухбайтовый редактор появляется вместо числового аргумента.
|
||||||
table = {pc.MsgType.BROADCAST: (pc.BroadcastType, "Broadcast.Type", "Body (0..15)"), pc.MsgType.DISCRETE: (pc.DiscreteType, "Discrete.Type", "Body (0..4095)"), pc.MsgType.ANALOG: (pc.AnalogType, "Analog.Type", "SensorID (0..4095)")} # Таблица определяет визуальные подписи существующих протокольных перечислений.
|
table = {pc.MsgType.BROADCAST: (pc.BroadcastType, "Broadcast.Type", "Body (0..15)"), pc.MsgType.DISCRETE: (pc.DiscreteType, "Discrete.Type", "Body (0..4095)"), pc.MsgType.ANALOG: (pc.AnalogType, "Analog.Type", "SensorID (0..4095)"), pc.MsgType.MODBUS: (pc.ModbusFunction, "Функция Modbus (1..4)", "StrAdr (0..4095)")} # Для единого Modbus селектор подтипа выбирает функцию Coil/Discrete/Holding/Input.
|
||||||
|
self.count.setEnabled(msg_type == pc.MsgType.MODBUS) # Количество доступно только для типа, использующего это поле Body.
|
||||||
current = self.sub.currentText() # Сохраняем выбранный подтип, если он допустим для нового сообщения.
|
current = self.sub.currentText() # Сохраняем выбранный подтип, если он допустим для нового сообщения.
|
||||||
self.sub.clear() # Старые варианты не должны остаться в новом типе сообщения.
|
self.sub.clear() # Старые варианты не должны остаться в новом типе сообщения.
|
||||||
self.sub.setEnabled(msg_type in table) # Отдельный подтип доступен только у трёх перечисленных типов.
|
self.sub.setEnabled(msg_type in table) # Подтип доступен для Broadcast/Discrete/Analog, а у Modbus выбирается функция 1…4.
|
||||||
if msg_type in table: # Подписи и значения перечислений берутся из одного места.
|
if msg_type in table: # Подписи и значения перечислений берутся из одного места.
|
||||||
enum, sub_label, arg_label = table[msg_type] # Получаем визуальное описание выбранного MsgType.
|
enum, sub_label, arg_label = table[msg_type] # Получаем визуальное описание выбранного MsgType.
|
||||||
self.sub.addItems([item.name for item in enum]) # Значения берутся из общего протокольного каталога.
|
self.sub.addItems([item.name for item in enum]) # Значения берутся из общего протокольного каталога.
|
||||||
@@ -128,9 +129,7 @@ class CanFrameFields(QWidget): # Компонент содержит тольк
|
|||||||
else: # Типы без отдельного подтипа используют Body по собственному назначению.
|
else: # Типы без отдельного подтипа используют Body по собственному назначению.
|
||||||
sub_label = "—" # Подпись явно показывает отсутствие подтипа.
|
sub_label = "—" # Подпись явно показывает отсутствие подтипа.
|
||||||
arg_label = "Body (обычно 0)" # Для PULSE сохраняем исходное описание аргумента.
|
arg_label = "Body (обычно 0)" # Для PULSE сохраняем исходное описание аргумента.
|
||||||
if msg_type in (pc.MsgType.MODBUS_DISCRETE, pc.MsgType.MODBUS_HOLDING, pc.MsgType.MODBUS_INPUT): # Только сохранённые типы 0x5…0x7 используют Modbus-адрес и количество.
|
if msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE: # GAS использует всё поле Body под адрес.
|
||||||
arg_label = "StrAdr (0..4095)" # Двенадцатибитный адрес показан в прежних единицах регистров.
|
|
||||||
elif msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE: # GAS использует всё поле Body под адрес.
|
|
||||||
arg_label = "Адрес регистра (0..65535)" # Полный 16-битный диапазон указан в подписи.
|
arg_label = "Адрес регистра (0..65535)" # Полный 16-битный диапазон указан в подписи.
|
||||||
elif msg_type == pc.MsgType.GAS_SLOT: # Новый тип 0x4 использует весь Body под номер слота, а не StrAdr/RegCount.
|
elif msg_type == pc.MsgType.GAS_SLOT: # Новый тип 0x4 использует весь Body под номер слота, а не StrAdr/RegCount.
|
||||||
arg_label = "Адрес слота (0..65535), Data: маска + 3 слова" # Напоминаем оператору о Word0 с маской и фиксированных позициях трёх значений.
|
arg_label = "Адрес слота (0..65535), Data: маска + 3 слова" # Напоминаем оператору о Word0 с маской и фиксированных позициях трёх значений.
|
||||||
@@ -148,8 +147,8 @@ class CanFrameFields(QWidget): # Компонент содержит тольк
|
|||||||
return pc.merge_discrete(pc.DiscreteType[self.sub.currentText()], arg) # Поле собирается общим протокольным модулем.
|
return pc.merge_discrete(pc.DiscreteType[self.sub.currentText()], arg) # Поле собирается общим протокольным модулем.
|
||||||
if msg_type == pc.MsgType.ANALOG: # Аналоговое сообщение передаёт SensorID в Body.
|
if msg_type == pc.MsgType.ANALOG: # Аналоговое сообщение передаёт SensorID в Body.
|
||||||
return pc.merge_analog(pc.AnalogType[self.sub.currentText()], arg) # Существующий кодек задаёт расположение подтипа и SensorID.
|
return pc.merge_analog(pc.AnalogType[self.sub.currentText()], arg) # Существующий кодек задаёт расположение подтипа и SensorID.
|
||||||
if msg_type in (pc.MsgType.MODBUS_DISCRETE, pc.MsgType.MODBUS_HOLDING, pc.MsgType.MODBUS_INPUT): # Формат Modbus применяется только к трём оставшимся типам 0x5…0x7.
|
if msg_type == pc.MsgType.MODBUS: # Один MsgType содержит адрес, количество 1…4 и выбранную функцию.
|
||||||
return pc.merge_modbus(arg, count) # Адрес и количество объединяются общей функцией протокола.
|
return pc.merge_modbus(arg, count, pc.ModbusFunction[self.sub.currentText()]) # Общий упаковщик проверяет диапазоны и кодирует функцию и количество со смещением на единицу.
|
||||||
if msg_type == pc.MsgType.ERROR: # Сохраняем существующую категорию ошибки с нулевым кодом источника.
|
if msg_type == pc.MsgType.ERROR: # Сохраняем существующую категорию ошибки с нулевым кодом источника.
|
||||||
return pc.merge_error(0, arg) # Размещение кода ошибки остаётся ответственностью общего ядра.
|
return pc.merge_error(0, arg) # Размещение кода ошибки остаётся ответственностью общего ядра.
|
||||||
return arg & 0xFFFF # Остальные типы сохраняют прежнее ограничение Body шестнадцатью битами.
|
return arg & 0xFFFF # Остальные типы сохраняют прежнее ограничение Body шестнадцатью битами.
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import unittest # Стандартный тестовый раннер проверяет wire-контракт без зависимости от GUI.
|
||||||
|
from protocan import protocan as pc # Используем публичный построитель и декодер CAN-сообщений.
|
||||||
|
from logic_analyzer.decoders.set_can import legacy # Тот же формат должен распознаваться при анализе аппаратного захвата.
|
||||||
|
|
||||||
|
|
||||||
|
class ModbusTests(unittest.TestCase): # Проверяем объединение банков, битовую упаковку и отбраковку неверных окон.
|
||||||
|
def test_functions_and_counts(self): # Все шестнадцать сочетаний должны сохранять функцию, количество и значения.
|
||||||
|
for function in range(1, 5): # Номера 1…4 соответствуют Coil, Discrete, Holding и Input.
|
||||||
|
for count in range(1, 5): # Количество ограничено 1…4 независимо от размера элемента банка.
|
||||||
|
body = pc.merge_modbus(0x123, count, function) # Адрес выбран так, чтобы старшие биты отличались от младших служебных полей.
|
||||||
|
self.assertEqual(body, 0x1230 | ((function - 1) << 2) | (count - 1)) # Эталон явно задаёт адрес [15:4], функцию [3:2] и количество [1:0].
|
||||||
|
self.assertEqual(pc.split_modbus(body), (0x123, count, function)) # Обратное преобразование возвращает физические значения 1…4.
|
||||||
|
values = [index % 2 for index in range(count)] if function <= 2 else [0x1234 + index for index in range(count)] # Битовые и регистровые банки содержат различимые последовательности.
|
||||||
|
data = bytes([sum(value << index for index, value in enumerate(values))]) if function <= 2 else b''.join(value.to_bytes(2, 'little') for value in values) # Биты начинаются с младшего, слова передаются little-endian.
|
||||||
|
identifier = pc.ProtoCanId.build(1, 1, 7, 3, pc.MsgType.MODBUS, body) # Каждый ответ имеет единый MsgType=0x5 и направление от устройства.
|
||||||
|
decoded = pc.decode(identifier, data) # Проверяем штатный путь CAN-журнала приложения.
|
||||||
|
self.assertFalse(decoded.warnings, decoded.warnings) # Допустимое окно не должно сопровождаться предупреждениями.
|
||||||
|
self.assertEqual(decoded.registers, list(zip(range(0x123, 0x123 + count), values))) # Для каждого бита и слова публикуется точный адрес.
|
||||||
|
captured = legacy(identifier, data) # Проверяем согласованность анализатора захватов с протокольной библиотекой.
|
||||||
|
self.assertEqual((captured['function'], captured['count'], captured['registers']), (function, count, decoded.registers)) # Оба публичных пути возвращают одинаковые физические значения.
|
||||||
|
|
||||||
|
def test_invalid_inputs_and_payloads(self): # Ошибки должны отклоняться до превращения в другой допустимый wire-код.
|
||||||
|
for args in ((0, 0, 1), (0, 5, 1), (0, 1, 0), (0, 1, 5), (-1, 1, 1), (4096, 1, 1), (4095, 2, 4)): # Охватываем обе границы полей и переполнение окна.
|
||||||
|
with self.assertRaises(ValueError): pc.merge_modbus(*args) # Неверные значения не маскируются и не оборачиваются.
|
||||||
|
self.assertEqual(pc.merge_modbus(4095, 1, 4), 0xFFFC) # Последний адрес остаётся доступным для одного регистра Input.
|
||||||
|
for body, data in ((0x1233, b'\x10'), (0x123B, b'\x01\x02'), (0xFFFF, b'\0' * 8), (0x123B, b'\0' * 9)): # Проверяем лишний бит, короткие данные, выход окна за 0xFFF и запрет молчаливой обрезки девятого октета.
|
||||||
|
decoded = pc.decode(0x1F350000 | body, data) # Формируем ответ устройства с единым типом 0x5.
|
||||||
|
self.assertTrue(decoded.warnings) # Каждая ошибка должна быть видима в журнале.
|
||||||
|
self.assertFalse(decoded.registers) # Некорректный payload не становится источником значений для тренда.
|
||||||
|
with self.assertRaises(ValueError): legacy(0x1F350000 | body, data) # Анализатор захвата использует те же проверки.
|
||||||
|
self.assertFalse(pc.decode(0x1735123F, b'').warnings) # Запрос ПМ с функцией 4 и количеством 4 не содержит данных.
|
||||||
|
self.assertTrue(pc.decode(0x1F35123F, b'').warnings) # Пустой ответ прибора является ошибкой протокола.
|
||||||
|
for msg_type in (6, 7): # Бывшие отдельные типы Modbus освобождены.
|
||||||
|
self.assertTrue(pc.decode(0x1F300000 | (msg_type << 16), b'').warnings) # Декодер не трактует старые MsgType как единый Modbus.
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__': # Допускаем прямой запуск теста на хосте.
|
||||||
|
unittest.main() # Стандартный код завершения отражает результат проверок.
|
||||||
Reference in New Issue
Block a user