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(&params); 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