diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000..c23774cdda5e3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "env": {}, + "args": [] + } + ] +} \ No newline at end of file diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index feb462368afca..a43aa4bda2ae5 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -69,6 +69,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/memcached" _ "github.com/influxdata/telegraf/plugins/inputs/mesos" _ "github.com/influxdata/telegraf/plugins/inputs/minecraft" + _ "github.com/influxdata/telegraf/plugins/inputs/modbus" _ "github.com/influxdata/telegraf/plugins/inputs/mongodb" _ "github.com/influxdata/telegraf/plugins/inputs/mqtt_consumer" _ "github.com/influxdata/telegraf/plugins/inputs/mysql" diff --git a/plugins/inputs/modbus/README.md b/plugins/inputs/modbus/README.md new file mode 100644 index 0000000000000..b35076d32774e --- /dev/null +++ b/plugins/inputs/modbus/README.md @@ -0,0 +1,67 @@ +# gomodbus Input Plugin + +This input plugin is a Fault-tolerant, fail-fast implementation of Modbus protocol in Go. + +### Supported functions +------------------- +Bit access: +* Read Discrete Inputs +* Read Coils +* Write Single Coil +* Write Multiple Coils + +16-bit access: +* Read Input Registers +* Read Holding Registers +* Write Single Register +* Write Multiple Registers +* Read/Write Multiple Registers +* Mask Write Register +* Read FIFO Queue + +### Supported formats +----------------- +* TCP +* Serial (RTU, ASCII) + +### Configuration: + +```toml +[[inputs.modbus]] + ## Set Modbus Config (Either TCP or RTU Client) + ## Modbust TCP Client + ## TCP Client = "localhost:502" + Client = "localhost:502" + + ## Modbus RTU Client + ## RTU Client = "/dev/ttyS0" + ## serial setup for RTUClient + # serial = [11520,8,"N",1] + + ## Call to device + SlaveAddress = 1 + + ## Function Code to Device + FunctionCode = 1 + + ## Device Memory Address + Address = 1 + + ## Quantity of Values to read/write + Quantity = 1 + + ## Array of values to write + # Values = [0] + + # Timeout in seconds + TimeOut = 5 +``` +Run: + +``` +telegraf --config telegraf.conf --input-filter modbus --test +``` + +### References +---------- +- [Modbus Specifications and Implementation Guides](http://www.modbus.org/specs.php) diff --git a/plugins/inputs/modbus/modbus.go b/plugins/inputs/modbus/modbus.go new file mode 100644 index 0000000000000..0392aa4bfe5cc --- /dev/null +++ b/plugins/inputs/modbus/modbus.go @@ -0,0 +1,282 @@ +package modbus + +import ( + "encoding/binary" + "fmt" + "log" + "os" + "strings" + "time" + + modbus "github.com/goburrow/modbus" + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/inputs" +) + +// Modbus library +type Modbus struct { + Client string + SlaveAddress uint8 + FunctionCode uint8 + Address uint16 + Quantity uint16 + Values []byte + TimeOut uint8 + Comm serial + Results []byte + err error + CX modbusClients +} + +type serial struct { + BaudRate int + Databits int + Parity string + Stopbits int +} + +type modbusClients struct { + connected bool + TCPhandler *modbus.TCPClientHandler + RTUhandler *modbus.RTUClientHandler + client modbus.Client +} + +// ModbusConfig example +var sampleConfig = ` + ## Set Modbus Config (Either TCP or RTU Client) + ## Modbust TCP Client + ## TCP Client = "localhost:502" + Client = "localhost:502" + + ## Modbus RTU Client + ## RTU Client = "/dev/ttyS0" + ## serial setup for RTUClient + # serial = [11520,8,"N",1] + + ## Call to device + SlaveAddress = 1 + + ## Function Code to Device + FunctionCode = 1 + + ## Device Memory Address + Address = 1 + + ## Quantity of Values to read/write + Quantity = 1 + + ## Array of values to write + # Values = [0] + + # Timeout in seconds + TimeOut = 5 +` + +// SampleConfig of modbus plugin +func (s *Modbus) SampleConfig() string { + return sampleConfig +} + +// Description of modbus plugin +func (s *Modbus) Description() string { + return "Fault-tolerant, fail-fast implementation of Modbus protocol in Go." +} + +// Gather TCP or RTU modbus parameters +func (s *Modbus) Gather(acc telegraf.Accumulator) (err error) { + + var inits bool + // Modbus TCP + if strings.Contains(s.Client, ":") { + inits = true + err = s.getTCPdata() + + } else if strings.ContainsAny(s.Client, "/") { + inits = true + err = s.getRTUdata() + } else { + inits = false + } + + fields := make(map[string]interface{}) + + if inits { + field := string(s.FunctionCode) + if s.Quantity == 1 { + + field += fmt.Sprintf("%04d", s.Address) + fields[field] = s.Results[0] + + } else { + for i := 1; i <= int(s.Quantity); i++ { + + offset := i - 1 + field1 := field + fmt.Sprintf("%04d", int(s.Address)+offset) + fields[field1] = s.Results[offset] + + } + } + } + + tags := make(map[string]string) + + acc.AddFields("modbus", fields, tags) + + return nil +} + +func (s *Modbus) createTCPClient() error { + if s.CX.TCPhandler == nil && !s.CX.connected { + s.CX.TCPhandler = modbus.NewTCPClientHandler(s.Client) + s.CX.TCPhandler.Timeout = time.Duration(s.TimeOut) * time.Second + s.CX.TCPhandler.SlaveId = s.SlaveAddress + + s.CX.TCPhandler.Logger = log.New(os.Stdout, "TCP: ", log.LstdFlags) + + s.err = s.CX.TCPhandler.Connect() + if s.err != nil { + s.CX.connected = false + return s.err + } + + defer s.CX.TCPhandler.Close() + + if s.CX.client == nil && !s.CX.connected { + s.CX.client = modbus.NewClient(s.CX.TCPhandler) + s.CX.connected = true + } + } + + return s.err +} + +func (s *Modbus) getTCPdata() (err error) { + // create TCPClient connection if not already done + s.err = s.createTCPClient() + if s.err != nil { + return s.err + } + // Function Codes + switch s.FunctionCode { + // FC01 = Read Coil Status + case 1: + s.Results, s.err = s.CX.client.ReadCoils(s.Address, s.Quantity) + + // FC02 = Read Input Status + case 2: + s.Results, s.err = s.CX.client.ReadDiscreteInputs(s.Address, s.Quantity) + + // FC03 = Read Holding Registers + case 3: + s.Results, s.err = s.CX.client.ReadHoldingRegisters(s.Address, s.Quantity) + + // FC04 = Read Input Registers + case 4: + s.Results, s.err = s.CX.client.ReadInputRegisters(s.Address, s.Quantity) + + // FC05 = Write Single Coil + case 5: + s.Results, s.err = s.CX.client.WriteSingleCoil(s.Address, binary.BigEndian.Uint16(s.Values)) + + // FC06 = Write Single Register + case 6: + s.Results, s.err = s.CX.client.WriteSingleRegister(s.Address, binary.BigEndian.Uint16(s.Values)) + + // FC15 = Write Multiple Coils + case 15: + s.Results, s.err = s.CX.client.WriteMultipleCoils(s.Address, s.Quantity, s.Values) + + // FC16 = Write Multiple Registers + case 16: + s.Results, s.err = s.CX.client.WriteMultipleRegisters(s.Address, s.Quantity, s.Values) + + default: + //do nothing + } + + return s.err +} + +func (s *Modbus) createRTUClient() (err error) { + + if s.CX.RTUhandler == nil && !s.CX.connected { + s.CX.RTUhandler = modbus.NewRTUClientHandler(s.Client) + s.CX.RTUhandler.BaudRate = s.Comm.BaudRate + s.CX.RTUhandler.DataBits = s.Comm.Databits + s.CX.RTUhandler.Parity = s.Comm.Parity + s.CX.RTUhandler.StopBits = s.Comm.Stopbits + s.CX.RTUhandler.SlaveId = s.SlaveAddress + s.CX.RTUhandler.Timeout = time.Duration(s.TimeOut) * time.Second + + s.CX.RTUhandler.Logger = log.New(os.Stdout, "RTU: ", log.LstdFlags) + + s.err = s.CX.RTUhandler.Connect() + if s.err != nil { + s.CX.connected = false + return s.err + } + defer s.CX.RTUhandler.Close() + + if s.CX.client == nil && !s.CX.connected { + s.CX.client = modbus.NewClient(s.CX.RTUhandler) + s.CX.connected = true + } + } + + return nil +} + +func (s *Modbus) getRTUdata() (err error) { + // create RTUClient connection if not already done + s.err = s.createRTUClient() + if s.err != nil { + return s.err + } + // Function Codes + switch s.FunctionCode { + // FC01 = Read Coil Status + case 1: + s.Results, s.err = s.CX.client.ReadCoils(s.Address, s.Quantity) + + // FC02 = Read Input Status + case 2: + s.Results, s.err = s.CX.client.ReadDiscreteInputs(s.Address, s.Quantity) + + // FC03 = Read Holding Registers + case 3: + s.Results, s.err = s.CX.client.ReadHoldingRegisters(s.Address, s.Quantity) + + // FC04 = Read Input Registers + case 4: + s.Results, s.err = s.CX.client.ReadInputRegisters(s.Address, s.Quantity) + + // FC05 = Write Single Coil + case 5: + s.Results, s.err = s.CX.client.WriteSingleCoil(s.Address, binary.BigEndian.Uint16(s.Values)) + + // FC06 = Write Single Register + case 6: + s.Results, s.err = s.CX.client.WriteSingleRegister(s.Address, binary.BigEndian.Uint16(s.Values)) + + // FC15 = Write Multiple Coils + case 15: + s.Results, s.err = s.CX.client.WriteMultipleCoils(s.Address, s.Quantity, s.Values) + + // FC16 = Write Multiple Registers + case 16: + s.Results, s.err = s.CX.client.WriteMultipleRegisters(s.Address, s.Quantity, s.Values) + + default: + //do nothing + } + + return err +} + +func init() { + + inputs.Add("modbus", func() telegraf.Input { + return &Modbus{} + }) +} diff --git a/plugins/inputs/modbus/modbus_test.go b/plugins/inputs/modbus/modbus_test.go new file mode 100644 index 0000000000000..8a8c9a5e7f04d --- /dev/null +++ b/plugins/inputs/modbus/modbus_test.go @@ -0,0 +1,346 @@ +package modbus + +import ( + "encoding/binary" + "testing" +) + +var test Modbus + +func update(test *Modbus) { + test.Client = "127.0.0.1:502" + test.SlaveAddress = 1 + test.FunctionCode = 0 + test.Address = 0 + test.Quantity = 1 + test.TimeOut = 5 +} + +// Testing Code for TCP Client +func TestGetTCPdataReadCoils(t *testing.T) { + // Note: Address may be offset by 1 + // Set bit @ address to query + t.Log("Testing TCPClient Connections: Read Coils") + update(&test) + test.FunctionCode = 1 + + test.err = test.getTCPdata() + + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if test.Results == nil { + t.Errorf("Expected value of 1 @ modbus address 00001, but it was %d instead", test.Results[0]) + } +} + +func TestGetTCPdataReadDiscreteInputs(t *testing.T) { + // Note: Address may be offset by 1 + // Set bits @ address to query + // Results will be in Byte format not binary + t.Log("Testing TCPClient Connections: Read Discrete Inputs") + update(&test) + test.FunctionCode = 2 + test.Address = 0 + test.Quantity = 3 + + test.err = test.getTCPdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if test.Results == nil { + t.Errorf("Expected value of 1 @ modbus address 10001, but it was %d instead", test.Results) + } +} + +func TestGetTCPdataReadHoldingRegister(t *testing.T) { + // Note: Address may be offset by 1 + // Set HoldingRegister address to xFF FF or b1111 1111 1111 1111 or d65535 + t.Log("Testing TCPClient Connections: Read Holding Registers") + update(&test) + test.FunctionCode = 3 + + test.err = test.getTCPdata() + + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if binary.BigEndian.Uint16(test.Results) != 65535 { + t.Errorf("Expected value of 65535 @ modbus address 40001, but it was %d instead", binary.BigEndian.Uint16(test.Results)) + } +} + +func TestGetTCPdataReadInputRegister(t *testing.T) { + // Note: Address may be offset by 1 + // Set InpuRegister address to xFF FF or b1111 1111 1111 1111 or d65535 + t.Log("Testing TCPClient Connections: Read Input Register") + update(&test) + test.FunctionCode = 4 + + test.err = test.getTCPdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if binary.BigEndian.Uint16(test.Results) != 65535 { + t.Errorf("Expected value of 65535 @ modbus address 30001, but it was %d instead", binary.BigEndian.Uint16(test.Results)) + } +} + +func TestGetTCPdataWriteSingleCoil(t *testing.T) { + // Note: Address may be offset by 1 + // Set InputCoils address to xFF or b1111 1111 or d255 + t.Log("Testing TCPClient Connections: Write Single Coil") + update(&test) + test.FunctionCode = 5 + test.Address = 0 + test.Values = []byte{0xff, 0x00} + + test.err = test.getTCPdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if binary.LittleEndian.Uint16(test.Results) != 255 { + t.Errorf("Expected value of 1 @ modbus address 10001, but it was %d instead", binary.LittleEndian.Uint16(test.Results)) + } +} + +func TestGetTCPdataWriteSingleRegister(t *testing.T) { + // Note: Address may be offset by 1 + // Set InputRegister address to xFF or b1111 1111 or d255 + t.Log("Testing TCPClient Connections: Write Single Register") + update(&test) + test.FunctionCode = 6 + test.Address = 1 + test.Values = []byte{0xff, 0x00} + + test.err = test.getTCPdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if binary.LittleEndian.Uint16(test.Results) != 255 { + t.Errorf("Expected value of 255 @ modbus address 30001, but it was %d instead", binary.LittleEndian.Uint16(test.Results)) + } +} + +func TestGetTCPdataWriteMultipleCoils(t *testing.T) { + // Note: Address may be offset by 1 + // Set InputRegister addresses 0-15 to xAA AA or b1010 1010 1010 1010 or d43690 + // Set InputRegister addresses 16-32 to x55 55 or b0101 0101 0101 0101 or d21845 + t.Log("Testing TCPClient Connections: Write Single Register") + update(&test) + test.FunctionCode = 15 + + test.Address = 0 + test.Quantity = 32 + //BigEndian = two bytes per register + test.Values = []byte{0xAA, 0xAA, 0x55, 0x55} + + test.err = test.getTCPdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if binary.BigEndian.Uint16(test.Results) != test.Quantity { + t.Errorf("Expected value of 32 @ modbus address 10000, but it was %d instead", binary.LittleEndian.Uint16(test.Results)) + } +} + +func TestGetTCPdataWriteMultipleRegisters(t *testing.T) { + // Note: Address may be offset by 1 + // Set InputRegister addresses 0 to xAA AA or b1010 1010 1010 1010 or d43690 + // Set InputRegister addresses 1 to x55 55 or b0101 0101 0101 0101 or d21845 + // Set InputRegister addresses 2 to xFF FF or b1111 1111 1111 1111 or d65535 + t.Log("Testing TCPClient Connections: Write Multiple Register") + update(&test) + test.FunctionCode = 16 + test.Address = 0 + test.Quantity = 3 + //BigEndian = two bytes per register + test.Values = []byte{0xAA, 0xAA, 0x55, 0x55, 0xFF, 0xFF} + + test.err = test.getTCPdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if binary.BigEndian.Uint16(test.Results) != test.Quantity { + t.Errorf("Expected value of 3 @ modbus address 40001, but it was %d instead", binary.LittleEndian.Uint16(test.Results)) + } +} + +func update2(test *Modbus) { + test.Client = "COM3" + test.SlaveAddress = 1 + test.FunctionCode = 0 + test.Address = 0 + test.Quantity = 1 + test.TimeOut = 5 + //serial connection + test.Comm.BaudRate = 9600 + test.Comm.Databits = 8 + test.Comm.Parity = "N" + test.Comm.Stopbits = 1 +} + +// Testing Code for RTU Client +func TestGetRTUdataReadCoils(t *testing.T) { + // Note: Address may be offset by 1 + // Set bit @ address to query + t.Log("Testing RTUClient Connections: Read Coils") + update2(&test) + test.FunctionCode = 1 + + test.err = test.getRTUdata() + + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if test.Results == nil { + t.Errorf("Expected value of 1 @ modbus address 00001, but it was %d instead", test.Results[0]) + } +} + +func TestGetRTUdataReadDiscreteInputs(t *testing.T) { + // Note: Address may be offset by 1 + // Set bits @ address to query + // Results will be in Byte format not binary + t.Log("Testing RTUClient Connections: Read Discrete Inputs") + update2(&test) + test.FunctionCode = 2 + test.Address = 0 + test.Quantity = 3 + + test.err = test.getRTUdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if test.Results == nil { + t.Errorf("Expected value of 1 @ modbus address 10001, but it was %d instead", test.Results[0]) + } +} + +func TestGetRTUdataReadHoldingRegister(t *testing.T) { + // Note: Address may be offset by 1 + // Set HoldingRegister address to xFF FF or b1111 1111 1111 1111 or d65535 + t.Log("Testing RTUClient Connections: Read Holding Registers") + update2(&test) + test.FunctionCode = 3 + + test.err = test.getRTUdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if binary.BigEndian.Uint16(test.Results) != 65535 { + t.Errorf("Expected value of 65535 @ modbus address 40001, but it was %d instead", binary.BigEndian.Uint16(test.Results)) + } +} + +func TestGetRTUdataReadInputRegister(t *testing.T) { + // Note: Address may be offset by 1 + // Set InpuRegister address to xFF FF or b1111 1111 1111 1111 or d65535 + t.Log("Testing RTUClient Connections: Read Input Register") + update2(&test) + test.FunctionCode = 4 + + test.err = test.getRTUdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if binary.BigEndian.Uint16(test.Results) != 65535 { + t.Errorf("Expected value of 65535 @ modbus address 30001, but it was %d instead", binary.BigEndian.Uint16(test.Results)) + } +} + +func TestGetRTUdataWriteSingleCoil(t *testing.T) { + // Note: Address may be offset by 1 + // Set InputCoils address to xFF or b1111 1111 or d255 + t.Log("Testing RTUClient Connections: Write Single Coil") + update2(&test) + test.FunctionCode = 5 + test.Address = 1 + test.Values = []byte{0xff, 0x00} + + test.err = test.getRTUdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if binary.LittleEndian.Uint16(test.Results) != 255 { + t.Errorf("Expected value of 1 @ modbus address 10001, but it was %d instead", binary.LittleEndian.Uint16(test.Results)) + } +} + +func TestGetRTUdataWriteSingleRegister(t *testing.T) { + // Note: Address may be offset by 1 + // Set InputRegister address to xFF or b1111 1111 or d255 + t.Log("Testing RTUClient Connections: Write Single Register") + update2(&test) + test.FunctionCode = 6 + test.Address = 1 + test.Values = []byte{0xff, 0x00} + + test.err = test.getRTUdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if binary.LittleEndian.Uint16(test.Results) != 255 { + t.Errorf("Expected value of 255 @ modbus address 30001, but it was %d instead", binary.LittleEndian.Uint16(test.Results)) + } +} + +func TestGetRTUdataWriteMultipleCoils(t *testing.T) { + // Note: Address may be offset by 1 + // Set InputRegister addresses 0-15 to xAA AA or b1010 1010 1010 1010 or d43690 + // Set InputRegister addresses 16-32 to x55 55 or b0101 0101 0101 0101 or d21845 + t.Log("Testing RTUClient Connections: Write Single Register") + update2(&test) + test.FunctionCode = 15 + + test.Address = 0 + test.Quantity = 32 + //BigEndian = two bytes per register + test.Values = []byte{0xAA, 0xAA, 0x55, 0x55} + + test.err = test.getRTUdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if binary.BigEndian.Uint16(test.Results) != test.Quantity { + t.Errorf("Expected value of 32 @ modbus address 10000, but it was %d instead", binary.LittleEndian.Uint16(test.Results)) + } +} + +func TestGetRTUdataWriteMultipleRegisters(t *testing.T) { + // Note: Address may be offset by 1 + // Set InputRegister addresses 0 to xAA AA or b1010 1010 1010 1010 or d43690 + // Set InputRegister addresses 1 to x55 55 or b0101 0101 0101 0101 or d21845 + // Set InputRegister addresses 2 to xFF FF or b1111 1111 1111 1111 or d65535 + t.Log("Testing RTUClient Connections: Write Multiple Register") + update2(&test) + test.FunctionCode = 16 + test.Address = 0 + test.Quantity = 3 + //BigEndian = two bytes per register + test.Values = []byte{0xAA, 0xAA, 0x55, 0x55, 0xFF, 0xFF} + + test.err = test.getRTUdata() + t.Log(test.Results) + if test.err != nil || test.Results == nil { + t.Fatal(test.err, test.Results) + } + if binary.BigEndian.Uint16(test.Results) != test.Quantity { + t.Errorf("Expected value of 3 @ modbus address 40001, but it was %d instead", binary.LittleEndian.Uint16(test.Results)) + } +}