/* ** Zabbix ** Copyright (C) 2001-2023 Zabbix SIA ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 2 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program; if not, write to the Free Software ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. **/ /* ** We use the library go-modbus (goburrow/modbus), which is ** distributed under the terms of the 3-Clause BSD License ** available at https://github.com/goburrow/modbus/blob/master/LICENSE **/ package modbus import ( "fmt" "time" "encoding/binary" "git.zabbix.com/ap/plugin-support/conf" "git.zabbix.com/ap/plugin-support/plugin" named "github.com/BurntSushi/locker" "github.com/goburrow/modbus" mblib "github.com/goburrow/modbus" ) // Plugin - type Plugin struct { plugin.Base options PluginOptions } //Session struct type Session struct { // Endpoint is a connection string consisting of a protocol scheme, a host address and a port or seral port name and attributes. Endpoint string `conf:"optional"` // SlaveID of modbus devices. SlaveID string `conf:"optional"` // Timeout of modbus devices. Timeout int `conf:"optional"` } // PluginOptions - type PluginOptions struct { // Timeout is the maximum time for waiting when a request has to be done. Default value equals the global timeout. Timeout int `conf:"optional,range=1:30"` // Sessions stores pre-defined named sets of connections settings. Sessions map[string]*Session `conf:"optional"` } type bits8 uint8 type bits16 uint16 // Set of supported modbus connection types const ( RTU bits8 = 1 << iota ASCII TCP ) // Serial - structure for storing the Modbus connection parameters type Serial struct { PortName string Speed uint32 DataBits uint8 Parity string StopBit uint8 } // Net - structure for storing the Modbus connection parameters type Net struct { Address string Port uint32 } // Endianness - byte order of received data type Endianness struct { order binary.ByteOrder middle bits8 } type mbParams struct { ReqType bits8 NetAddr string Serial *Serial SlaveID uint8 FuncID uint8 MemAddr uint16 RetType bits16 RetCount uint Count uint16 Endianness Endianness Offset uint16 } // Set of supported types const ( Bit bits16 = 1 << iota Int8 Uint8 Int16 Uint16 Int32 Uint32 Float Uint64 Double ) // Set of supported byte orders const ( Be bits8 = 1 << iota Le Mbe Mle ) // Set of supported modbus functions const ( ReadCoil = 1 ReadDiscrete = 2 ReadHolding = 3 ReadInput = 4 ) var impl Plugin func init() { plugin.RegisterMetrics(&impl, "Modbus", "modbus.get", "Returns a JSON array of the requested values, usage: modbus.get[endpoint,,,
,,,,].") } // Export - main function of plugin func (p *Plugin) Export(key string, params []string, ctx plugin.ContextProvider) (result interface{}, err error) { if key != "modbus.get" { return nil, plugin.UnsupportedMetricError } if len(params) == 0 || len(params) > 8 { return nil, fmt.Errorf("Invalid number of parameters:%d", len(params)) } timeout := p.options.Timeout session, ok := p.options.Sessions[params[0]] if ok { if session.Timeout > 0 { timeout = session.Timeout } if len(session.Endpoint) > 0 { params[0] = session.Endpoint } if len(session.SlaveID) > 0 { if len(params) == 1 { params = append(params, session.SlaveID) } else if len(params[1]) == 0 { params[1] = session.SlaveID } } } var mbparams *mbParams if mbparams, err = parseParams(¶ms); err != nil { return nil, err } var rawVal []byte if rawVal, err = modbusRead(mbparams, timeout); err != nil { return nil, err } if result, err = pack2Json(rawVal, mbparams); err != nil { return nil, err } return result, nil } // Configure implements the Configurator interface. // Initializes configuration structures. func (p *Plugin) Configure(global *plugin.GlobalOptions, options interface{}) { if err := conf.Unmarshal(options, &p.options); err != nil { p.Errf("cannot unmarshal configuration options: %s", err) } if p.options.Timeout == 0 { p.options.Timeout = global.Timeout } } // Validate implements the Configurator interface. // Returns an error if validation of a plugin's configuration is failed. func (p *Plugin) Validate(options interface{}) error { var ( opts PluginOptions err error ) if err = conf.Unmarshal(options, &opts); err != nil { return err } if opts.Timeout > 30 || opts.Timeout < 0 { return fmt.Errorf("Unacceptable Timeout value:%d", opts.Timeout) } for _, s := range opts.Sessions { if s.Timeout > 30 || s.Timeout < 0 { return fmt.Errorf("Unacceptable session Timeout value:%d", s.Timeout) } var p mbParams var err error if p.ReqType, err = getReqType(s.Endpoint); err != nil { return err } switch p.ReqType { case RTU, ASCII: if p.Serial, err = getSerial(s.Endpoint); err != nil { return err } case TCP: if p.NetAddr, err = getNetAddr(s.Endpoint); err != nil { return err } default: return fmt.Errorf("Unsupported modbus protocol") } if p.SlaveID, err = getSlaveID(&[]string{s.SlaveID}, 0, p.ReqType); err != nil { return err } } p.Debugf("Config is valid") return nil } // connecting and receiving data from modbus device func modbusRead(p *mbParams, timeout int) (results []byte, err error) { handler := newHandler(p, timeout) var lockName string if p.ReqType == TCP { lockName = p.NetAddr } else { lockName = p.Serial.PortName } named.Lock(lockName) switch p.ReqType { case TCP: err = handler.(*mblib.TCPClientHandler).Connect() defer handler.(*mblib.TCPClientHandler).Close() case RTU: err = handler.(*mblib.RTUClientHandler).Connect() defer handler.(*mblib.RTUClientHandler).Close() case ASCII: err = handler.(*mblib.ASCIIClientHandler).Connect() defer handler.(*mblib.ASCIIClientHandler).Close() } if err != nil { named.Unlock(lockName) return nil, fmt.Errorf("Unable to connect: %s", err) } client := mblib.NewClient(handler) switch p.FuncID { case ReadCoil: results, err = client.ReadCoils(p.MemAddr, p.Count) case ReadDiscrete: results, err = client.ReadDiscreteInputs(p.MemAddr, p.Count) case ReadHolding: results, err = client.ReadHoldingRegisters(p.MemAddr, p.Count) case ReadInput: results, err = client.ReadInputRegisters(p.MemAddr, p.Count) } named.Unlock(lockName) if err != nil { return nil, fmt.Errorf("Unable to read: %s", err) } else if len(results) == 0 { return nil, fmt.Errorf("Unable to read data") } return results, nil } // make new modbus handler depend on connection type func newHandler(p *mbParams, timeout int) (handler mblib.ClientHandler) { switch p.ReqType { case TCP: h := mblib.NewTCPClientHandler(p.NetAddr) h.SlaveId = p.SlaveID h.Timeout = time.Duration(timeout) * time.Second handler = h case RTU: h := modbus.NewRTUClientHandler(p.Serial.PortName) h.BaudRate = int(p.Serial.Speed) h.DataBits = int(p.Serial.DataBits) h.Parity = p.Serial.Parity h.StopBits = int(p.Serial.StopBit) h.SlaveId = p.SlaveID h.Timeout = time.Duration(timeout) * time.Second handler = h case ASCII: h := modbus.NewASCIIClientHandler(p.Serial.PortName) h.BaudRate = int(p.Serial.Speed) h.DataBits = int(p.Serial.DataBits) h.Parity = p.Serial.Parity h.StopBits = int(p.Serial.StopBit) h.SlaveId = p.SlaveID h.Timeout = time.Duration(timeout) * time.Second handler = h } return handler }