File Coverage

blib/lib/POCSAG/PISS.pm
Criterion Covered Total %
statement 12 138 8.7
branch 0 32 0.0
condition n/a
subroutine 4 19 21.0
pod 8 8 100.0
total 24 197 12.1


line stmt bran cond sub pod time code
1              
2             package POCSAG::PISS;
3              
4             =head1 NAME
5              
6             POCSAG::PISS - A perl module for accessing the PISS modem
7              
8             =head1 ABSTRACT
9              
10             PISS is a simple protocol to talk to a synchronous POCSAG bit-banger
11             module. At concept level, much like KISS (Keep It Simple Stupid), but
12             for POCSAG instead of AX.25.
13              
14             =head1 DESCRIPTION
15              
16             Unless a debugging mode is enabled, all errors and warnings are reported
17             through the API (as opposed to printing on STDERR or STDOUT), so that
18             they can be reported nicely on the user interface of an application.
19              
20             =head1 OBJECT INTERFACE
21              
22             =cut
23              
24 1     1   1014 use strict;
  1         2  
  1         43  
25 1     1   6 use warnings;
  1         2  
  1         34  
26              
27 1     1   1420 use Device::SerialPort;
  1         40469  
  1         93  
28              
29 1     1   15 use Data::Dumper;
  1         2  
  1         1965  
30              
31             our $VERSION = '1.00';
32              
33             #
34             # Configuration
35             #
36              
37             =over
38              
39             =item new(config)
40              
41             Returns a new instance of the PISS modem driver. Usage:
42              
43             my $modem = new POCSAG::PISS(
44             'serial' => '/dev/ttyUSB0',
45             'serial_speed' => 9600,
46             'max_tx_len' => 1000,
47             );
48              
49             =back
50              
51             =cut
52              
53             sub new
54             {
55 0     0 1   my $class = shift;
56 0           my $self = bless { @_ }, $class;
57            
58 0           $self->{'initialized'} = 0;
59 0           $self->{'name'} = 'POCSAG::PISS';
60 0           $self->{'version'} = '1.0';
61            
62             # store config
63 0           my %h = @_;
64 0           $self->{'config'} = \%h;
65             #print "settings: " . Dumper(\%h);
66            
67 0           $self->{'debug'} = ( $self->{'config'}->{'debug'} );
68            
69 0           $self->_debug('initializing');
70            
71 0           $self->_clear_errors();
72            
73 0           $self->{'piss_seq'} = 0;
74 0           $self->{'max_tx_len'} = $self->{'config'}->{'max_tx_len'};
75            
76             # validate settings
77 0           foreach my $k ('serial', 'serial_speed') {
78 0 0         if (!defined $h{$k}) {
79 0           return $self->_critical("Mandatory config setting '$k' not set!");
80             }
81             }
82            
83 0           return $self;
84             }
85              
86             # report a critical error
87              
88             sub _critical($$)
89             {
90 0     0     my($self, $msg) = @_;
91            
92 0           warn $self->{'name'} . " - " . $msg . "\n";
93            
94 0           $self->{'last_err_code'} = 'CRITICAL';
95 0           $self->{'last_err_msg'} = $msg;
96            
97 0           return;
98             }
99              
100             # report an error
101              
102             sub _error($$$)
103             {
104 0     0     my($self, $code, $msg) = @_;
105            
106 0 0         if ($self->{'debug'}) {
107 0           warn $self->{'name'} . " ERROR $code: $msg\n";
108             }
109            
110 0           $self->{'last_err_code'} = $code;
111 0           $self->{'last_err_msg'} = $msg;
112            
113 0           return 0;
114             }
115              
116             # fetch errors
117              
118             =over
119              
120             =item get_error($modem)
121              
122             Returns the error code and error message string for the last
123             error experienced.
124              
125             my($code, $message) = $modem->get_error();
126              
127             =back
128              
129             =cut
130              
131             sub get_error($)
132             {
133 0     0 1   my($self) = @_;
134            
135 0           return ($self->{'last_err_code'}, $self->{'last_err_msg'});
136             }
137              
138             =over
139              
140             =item error_msg($modem)
141              
142             Gets just the error message string for the last
143             error experienced. Good for
144              
145             $modem->open() || die "Failed to open modem: " . $modem->error_msg();
146              
147             =back
148              
149             =cut
150              
151              
152             sub error_msg($)
153             {
154 0     0 1   my($self) = @_;
155            
156 0           return $self->{'last_err_msg'};
157             }
158              
159             # clear the error flags
160              
161             sub _clear_errors($)
162             {
163 0     0     my($self) = @_;
164            
165 0           $self->{'last_err_code'} = 'ok';
166 0           $self->{'last_err_msg'} = 'no error reported';
167             }
168              
169             # report a debug log
170              
171             sub _debug($$)
172             {
173 0     0     my($self, $msg) = @_;
174            
175 0 0         return if (!$self->{'debug'});
176            
177 0           warn $self->{'name'} . " DEBUG $msg\n";
178             }
179              
180             #
181             #### Serial port functions
182             #
183              
184             sub _serial_readflush($)
185             {
186 0     0     my($self) = @_;
187            
188 0           $self->_debug("serial_readflush start");
189            
190 0           while (1) {
191 0           my $s = $self->{'port'}->read(100);
192 0           $self->_debug("read: $s");
193 0 0         last if ($s eq '');
194             }
195            
196 0           $self->_debug("complete!");
197             }
198              
199             =over
200              
201             =item open()
202              
203             Opens the serial device after locking it using a lock file in /var/lock,
204             sets serial port parameters, and flushes the input buffer by reading
205             whatever the modem has transmitted to us since we last read from the port.
206              
207             The flushing part does take a couple of seconds, so be patient.
208              
209             =back
210              
211             =cut
212              
213             sub open($)
214             {
215 0     0 1   my($self) = @_;
216            
217 0           $self->_debug("opening serial");
218 0           my $lockfile = $self->{'config'}->{'serial'};
219 0           $lockfile =~ s/^.*\///;
220 0           $lockfile = "/var/lock/LCK..$lockfile";
221 0           my $port = new Device::SerialPort($self->{'config'}->{'serial'}, 0, $lockfile);
222 0 0         if (!$port) {
223 0           $self->_critical("Can't open serial port " . $self->{'config'}->{'serial'} . ": $!");
224 0           return;
225             }
226            
227 0           $self->{'port'} = $port;
228            
229 0           $port->databits(8);
230 0           $port->baudrate($self->{'config'}->{'serial_speed'});
231 0           $port->parity("none");
232 0           $port->stopbits(1);
233 0           $port->handshake("none");
234            
235 0           $port->read_char_time(0);
236 0           $port->read_const_time(5000);
237            
238 0 0         if (!$port->write_settings) {
239 0           $self->_critical("Can't write serial settings: $!");
240 0           $self->close();
241 0           return;
242             }
243            
244 0           $self->_serial_readflush();
245            
246 0           return 1;
247             }
248              
249             =over
250              
251             =item close()
252              
253             Closes the serial device.
254              
255             =back
256              
257             =cut
258              
259             sub close($)
260             {
261 0     0 1   my($self) = @_;
262            
263 0           $self->_debug("closing serial");
264 0 0         $self->{'port'}->close || $self->_error("serial_err", "serial close failed: $!");
265 0           undef $self->{'port'};
266             }
267              
268             =over
269              
270             =item keepalive()
271              
272             Reopens the serial device, if needed, if it has been closed due to a error for example.
273              
274             =back
275              
276             =cut
277              
278             sub keepalive($)
279             {
280 0     0 1   my($self) = @_;
281            
282             #$self->_debug("serial keepalive...");
283            
284 0 0         if (!$self->{'port'}) {
285 0           $self->open();
286             }
287             }
288              
289             sub _serial_write($$)
290             {
291 0     0     my($self, $cmd) = @_;
292            
293 0           my $len = length($cmd);
294 0           my $wrote = $self->{'port'}->write($cmd);
295 0 0         if (!$wrote) {
296 0           $self->_error("serial_err", "Failed to write to serial port: $!");
297 0           return;
298             }
299            
300 0 0         if ($wrote != $len) {
301 0           $self->_error("serial_err", "Write to serial port incomplete: wrote $wrote of $len");
302 0           return;
303             }
304            
305 0           return 1;
306             }
307              
308              
309              
310             #
311             #### Actual PISS protocol commands
312             #
313              
314             sub _piss_cmd($)
315             {
316 0     0     my($self, $cmd) = @_;
317            
318 0           $self->_serial_write($cmd);
319            
320 0           $self->_debug("piss_cmd wrote cmd, reading...\n");
321 0           my $timeout = 60;
322 0           my $start_t = time();
323            
324 0           my $rbuf = '';
325 0           while (1) {
326 0           my $c = $self->{'port'}->read(1);
327 0 0         if (!defined $c) {
328 0           $self->_debug("piss_cmd read returned undefined");
329             } else {
330 0           $rbuf .= $c;
331            
332 0           while ($rbuf =~ s/(FAULT.*?)[\r\n]//s) {
333 0           $self->_error("piss_fault", "PISS FAULT REPORTED: $1");
334             }
335            
336 0           while ($rbuf =~ s/R\s+(.)\s+(\d+)\s+(\d+)[\r\n]//s) {
337 0           $self->_debug("R id $1 len $2 maxlen $3");
338 0           $self->{'max_tx_len'} = $3;
339             }
340            
341 0           while ($rbuf =~ s/OK\s+(.)[\r\n]//s) {
342 0           $self->_debug("Transmitted ok: $1");
343             }
344            
345 0           while ($rbuf =~ s/ER\s+(.)\s+(.*?)[\r\n]//s) {
346 0           $self->_error("piss_err", "PISS ERROR id $1: $2");
347             }
348             }
349            
350 0 0         last if ($rbuf =~ /\.[\n\r]+/s);
351 0 0         if (time() - $start_t >= $timeout) {
352 0           $self->_error("piss_tout", "piss_cmd timed out at $timeout s");
353 0           return 0;
354             }
355             }
356            
357 0           $self->_debug("piss_cmd read: $rbuf");
358            
359 0           return 1;
360             }
361              
362             =over
363              
364             =item max_tx_len()
365              
366             Returns the maximum length of a transmit buffer the modem is willing to take.
367             Depends on the available memory on the modem, and it's internal data set size.
368             Whatever this function returns, should be passed to POCSAG::Encode.
369              
370             =back
371              
372             =cut
373              
374             sub max_tx_len($)
375             {
376 0     0 1   my($self) = @_;
377            
378 0           return $self->{'max_tx_len'};
379             }
380              
381             =over
382              
383             =item brraaap($encoded)
384              
385             Transmits an encoded message, as returned by POCSAG::Encode::generate().
386              
387             =back
388              
389             =cut
390              
391             sub brraaap($$)
392             {
393 0     0 1   my($self, $encoded) = @_;
394            
395 0           $self->{'piss_seq'}++;
396 0 0         $self->{'piss_seq'} = 0 if ($self->{'piss_seq'} == 26);
397            
398 0           my $seqid = chr($self->{'piss_seq'} + 97);
399            
400 0 0         if (length($encoded) > $self->{'max_tx_len'}) {
401 0           $self->_error("piss_toolong", "piss_send_msg: Too long message: " . length($encoded) . " is larger than maximum of " . $self->{'max_tx_len'});
402 0           return;
403             }
404            
405 0           my $cmd = "T" . $seqid . "1" . unpack('H*', $encoded) . "X";
406 0           $self->_debug("piss_send_msg $cmd, length " . length($encoded));
407 0 0         if (!$self->_piss_cmd($cmd)) {
408 0           $self->_debug("piss_send_msg: piss_cmd failed: " . $self->error_msg());
409 0           return;
410             }
411            
412 0           $self->_debug("piss_send_msg done");
413            
414 0           return 1;
415             }
416              
417             =over
418              
419             =item close()
420              
421             Closes the modem device. Can be reopened with open().
422              
423             =back
424              
425             =cut