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:
- Go to cmd directory first (... / cmd)
- Execute the device MODBUS file in the directory and start it as an edgex device MODBUS process
- Go back to PWD directory
- If you fall into a trap, perform the cleanup function and exit
- 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:
- Enter GIT_ROOT
- 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
- If trapped in a trap, perform the cleanup function
- Check whether there is vendor.bk, and if so, change vendor.bk to vendor (equivalent to backup and restore)
- If the vendor folder exists, back it up so that we can build a new one
- When go mod, the vendor file created in the project will be copied from the dependent package
- Open nullglobbing so that if there is nothing in the cmd directory, we won't do anything in this loop
- If the attribute.txt file cannot be found, exit with 1 and prompt that the file cannot be found
- 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
- 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:
- Set service name
- Establish a protocol driver for docking
- 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
- Incoming configuration protocol set
- Check modbus protocol form
- Error prompt for incorrect configuration
- If the protocol is RTU, call the function to create RTU type connection information
- If the protocol is TCP, call the function to create TCP type connection information
- Parse integer configuration parameters
- Get configuration parameters
- 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
- Check the parameter format, and give an error prompt if there is an error
- Format the obtained parameters
- 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
- Convert according to the data type in the configuration
- If it is a data type that can be exchanged, it is determined whether to exchange according to the configuration
- If rawType exists, convert to actual type in addition to binary to rawType
- Convert actual input to binary data
- Convert according to the data type in the configuration, but note that the data length of the corresponding register cannot be exceeded
- If it is a data type that can be exchanged, it is determined whether to exchange according to the configuration
- 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:
- Create connection information
- Create a device client
- Disconnect when finished
- Processing command requests
- Create command information
- Fetch from device client
- Convert binary data to readable data
- Processing write instructions:
- Create connection information
- Create a device client
- Disconnect when finished
- Processing command requests
- Create command information
- Convert the read setting data to binary data
- 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 }