File Coverage

blib/lib/Device/Modbus/Server.pm
Criterion Covered Total %
statement 126 127 99.2
branch 46 48 95.8
condition 33 38 86.8
subroutine 23 23 100.0
pod 0 10 0.0
total 228 246 92.6


line stmt bran cond sub pod time code
1             package Device::Modbus::Server;
2              
3 4     4   7742 use Device::Modbus;
  4         9  
  4         96  
4 4     4   1572 use Device::Modbus::Request;
  4         5  
  4         111  
5 4     4   1502 use Device::Modbus::Response;
  4         5  
  4         95  
6 4     4   18 use Device::Modbus::Exception;
  4         4  
  4         85  
7 4     4   1070 use Device::Modbus::Unit;
  4         7  
  4         82  
8              
9 4     4   2507 use Try::Tiny;
  4         4239  
  4         188  
10 4     4   19 use Carp;
  4         4  
  4         150  
11 4     4   15 use strict;
  4         4  
  4         60  
12 4     4   13 use warnings;
  4         3  
  4         4962  
13              
14             sub proto {
15             return {
16 5     5 0 566 units => {},
17             log_level => 2,
18             timeout => 5,
19             };
20             }
21              
22             ### Unit management
23              
24             sub units {
25 34     34 0 291 my $self = shift;
26 34         73 return $self->{units};
27             }
28              
29             sub add_server_unit {
30 5     5 0 1390 my ($self, $unit, $id) = @_;
31              
32 5 100 100     58 if (ref $unit && $unit->isa('Device::Modbus::Unit')) {
33 3         15 $unit->init_unit;
34 2         8 $self->units->{$unit->id} = $unit;
35 2         4 return $unit;
36             }
37             else {
38 2         263 croak "Units must be subclasses of Device::Modbus::Unit";
39             }
40             }
41              
42             sub get_server_unit {
43 16     16 0 37 my ($self, $unit_id) = @_;
44 16         18 return $self->units->{$unit_id};
45             }
46              
47             # To be overrided in subclasses
48             sub init_server {
49 1     1 0 110 croak "Server must be initialized\n";
50             }
51              
52              
53             ### Request parsing
54              
55             sub receive_request {
56 37     37 0 23238 my $self = shift;
57 37         64 $self->read_port;
58 37         173 my $adu = $self->new_adu();
59 37         57 $self->parse_header($adu);
60 37         91 $self->parse_pdu($adu);
61 37         61 $self->parse_footer($adu);
62 37         89 return $adu;
63             }
64              
65             sub parse_pdu {
66 37     37 0 35 my ($self, $adu) = @_;
67 37         32 my $request;
68            
69 37         83 my $code = $self->parse_buffer(1,'C');
70              
71 37 100 100     511 if ($code == 0x01 || $code == 0x02 || $code == 0x03 || $code == 0x04) {
    100 100        
    100 100        
    100 100        
    100          
72             # Read coils, discrete inputs, holding registers, input registers
73 16         28 my ($address, $quantity) = $self->parse_buffer(4,'nn');
74              
75 16         104 $request = Device::Modbus::Request->new(
76             code => $code,
77             address => $address,
78             quantity => $quantity
79             );
80             }
81             elsif ($code == 0x05 || $code == 0x06) {
82             # Write single coil and single register
83 5         10 my ($address, $value) = $self->parse_buffer(4, 'nn');
84 5 100 100     44 if ($code == 0x05 && $value != 0xFF00 && $value != 0) {
      100        
85 1         4 $request = Device::Modbus::Exception->new(
86             code => $code + 0x80,
87             exception_code => 3
88             );
89             }
90             else {
91 4         13 $request = Device::Modbus::Request->new(
92             code => $code,
93             address => $address,
94             value => $value
95             );
96             }
97             }
98             elsif ($code == 0x0F) {
99             # Write multiple coils
100 6         11 my ($address, $qty, $bytes) = $self->parse_buffer(5, 'nnC');
101 6 100       67 my $bytes_qty = $qty % 8 ? int($qty/8) + 1 : $qty/8;
102              
103 6 100       13 if ($bytes == $bytes_qty) {
104 5         8 my (@values) = $self->parse_buffer($bytes, 'C*');
105 5         69 @values = Device::Modbus->explode_bit_values(@values);
106              
107 5         135 $request = Device::Modbus::Request->new(
108             code => $code,
109             address => $address,
110             quantity => $qty,
111             bytes => $bytes,
112             values => \@values
113             );
114             }
115             else {
116 1         5 $request = Device::Modbus::Exception->new(
117             code => $code + 0x80,
118             exception_code => 3
119             );
120             }
121             }
122             elsif ($code == 0x10) {
123             # Write multiple registers
124 4         10 my ($address, $qty, $bytes) = $self->parse_buffer(5, 'nnC');
125              
126 4 100       49 if ($bytes == 2 * $qty) {
127 3         6 my (@values) = $self->parse_buffer($bytes, 'n*');
128              
129 3         31 $request = Device::Modbus::Request->new(
130             code => $code,
131             address => $address,
132             quantity => $qty,
133             bytes => $bytes,
134             values => \@values
135             );
136             }
137             else {
138 1         5 $request = Device::Modbus::Exception->new(
139             code => $code + 0x80,
140             exception_code => 3
141             );
142             }
143             }
144             elsif ($code == 0x17) {
145             # Read/Write multiple registers
146 5         12 my ($read_addr, $read_qty, $write_addr, $write_qty, $bytes)
147             = $self->parse_buffer(9, 'nnnnC');
148              
149 5 100       29 if ($bytes == 2 * $write_qty) {
150 4         7 my (@values) = $self->parse_buffer($bytes, 'n*');
151              
152 4         28 $request = Device::Modbus::Request->new(
153             code => $code,
154             read_address => $read_addr,
155             read_quantity => $read_qty,
156             write_address => $write_addr,
157             write_quantity => $write_qty,
158             bytes => $bytes,
159             values => \@values
160             );
161             }
162             else {
163 1         5 $request = Device::Modbus::Exception->new(
164             code => $code + 0x80,
165             exception_code => 3
166             );
167             }
168             }
169             else {
170             # Unimplemented function
171 1         5 $request = Device::Modbus::Exception->new(
172             code => $code + 0x80,
173             exception_code => 1,
174             );
175             }
176              
177 37         200 $adu->message($request);
178 37         35 return $request;
179             }
180              
181             ### Server code
182              
183             # 'Read Coils' => 0x01,
184             # 'Read Discrete Inputs' => 0x02,
185             # 'Read Holding Registers' => 0x03,
186             # 'Read Input Registers' => 0x04,
187             # 'Write Single Coil' => 0x05,
188             # 'Write Single Register' => 0x06,
189             # 'Write Multiple Coils' => 0x0F,
190             # 'Write Multiple Registers' => 0x10,
191             # 'Read/Write Multiple Registers' => 0x17,
192              
193             #my %area_and_mode_for = (
194             my %can_read_zone = (
195             0x01 => ['discrete_coils', 'read' ],
196             0x02 => ['discrete_inputs', 'read' ],
197             0x03 => ['holding_registers', 'read' ],
198             0x04 => ['input_registers', 'read' ],
199             0x17 => ['holding_registers', 'read' ],
200             );
201              
202             my %can_write_zone = (
203             0x05 => ['discrete_coils', 'write' ],
204             0x06 => ['holding_registers', 'write' ],
205             0x0F => ['discrete_coils', 'write' ],
206             0x10 => ['holding_registers', 'write' ],
207             0x17 => ['holding_registers', 'write' ],
208             );
209              
210             sub modbus_server {
211 15     15 0 49 my ($server, $adu) = @_;
212              
213             ### Make sure the requested unit exists in this server
214 15 50       23 unless (exists $server->units->{$adu->unit}) {
215 0         0 return $server->request_for_others($adu);
216             }
217            
218             ### Process write requests first
219 15 100       31 if (exists $can_write_zone{ $adu->code }) {
220 7         8 my ($zone, $mode) = @{$can_write_zone{$adu->code}};
  7         10  
221 7         18 my $resp = $server->process_write_requests($adu, $zone, $mode);
222 7 100       21 return $resp if $resp;
223             }
224            
225             ### Process read requests last
226 9         10 my ($zone, $mode) = @{$can_read_zone{$adu->code}};
  9         19  
227 9         24 my $resp = $server->process_read_requests($adu, $zone, $mode);
228 9         15 return $resp;
229             }
230              
231             sub process_write_requests {
232 7     7 0 9 my ($server, $adu, $zone, $mode) = @_;
233              
234 7         12 my $unit = $server->get_server_unit($adu->unit);
235 7         18 my $code = $adu->code;
236              
237 7   66     13 my $address = $adu->message->{address} // $adu->message->{write_address};
238 7   100     11 my $values = $adu->message->{values} // [ $adu->message->{value} ];
239 7         8 my $quantity = @$values;
240              
241             # Find the requested address within unit's addresses
242 7         33 $server->log(4, "Routing 'write' zone: <$zone> address: <$address> qty: <$quantity>");
243 7         22 my $match = $unit->route($zone, $mode, $address, $quantity);
244 7 100       25 $server->log(4, 'Match was' . (ref $match ? ' ' : ' not ') . 'successful');
245              
246             return Device::Modbus::Exception->new(
247 7 100       28 function => $Device::Modbus::function_for{$code},
248             exception_code => $match,
249             unit => $adu->unit
250             ) unless ref $match;
251              
252              
253             # Execute the requested route with the given parameters
254 4         3 my $response;
255             try {
256 4     4   102 $match->routine->($unit, $server, $adu->message, $address, $quantity, $values);
257             }
258             catch {
259 1     1   14 $server->log(4,
260             "Action failed for 'write' zone: <$zone> address: <$address> quantity: <$quantity> error: $_ ");
261            
262             $response = Device::Modbus::Exception->new(
263 1         3 function => $Device::Modbus::function_for{$code},
264             exception_code => 4,
265             unit => $adu->unit
266             );
267 4         29 };
268 4 100       3088 return $response if defined $response;
269              
270             # Build the response
271             # Write single values
272 3 100 66     23 if ($code == 0x05 || $code == 0x06) {
    100 66        
    50          
273 1         7 $response = Device::Modbus::Response->new(
274             code => $code,
275             address => $address,
276             value => $values->[0]
277             );
278             }
279             # Write multiple values
280             elsif ($code == 0x0F || $code == 0x10) {
281 1         4 $response = Device::Modbus::Response->new(
282             code => $code,
283             address => $address,
284             quantity => $quantity
285             );
286             }
287             elsif ($code == 0x17) {
288             # 0x17 must perform a read operation afterwards
289 1         1 $response = '';
290             }
291              
292 3         6 return $response;
293             }
294              
295             sub process_read_requests {
296 9     9 0 11 my ($server, $adu, $zone, $mode) = @_;
297              
298 9         15 my $unit = $server->get_server_unit($adu->unit);
299 9         17 my $code = $adu->code;
300              
301 9   66     14 my $address = $adu->message->{address} // $adu->message->{write_address};
302 9   66     15 my $quantity = $adu->message->{quantity} // $adu->message->{read_quantity};
303              
304 9         32 $server->log(4, "Routing 'read' zone: <$zone> address: <$address> quantity: <$quantity>");
305 9         36 my $match = $unit->route($zone, 'read', $address, $quantity);
306 9 100       33 $server->log(4,
307             'Match was' . (ref $match ? ' ' : ' not ') . 'successful');
308              
309             return Device::Modbus::Exception->new(
310 9 100       30 function => $Device::Modbus::function_for{$code},
311             exception_code => $match,
312             unit => $adu->unit
313             ) unless ref $match;
314            
315 4         4 my @vals;
316             my $response;
317             try {
318 4     4   73 @vals = $match->routine->($unit, $server, $adu->message, $address, $quantity);
319 3 100       2253 croak 'Quantity of returned values differs from request'
320             unless scalar @vals == $quantity;
321             }
322             catch {
323 2     2   56 $server->log(4,
324             "Action failed for 'read' zone: <$zone> address: <$address> quantity: <$quantity> -- $_");
325            
326             $response = Device::Modbus::Exception->new(
327 2         5 function => $Device::Modbus::function_for{$code},
328             exception_code => 4,
329             unit => $adu->unit
330             );
331 4         21 };
332              
333 4 100       34 unless (defined $response) {
334 2         7 $response = Device::Modbus::Response->new(
335             code => $code,
336             values => \@vals
337             );
338             }
339            
340 4         6 return $response;
341             }
342              
343             1;
344             __END__