File Coverage

blib/lib/Device/Modbus/Server.pm
Criterion Covered Total %
statement 137 138 99.2
branch 31 32 96.8
condition 17 20 85.0
subroutine 24 24 100.0
pod 0 10 0.0
total 209 224 93.3


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