1/* 2** Zabbix 3** Copyright (C) 2001-2021 Zabbix SIA 4** 5** This program is free software; you can redistribute it and/or modify 6** it under the terms of the GNU General Public License as published by 7** the Free Software Foundation; either version 2 of the License, or 8** (at your option) any later version. 9** 10** This program is distributed in the hope that it will be useful, 11** but WITHOUT ANY WARRANTY; without even the implied warranty of 12** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13** GNU General Public License for more details. 14** 15** You should have received a copy of the GNU General Public License 16** along with this program; if not, write to the Free Software 17** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18**/ 19 20/* 21** We use the library go-modbus (goburrow/modbus), which is 22** distributed under the terms of the 3-Clause BSD License 23** available at https://github.com/goburrow/modbus/blob/master/LICENSE 24**/ 25 26package modbus 27 28import ( 29 "fmt" 30 "time" 31 32 "encoding/binary" 33 34 named "github.com/BurntSushi/locker" 35 "github.com/goburrow/modbus" 36 mblib "github.com/goburrow/modbus" 37 "zabbix.com/pkg/conf" 38 "zabbix.com/pkg/plugin" 39) 40 41// Plugin - 42type Plugin struct { 43 plugin.Base 44 options PluginOptions 45} 46 47//Session struct 48type Session struct { 49 // Endpoint is a connection string consisting of a protocol scheme, a host address and a port or seral port name and attributes. 50 Endpoint string `conf:"optional"` 51 52 // SlaveID of modbus devices. 53 SlaveID string `conf:"optional"` 54 55 // Timeout of modbus devices. 56 Timeout int `conf:"optional"` 57} 58 59// PluginOptions - 60type PluginOptions struct { 61 // Timeout is the maximum time for waiting when a request has to be done. Default value equals the global timeout. 62 Timeout int `conf:"optional,range=1:30"` 63 64 // Sessions stores pre-defined named sets of connections settings. 65 Sessions map[string]*Session `conf:"optional"` 66} 67 68type bits8 uint8 69type bits16 uint16 70 71// Set of supported modbus connection types 72const ( 73 RTU bits8 = 1 << iota 74 ASCII 75 TCP 76) 77 78// Serial - structure for storing the Modbus connection parameters 79type Serial struct { 80 PortName string 81 Speed uint32 82 DataBits uint8 83 Parity string 84 StopBit uint8 85} 86 87// Net - structure for storing the Modbus connection parameters 88type Net struct { 89 Address string 90 Port uint32 91} 92 93// Endianness - byte order of received data 94type Endianness struct { 95 order binary.ByteOrder 96 middle bits8 97} 98type mbParams struct { 99 ReqType bits8 100 NetAddr string 101 Serial *Serial 102 SlaveID uint8 103 FuncID uint8 104 MemAddr uint16 105 RetType bits16 106 RetCount uint 107 Count uint16 108 Endianness Endianness 109 Offset uint16 110} 111 112// Set of supported types 113const ( 114 Bit bits16 = 1 << iota 115 Int8 116 Uint8 117 Int16 118 Uint16 119 Int32 120 Uint32 121 Float 122 Uint64 123 Double 124) 125 126// Set of supported byte orders 127const ( 128 Be bits8 = 1 << iota 129 Le 130 Mbe 131 Mle 132) 133 134// Set of supported modbus functions 135const ( 136 ReadCoil = 1 137 ReadDiscrete = 2 138 ReadHolding = 3 139 ReadInput = 4 140) 141 142var impl Plugin 143 144func init() { 145 plugin.RegisterMetrics(&impl, "Modbus", 146 "modbus.get", "Returns a JSON array of the requested values, usage: modbus.get[endpoint,<slave id>,<function>,<address>,<count>,<type>,<endianness>,<offset>].") 147} 148 149// Export - main function of plugin 150func (p *Plugin) Export(key string, params []string, ctx plugin.ContextProvider) (result interface{}, err error) { 151 152 if key != "modbus.get" { 153 return nil, plugin.UnsupportedMetricError 154 } 155 156 if len(params) == 0 || len(params) > 8 { 157 return nil, fmt.Errorf("Invalid number of parameters:%d", len(params)) 158 } 159 160 timeout := p.options.Timeout 161 session, ok := p.options.Sessions[params[0]] 162 if ok { 163 if session.Timeout > 0 { 164 timeout = session.Timeout 165 } 166 167 if len(session.Endpoint) > 0 { 168 params[0] = session.Endpoint 169 } 170 171 if len(session.SlaveID) > 0 { 172 if len(params) == 1 { 173 params = append(params, session.SlaveID) 174 } else if len(params[1]) == 0 { 175 params[1] = session.SlaveID 176 } 177 } 178 } 179 180 var mbparams *mbParams 181 if mbparams, err = parseParams(¶ms); err != nil { 182 return nil, err 183 } 184 185 var rawVal []byte 186 if rawVal, err = modbusRead(mbparams, timeout); err != nil { 187 return nil, err 188 } 189 190 if result, err = pack2Json(rawVal, mbparams); err != nil { 191 return nil, err 192 } 193 194 return result, nil 195} 196 197// Configure implements the Configurator interface. 198// Initializes configuration structures. 199func (p *Plugin) Configure(global *plugin.GlobalOptions, options interface{}) { 200 if err := conf.Unmarshal(options, &p.options); err != nil { 201 p.Errf("cannot unmarshal configuration options: %s", err) 202 } 203 204 if p.options.Timeout == 0 { 205 p.options.Timeout = global.Timeout 206 } 207} 208 209// Validate implements the Configurator interface. 210// Returns an error if validation of a plugin's configuration is failed. 211func (p *Plugin) Validate(options interface{}) error { 212 var ( 213 opts PluginOptions 214 err error 215 ) 216 217 if err = conf.Unmarshal(options, &opts); err != nil { 218 return err 219 } 220 221 if opts.Timeout > 30 || opts.Timeout < 0 { 222 return fmt.Errorf("Unacceptable Timeout value:%d", opts.Timeout) 223 } 224 225 for _, s := range opts.Sessions { 226 if s.Timeout > 30 || s.Timeout < 0 { 227 return fmt.Errorf("Unacceptable session Timeout value:%d", s.Timeout) 228 } 229 230 var p mbParams 231 var err error 232 if p.ReqType, err = getReqType(s.Endpoint); err != nil { 233 return err 234 } 235 236 switch p.ReqType { 237 case RTU, ASCII: 238 if p.Serial, err = getSerial(s.Endpoint); err != nil { 239 return err 240 } 241 case TCP: 242 if p.NetAddr, err = getNetAddr(s.Endpoint); err != nil { 243 return err 244 } 245 default: 246 return fmt.Errorf("Unsupported modbus protocol") 247 } 248 249 if p.SlaveID, err = getSlaveID(&[]string{s.SlaveID}, 0, p.ReqType); err != nil { 250 return err 251 } 252 } 253 254 p.Debugf("Config is valid") 255 256 return nil 257} 258 259// connecting and receiving data from modbus device 260func modbusRead(p *mbParams, timeout int) (results []byte, err error) { 261 handler := newHandler(p, timeout) 262 var lockName string 263 if p.ReqType == TCP { 264 lockName = p.NetAddr 265 } else { 266 lockName = p.Serial.PortName 267 } 268 269 named.Lock(lockName) 270 271 switch p.ReqType { 272 case TCP: 273 err = handler.(*mblib.TCPClientHandler).Connect() 274 defer handler.(*mblib.TCPClientHandler).Close() 275 case RTU: 276 err = handler.(*mblib.RTUClientHandler).Connect() 277 defer handler.(*mblib.RTUClientHandler).Close() 278 case ASCII: 279 err = handler.(*mblib.ASCIIClientHandler).Connect() 280 defer handler.(*mblib.ASCIIClientHandler).Close() 281 } 282 283 if err != nil { 284 named.Unlock(lockName) 285 return nil, fmt.Errorf("Unable to connect: %s", err) 286 } 287 288 client := mblib.NewClient(handler) 289 switch p.FuncID { 290 case ReadCoil: 291 results, err = client.ReadCoils(p.MemAddr, p.Count) 292 case ReadDiscrete: 293 results, err = client.ReadDiscreteInputs(p.MemAddr, p.Count) 294 case ReadHolding: 295 results, err = client.ReadHoldingRegisters(p.MemAddr, p.Count) 296 case ReadInput: 297 results, err = client.ReadInputRegisters(p.MemAddr, p.Count) 298 } 299 300 named.Unlock(lockName) 301 302 if err != nil { 303 return nil, fmt.Errorf("Unable to read: %s", err) 304 } else if len(results) == 0 { 305 return nil, fmt.Errorf("Unable to read data") 306 } 307 308 return results, nil 309} 310 311// make new modbus handler depend on connection type 312func newHandler(p *mbParams, timeout int) (handler mblib.ClientHandler) { 313 switch p.ReqType { 314 case TCP: 315 h := mblib.NewTCPClientHandler(p.NetAddr) 316 h.SlaveId = p.SlaveID 317 h.Timeout = time.Duration(timeout) * time.Second 318 handler = h 319 case RTU: 320 h := modbus.NewRTUClientHandler(p.Serial.PortName) 321 h.BaudRate = int(p.Serial.Speed) 322 h.DataBits = int(p.Serial.DataBits) 323 h.Parity = p.Serial.Parity 324 h.StopBits = int(p.Serial.StopBit) 325 h.SlaveId = p.SlaveID 326 h.Timeout = time.Duration(timeout) * time.Second 327 handler = h 328 case ASCII: 329 h := modbus.NewASCIIClientHandler(p.Serial.PortName) 330 h.BaudRate = int(p.Serial.Speed) 331 h.DataBits = int(p.Serial.DataBits) 332 h.Parity = p.Serial.Parity 333 h.StopBits = int(p.Serial.StopBit) 334 h.SlaveId = p.SlaveID 335 h.Timeout = time.Duration(timeout) * time.Second 336 handler = h 337 } 338 return handler 339} 340