rmodbus 1.0.0-java

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,268 @@
1
+ # RModBus - free implementation of ModBus protocol on Ruby.
2
+ #
3
+ # Copyright (C) 2008-2011 Timin Aleksey
4
+ # Copyright (C) 2010 Kelley Reynolds
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+
16
+ module ModBus
17
+ class Slave
18
+ include Errors
19
+ include Common
20
+ # Number of times to retry on read and read timeouts
21
+ attr_accessor :read_retries, :read_retry_timeout, :uid
22
+
23
+ Exceptions = {
24
+ 1 => IllegalFunction.new("The function code received in the query is not an allowable action for the server"),
25
+ 2 => IllegalDataAddress.new("The data address received in the query is not an allowable address for the server"),
26
+ 3 => IllegalDataValue.new("A value contained in the query data field is not an allowable value for server"),
27
+ 4 => SlaveDeviceFailure.new("An unrecoverable error occurred while the server was attempting to perform the requested action"),
28
+ 5 => Acknowledge.new("The server has accepted the request and is processing it, but a long duration of time will be required to do so"),
29
+ 6 => SlaveDeviceBus.new("The server is engaged in processing a long duration program command"),
30
+ 8 => MemoryParityError.new("The extended file area failed to pass a consistency check")
31
+ }
32
+ def initialize(uid, io)
33
+ @uid = uid
34
+ @read_retries = 10
35
+ @read_retry_timeout = 1
36
+ @io = io
37
+ end
38
+
39
+ # Returns a ModBus::ReadWriteProxy hash interface for coils
40
+ #
41
+ # @example
42
+ # coils[addr] => [1]
43
+ # coils[addr1..addr2] => [1, 0, ..]
44
+ # coils[addr] = 0 => [0]
45
+ # coils[addr1..addr2] = [1, 0, ..] => [1, 0, ..]
46
+ #
47
+ # @return [ReadWriteProxy] proxy object
48
+ def coils
49
+ ModBus::ReadWriteProxy.new(self, :coil)
50
+ end
51
+
52
+ # Read coils
53
+ #
54
+ # @example
55
+ # read_coils(addr, ncoils) => [1, 0, ..]
56
+ #
57
+ # @param [Integer] addr address first coil
58
+ # @param [Integer] ncoils number coils
59
+ # @return [Array] coils
60
+ def read_coils(addr, ncoils)
61
+ query("\x1" + addr.to_word + ncoils.to_word).unpack_bits[0..ncoils-1]
62
+ end
63
+ alias_method :read_coil, :read_coils
64
+
65
+ # Write a single coil
66
+ #
67
+ # @example
68
+ # write_single_coil(1, 0) => self
69
+ #
70
+ # @param [Integer] addr address coil
71
+ # @param [Integer] val value coil (0 or other)
72
+ # @return self
73
+ def write_single_coil(addr, val)
74
+ if val == 0
75
+ query("\x5" + addr.to_word + 0.to_word)
76
+ else
77
+ query("\x5" + addr.to_word + 0xff00.to_word)
78
+ end
79
+ self
80
+ end
81
+ alias_method :write_coil, :write_single_coil
82
+
83
+ # Write multiple coils
84
+ #
85
+ # @example
86
+ # write_multiple_coils(1, [0,1,0,1]) => self
87
+ #
88
+ # @param [Integer] addr address first coil
89
+ # @param [Array] vals written coils
90
+ def write_multiple_coils(addr, vals)
91
+ nbyte = ((vals.size-1) >> 3) + 1
92
+ sum = 0
93
+ (vals.size - 1).downto(0) do |i|
94
+ sum = sum << 1
95
+ sum |= 1 if vals[i] > 0
96
+ end
97
+
98
+ s_val = ""
99
+ nbyte.times do
100
+ s_val << (sum & 0xff).chr
101
+ sum >>= 8
102
+ end
103
+
104
+ query("\xf" + addr.to_word + vals.size.to_word + nbyte.chr + s_val)
105
+ self
106
+ end
107
+ alias_method :write_coils, :write_multiple_coils
108
+
109
+ # Returns a ModBus::ReadOnlyProxy hash interface for discrete inputs
110
+ #
111
+ # @example
112
+ # discrete_inputs[addr] => [1]
113
+ # discrete_inputs[addr1..addr2] => [1, 0, ..]
114
+ #
115
+ # @return [ReadOnlyProxy] proxy object
116
+ def discrete_inputs
117
+ ModBus::ReadOnlyProxy.new(self, :discrete_input)
118
+ end
119
+
120
+ # Read discrete inputs
121
+ #
122
+ # @example
123
+ # read_discrete_inputs(addr, ninputs) => [1, 0, ..]
124
+ #
125
+ # @param [Integer] addr address first input
126
+ # @param[Integer] ninputs number inputs
127
+ # @return [Array] inputs
128
+ def read_discrete_inputs(addr, ninputs)
129
+ query("\x2" + addr.to_word + ninputs.to_word).unpack_bits[0..ninputs-1]
130
+ end
131
+ alias_method :read_discrete_input, :read_discrete_inputs
132
+
133
+ # Returns a read/write ModBus::ReadOnlyProxy hash interface for coils
134
+ #
135
+ # @example
136
+ # input_registers[addr] => [1]
137
+ # input_registers[addr1..addr2] => [1, 0, ..]
138
+ #
139
+ # @return [ReadOnlyProxy] proxy object
140
+ def input_registers
141
+ ModBus::ReadOnlyProxy.new(self, :input_register)
142
+ end
143
+
144
+ # Read input registers
145
+ #
146
+ # @example
147
+ # read_input_registers(1, 5) => [1, 0, ..]
148
+ #
149
+ # @param [Integer] addr address first registers
150
+ # @param [Integer] nregs number registers
151
+ # @return [Array] registers
152
+ def read_input_registers(addr, nregs)
153
+ query("\x4" + addr.to_word + nregs.to_word).unpack('n*')
154
+ end
155
+ alias_method :read_input_register, :read_input_registers
156
+
157
+ # Returns a ModBus::ReadWriteProxy hash interface for holding registers
158
+ #
159
+ # @example
160
+ # holding_registers[addr] => [123]
161
+ # holding_registers[addr1..addr2] => [123, 234, ..]
162
+ # holding_registers[addr] = 123 => 123
163
+ # holding_registers[addr1..addr2] = [234, 345, ..] => [234, 345, ..]
164
+ #
165
+ # @return [ReadWriteProxy] proxy object
166
+ def holding_registers
167
+ ModBus::ReadWriteProxy.new(self, :holding_register)
168
+ end
169
+
170
+ # Read holding registers
171
+ #
172
+ # @example
173
+ # read_holding_registers(1, 5) => [1, 0, ..]
174
+ #
175
+ # @param [Integer] addr address first registers
176
+ # @param [Integer] nregs number registers
177
+ # @return [Array] registers
178
+ def read_holding_registers(addr, nregs)
179
+ query("\x3" + addr.to_word + nregs.to_word).unpack('n*')
180
+ end
181
+ alias_method :read_holding_register, :read_holding_registers
182
+
183
+ # Write a single holding register
184
+ #
185
+ # @example
186
+ # write_single_register(1, 0xaa) => self
187
+ #
188
+ # @param [Integer] addr address registers
189
+ # @param [Integer] val written to register
190
+ # @return self
191
+ def write_single_register(addr, val)
192
+ query("\x6" + addr.to_word + val.to_word)
193
+ self
194
+ end
195
+ alias_method :write_holding_register, :write_single_register
196
+
197
+
198
+ # Write multiple holding registers
199
+ #
200
+ # @example
201
+ # write_multiple_registers(1, [0xaa, 0]) => self
202
+ #
203
+ # @param [Integer] addr address first registers
204
+ # @param [Array] val written registers
205
+ # @return self
206
+ def write_multiple_registers(addr, vals)
207
+ s_val = ""
208
+ vals.each do |reg|
209
+ s_val << reg.to_word
210
+ end
211
+
212
+ query("\x10" + addr.to_word + vals.size.to_word + (vals.size * 2).chr + s_val)
213
+ self
214
+ end
215
+ alias_method :write_holding_registers, :write_multiple_registers
216
+
217
+ # Mask a holding register
218
+ #
219
+ # @example
220
+ # mask_write_register(1, 0xAAAA, 0x00FF) => self
221
+ # @param [Integer] addr address registers
222
+ # @param [Integer] and_mask mask for AND operation
223
+ # @param [Integer] or_mask mask for OR operation
224
+ def mask_write_register(addr, and_mask, or_mask)
225
+ query("\x16" + addr.to_word + and_mask.to_word + or_mask.to_word)
226
+ self
227
+ end
228
+
229
+ # Request pdu to slave device
230
+ #
231
+ # @param [String] pdu request to slave
232
+ # @return [String] received data
233
+ #
234
+ # @raise [ModBusTimeout] timed out during read attempt
235
+ # @raise [ModBusException] unknown error
236
+ # @raise [IllegalFunction] function code received in the query is not an allowable action for the server
237
+ # @raise [IllegalDataAddress] data address received in the query is not an allowable address for the server
238
+ # @raise [IllegalDataValue] value contained in the query data field is not an allowable value for server
239
+ # @raise [SlaveDeviceFailure] unrecoverable error occurred while the server was attempting to perform the requested action
240
+ # @raise [Acknowledge] server has accepted the request and is processing it, but a long duration of time will be required to do so
241
+ # @raise [SlaveDeviceBus] server is engaged in processing a long duration program command
242
+ # @raise [MemoryParityError] extended file area failed to pass a consistency check
243
+ def query(pdu)
244
+ tried = 0
245
+ begin
246
+ timeout(@read_retry_timeout, ModBusTimeout) do
247
+ send_pdu(pdu)
248
+ pdu = read_pdu
249
+ end
250
+ rescue ModBusTimeout => err
251
+ log "Timeout of read operation: (#{@read_retries - tried})"
252
+ tried += 1
253
+ retry unless tried >= @read_retries
254
+ raise ModBusTimeout.new, "Timed out during read attempt"
255
+ end
256
+
257
+ return nil if pdu.size == 0
258
+
259
+ if pdu.getbyte(0) >= 0x80
260
+ exc_id = pdu.getbyte(1)
261
+ raise Exceptions[exc_id] unless Exceptions[exc_id].nil?
262
+
263
+ raise ModBusException.new, "Unknown error"
264
+ end
265
+ pdu[2..-1]
266
+ end
267
+ end
268
+ end
data/lib/rmodbus/sp.rb ADDED
@@ -0,0 +1,45 @@
1
+ # RModBus - free implementation of ModBus protocol in Ruby.
2
+ #
3
+ # Copyright (C) 2011 Timin Aleksey
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 3 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
+ require 'serialport'
16
+
17
+ module ModBus
18
+ module SP
19
+ attr_reader :port, :baud, :data_bits, :stop_bits, :parity, :read_timeout
20
+ # Open serial port
21
+ # @param [String] port name serial ports ("/dev/ttyS0" POSIX, "com1" - Windows)
22
+ # @param [Integer] baud rate serial port (default 9600)
23
+ # @param [Hash] opts the options of serial port
24
+ #
25
+ # @option opts [Integer] :data_bits from 5 to 8
26
+ # @option opts [Integer] :stop_bits 1 or 2
27
+ # @option opts [Integer] :parity NONE, EVEN or ODD
28
+ # @option opts [Integer] :read_timeout default 100 ms
29
+ # @return [SerialPort] io serial port
30
+ def open_serial_port(port, baud, opts = {})
31
+ @port, @baud = port, baud
32
+
33
+ @data_bits, @stop_bits, @parity, @read_timeout = 8, 1, SerialPort::NONE, 100
34
+
35
+ @data_bits = opts[:data_bits] unless opts[:data_bits].nil?
36
+ @stop_bits = opts[:stop_bits] unless opts[:stop_bits].nil?
37
+ @parity = opts[:parity] unless opts[:parity].nil?
38
+ @read_timeout = options[:read_timeout] unless opts[:read_timeout].nil?
39
+
40
+ io = SerialPort.new(@port, @baud, @data_bits, @stop_bits, @parity)
41
+ io.read_timeout = @read_timeout
42
+ io
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,49 @@
1
+ # RModBus - free implementation of ModBus protocol on Ruby.
2
+ #
3
+ # Copyright (C) 2011 Timin Aleksey
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 3 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
+ require 'socket'
15
+ require 'timeout'
16
+
17
+ module ModBus
18
+ module TCP
19
+ include Timeout
20
+ attr_reader :ipaddr, :port
21
+
22
+ private
23
+ # Open TCP socket
24
+ #
25
+ # @param [String] ipaddr IP address of remote server
26
+ # @param [Integer] port connection port
27
+ # @param [Hash] opts options of connection
28
+ # @option opts [Float, Integer] :connect_timeout seconds timeout for open socket
29
+ # @return [TCPSocket] socket
30
+ #
31
+ # @raise [ModBusTimeout] timed out attempting to create connection
32
+ def open_tcp_connection(ipaddr, port, opts = {})
33
+ @ipaddr, @port = ipaddr, port
34
+
35
+ opts[:connect_timeout] ||= 1
36
+
37
+ io = nil
38
+ begin
39
+ timeout(opts[:connect_timeout], ModBusTimeout) do
40
+ io = TCPSocket.new(@ipaddr, @port)
41
+ end
42
+ rescue ModBusTimeout => err
43
+ raise ModBusTimeout.new, 'Timed out attempting to create connection'
44
+ end
45
+
46
+ io
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ # RModBus - free implementation of ModBus protocol on Ruby.
2
+ #
3
+ # Copyright (C) 2008-2011 Timin Aleksey
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 3 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
+ module ModBus
15
+ # TCP client implementation
16
+ # @example
17
+ # TCPClient.connect('127.0.0.1', 502) do |cl|
18
+ # cl.with_slave(uid) do |slave|
19
+ # slave.holding_registers[0..100]
20
+ # end
21
+ # end
22
+ #
23
+ # @see TCPClient#open_connection
24
+ # @see Client#initialize
25
+ class TCPClient < Client
26
+ include TCP
27
+
28
+ protected
29
+ # Open TCP\IP connection
30
+ # @see TCP::open_connection
31
+ def open_connection(ipaddr, port = 502, opts = {})
32
+ open_tcp_connection(ipaddr, port, opts)
33
+ end
34
+
35
+ def get_slave(uid, io)
36
+ TCPSlave.new(uid, io)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,61 @@
1
+ # RModBus - free implementation of ModBus protocol on Ruby.
2
+ #
3
+ # Copyright (C) 2008 Timin Aleksey
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 3 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
+ require 'gserver'
15
+
16
+ module ModBus
17
+ # TCP server implementation
18
+ # @example
19
+ # srv = TCPServer.new(10002, 1)
20
+ # srv.coils = [1,0,1,1]
21
+ # srv.discrete_inputs = [1,1,0,0]
22
+ # srv.holding_registers = [1,2,3,4]
23
+ # srv.input_registers = [1,2,3,4]
24
+ # srv.debug = true
25
+ # srv.start
26
+ class TCPServer < GServer
27
+ include Common
28
+ include Server
29
+
30
+ # Init server
31
+ # @param [Integer] port listen port
32
+ # @param [Integer] uid slave device
33
+ def initialize(port = 502, uid = 1)
34
+ @uid = uid
35
+ super(port)
36
+ end
37
+
38
+ # Serve requests
39
+ # @param [TCPSocket] io socket
40
+ def serve(io)
41
+ loop do
42
+ req = io.read(7)
43
+ if req[2,2] != "\x00\x00" or req.getbyte(6) != @uid
44
+ io.close
45
+ break
46
+ end
47
+
48
+ tr = req[0,2]
49
+ len = req[4,2].unpack('n')[0]
50
+ req = io.read(len - 1)
51
+ log "Server RX (#{req.size} bytes): #{logging_bytes(req)}"
52
+
53
+ pdu = exec_req(req, @coils, @discrete_inputs, @holding_registers, @input_registers)
54
+
55
+ resp = tr + "\0\0" + (pdu.size + 1).to_word + @uid.chr + pdu
56
+ log "Server TX (#{resp.size} bytes): #{logging_bytes(resp)}"
57
+ io.write resp
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,64 @@
1
+ # RModBus - free implementation of ModBus protocol on Ruby.
2
+ #
3
+ # Copyright (C) 2011 Timin Aleksey
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 3 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
+ module ModBus
15
+ # TCP slave implementation
16
+ # @example
17
+ # TCP.connect('127.0.0.1', 10002) do |cl|
18
+ # cl.with_slave(uid) do |slave|
19
+ # slave.holding_registers[0..100]
20
+ # end
21
+ # end
22
+ #
23
+ # @see RTUViaTCPClient#open_connection
24
+ # @see Client#with_slave
25
+ # @see Slave
26
+ class TCPSlave < Slave
27
+ attr_reader :transaction
28
+
29
+ # @see Slave::initialize
30
+ def initialize(uid, io)
31
+ @transaction = 0
32
+ super(uid, io)
33
+ end
34
+
35
+ private
36
+ # overide method for RTU over TCP implamentaion
37
+ # @see Slave#query
38
+ def send_pdu(pdu)
39
+ @transaction = 0 if @transaction.next > 65535
40
+ @transaction += 1
41
+ msg = @transaction.to_word + "\0\0" + (pdu.size + 1).to_word + @uid.chr + pdu
42
+ @io.write msg
43
+
44
+ log "Tx (#{msg.size} bytes): " + logging_bytes(msg)
45
+ end
46
+
47
+ # overide method for RTU over TCP implamentaion
48
+ # @see Slave#query
49
+ def read_pdu
50
+ header = @io.read(7)
51
+ if header
52
+ tin = header[0,2].unpack('n')[0]
53
+ raise Errors::ModBusException.new("Transaction number mismatch") unless tin == @transaction
54
+ len = header[4,2].unpack('n')[0]
55
+ msg = @io.read(len-1)
56
+
57
+ log "Rx (#{(header + msg).size} bytes): " + logging_bytes(header + msg)
58
+ msg
59
+ else
60
+ raise Errors::ModBusException.new("Server did not respond")
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,17 @@
1
+ # RModBus - free implementation of ModBus protocol on Ruby.
2
+ #
3
+ # Copyright (C) 2011 Timin Aleksey
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 3 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
+ module ModBus
15
+ # Package version
16
+ VERSION = '1.0.0'
17
+ end
data/lib/rmodbus.rb ADDED
@@ -0,0 +1,35 @@
1
+ # RModBus - free implementation of ModBus protocol on Ruby.
2
+ # Copyright (C) 2008 - 2011 Timin Aleksey
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ require 'rmodbus/errors'
13
+ require 'rmodbus/ext'
14
+ require 'rmodbus/common'
15
+ require 'rmodbus/rtu'
16
+ require 'rmodbus/tcp'
17
+ require 'rmodbus/slave'
18
+ require 'rmodbus/client'
19
+ require 'rmodbus/server'
20
+ require 'rmodbus/tcp_slave'
21
+ require 'rmodbus/tcp_client'
22
+ require 'rmodbus/tcp_server'
23
+
24
+ # jruby not support serial RTU protocol yet
25
+ unless RUBY_PLATFORM == "java"
26
+ require 'rmodbus/sp'
27
+ require 'rmodbus/rtu_slave'
28
+ require 'rmodbus/rtu_client'
29
+ require 'rmodbus/rtu_server'
30
+ end
31
+
32
+ require 'rmodbus/rtu_via_tcp_slave'
33
+ require 'rmodbus/rtu_via_tcp_client'
34
+ require 'rmodbus/rtu_via_tcp_server'
35
+ require 'rmodbus/proxy'
@@ -0,0 +1,31 @@
1
+ require 'rmodbus'
2
+ include ModBus
3
+
4
+ describe Client do
5
+ before do
6
+ @cl = Client.new
7
+ end
8
+
9
+ it "should give object provider for slave" do
10
+ slave = @cl.with_slave(1)
11
+ slave.uid.should eq(1)
12
+ end
13
+
14
+ it "should give object provider for slave in block" do
15
+ @cl.with_slave(1) do |slave|
16
+ slave.uid.should eq(1)
17
+ end
18
+ end
19
+
20
+ it "should connect with TCP server" do
21
+ Client.connect do |cl|
22
+ cl.should be_instance_of(Client)
23
+ end
24
+ end
25
+
26
+ it ":new alias :connect" do
27
+ Client.new do |cl|
28
+ cl.should be_instance_of(Client)
29
+ end
30
+ end
31
+ end