IndustrialDeviceController/Software/HighLevelApp/drivers/modbus/modbus.c (825 lines of code) (raw):
/* Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdbool.h>
#include <inttypes.h>
#include <math.h>
#include <init/globals.h>
#include <init/device_hal.h>
#include <utils/utils.h>
#include <utils/llog.h>
#include <utils/timer.h>
#include <driver/modbus.h>
#include <frozen/frozen.h>
#include <safeclib/safe_lib.h>
#include "modbus_transport.h"
#define MAX_FIELD_LENGTH 100
#define MODBUS_MAX_BIT_PER_READ 0x7D0
#define MODBUS_MAX_WORD_PER_READ 0x7D
#define MODBUS_MAX_BIT_PER_WRITE 0x7B0
#define MODBUS_MAX_WORD_PER_WRITE 0x7B
#define MODBUS_READ_REQUEST_FRAME_LENGTH 5
// data point definition array field sequence
enum {
MODBUS_SCHEMA_FIELD_KEY,
MODBUS_SCHEMA_FIELD_NUMBER,
MODBUS_SCHEMA_FIELD_TYPE,
MODBUS_SCHEMA_FIELD_BIT,
MODBUS_SCHEMA_FIELD_MULTIPLIER,
MODBUS_SCHEMA_FIELD_OFFSET,
MODBUS_SCHEMA_FIELD_LAST
};
// intentionally to make it align with the address number.
// register number Data Addresses Table Name
// 000001 - 065536 0000 - FFFF Discrete Output Coils
// 100001 - 165536 0000 - FFFF Discrete Input Contacts
// 300001 - 365536 0000 - FFFF Analog Input Registers
// 400001 - 465536 0000 - FFFF Analog Output Holding Registers
enum {
COIL = 0,
DISCRETE_INPUT = 1,
INVALID = 2,
INPUT_REGISTER = 3,
HOLDING_REGISTER= 4
};
static const char *REG_NAMES[] =
{
"COIL",
"DISCRETE_INPUT",
"INVALID",
"INPUT_REGISTER",
"HOLDING_REGISTER"
};
// modbus protocol only defined two data type, bit (bool) or register (uint16)
// OEM defined more data type based on register like int32,uint32, float etc.
// This require additional decoding when getting a data point.
// In this code, we refer to raw bit/register value as "value" while deoded data
// as "point"
enum {
TYPE_INVALID,
TYPE_BIT,
TYPE_BYTE,
TYPE_INT16,
TYPE_UINT16,
TYPE_INT32_BE, // 0x1234 as [0] = 0x12, [1] = 0x34
TYPE_INT32_LE, // 0x1234 as [0] = 0x34, [1] = 0x12
TYPE_UINT32_BE,
TYPE_UINT32_LE,
TYPE_FLOAT_BE,
TYPE_FLOAT_LE,
TYPE_INT64_BE, //0x12345678 as [0]=0x12, [1]=0x34, [2]=0x56, [3]=0x78
TYPE_INT64_LE, //0x12345678 as [0]=0x78, [1]=0x56, [2]=0x34, [3]=0x12
};
// key - standard data point name, we don't care vendor name
// reg_type - coil, discrete, input register, holding register as defined in modbus protocol
// data_type - as defined above, bit, byte, uint16, int16, uint32, int32, float
// addr - register offset part only, leading digit been extracted to reg_type already
// bit - Some vendor use one bit of input/holding register to represent binary value, bit offset
// this indicate the bit offset in the register, for bit data type, one bit, for byte data
// type, assume consecutive 8 bits. Not allow byte to spread across registers.
// scale & offset is to represent float data point value in integer on device does not support float
// A scaled integer value is calculated using the following equation:
// register_value = measured_value * scale + value_offset
// measured_value = (register_value - value_offset) / scale
// this only apply to integer, note the point datatype only defined how we interpret the register value
// the measured value could be anything after calculated with multiplier and value_offset.
// implement data_point_t interface, so make sure
// key, value, next is at the head
typedef struct modbus_device_t modbus_device_t;
struct modbus_device_t {
struct device_driver_t base;
device_protocol_t protocol;
bool opened;
modbus_transport_t *transport;
};
struct register_buffer_t {
uint8_t reg_type;
uint16_t begin_addr;
uint16_t end_addr;
uint16_t buf[2000];
};
/// <summary>
/// parse the response for modbus request to extract value
/// </summary>
/// <param name="modbus">this</param>
/// <param name="request">request pdu</param>
/// <param name="response">response pdu</param>
/// <param name="len_rsp">length ofresponse pdu</param>
/// <param name="regs">buffer to hold parsed value</param>
/// <returns>0 on success, or error code if failed</returns>
static err_code parse_read_response(modbus_device_t *modbus, uint8_t *request, uint8_t *response, int16_t len_rsp,
uint16_t *regs)
{
// minimum 2 bytes, even in error case
if (len_rsp < 2) {
LOGE("response less than 2 bytes");
return DEVICE_E_PROTOCOL;
}
uint8_t function_req = request[0];
uint8_t function_rsp = response[0];
uint16_t quantity_req = (request[3] << 8) + request[4];
uint8_t function = response[0];
// if succeed, server echo back function code
if (function_rsp == function_req) {
uint8_t byte_count = response[1];
uint8_t *payload = response + 2;
if (byte_count + 2 != len_rsp) {
LOGE("byte count not match header");
return DEVICE_E_PROTOCOL;
}
if ((function == FC_READ_INPUT_REGISTERS) || (function == FC_READ_HOLDING_REGISTERS)) {
// check bytes received match requested
if (byte_count != 2 * quantity_req) {
LOGE("byte count not match requested");
return DEVICE_E_PROTOCOL;
}
for (uint16_t i = 0; i < quantity_req; i++) {
// modbus is "big-endian" protocol
regs[i] = (payload[i * 2] << 8) + payload[i * 2 + 1];
}
} else if (function == FC_READ_COILS || function == FC_READ_DISCRETE_INPUTS) {
// check bytes received match requested
if (byte_count != (quantity_req + 7) / 8) {
LOGE("byte count not match request");
return DEVICE_E_PROTOCOL;
}
for (uint16_t i = 0; i < quantity_req; i++) {
regs[i] = (payload[i / 8] & (0x01u << (i % 8))) ? 1 : 0;
}
}
} else if (function_rsp == (function_req | 0x80)) {
// server echo back function code with LSB set when something wrong
uint8_t exception_code = response[1];
LOGW("Exception code: %d", exception_code);
return DEVICE_E_PROTOCOL;
} else {
LOGW("Invalid server response: PDU:%s", hex(response, len_rsp));
return DEVICE_E_PROTOCOL;
}
return DEVICE_OK;
}
/// <summary>
/// Build and send read request pdu, then receive and parse response
/// </summary>
/// <param name="modbus">this</param>
/// <param name="function_code">funtion code</param>
/// <param name="addr">start address</param>
/// <param name="quantity">number of register request</param>
/// <param name="regs">out buffer to hold register value</param>
/// <param name="timeout">the value of timer in ms for this operation</param>
/// <returns>0 on success, or error code on failure</returns>
static err_code handle_read_request(modbus_device_t *modbus, uint8_t slave_id, uint8_t function_code, uint16_t addr,
uint16_t quantity, uint16_t *regs, int32_t timeout)
{
uint8_t request[5];
uint8_t response[MODBUS_MAX_PDU_SIZE];
request[0] = function_code; // MODBUS FUNCTION CODE
request[1] = (addr >> 8) & 0xFF; // START REGISTER (Hi)
request[2] = addr & 0xFF; // START REGISTER (Lo)
request[3] = (quantity >> 8) & 0xFF; // NUMBER OF REGISTERS (Hi)
request[4] = quantity & 0xFF; // NUMBER OF REGISTERS (Lo)
// send request
struct timespec poll_sw;
timer_stopwatch_start(&poll_sw);
int err = modbus->transport->send_request(modbus->transport, slave_id, request, MODBUS_READ_REQUEST_FRAME_LENGTH,
timeout);
if (err) {
LOGE("Failed to send request:%s", err_str(err));
return err;
}
// recv response
int32_t len_rsp;
// sending is not a blocked operation, so only use timeout for receiving
int32_t elapse_ms = timer_stopwatch_stop(&poll_sw);
if (elapse_ms >= timeout) {
return DEVICE_E_TIMEOUT;
}
err = modbus->transport->recv_response(modbus->transport, slave_id, response, &len_rsp, timeout - elapse_ms);
if (err) {
LOGE("Failed to receive response:%s", err_str(err));
return err;
}
// parse reponse
return parse_read_response(modbus, request, response, len_rsp, regs);
}
/// <summary>
/// parse the response of write request
/// </summary>
/// <param name="modbus">this</param>
/// <param name="request">request pdu</param>
/// <param name="response">response pdu</param>
/// <param name="len_rsp">length of response pdu</param>
/// <returns>0 on success, or error code if failed</returns>
static err_code parse_write_response(modbus_device_t *modbus, uint8_t *request, uint8_t *response, int16_t len_rsp)
{
// minimum 2 bytes, even in error case
if (len_rsp < 2) {
LOGE("reponse less than 2 bytes");
return DEVICE_E_PROTOCOL;
}
uint8_t function_req = request[0];
uint8_t function_rsp = response[0];
// if succeed, server echo back function code
if (function_rsp == function_req) {
uint16_t addr_req = (request[1] << 8) + request[2];
uint16_t addr_rsp = (response[1] << 8) + response[2];
uint16_t quantity_req = (request[3] << 8) + request[4];
uint16_t quantity_rsp = (response[3] << 8) + response[4];
if ((addr_req != addr_rsp) || (quantity_req != quantity_rsp)) {
LOGE("Invalid packet received\n");
return DEVICE_E_PROTOCOL;
}
} else if (function_rsp == (function_req | 0x80)) {
// server echo back error code
uint8_t exception_code = response[1];
LOGW("Exception code: %d", exception_code);
return DEVICE_E_PROTOCOL;
} else {
LOGW("Don't understand server response");
return DEVICE_E_PROTOCOL;
}
return DEVICE_OK;
}
/// <summary>
/// Build and send read request pdu, then receive and parse response
// </summary>
/// <param name="modbus">this</param>
/// <param name="function_code">funtion code</param>
/// <param name="addr">start address</param>
/// <param name="quantity">number of register request</param>
/// <param name="regs">in buffer to hold register value to write</param>
/// <param name="timeout">the value of timer in ms for this operation</param>
/// <returns>0 on success, or error code on failure</returns>
static err_code handle_write_request(modbus_device_t *modbus, uint8_t slave_id, uint8_t function_code, uint16_t addr,
uint16_t quantity, uint16_t *regs, int32_t timeout)
{
uint8_t request[MODBUS_MAX_PDU_SIZE];
uint8_t response[MODBUS_MAX_PDU_SIZE];
memset(request, 0, sizeof(request));
memset(response, 0, sizeof(response));
request[0] = function_code; // MODBUS FUNCTION CODE
request[1] = (addr >> 8) & 0xFF; // START REGISTER (Hi)
request[2] = addr & 0xFF; // START REGISTER (Lo)
request[3] = (quantity >> 8) & 0xFF; // NUMBER OF REGISTERS (Hi)
request[4] = quantity & 0xFF; // NUMBER OF REGISTERS (Lo)
if (function_code == FC_WRITE_COILS) {
request[5] = (quantity + 7) / 8; // BYTE COUNT
for (uint16_t i = 0; i < quantity; i++) {
uint16_t byte_i = i / 8;
uint16_t bit_i = i % 8;
if (regs[i]) {
request[6 + byte_i] |= (1 << bit_i);
}
else {
request[6 + byte_i] &= ~(1 << bit_i);
}
}
} else if (function_code == FC_WRITE_HOLDING_REGISTERS) {
request[5] = quantity * 2; // BYTE COUNT
unsigned char *output = (request + 6);
for (int i = 0; i < quantity; i++) {
output[2 * i] = (regs[i] >> 8);
output[2 * i + 1] = regs[i] & 0xFF;
}
}
// all write request has 6 bytes header plus addtional data
struct timespec poll_sw;
timer_stopwatch_start(&poll_sw);
uint16_t len_req = 6 + request[5];
err_code err = modbus->transport->send_request(modbus->transport, slave_id, request, len_req, timeout);
if (err) {
LOGE("Failed to send request:%s", err_str(err));
return err;
}
int32_t len_rsp;
int32_t elapse_ms = timer_stopwatch_stop(&poll_sw);
if (elapse_ms >= timeout) {
return DEVICE_E_TIMEOUT;
}
err = modbus->transport->recv_response(modbus->transport, slave_id, response, &len_rsp, timeout - elapse_ms);
if (err) {
LOGE("Failed to receive response:%s", err_str(err));
return err;
}
return parse_write_response(modbus, request, response, len_rsp);
}
err_code mb_read_register(modbus_device_t *modbus, uint8_t slave_id, uint8_t reg_type, uint16_t addr,
uint16_t quantity, uint16_t *regs, int32_t timeout)
{
uint8_t fc = FC_INVALID;
switch (reg_type) {
case COIL:
fc = FC_READ_COILS;
break;
case DISCRETE_INPUT:
fc = FC_READ_DISCRETE_INPUTS;
break;
case INPUT_REGISTER:
fc = FC_READ_INPUT_REGISTERS;
break;
case HOLDING_REGISTER:
fc = FC_READ_HOLDING_REGISTERS;
}
if (fc != FC_INVALID) {
return handle_read_request(modbus, slave_id, fc, addr, quantity, regs, timeout);
}
else {
return DEVICE_E_INVALID;
}
}
err_code mb_write_register(modbus_device_t *modbus, uint8_t slave_id, uint8_t reg_type, uint16_t addr, uint16_t quantity,
uint16_t *regs, int32_t timeout)
{
uint8_t fc = FC_INVALID;
switch (reg_type) {
case COIL:
fc = FC_WRITE_COILS;
break;
case HOLDING_REGISTER:
fc = FC_WRITE_HOLDING_REGISTERS;
break;
}
if (fc != FC_INVALID) {
return handle_write_request(modbus, slave_id, fc, addr, quantity, regs, timeout);
}
else {
return DEVICE_E_INVALID;
}
}
// -------------------------- data representation layer --------------------------
/// <summary>
/// get number of register for a data points, e.g. FLOAT need 2 register
/// </summary>
/// <param name="mp">modbus point to be decoded</param>
/// <return>number of register to decode given data point</return>
static uint8_t num_reg(modbus_point_t *mp)
{
if (mp->data_type <= TYPE_UINT16) {
return 1;
}
else if (mp->data_type <= TYPE_FLOAT_LE) {
return 2;
}
else if (mp->data_type <= TYPE_INT64_LE) {
return 4;
}
else {
return 0;
}
}
/// <summary>
/// encode one data point value from measured value to register value
/// </summary>
/// <param name="mp">point to be encoded</param>
/// <param name="value_str">value to be encoded into point</param>
/// <returns>0 on success, or error code on failure</returns>
static err_code encode_point(modbus_device_t *modbus, modbus_point_t *mp, const char *str_value, uint16_t *reg)
{
switch (mp->data_type) {
case TYPE_BIT: {
// default to 0
uint8_t value = strtol(str_value, NULL, 10);
if (mp->reg_type == COIL) {
reg[0] = value ? 1 : 0;
} else if (mp->reg_type == HOLDING_REGISTER) {
int bit_offset = mp->bit_offset;
if (value) {
reg[0] |= 1 << bit_offset;
}
else {
reg[0] &= ~(1 << bit_offset);
}
}
break;
}
case TYPE_BYTE: {
uint8_t value = strtod(str_value, NULL) * mp->scale + mp->value_offset;
if (mp->reg_type == HOLDING_REGISTER) {
for (int i = mp->bit_offset; i < mp->bit_offset + 8; i++) {
if (value & (1 << (i - mp->bit_offset))) {
reg[0] |= 1 << i;
}
else {
reg[0] &= ~(1 << i);
}
}
}
break;
}
case TYPE_INT16:
case TYPE_UINT16: {
reg[0] = strtod(str_value, NULL) * mp->scale + mp->value_offset;
break;
}
case TYPE_INT32_BE:
case TYPE_UINT32_BE: {
uint32_t value = strtod(str_value, NULL) * mp->scale + mp->value_offset;
reg[0] = (value >> 16);
reg[1] = value & 0xFFFF;
break;
}
case TYPE_INT32_LE:
case TYPE_UINT32_LE: {
uint32_t value = strtod(str_value, NULL) * mp->scale + mp->value_offset;
reg[1] = (value >> 16);
reg[0] = value & 0xFFFF;
break;
}
case TYPE_FLOAT_BE: {
float value_f = strtof(str_value, NULL) * mp->scale + mp->value_offset;
uint32_t value_u32 = *((uint32_t *)&value_f);
reg[0] = (value_u32 >> 16);
reg[1] = value_u32 & 0xFFFF;
break;
}
case TYPE_FLOAT_LE: {
float value_f = strtof(str_value, NULL) * mp->scale + mp->value_offset;
uint32_t value_u32 = *((uint32_t *)&value_f);
reg[1] = (value_u32 >> 16);
reg[0] = value_u32 & 0xFFFF;
break;
}
case TYPE_INT64_BE: {
int64_t rv = 0;
if (mp->scale != 1) {
rv = strtod(str_value, NULL) * mp->scale + mp->value_offset;
}
else {
rv = strtoll(str_value, NULL, 10) + mp->value_offset;
}
reg[0] = (rv >> 48) & 0xFFFF;
reg[1] = (rv >> 32) & 0xFFFF;
reg[2] = (rv >> 16) & 0xFFFF;
reg[3] = rv & 0xFFFF;
break;
}
case TYPE_INT64_LE: {
int64_t rv = 0;
if (mp->scale != 1) {
rv = strtod(str_value, NULL) * mp->scale + mp->value_offset;
}
else {
rv = strtoll(str_value, NULL, 10) + mp->value_offset;
}
reg[3] = (rv >> 48) & 0xFFFF;
reg[2] = (rv >> 32) & 0xFFFF;
reg[1] = (rv >> 16) & 0xFFFF;
reg[0] = rv & 0xFFFF;
break;
}
default:
return DEVICE_E_PROTOCOL;
}
return DEVICE_OK;
}
/// <summary>
/// decode data point from register value to measured value
/// </summary>
/// <param name="point">point to be decoded</param>
/// <param name="value_str">buffer hold the decoded value</param>
/// <param name="value_str">size of buffer</param>
/// <returns>0 on success, or error code on failure</returns>
static err_code decode_point(modbus_device_t *modbus, modbus_point_t *mp, uint16_t *regs, double *value)
{
// parse data
switch (mp->data_type) {
case TYPE_BIT: {
if ((mp->reg_type == INPUT_REGISTER) || (mp->reg_type == HOLDING_REGISTER)) {
uint8_t bit_offset = mp->bit_offset;
*value = (regs[0] & (1 << bit_offset)) ? 1 : 0;
} else if ((mp->reg_type == COIL) || (mp->reg_type == DISCRETE_INPUT)) {
*value = regs[0] ? 1 : 0;
}
*value -= mp->value_offset;
break;
}
case TYPE_BYTE: {
uint8_t rv = (regs[0] >> mp->bit_offset) & 0xFF;
*value = (double)(rv - mp->value_offset);
break;
}
case TYPE_INT16: {
int16_t rv = regs[0];
*value = ((double)rv - mp->value_offset);
break;
}
case TYPE_UINT16: {
uint16_t rv = regs[0];
*value = ((double)rv - mp->value_offset);
break;
}
case TYPE_UINT32_BE: {
uint32_t rv = (regs[0] << 16) + regs[1];
*value = ((double)rv - mp->value_offset);
break;
}
case TYPE_UINT32_LE: {
uint32_t rv = (regs[1] << 16) + regs[0];
*value = ((double)rv - mp->value_offset);
break;
}
case TYPE_INT32_BE: {
int32_t rv = ((regs[0]) << 16) + regs[1];
*value = ((double)rv - mp->value_offset);
break;
}
case TYPE_INT32_LE: {
int32_t rv = ((regs[1]) << 16) + regs[0];
*value = ((double)rv - mp->value_offset);
break;
}
case TYPE_FLOAT_BE: {
uint32_t u32 = (regs[0] << 16) + regs[1];
float rv = *((float *)&u32);
*value = ((double)rv - mp->value_offset);
break;
}
case TYPE_FLOAT_LE: {
uint32_t u32 = (regs[1] << 16) + regs[0];
float rv = *((float *)&u32);
*value = ((double)rv - mp->value_offset);
break;
}
case TYPE_INT64_BE: {
int64_t rv = regs[0];
rv = (rv << 16) + regs[1];
rv = (rv << 16) + regs[2];
rv = (rv << 16) + regs[3];
*value = rv - mp->value_offset;
break;
}
case TYPE_INT64_LE: {
int64_t rv = regs[3];
rv = (rv << 16) + regs[2];
rv = (rv << 16) + regs[1];
rv = (rv << 16) + regs[0];
*value = rv - mp->value_offset;
break;
}
default:
return DEVICE_E_INVALID;
}
if (!is_double_equal(mp->scale, 1)) {
*value /= mp->scale;
// If value is -0, set to be 0
if (is_double_equal(*value, -0))
*value = 0;
}
return DEVICE_OK;
}
static err_code parse_point_definition(const char *ptr, int16_t len, data_point_t *p)
{
if (!ptr || len <= 0 || !p) {
return DEVICE_E_CONFIG;
}
char buf[MAX_FIELD_LENGTH + 1];
modbus_point_t *mp = &(p->d.modbus);
// set default value
mp->value_offset = 0;
mp->scale = 1;
mp->bit_offset = 0;
int16_t field_num = 0;
for (int16_t b = 0, e = 0; b < len; b = e + 1, e = b) {
while (e < len && ptr[e] != ':') {
e++;
}
if (e - b > MAX_FIELD_LENGTH) {
return DEVICE_E_CONFIG;
}
switch (field_num) {
case MODBUS_SCHEMA_FIELD_KEY:
strncpy_s(p->key, e - b + 1, ptr + b, e - b);
break;
case MODBUS_SCHEMA_FIELD_NUMBER: {
strncpy_s(buf, sizeof(buf), ptr + b, e - b);
errno = 0;
uint32_t number = strtol(buf, NULL, 10);
mp->reg_type = number / 100000;
mp->addr = number % 100000 - 1; // number start from 1, but address is from 0
if (errno || (mp->reg_type != 0 && mp->reg_type != 1 && mp->reg_type != 3 && mp->reg_type != 4)) {
return DEVICE_E_CONFIG;
};
break;
}
case MODBUS_SCHEMA_FIELD_TYPE: {
strncpy_s(buf, sizeof(buf), ptr + b, e - b);
errno = 0;
mp->data_type = strtol(buf, NULL, 10);
if (errno || mp->data_type == TYPE_INVALID || mp->data_type > TYPE_INT64_LE) {
return DEVICE_E_CONFIG;
}
break;
}
case MODBUS_SCHEMA_FIELD_BIT: {
strncpy_s(buf, sizeof(buf), ptr + b, e - b);
errno = 0;
mp->bit_offset = strtol(buf, NULL, 10);
if (errno || mp->bit_offset > 15) {
return DEVICE_E_CONFIG;
}
break;
}
case MODBUS_SCHEMA_FIELD_MULTIPLIER: {
strncpy_s(buf, sizeof(buf), ptr + b, e - b);
errno = 0;
double multiplier = strtod(buf, NULL);
if (errno || multiplier == 0) {
return DEVICE_E_CONFIG;
}
mp->scale = 1 / multiplier;
break;
}
case MODBUS_SCHEMA_FIELD_OFFSET: {
strncpy_s(buf, sizeof(buf), ptr + b, e - b);
errno = 0;
mp->value_offset = strtol(buf, NULL, 10);
if (errno) {
return DEVICE_E_CONFIG;
}
break;
}
}
field_num++;
}
// minimum is key, number, type
if (field_num < 3) {
return DEVICE_E_CONFIG;;
} else {
return DEVICE_OK;
}
}
// same type of point, address same (bit) or increasing
// allow overlap does not allow hole
static bool can_combine(modbus_point_t *current, modbus_point_t *next)
{
// Check integer overflow
uint32_t endAddr = current->addr + num_reg(current);
return (next && (next->reg_type == current->reg_type) && (next->addr >= current->addr) &&
(endAddr <= 0x00FFFF && next->addr <= endAddr));
}
static int find_modbus_point_index_by_key(data_schema_t *schema, const char *key)
{
for (int i = 0; i < schema->num_point; i++) {
if (strcmp(schema->points[i].key, key) == 0) {
return i;
}
}
return -1;
}
static bool is_register_value_in_buffer(modbus_point_t *mp, struct register_buffer_t *rbuf)
{
return (mp->reg_type == rbuf->reg_type) && (mp->addr >= rbuf->begin_addr) && (mp->addr <= rbuf->end_addr);
}
static size_t calc_register_quantity_to_read(int mpi, data_schema_t *schema)
{
modbus_point_t *mp = &schema->points[mpi].d.modbus;
int quantity = 0;
int max_quantity =
(mp->reg_type == COIL || mp->reg_type == DISCRETE_INPUT) ? MODBUS_MAX_BIT_PER_READ : MODBUS_MAX_WORD_PER_READ;
if (schema->flags & FLAG_NO_BATCH) {
int j = mpi + 1;
while ((j < schema->num_point) && can_combine(&schema->points[j - 1].d.modbus, &schema->points[j].d.modbus)) {
j++;
}
modbus_point_t *mp_end = &schema->points[j - 1].d.modbus;
quantity = MIN(max_quantity, (mp_end->addr + num_reg(mp_end) - mp->addr));
} else {
quantity = num_reg(mp);
for (int j = mpi + 1; j < schema->num_point; j++) {
modbus_point_t *next = &schema->points[j].d.modbus;
if (next->reg_type == mp->reg_type) {
int nreg = next->addr - mp->addr + num_reg(next);
if (nreg <= max_quantity) {
quantity = nreg;
continue;
}
}
break;
}
}
return quantity;
}
// ------------------------ public interface --------------------------------
// setup connection with device and verify communication of given channel (slave_id)
err_code modbus_open(void *instance, uint32_t unit_id, int32_t timeout)
{
modbus_device_t *self = (modbus_device_t*)instance;
if (self->opened) {
return DEVICE_OK;
}
struct timespec poll_sw;
timer_stopwatch_start(&poll_sw);
if (self->transport->transport_open(self->transport, timeout) != 0) {
return DEVICE_E_IO;
}
int32_t elapse_ms = timer_stopwatch_stop(&poll_sw);
if (elapse_ms >= timeout) {
return DEVICE_E_TIMEOUT;
}
self->opened = true;
return DEVICE_OK;
}
err_code modbus_close(void *instance)
{
modbus_device_t *self = (modbus_device_t*)instance;
ASSERT(self);
LOGD("modbus_close");
if (!self->opened) {
self->opened = false;
self->transport->transport_close(self->transport);
}
return DEVICE_OK;
}
err_code modbus_get_point(void *instance, uint32_t unit_id, const char *key,
data_schema_t *schema, telemetry_t *telemetry, int32_t timeout)
{
modbus_device_t *self = (modbus_device_t*)instance;
if (!self->opened) {
return DEVICE_E_BROKEN;
}
int index = find_modbus_point_index_by_key(schema, key);
if (index < 0) {
LOGE("Can't read invalid data point %s", key);
return DEVICE_E_INVALID;
}
modbus_point_t *mp = &schema->points[index].d.modbus;
uint16_t regs[2];
memset(regs, 0, sizeof(regs));
err_code err =
mb_read_register(self, unit_id, mp->reg_type, mp->addr + schema->offset, num_reg(mp), regs, timeout);
if (err) {
LOGW("Failed to read point '%s':%s:%d", schema->points[index].key, REG_NAMES[mp->reg_type], mp->addr);
return err;
}
double new_value = NAN;
err = decode_point(self, mp, regs, &new_value);
if (err) {
LOGW("Failed to decode data point %s", schema->points[index].key);
return err;
}
clear_mask(telemetry->str_mask, index);
if (is_double_equal(telemetry->values[index].num, new_value)) {
clear_mask(telemetry->cov_mask, index);
} else {
telemetry->values[index].num = new_value;
set_mask(telemetry->cov_mask, index);
}
return DEVICE_OK;
}
err_code modbus_get_point_list_internal(void *instance, uint32_t unit_id,
data_schema_t *schema, telemetry_t *telemetry, int32_t timeout)
{
modbus_device_t *self = (modbus_device_t*)instance;
if (!self->opened) {
return DEVICE_E_BROKEN;
}
struct register_buffer_t rbuf = {.reg_type = INVALID};
struct timespec poll_sw;
timer_stopwatch_start(&poll_sw);
for (int i = 0; i < schema->num_point; i++) {
modbus_point_t *mp = &schema->points[i].d.modbus;
int32_t elapse_ms = timer_stopwatch_stop(&poll_sw);
if (elapse_ms >= timeout) {
return DEVICE_E_TIMEOUT;
}
if (!is_register_value_in_buffer(mp, &rbuf)) {
int quantity = calc_register_quantity_to_read(i, schema);
LOGV("Read [%s:%d+%d]", REG_NAMES[mp->reg_type], mp->addr, quantity);
err_code err = mb_read_register(self, unit_id, mp->reg_type, mp->addr + schema->offset, quantity, rbuf.buf,
timeout - elapse_ms);
if (err) {
LOGE("Failed to read registers: %s", err_str(err));
return err;
}
rbuf.begin_addr = mp->addr;
rbuf.end_addr = mp->addr + quantity;
rbuf.reg_type = mp->reg_type;
}
double new_value = NAN;
if (decode_point(self, mp, &rbuf.buf[mp->addr - rbuf.begin_addr], &new_value) != 0) {
LOGW("Failed to decode data point %s", schema->points[i].key);
return DEVICE_E_PROTOCOL;
}
set_telemetry_number_value(telemetry, i, new_value);
LOGV("%s=%.2f", schema->points[i].key, new_value);
}
return DEVICE_OK;
}
err_code modbus_get_point_list(void *instance, uint32_t unit_id,
data_schema_t *schema, telemetry_t *telemetry, int32_t timeout)
{
modbus_device_t *self = (modbus_device_t*)instance;
if (!self->opened) {
return DEVICE_E_BROKEN;
}
return modbus_get_point_list_internal(instance, unit_id, schema, telemetry, timeout);
}
err_code modbus_set_point(void *instance, uint32_t unit_id, const char *key, const char *value,
data_schema_t *schema, int32_t timeout)
{
modbus_device_t *self = (modbus_device_t*)instance;
if (!self->opened) {
return DEVICE_E_BROKEN;
}
int index = find_modbus_point_index_by_key(schema, key);
if (index < 0) {
LOGE("Can't write invalid data point");
return DEVICE_E_INVALID;
}
modbus_point_t *mp = &schema->points[index].d.modbus;
if (mp->reg_type != COIL && mp->reg_type != HOLDING_REGISTER) {
LOGE("Can't write invalid data point %s", key);
return DEVICE_E_INVALID;
}
uint16_t regs[4] = {0, 0, 0, 0};
err_code err = encode_point(self, mp, value, regs);
if (err) {
LOGE("Failed to encode point %s=%s:%s", key, value, err_str(err));
return err;
}
err = mb_write_register(self, unit_id, mp->reg_type, mp->addr + schema->offset, num_reg(mp), regs, timeout);
if (err) {
LOGE("Failed to write data %s=%s", key, value, err_str(err));
return err;
}
return DEVICE_OK;
}
device_protocol_t modbus_get_protocol(void *instance)
{
return ((modbus_device_t*)instance)->protocol;
}
err_code modbus_create_point_table(struct json_token *points_def, int *npoints, data_point_t **ppoints)
{
if (!points_def || !points_def->ptr || points_def->len == 0) {
LOGW("schema has no points defintion");
*npoints = 0;
*ppoints = NULL;
return DEVICE_OK;
}
const char *ptr = points_def->ptr;
int len = points_def->len;
data_point_t *points = NULL;
int num_point = 0;
size_t total_key_len = 0;
for (int b = 0, e = 0; b < len; b = e + 1, e = b) {
while (e < len && ptr[e] != ':') {
e++;
}
total_key_len += e - b;
while (e < len && ptr[e] != ',') {
e++;
}
num_point++;
}
points = CALLOC(num_point, sizeof(data_point_t));
// All keys been put in a single memory block to avoid fragment
char *keys = MALLOC(total_key_len + num_point);
for (int b = 0, e = 0, i=0; b < len && i< num_point; b = e + 1, e = b, i++) {
while (e < len && ptr[e] != ',') {
e++;
}
data_point_t *p = &points[i];
p->key = keys;
if (parse_point_definition(ptr + b, e - b, p) != DEVICE_OK) {
LOGE("Failed to parse definition: [%.*s]", e-b, ptr+b);
modbus_destroy_point_table(points, num_point);
return DEVICE_E_CONFIG;
}
keys += strlen(p->key) + 1; // make sure to include null terminate
}
*npoints = num_point;
*ppoints = points;
return DEVICE_OK;
}
void modbus_destroy_point_table(data_point_t *points, int npoint)
{
ASSERT(points);
// All keys been put in a single memory block to avoid fragment
FREE(points[0].key);
FREE(points);
}
// modbus connection string will be in two format
// for modbus tcp, "unitid,<ip>""
// for modbus rtu, "unitid,<uart config>""
device_driver_t *modbus_create_driver(device_protocol_t protocol, const char *conn_str)
{
// modbus rtu require conn_str as uart_config
// modbus tcp require conn_str as ip:port
if (!conn_str) {
return NULL;
}
modbus_device_t *modbus = (modbus_device_t *)CALLOC(1, sizeof(modbus_device_t));
modbus->base.driver_open = modbus_open;
modbus->base.driver_close = modbus_close;
modbus->base.get_point = modbus_get_point;
modbus->base.get_point_list = modbus_get_point_list;
modbus->base.set_point = modbus_set_point;
modbus->base.get_protocol = modbus_get_protocol;
modbus->protocol = protocol;
modbus->opened = false;
modbus->transport = modbus_create_transport(protocol, conn_str);
if (!modbus->transport) {
LOGE("Failed to create transport");
modbus_destroy_driver((device_driver_t*)modbus);
return NULL;
}
LOGI("Created modbus driver: protocol=%s, connection=%s", protocol2str(protocol), conn_str);
return (device_driver_t*)modbus;
}
void modbus_destroy_driver(device_driver_t *instance)
{
modbus_device_t *self = (modbus_device_t*)instance;
ASSERT(self);
LOGI("Destroy modbus driver");
if (self->transport) {
modbus_destroy_transport(self->protocol, self->transport);
}
FREE(self);
}