/* ** Copyright (C) 2001-2025 Zabbix SIA ** ** This program is free software: you can redistribute it and/or modify it under the terms of ** the GNU Affero General Public License as published by the Free Software Foundation, version 3. ** ** 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 Affero General Public License for more details. ** ** You should have received a copy of the GNU Affero General Public License along with this program. ** If not, see . **/ /* ** 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 ( "encoding/binary" "fmt" "time" named "github.com/BurntSushi/locker" "github.com/goburrow/modbus" mblib "github.com/goburrow/modbus" "golang.zabbix.com/sdk/conf" "golang.zabbix.com/sdk/errs" "golang.zabbix.com/sdk/plugin" ) // 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 { // Sessions stores pre-defined named sets of connections settings. Sessions map[string]*Session `conf:"optional"` } type ( bits8 uint8 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() { err := plugin.RegisterMetrics( &impl, "Modbus", "modbus.get", "Returns a JSON array of the requested values, usage: "+ "modbus.get[endpoint,,,
,,,,].", ) if err != nil { panic(errs.Wrap(err, "failed to register metrics")) } } // 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 := ctx.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.UnmarshalStrict(options, &p.options); err != nil { p.Errf("cannot unmarshal configuration options: %s", err) } } // 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 ) err = conf.UnmarshalStrict(options, &opts) if err != nil { return err } 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 }