Research on device MODBUS go code of edgefbundry


bin

edgex-launch.sh

Purpose:

  • Start all EdgeX Go binaries (must be built before)

Premise:

  • Both Consul and MongoDB are installed and running

clear:

  • Kill the process edgex device MODBUS

technological process:

  1. Go to cmd directory first (... / cmd)
  2. Execute the device MODBUS file in the directory and start it as an edgex device MODBUS process
  3. Go back to PWD directory
  4. If you fall into a trap, perform the cleanup function and exit
  5. Loop operation monitoring trap
###
# Start all EdgeX Go binaries (must be built before)
#
# The premise is that both Consul and MongoDB are installed and running
#
###

DIR=$PWD
CMD=../cmd

function cleanup {
	pkill edgex-device-modbus  #Clear - > kill process
}

cd $CMD  #Go to cmd directory first (.. / cmd)
exec -a edgex-device-modbus ./device-modbus &  #Execute the device MODBUS file in the directory and start it as an edgex device MODBUS process
cd $DIR  #Go back to PWD directory


trap cleanup EXIT  #If you fall into a trap, perform the cleanup function and exit

while : ; do sleep 1 ; done  ##Loop operation monitoring trap

test-attribution-txt.sh

Environment variables:

  • Get the directory where the script is located
  • Extract and enter the directory in the first parameter of bash script. You do not want to display the output results on the screen. If the execution is successful, pass the current directory to SCRIPT_DIR (script directory)
  • Extract script_ Parent directory of dir

clear:

  • Enter Git_ Restore the root directory to the directory where the vendor exists
  • Force deletion of vendor folder

technological process:

  1. Enter GIT_ROOT
  2. Check whether there is vendor.bk (backup). If there is, exit with 1 and prompt that the file exists. You need to remove it before continuing
  3. If trapped in a trap, perform the cleanup function
  4. Check whether there is vendor.bk, and if so, change vendor.bk to vendor (equivalent to backup and restore)
  5. If the vendor folder exists, back it up so that we can build a new one
  6. When go mod, the vendor file created in the project will be copied from the dependent package
  7. Open nullglobbing so that if there is nothing in the cmd directory, we won't do anything in this loop
  8. If the attribute.txt file cannot be found, exit with 1 and prompt that the file cannot be found
  9. Loop through each lib file in the vendor folder to ensure that lib exists and is clear. Otherwise, you will be prompted that a Lib in the file is missing. Please add it
  10. Back to GIT_ROOT directory

The content of attribute.txt is an open source project referenced in the device service MODBUS go.

#!/bin/bash -e

# Get the directory where the script is located
# From https://stackoverflow.com/a/246128/10102404
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"  # Extract and enter the directory in the first parameter of bash script. You do not want to display the output results on the screen. If the execution is successful, pass the current directory to SCRIPT_DIR (script directory)
GIT_ROOT=$(dirname "$SCRIPT_DIR")  # Extract script_ Parent directory of dir

EXIT_CODE=0

cd "$GIT_ROOT"  # Enter GIT_ROOT

if [ -d vendor.bk ]; then  # Check whether there is vendor.bk (backup). If there is, exit with 1 and prompt that the file exists. You need to remove it before continuing
    echo "vendor.bk exits - remove before continuing"
    exit 1
fi

trap cleanup 0 1 2 3 6

cleanup()
{
    cd "$GIT_ROOT"  # Enter Git_ Restore the root directory to the directory where the vendor exists
    rm -rf vendor  # Force deletion of vendor folder
    if [ -d vendor.bk ]; then # Check whether there is vendor.bk, and if so, change vendor.bk to vendor (equivalent to backup and restore)
        mv vendor.bk vendor
    fi
    exit $EXIT_CODE
}

# If the vendor folder exists, back it up so that we can build a new one
if [ -d vendor ]; then
    mv vendor vendor.bk
fi

GO111MODULE=on go mod vendor  # Create a vendor file in the project and copy the dependent package

# Open nullglobbing so that if there is nothing in the cmd directory, we won't do anything in this loop
shopt -s nullglob


if [ ! -f Attribution.txt ]; then  # If the attribute.txt file cannot be found, exit with 1 and prompt that the file cannot be found, please add
    echo "An Attribution.txt file is missing, please add"
    EXIT_CODE=1
else
    # Loop through each lib file in the vendor folder to ensure that lib exists and is clear. Otherwise, you will be prompted that a Lib in the file is missing. Please add it
    while IFS= read -r lib; do
        if ! grep -q "$lib" Attribution.txt && [ "$lib" != "explicit" ]; then
            echo "An attribution for $lib is missing from in Attribution.txt, please add"
            # need to do this in a bash subshell, see SC2031
            (( EXIT_CODE=1 ))
        fi
    done < <(grep '#' < "$GIT_ROOT/vendor/modules.txt" | awk '{print $2}')
fi

cd "$GIT_ROOT"  # Back to GIT_ROOT directory

cmd

main.go

technological process:

  1. Set service name
  2. Establish a protocol driver for docking
  3. Docking with SDK
// -*- Mode: Go; indent-tabs-mode: t -*-
//
// Copyright (C) 2018-2021 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package main

import (
	"github.com/edgexfoundry/device-sdk-go/v2/pkg/startup"

	"github.com/edgexfoundry/device-modbus-go"
	"github.com/edgexfoundry/device-modbus-go/internal/driver"
)

const (
	serviceName string = "device-modbus"  // Set service name
)

func main() {
	sd := driver.NewProtocolDriver()  // Establish a protocol driver for docking
	startup.Bootstrap(serviceName, device_modbus.Version, sd)  // Docking with SDK
}

internal/driver

config.go

Structure:

  • A structure that integrates Modbus RTU and Modbus TCP parameters

Function:

  • Create connection information
    1. Incoming configuration protocol set
    2. Check modbus protocol form
    3. Error prompt for incorrect configuration
    4. If the protocol is RTU, call the function to create RTU type connection information
    5. If the protocol is TCP, call the function to create TCP type connection information
  • Parse integer configuration parameters
    1. Get configuration parameters
    2. Convert to int type
  • Create RTU type connection information
    • Parsing parameters corresponding to RTU protocol and initializing ConnectionInfo structure
  • Create TCP type connection information
    • Parse the corresponding parameters of TCP protocol and initialize the ConnectionInfo structure
// -*- Mode: Go; indent-tabs-mode: t -*-
//
// Copyright (C) 2019-2021 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package driver

import (
	"fmt"
	"strconv"

	"github.com/edgexfoundry/go-mod-core-contracts/v2/models"
)

// ConnectionInfo is the information required to connect the device
type ConnectionInfo struct {// A structure that combines RTU and TCP parameters
	Protocol string			//agreement
	Address  string			//address
	Port     int			//port
	BaudRate int			//Baud rate
	DataBits int			//Data bit
	StopBits int			//Stop bit
	Parity   string			//check
	UnitID   uint8			//Slave address
	// Connection & read timeout (seconds)
	Timeout int
	// Idle timeout (seconds) disconnect
	IdleTimeout int
}

func createConnectionInfo(protocols map[string]models.ProtocolProperties) (info *ConnectionInfo, err error) {   // Pass in the configuration protocol set, parse and generate the corresponding connection information
	protocolRTU, rtuExist := protocols[ProtocolRTU]// Check modbus protocol form
	protocolTCP, tcpExist := protocols[ProtocolTCP]

	if rtuExist && tcpExist {// Error prompt for incorrect configuration
		return info, fmt.Errorf("unsupported multiple protocols, please choose %s or %s, not both", ProtocolRTU, ProtocolTCP)
	} else if !rtuExist && !tcpExist {
		return info, fmt.Errorf("unable to create connection info, protocol config '%s' or %s not exist", ProtocolRTU, ProtocolTCP)
	}

	if rtuExist {// If the configuration is RTU, the connection information of RTU type is created
		info, err = createRTUConnectionInfo(protocolRTU)
		if err != nil {
			return nil, err
		}
	} else if tcpExist {// If TCP is configured, TCP type connection information is created
		info, err = createTcpConnectionInfo(protocolTCP)
		if err != nil {
			return nil, err
		}
	}

	return info, nil
}

func parseIntValue(properties map[string]string, key string) (int, error) { // Resolve configuration to int
	str, ok := properties[key] // Get configuration parameters
	if !ok {
		return 0, fmt.Errorf("protocol config '%s' not exist", key)
	}
	val, err := strconv.Atoi(str) // Convert to int type
	if err != nil {
		return 0, fmt.Errorf("fail to parse protocol config '%s', %v", key, err)
	}
	return val, nil
}

func createRTUConnectionInfo(rtuProtocol map[string]string) (info *ConnectionInfo, err error) { // Parsing protocol parameters to create ConnectionInfo structure
	errorMessage := "unable to create RTU connection info, protocol config '%s' not exist"
	address, ok := rtuProtocol[Address]
	if !ok {
		return nil, fmt.Errorf(errorMessage, Address)
	}

	us, ok := rtuProtocol[UnitID]
	if !ok {
		return nil, fmt.Errorf(errorMessage, UnitID)
	}
	unitID, err := strconv.ParseUint(us, 0, 8)
	if err != nil {
		return nil, fmt.Errorf("uintID value out of range(0–255). Error: %v", err)
	}

	baudRate, err := parseIntValue(rtuProtocol, BaudRate)
	if err != nil {
		return nil, err
	}

	dataBits, err := parseIntValue(rtuProtocol, DataBits)
	if err != nil {
		return nil, err
	}

	stopBits, err := parseIntValue(rtuProtocol, StopBits)
	if err != nil {
		return nil, err
	}

	parity, ok := rtuProtocol[Parity]
	if !ok {
		return nil, fmt.Errorf(errorMessage, Parity)
	}
	if parity != "N" && parity != "O" && parity != "E" {
		return nil, fmt.Errorf("invalid parity value, it should be N(None) or O(Odd) or E(Even)")
	}

	timeout, err := parseIntValue(rtuProtocol, Timeout)
	if err != nil {
		return nil, err
	}

	idleTimeout, err := parseIntValue(rtuProtocol, IdleTimeout)
	if err != nil {
		return nil, err
	}

	return &ConnectionInfo{
		Protocol:    ProtocolRTU,
		Address:     address,
		BaudRate:    baudRate,
		DataBits:    dataBits,
		StopBits:    stopBits,
		Parity:      parity,
		UnitID:      byte(unitID),
		Timeout:     timeout,
		IdleTimeout: idleTimeout,
	}, nil
}

func createTcpConnectionInfo(tcpProtocol map[string]string) (info *ConnectionInfo, err error) {// Parsing protocol parameters to create ConnectionInfo structure
	errorMessage := "unable to create TCP connection info, protocol config '%s' not exist"
	address, ok := tcpProtocol[Address]
	if !ok {
		return nil, fmt.Errorf(errorMessage, Address)
	}

	portString, ok := tcpProtocol[Port]
	if !ok {
		return nil, fmt.Errorf(errorMessage, Port)
	}
	port, err := strconv.ParseUint(portString, 0, 16)
	if err != nil {
		return nil, fmt.Errorf("port value out of range(0–65535). Error: %v", err)
	}

	unitIDString, ok := tcpProtocol[UnitID]
	if !ok {
		return nil, fmt.Errorf(errorMessage, UnitID)
	}
	unitID, err := strconv.ParseUint(unitIDString, 0, 8)
	if err != nil {
		return nil, fmt.Errorf("uintID value out of range(0–255). Error: %v", err)
	}

	timeout, err := parseIntValue(tcpProtocol, Timeout)
	if err != nil {
		return nil, err
	}

	idleTimeout, err := parseIntValue(tcpProtocol, IdleTimeout)
	if err != nil {
		return nil, err
	}

	return &ConnectionInfo{
		Protocol:    ProtocolTCP,
		Address:     address,
		Port:        int(port),
		UnitID:      byte(unitID),
		Timeout:     timeout,
		IdleTimeout: idleTimeout,
	}, nil
}

constant.go

Constant:

  • Data type characterization
  • Parameter name characterization

map (digit correspondence table):

  • PrimaryTableBitCountMap
  • ValueTypeBitCountMap
// -*- Mode: Go; indent-tabs-mode: t -*-
//
// Copyright (C) 2018-2021 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package driver

import (
	"github.com/edgexfoundry/go-mod-core-contracts/v2/common"
)

const (// Data type characterization, parameter name characterization
	BOOL = "BOOL"

	INT16 = "INT16"
	INT32 = "INT32"
	INT64 = "INT64"

	UINT16 = "UINT16"
	UINT32 = "UINT32"
	UINT64 = "UINT64"

	FLOAT32 = "FLOAT32"
	FLOAT64 = "FLOAT64"

	DISCRETES_INPUT   = "DISCRETES_INPUT"
	COILS             = "COILS"
	INPUT_REGISTERS   = "INPUT_REGISTERS"
	HOLDING_REGISTERS = "HOLDING_REGISTERS"

	PRIMARY_TABLE    = "primaryTable"
	STARTING_ADDRESS = "startingAddress"
	IS_BYTE_SWAP     = "isByteSwap"
	IS_WORD_SWAP     = "isWordSwap"
	// RAW_TYPE defines the type of binary data read from the Modbus device 
	RAW_TYPE = "rawType"

	// STRING_REGISTER_SIZE, for example, "abcd" requires 4 bytes, which requires two registers (2 words), so STRING_REGISTER_SIZE=2
	STRING_REGISTER_SIZE   = "stringRegisterSize"
	SERVICE_STOP_WAIT_TIME = 1
)

var PrimaryTableBitCountMap = map[string]uint16{// Defines the number of bits corresponding to various registers
	DISCRETES_INPUT:   1,
	COILS:             1,
	INPUT_REGISTERS:   16,
	HOLDING_REGISTERS: 16,
}

var ValueTypeBitCountMap = map[string]uint16{// The number of bits corresponding to various data types is defined
	common.ValueTypeInt16: 16,
	common.ValueTypeInt32: 32,
	common.ValueTypeInt64: 64,

	common.ValueTypeUint16: 16,
	common.ValueTypeUint32: 32,
	common.ValueTypeUint64: 64,

	common.ValueTypeFloat32: 32,
	common.ValueTypeFloat64: 64,

	common.ValueTypeBool:   1,
	common.ValueTypeString: 16,
}

deviceclient.go

Interface:

  • open a connection
  • Close connection
  • Get value
  • Set value

Structure:

  • Parameters required for Modbus generation instructions
  • Corresponding parameters in DeviceProfile

Function:

  • Create instruction information
    1. Check the parameter format, and give an error prompt if there is an error
    2. Format the obtained parameters
    3. Initialization instruction information structure
  • Calculate register length
    • The required number of registers is calculated according to the corresponding data format and the data bits of the register
  • Convert binary data to actual results
    1. Convert according to the data type in the configuration
    2. If it is a data type that can be exchanged, it is determined whether to exchange according to the configuration
    3. If rawType exists, convert to actual type in addition to binary to rawType
  • Convert actual input to binary data
    1. Convert according to the data type in the configuration, but note that the data length of the corresponding register cannot be exceeded
    2. If it is a data type that can be exchanged, it is determined whether to exchange according to the configuration
    3. If rawType exists, convert the actual type to rawType and then to binary
  • Calculate the number of bytes corresponding to the register
  • Convert binary
// -*- Mode: Go; indent-tabs-mode: t -*-
//
// Copyright (C) 2018-2021 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package driver

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"math"
	"strings"
	"time"

	"github.com/edgexfoundry/device-sdk-go/v2/pkg/models"
	"github.com/edgexfoundry/go-mod-core-contracts/v2/common"
	"github.com/edgexfoundry/go-mod-core-contracts/v2/errors"
)

// DeviceClient is an interface that needs to be implemented by modbus client library
// It is responsible for processing connections, reading data byte values, and writing data byte values
type DeviceClient interface {
	OpenConnection() error
	GetValue(commandInfo interface{}) ([]byte, error)
	SetValue(commandInfo interface{}, value []byte) error
	CloseConnection() error
}

// CommandInfo is command information
type CommandInfo struct {
	PrimaryTable    string
	StartingAddress uint16
	ValueType       string
	// How many registers do I need to read
	Length     uint16
	IsByteSwap bool
	IsWordSwap bool
	RawType    string
}

func createCommandInfo(req *models.CommandRequest) (*CommandInfo, error) {// device profile parameter verification, format adjustment, and finally create the command information structure
	if _, ok := req.Attributes[PRIMARY_TABLE]; !ok {
		return nil, errors.NewCommonEdgeX(errors.KindContractInvalid, fmt.Sprintf("attribute %s not exists", PRIMARY_TABLE), nil)
	}
	primaryTable := fmt.Sprintf("%v", req.Attributes[PRIMARY_TABLE])
	primaryTable = strings.ToUpper(primaryTable)

	if _, ok := req.Attributes[STARTING_ADDRESS]; !ok {
		return nil, errors.NewCommonEdgeX(errors.KindContractInvalid, fmt.Sprintf("attribute %s not exists", STARTING_ADDRESS), nil)
	}
	startingAddress, err := castStartingAddress(req.Attributes[STARTING_ADDRESS])
	if err != nil {
		return nil, errors.NewCommonEdgeX(errors.Kind(err), fmt.Sprintf("fail to cast %s", STARTING_ADDRESS), err)
	}

	var rawType = req.Type
	if _, ok := req.Attributes[RAW_TYPE]; ok {
		rawType = fmt.Sprintf("%v", req.Attributes[RAW_TYPE])
		rawType, err = normalizeRawType(rawType)
		if err != nil {
			return nil, err
		}
	}
	var length uint16
	if req.Type == common.ValueTypeString {
		length, err = castStartingAddress(req.Attributes[STRING_REGISTER_SIZE])
		if err != nil {
			return nil, err
		} else if (length > 123) || (length < 1) {
			return nil, errors.NewCommonEdgeX(errors.KindLimitExceeded, fmt.Sprintf("register size should be within the range of 1~123, get %v.", length), nil)
		}
	} else {
		length = calculateAddressLength(primaryTable, rawType)
	}

	var isByteSwap = false
	if _, ok := req.Attributes[IS_BYTE_SWAP]; ok {
		isByteSwap, err = castSwapAttribute(req.Attributes[IS_BYTE_SWAP])
		if err != nil {
			return nil, errors.NewCommonEdgeX(errors.Kind(err), fmt.Sprintf("fail to cast %s", IS_BYTE_SWAP), err)
		}
	}

	var isWordSwap = false
	if _, ok := req.Attributes[IS_WORD_SWAP]; ok {
		isWordSwap, err = castSwapAttribute(req.Attributes[IS_WORD_SWAP])
		if err != nil {
			return nil, errors.NewCommonEdgeX(errors.Kind(err), fmt.Sprintf("fail to cast %s", IS_WORD_SWAP), err)
		}
	}

	return &CommandInfo{
		PrimaryTable:    primaryTable,
		StartingAddress: startingAddress,
		ValueType:       req.Type,
		Length:          length,
		IsByteSwap:      isByteSwap,
		IsWordSwap:      isWordSwap,
		RawType:         rawType,
	}, nil
}

func calculateAddressLength(primaryTable string, valueType string) uint16 { //The required number of registers is calculated according to the corresponding data format and the data bits of the register
	var primaryTableBit = PrimaryTableBitCountMap[primaryTable]
	var valueTypeBitCount = ValueTypeBitCountMap[valueType]

	var length = valueTypeBitCount / primaryTableBit
	if length < 1 {
		length = 1
	}

	return length
}

// TransformDataBytesToResult is used to convert the binary data of the device to the specified value type as the actual result
func TransformDataBytesToResult(req *models.CommandRequest, dataBytes []byte, commandInfo *CommandInfo) (*models.CommandValue, error) {
	var err error
	var res interface{}
	var result = &models.CommandValue{}

	switch commandInfo.ValueType {
	case common.ValueTypeUint16:
		res = binary.BigEndian.Uint16(dataBytes)
	case common.ValueTypeUint32:
		res = binary.BigEndian.Uint32(swap32BitDataBytes(dataBytes, commandInfo.IsByteSwap, commandInfo.IsWordSwap))
	case common.ValueTypeUint64:
		res = binary.BigEndian.Uint64(dataBytes)
	case common.ValueTypeInt16:
		res = int16(binary.BigEndian.Uint16(dataBytes))
	case common.ValueTypeInt32:
		res = int32(binary.BigEndian.Uint32(swap32BitDataBytes(dataBytes, commandInfo.IsByteSwap, commandInfo.IsWordSwap)))
	case common.ValueTypeInt64:
		res = int64(binary.BigEndian.Uint64(dataBytes))
	case common.ValueTypeFloat32:
		switch commandInfo.RawType {
		case common.ValueTypeFloat32:
			raw := binary.BigEndian.Uint32(swap32BitDataBytes(dataBytes, commandInfo.IsByteSwap, commandInfo.IsWordSwap))
			res = math.Float32frombits(raw)
		case common.ValueTypeInt16:
			raw := int16(binary.BigEndian.Uint16(dataBytes))
			res = float32(raw)
			driver.Logger.Debugf("According to the rawType %s and the value type %s, convert integer %d to float %v ", INT16, FLOAT32, res, result.ValueToString())
		case common.ValueTypeUint16:
			raw := binary.BigEndian.Uint16(dataBytes)
			res = float32(raw)
			driver.Logger.Debugf("According to the rawType %s and the value type %s, convert integer %d to float %v ", UINT16, FLOAT32, res, result.ValueToString())
		}
	case common.ValueTypeFloat64:
		switch commandInfo.RawType {
		case common.ValueTypeFloat64:
			raw := binary.BigEndian.Uint64(dataBytes)
			res = math.Float64frombits(raw)
		case common.ValueTypeInt16:
			raw := int16(binary.BigEndian.Uint16(dataBytes))
			res = float64(raw)
			driver.Logger.Debugf("According to the rawType %s and the value type %s, convert integer %d to float %v ", INT16, FLOAT64, res, result.ValueToString())
		case common.ValueTypeUint16:
			raw := binary.BigEndian.Uint16(dataBytes)
			res = float64(raw)
			driver.Logger.Debugf("According to the rawType %s and the value type %s, convert integer %d to float %v ", UINT16, FLOAT64, res, result.ValueToString())
		}
	case common.ValueTypeBool:
		res = false
		// to find the 1st bit of the dataBytes by mask it with 2^0 = 1 (00000001)
		if (dataBytes[0] & 1) > 0 {
			res = true
		}
	case common.ValueTypeString:
		res = string(bytes.Trim(dataBytes, string(rune(0))))
	default:
		return nil, fmt.Errorf("return result fail, none supported value type: %v", commandInfo.ValueType)
	}

	result, err = models.NewCommandValue(req.DeviceResourceName, commandInfo.ValueType, res)
	if err != nil {
		return nil, err
	}
	result.Origin = time.Now().UnixNano()

	driver.Logger.Debugf("Transfer dataBytes to CommandValue(%v) successful.", result.ValueToString())
	return result, nil
}

// TransformCommandValueToDataBytes converts the read value into binary data for data transmission through Modbus protocol.
func TransformCommandValueToDataBytes(commandInfo *CommandInfo, value *models.CommandValue) ([]byte, error) {
	var err error
	var byteCount = calculateByteCount(commandInfo)
	var dataBytes []byte
	buf := new(bytes.Buffer)
	if commandInfo.ValueType != common.ValueTypeString {// Convert non String data
		err = binary.Write(buf, binary.BigEndian, value.Value)
		if err != nil {
			return nil, fmt.Errorf("failed to transform %v to []byte", value.Value)
		}

		numericValue := buf.Bytes()
		var maxSize = uint16(len(numericValue))
		dataBytes = numericValue[maxSize-byteCount : maxSize]// Take only the last part with data
	}

	_, ok := ValueTypeBitCountMap[commandInfo.ValueType]// If the data format is not supported, there is an error prompt
	if !ok {
		err = fmt.Errorf("none supported value type : %v \n", commandInfo.ValueType)
		return dataBytes, err
	}

	if commandInfo.ValueType == common.ValueTypeUint32 || commandInfo.ValueType == common.ValueTypeInt32 || commandInfo.ValueType == common.ValueTypeFloat32 {// The data types that can be used for swap operations are exchanged according to the parameters passed in whether to exchange or not
		dataBytes = swap32BitDataBytes(dataBytes, commandInfo.IsByteSwap, commandInfo.IsWordSwap)
	}

	// This function converts a floating-point value to a 32-bit integer value based on the rawType conversion value
	if commandInfo.ValueType == common.ValueTypeFloat32 {
		val, edgexErr := value.Float32Value()
		if edgexErr != nil {
			return dataBytes, edgexErr
		}
		if commandInfo.RawType == common.ValueTypeInt16 {
			dataBytes, err = getBinaryData(int16(val))
			if err != nil {
				return dataBytes, err
			}
		} else if commandInfo.RawType == common.ValueTypeUint16 {
			dataBytes, err = getBinaryData(uint16(val))
			if err != nil {
				return dataBytes, err
			}
		}
	} else if commandInfo.ValueType == common.ValueTypeFloat64 {
		val, edgexErr := value.Float64Value()
		if edgexErr != nil {
			return dataBytes, edgexErr
		}
		if commandInfo.RawType == common.ValueTypeInt16 {
			dataBytes, err = getBinaryData(int16(val))
			if err != nil {
				return dataBytes, err
			}
		} else if commandInfo.RawType == common.ValueTypeUint16 {
			dataBytes, err = getBinaryData(uint16(val))
			if err != nil {
				return dataBytes, err
			}
		}
	} else if commandInfo.ValueType == common.ValueTypeString {// The actual register length is not exceeded when converting String data
		// Cast value of string type
		oriStr := value.ValueToString()
		tempBytes := []byte(oriStr)
		bytesL := len(tempBytes)
		oriByteL := int(commandInfo.Length * 2)
		if bytesL < oriByteL {
			less := make([]byte, oriByteL-bytesL)
			dataBytes = append(tempBytes, less...)
		} else if bytesL > oriByteL {
			dataBytes = tempBytes[:oriByteL]
		} else {
			dataBytes = []byte(oriStr)
		}
	}
	driver.Logger.Debugf("Transfer CommandValue to dataBytes for write command, %v, %v", commandInfo.ValueType, dataBytes)
	return dataBytes, err
}

func calculateByteCount(commandInfo *CommandInfo) uint16 {// The number of bytes of data is calculated according to the number and type of registers
	var byteCount uint16
	if commandInfo.PrimaryTable == HOLDING_REGISTERS || commandInfo.PrimaryTable == INPUT_REGISTERS {
		byteCount = commandInfo.Length * 2
	} else {
		byteCount = commandInfo.Length
	}

	return byteCount
}

func getBinaryData(val interface{}) (dataBytes []byte, err error) {// Convert to binary
	buf := new(bytes.Buffer)
	err = binary.Write(buf, binary.BigEndian, val)// Write the value to buf in big endian format
	if err != nil {
		return dataBytes, err
	}
	dataBytes = buf.Bytes()// Convert to a byte array
	return dataBytes, err
}

driver.go

technological process:

  • Processing read instructions:
  1. Create connection information
  2. Create a device client
  3. Disconnect when finished
  4. Processing command requests
  5. Create command information
  6. Fetch from device client
  7. Convert binary data to readable data
  • Processing write instructions:
  1. Create connection information
  2. Create a device client
  3. Disconnect when finished
  4. Processing command requests
  5. Create command information
  6. Convert the read setting data to binary data
  7. Write value to device client
// -*- Mode: Go; indent-tabs-mode: t -*-
//
// Copyright (C) 2018-2021 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

// Package driver is used to execute device-sdk's commands
package driver

import (
	"fmt"
	"sync"
	"time"

	sdkModel "github.com/edgexfoundry/device-sdk-go/v2/pkg/models"
	"github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger"
	"github.com/edgexfoundry/go-mod-core-contracts/v2/models"
)

var once sync.Once
var driver *Driver

type Driver struct {
	Logger              logger.LoggingClient
	AsyncCh             chan<- *sdkModel.AsyncValues
	mutex               sync.Mutex
	addressMap          map[string]chan bool
	workingAddressCount map[string]int
	stopped             bool
}

var concurrentCommandLimit = 100

func (d *Driver) DisconnectDevice(deviceName string, protocols map[string]models.ProtocolProperties) error {
	d.Logger.Warn("Driver's DisconnectDevice function didn't implement")
	return nil
}

// The address marked by lockAddress is not available because the actual device can only process one request at a time
func (d *Driver) lockAddress(address string) error {
	if d.stopped {
		return fmt.Errorf("service attempts to stop and unable to handle new request")
	}
	d.mutex.Lock()
	lock, ok := d.addressMap[address]
	if !ok {
		lock = make(chan bool, 1)
		d.addressMap[address] = lock
	}

	// workingAddressCount is used to check the execution of high-frequency commands to avoid goroutine blocking
	count, ok := d.workingAddressCount[address]
	if !ok {
		d.workingAddressCount[address] = 1
	} else if count >= concurrentCommandLimit {
		d.mutex.Unlock()
		errorMessage := fmt.Sprintf("High-frequency command execution. There are %v commands with the same address in the queue", concurrentCommandLimit)
		d.Logger.Error(errorMessage)
		return fmt.Errorf(errorMessage)
	} else {
		d.workingAddressCount[address] = count + 1
	}

	d.mutex.Unlock()
	lock <- true

	return nil
}

// unlockAddress removes the tag after completion
func (d *Driver) unlockAddress(address string) {
	d.mutex.Lock()
	lock := d.addressMap[address]
	d.workingAddressCount[address] = d.workingAddressCount[address] - 1
	d.mutex.Unlock()
	<-lock
}

// lockableAddress returns a lockable address according to the protocol
func (d *Driver) lockableAddress(info *ConnectionInfo) string {
	var address string
	if info.Protocol == ProtocolTCP {
		address = fmt.Sprintf("%s:%d", info.Address, info.Port)
	} else {
		address = info.Address
	}
	return address
}

func (d *Driver) HandleReadCommands(deviceName string, protocols map[string]models.ProtocolProperties, reqs []sdkModel.CommandRequest) (responses []*sdkModel.CommandValue, err error) {// Processing read instructions
	connectionInfo, err := createConnectionInfo(protocols)//Create connection information
	if err != nil {
		driver.Logger.Errorf("Fail to create read command connection info. err:%v \n", err)
		return responses, err
	}

	err = d.lockAddress(d.lockableAddress(connectionInfo))
	if err != nil {
		return responses, err
	}
	defer d.unlockAddress(d.lockableAddress(connectionInfo))

	responses = make([]*sdkModel.CommandValue, len(reqs))
	var deviceClient DeviceClient

	// create device client and open connection
	deviceClient, err = NewDeviceClient(connectionInfo)// Create a device client
	if err != nil {
		driver.Logger.Infof("Read command NewDeviceClient failed. err:%v \n", err)
		return responses, err
	}

	err = deviceClient.OpenConnection()// Connecting equipment
	if err != nil {
		driver.Logger.Infof("Read command OpenConnection failed. err:%v \n", err)
		return responses, err
	}

	defer deviceClient.CloseConnection()// Turn off the device when finished

	// Processing command requests
	for i, req := range reqs {
		res, err := handleReadCommandRequest(deviceClient, req)
		if err != nil {
			driver.Logger.Infof("Read command failed. Cmd:%v err:%v \n", req.DeviceResourceName, err)
			return responses, err
		}

		responses[i] = res
	}

	return responses, nil
}

func handleReadCommandRequest(deviceClient DeviceClient, req sdkModel.CommandRequest) (*sdkModel.CommandValue, error) {// Processing command requests
	var response []byte
	var result = &sdkModel.CommandValue{}
	var err error

	commandInfo, err := createCommandInfo(&req)// Create command information
	if err != nil {
		return nil, err
	}

	response, err = deviceClient.GetValue(commandInfo)// Fetch from device client
	if err != nil {
		return result, err
	}

	result, err = TransformDataBytesToResult(&req, response, commandInfo)// Convert binary data to readable data

	if err != nil {
		return result, err
	} else {
		driver.Logger.Infof("Read command finished. Cmd:%v, %v \n", req.DeviceResourceName, result)
	}

	return result, nil
}

func (d *Driver) HandleWriteCommands(deviceName string, protocols map[string]models.ProtocolProperties, reqs []sdkModel.CommandRequest, params []*sdkModel.CommandValue) error {// Processing write instructions
	connectionInfo, err := createConnectionInfo(protocols)
	if err != nil {
		driver.Logger.Errorf("Fail to create write command connection info. err:%v \n", err)
		return err
	}

	err = d.lockAddress(d.lockableAddress(connectionInfo))
	if err != nil {
		return err
	}
	defer d.unlockAddress(d.lockableAddress(connectionInfo))

	var deviceClient DeviceClient

	// create device client and open connection
	deviceClient, err = NewDeviceClient(connectionInfo)// New device client
	if err != nil {
		return err
	}

	err = deviceClient.OpenConnection()// Connecting equipment
	if err != nil {
		return err
	}

	defer deviceClient.CloseConnection()// Disconnect when finished

	// Processing command requests
	for i, req := range reqs {
		err = handleWriteCommandRequest(deviceClient, req, params[i])
		if err != nil {
			d.Logger.Error(err.Error())
			break
		}
	}

	return err
}

func handleWriteCommandRequest(deviceClient DeviceClient, req sdkModel.CommandRequest, param *sdkModel.CommandValue) error {// Processing write request
	var err error

	commandInfo, err := createCommandInfo(&req)// Create command information
	if err != nil {
		return err
	}

	dataBytes, err := TransformCommandValueToDataBytes(commandInfo, param)// Convert written data into binary data for easy transmission
	if err != nil {
		return fmt.Errorf("transform command value failed, err: %v", err)
	}

	err = deviceClient.SetValue(commandInfo, dataBytes)// Write to client
	if err != nil {
		return fmt.Errorf("handle write command request failed, err: %v", err)
	}

	driver.Logger.Infof("Write command finished. Cmd:%v \n", req.DeviceResourceName)
	return nil
}

func (d *Driver) Initialize(lc logger.LoggingClient, asyncCh chan<- *sdkModel.AsyncValues, deviceCh chan<- []sdkModel.DiscoveredDevice) error {
	d.Logger = lc
	d.AsyncCh = asyncCh
	d.addressMap = make(map[string]chan bool)
	d.workingAddressCount = make(map[string]int)
	return nil
}

func (d *Driver) Stop(force bool) error {
	d.stopped = true
	if !force {
		d.waitAllCommandsToFinish()
	}
	for _, locked := range d.addressMap {
		close(locked)
	}
	return nil
}

// Waitalcommandstofinish is used to check and wait for unfinished work
func (d *Driver) waitAllCommandsToFinish() {
loop:
	for {
		for _, count := range d.workingAddressCount {
			if count != 0 {
				// wait a moment and check again
				time.Sleep(time.Second * SERVICE_STOP_WAIT_TIME)
				continue loop
			}
		}
		break loop
	}
}

func (d *Driver) AddDevice(deviceName string, protocols map[string]models.ProtocolProperties, adminState models.AdminState) error {
	d.Logger.Debugf("Device %s is added", deviceName)
	return nil
}

func (d *Driver) UpdateDevice(deviceName string, protocols map[string]models.ProtocolProperties, adminState models.AdminState) error {
	d.Logger.Debugf("Device %s is updated", deviceName)
	return nil
}

func (d *Driver) RemoveDevice(deviceName string, protocols map[string]models.ProtocolProperties) error {
	d.Logger.Debugf("Device %s is removed", deviceName)
	return nil
}

func NewProtocolDriver() sdkModel.ProtocolDriver {
	once.Do(func() {
		driver = new(Driver)
	})
	return driver
}

modbusclient.go

// -*- Mode: Go; indent-tabs-mode: t -*-
//
// Copyright (C) 2018-2021 IOTech Ltd
//
// SPDX-License-Identifier: Apache-2.0

package driver

import (
	"encoding/binary"
	"fmt"
	"log"
	"os"
	"strings"
	"time"

	MODBUS "github.com/goburrow/modbus"
)

// ModbusClient is used to connect devices and read / write values
type ModbusClient struct {
	// IsModbusTcp is a value indicating the connection type
	IsModbusTcp bool
	// TCPClientHandler is ued for holding device TCP connection
	TCPClientHandler MODBUS.TCPClientHandler
	// TCPClientHandler is ued for holding device RTU connection
	RTUClientHandler MODBUS.RTUClientHandler

	client MODBUS.Client
}

func (c *ModbusClient) OpenConnection() error {// Realize connection with Modbus device
	var err error
	var newClient MODBUS.Client
	if c.IsModbusTcp {
		err = c.TCPClientHandler.Connect()
		newClient = MODBUS.NewClient(&c.TCPClientHandler)
		driver.Logger.Info(fmt.Sprintf("Modbus client create TCP connection."))
	} else {
		err = c.RTUClientHandler.Connect()
		newClient = MODBUS.NewClient(&c.RTUClientHandler)
		driver.Logger.Info(fmt.Sprintf("Modbus client create RTU connection."))
	}
	c.client = newClient
	return err
}

func (c *ModbusClient) CloseConnection() error {// Disconnect from Modbus device
	var err error
	if c.IsModbusTcp {
		err = c.TCPClientHandler.Close()

	} else {
		err = c.RTUClientHandler.Close()
	}
	return err
}

func (c *ModbusClient) GetValue(commandInfo interface{}) ([]byte, error) {// Read from device
	var modbusCommandInfo = commandInfo.(*CommandInfo)

	var response []byte
	var err error

	switch modbusCommandInfo.PrimaryTable {// Select the corresponding function to read the instruction according to the register type in the instruction information
	case DISCRETES_INPUT:
		response, err = c.client.ReadDiscreteInputs(modbusCommandInfo.StartingAddress, modbusCommandInfo.Length)
	case COILS:
		response, err = c.client.ReadCoils(modbusCommandInfo.StartingAddress, modbusCommandInfo.Length)

	case INPUT_REGISTERS:
		response, err = c.client.ReadInputRegisters(modbusCommandInfo.StartingAddress, modbusCommandInfo.Length)
	case HOLDING_REGISTERS:
		response, err = c.client.ReadHoldingRegisters(modbusCommandInfo.StartingAddress, modbusCommandInfo.Length)
	default:
		driver.Logger.Error("None supported primary table! ")
	}

	if err != nil {
		return response, err
	}

	driver.Logger.Info(fmt.Sprintf("Modbus client GetValue's results %v", response))

	return response, nil
}

func (c *ModbusClient) SetValue(commandInfo interface{}, value []byte) error {//Take value from equipment
	var modbusCommandInfo = commandInfo.(*CommandInfo)

	// Write value to device
	var result []byte
	var err error

	switch modbusCommandInfo.PrimaryTable {// According to the register type in the instruction information, select the corresponding function to realize the write instruction
	case DISCRETES_INPUT:
		err = fmt.Errorf("Error: DISCRETES_INPUT is Read-Only..!!")

	case COILS:
		result, err = c.client.WriteMultipleCoils(uint16(modbusCommandInfo.StartingAddress), modbusCommandInfo.Length, value)

	case INPUT_REGISTERS:
		err = fmt.Errorf("Error: INPUT_REGISTERS is Read-Only..!!")

	case HOLDING_REGISTERS:
		if modbusCommandInfo.Length == 1 {
			result, err = c.client.WriteSingleRegister(uint16(modbusCommandInfo.StartingAddress), binary.BigEndian.Uint16(value))
		} else {
			result, err = c.client.WriteMultipleRegisters(uint16(modbusCommandInfo.StartingAddress), modbusCommandInfo.Length, value)
		}
	default:
	}

	if err != nil {
		return err
	}
	driver.Logger.Info(fmt.Sprintf("Modbus client SetValue successful, results: %v", result))

	return nil
}

func NewDeviceClient(connectionInfo *ConnectionInfo) (*ModbusClient, error) {// Configure the parameters in the connection information into the ClientHandler
	client := new(ModbusClient)
	var err error
	if connectionInfo.Protocol == ProtocolTCP {
		client.IsModbusTcp = true
	}
	if client.IsModbusTcp {
		client.TCPClientHandler.Address = fmt.Sprintf("%s:%d", connectionInfo.Address, connectionInfo.Port)
		client.TCPClientHandler.SlaveId = byte(connectionInfo.UnitID)
		client.TCPClientHandler.Timeout = time.Duration(connectionInfo.Timeout) * time.Second
		client.TCPClientHandler.IdleTimeout = time.Duration(connectionInfo.IdleTimeout) * time.Second
		client.TCPClientHandler.Logger = log.New(os.Stdout, "", log.LstdFlags)
	} else {
		serialParams := strings.Split(connectionInfo.Address, ",")
		client.RTUClientHandler.Address = serialParams[0]
		client.RTUClientHandler.SlaveId = byte(connectionInfo.UnitID)
		client.RTUClientHandler.Timeout = time.Duration(connectionInfo.Timeout) * time.Second
		client.RTUClientHandler.IdleTimeout = time.Duration(connectionInfo.IdleTimeout) * time.Second
		client.RTUClientHandler.BaudRate = connectionInfo.BaudRate
		client.RTUClientHandler.DataBits = connectionInfo.DataBits
		client.RTUClientHandler.StopBits = connectionInfo.StopBits
		client.RTUClientHandler.Parity = connectionInfo.Parity
		client.RTUClientHandler.Logger = log.New(os.Stdout, "", log.LstdFlags)
	}
	return client, err
}


Keywords: Go edge

Added by shure2 on Sat, 06 Nov 2021 22:56:33 +0200