File Coverage

blib/lib/POCSAG/Encode.pm
Criterion Covered Total %
statement 9 184 4.8
branch 0 42 0.0
condition 0 6 0.0
subroutine 3 17 17.6
pod 2 2 100.0
total 14 251 5.5


line stmt bran cond sub pod time code
1              
2             package POCSAG::Encode;
3              
4             =head1 NAME
5              
6             POCSAG::Encode - A perl module for encoding messages in the POCSAG binary protocol.
7              
8             =head1 ABSTRACT
9              
10             This module encodes text messages in the POCSAG protocol. It returns
11             a binary string which can be fed synchronously to an FSK transmitter
12             at 512 bit/s per second.
13              
14             =head1 DESCRIPTION
15              
16             The module's generate function generates a single complete binary
17             POCSAG transmission, which consists of:
18              
19             =over
20              
21             =item *
22              
23             A preamble consisting of Synchronisation Codewords (CWs)
24              
25             =item *
26              
27             One or more messages consisting of an Address Codeword and one or more
28             Message Codewords
29              
30             =item *
31              
32             Synchronisation Codewords at regular intervals between the codeword batches,
33             to keep the receivers in sync with the ongoing transmission
34              
35             =item *
36              
37             Idle Codewords before, between and after the messages, for mandatory padding to
38             correctly place the Address Codewords in the correct frame boundaries.
39              
40             =back
41              
42             Because the preamble is long, it makes sense to send a large amount of messages in
43             a batch to minimize transmitter key-down time and save RF channel time. Also, having
44             a larger number of messages to transmit in a single transmission
45             makes it possible to reduce the amount of Idle Codewords
46             in the transmission by optimizing the order in which the messages are sent.
47              
48             This module currently has a very simple optimizer, which does not do a deeper search
49             for the most optimal transmit order, but instead only considers the next message to
50             be transmitted based on the minimum amount of Idle Codewords needed before the
51             address frame for the next message can be sent.
52              
53             Unless a debugging mode is enabled, all errors and warnings are reported
54             through the API (as opposed to printing on STDERR or STDOUT), so that
55             they can be reported nicely on the user interface of an application.
56              
57             =head1 FUNCTIONS
58              
59             =cut
60              
61 1     1   9379 use strict;
  1         2  
  1         54  
62 1     1   7 use warnings;
  1         1  
  1         45  
63              
64 1     1   1386 use Data::Dumper;
  1         50351  
  1         2317  
65              
66             our $VERSION = '1.00';
67              
68             #
69             # Configuration
70             #
71              
72             my $debug = 0;
73              
74             #
75             # Constants
76             #
77              
78             # The POCSAG transmission starts with 576 bit reversals (101010...).
79             # That's 576/8 == 72 bytes of 0xAA.
80             my $POCSAG_PREAMBLE = pack('H*', 'AA') x (576/8);
81              
82             # The Frame Synchronisation (FS) code is 32 bits:
83             # 01111100 11010010 00010101 11011000
84             my $POCSAG_FS_CW = pack('H*', '7CD215D8');
85              
86             # The Idle Codeword:
87             # 01111010 10001001 11000001 10010111
88             my $POCSAG_IDLE_CW = pack('H*', '7A89C197');
89              
90             #_debug("preamble is: " . hex_dump($POCSAG_PREAMBLE));
91             #_debug("preamble length is " . length($POCSAG_PREAMBLE) . " bytes");
92             #_debug("POCSAG_FS_CW is " . hex_dump($POCSAG_FS_CW));
93             #_debug("POCSAG_IDLE_CW is " . hex_dump($POCSAG_IDLE_CW));
94              
95             #_debug("integer in hex, network byte order: " . hex_dump(pack('N', 152151251)));
96              
97             #
98             # Converts a binary string to a hex dump - slow but good for debug logging
99             #
100              
101             sub _hex_dump($)
102             {
103 0     0     my($s) = @_;
104            
105 0           my $out = '';
106            
107 0           my $l = length($s);
108            
109 0           my $bytes_in_a_chunk = 4;
110 0           my $bytes_in_a_row = $bytes_in_a_chunk * 8;
111            
112             # this is a bit slow, but it's only used for debugging
113 0           for (my $i = 0; $i < $l; $i += 1) {
114 0 0 0       if ($i % $bytes_in_a_row == 0 && $i != 0) {
    0 0        
115 0           $out .= "\n";
116             } elsif ($i % $bytes_in_a_chunk == 0 && $i != 0) {
117 0           $out .= ' ';
118             }
119 0           $out .= sprintf('%02x', ord(substr($s, $i, 1)));
120             }
121 0           $out .= "\n";
122            
123 0           return $out;
124             }
125              
126             #
127             # Returns an integer as a hex string
128             #
129              
130             sub _hex_int($)
131             {
132 0     0     my($i) = @_;
133            
134 0           return unpack('H*', pack('N', $i));
135             }
136              
137             #
138             # Debug logging warn
139             #
140              
141             sub _debug($)
142             {
143 0 0   0     return if (!$debug);
144            
145 0           warn "Pocsag::Encode DEBUG: @_\n";
146             }
147              
148             #
149             # Calculate binary checksum and parity for a codeword
150             #
151              
152             sub _calculate_bch_and_parity($)
153             {
154 0     0     my($cw) = @_;
155            
156             # make sure the 11 LSB are 0.
157 0           $cw &= 0xFFFFF800;
158            
159 0           my $local_cw = 0;
160 0           my $parity = 0;
161            
162             # calculate bch
163 0           $local_cw = $cw;
164 0           for (my $bit = 1; $bit <= 21; $bit++) {
165 0 0         $cw ^= 0xED200000 if ($cw & 0x80000000);
166 0           $cw = $cw << 1;
167             }
168 0           $local_cw |= ($cw >> 21);
169             # at this point $local_cw has codeword with bch
170            
171             # calculate parity
172 0           $cw = $local_cw;
173 0           for (my $bit = 1; $bit <= 32; $bit++) {
174 0 0         $parity++ if ($cw & 0x80000000);
175 0           $cw = $cw << 1;
176             }
177            
178             # turn last bit to 1 depending on parity
179 0 0         my $cw_with_parity = ($parity % 2) ? $local_cw + 1 : $local_cw;
180            
181 0           _debug(" bch_and_parity returning " . _hex_int($cw_with_parity));
182 0           return $cw_with_parity;
183             }
184              
185             #
186             # Given the numeric destination address and function, generate an address codeword.
187             #
188              
189             sub _address_codeword($$)
190             {
191 0     0     my($in_addr, $function) = @_;
192            
193             # POCSAG recommendation 1.3.2
194             # The three least significant bits are not transmitted but
195             # serve to define the frame in which the address codeword
196             # must be transmitted.
197             # So we take them away.
198 0           my $addr_frame_bits = $in_addr & 0x3;
199            
200             # shift address to right by two bits to remove the least significant
201             # bits
202 0           my $addr = $in_addr >> 3;
203            
204             # truncate address to 18 bits
205 0           $addr &= 0x3FFFF;
206            
207             # truncate function to 2 bits
208 0           $function &= 0x3;
209            
210             # codeword without parity
211 0           my $codeword = ($addr << 13) | ($function << 11);
212            
213 0           _debug(" generated address codeword for $in_addr function $function: " . _hex_int($codeword));
214            
215 0           return _calculate_bch_and_parity($codeword);
216             }
217              
218             #
219             # Append a message content codeword to the message, calculating bch+parity for it
220             #
221              
222             sub _append_message_codeword($$)
223             {
224 0     0     my($posref, $word) = @_;
225            
226 0           $$posref++;
227            
228 0           return pack('N', _calculate_bch_and_parity($word | (1 << 31)));
229             }
230              
231             #
232             # Reverse the bits in a byte. Used to encode characters in a text message,
233             # since the opposite order is used when transmitting POCSAG text.
234             #
235              
236             sub _reverse_bits($)
237             {
238 0     0     my($in) = @_;
239            
240 0           my $out = 0;
241            
242 0           for (my $i = 0; $i < 7; $i++) {
243 0           $out |= (($in >> $i) & 1) << 6-$i;
244             }
245            
246 0           return $out;
247             }
248              
249             #
250             # Append text message content to the transmission blob.
251             #
252              
253             sub _append_content_text($)
254             {
255 0     0     my($content) = @_;
256            
257 0           my $out = '';
258 0           _debug("append_content_text: $content");
259            
260 0           my $l = length($content);
261 0           my $bitpos = 0;
262 0           my $word = 0;
263 0           my $leftbits = 0;
264 0           my $leftval = 0;
265 0           my $pos = 0;
266            
267             # walk through characters in message
268 0           for (my $i = 0; $i < $l; $i++) {
269             # make sure it's 7 bits
270 0           my $char = ord(substr($content, $i, 1)) & 0x7f;
271            
272 0           _debug(" char $i: $char");
273            
274 0           $char = _reverse_bits($char);
275            
276             # if the bits won't fit:
277 0 0         if ($bitpos+7 > 20) {
278 0           my $space = 20 - $bitpos;
279             # leftbits least significant bits of $char are left over in the next word
280 0           $leftbits = 7 - $space;
281 0           $leftval = $char;
282 0           _debug(" bits of char won't fit since bitpos is $bitpos, got $space bits free, leaving $leftbits bits in next word");
283             }
284            
285 0           $word |= $char << (31 - 7 - $bitpos);
286 0           $bitpos += 7;
287            
288 0 0         if ($bitpos >= 20) {
289 0           _debug(" appending word: " . _hex_int($word));
290 0           $out .= _append_message_codeword(\$pos, $word);
291 0           $word = 0;
292 0           $bitpos = 0;
293             }
294            
295 0 0         if ($leftbits) {
296 0           $word |= $char << (31 - $leftbits);
297 0           $bitpos = $leftbits;
298 0           $leftbits = 0;
299             }
300             }
301            
302 0 0         if ($bitpos) {
303 0           _debug(" got $bitpos bits in word at end of text, word: " . _hex_int($word));
304 0           my $step = 0;
305             #_debug(" filling the word");
306 0           while ($bitpos < 20) {
307             #_debug(" bitpos $bitpos step $step");
308 0 0         if ($step == 2) {
309             #_debug(" setting to 1");
310 0           $word |= 1 << (30 - $bitpos);
311             }
312 0           $bitpos++;
313 0           $step++;
314 0 0         $step = 0 if ($step == 7)
315             }
316 0           $out .= _append_message_codeword(\$pos, $word);
317             }
318            
319 0           return ($pos, $out);
320             }
321              
322             #
323             # Append content to a message
324             #
325              
326             sub _append_content($$)
327             {
328 0     0     my($type, $content) = @_;
329            
330 0 0         if ($type eq 'a') {
    0          
331             # alphanumeric
332 0           return _append_content_text($content);
333             } elsif ($type eq 'n') {
334             # TODO: numeric message: unsupported
335 0           return (0, '');
336             }
337             }
338              
339             #
340             # Append a single message to the end of the transmission blob.
341             #
342              
343             sub _append_message($$)
344             {
345 0     0     my($startpos, $msg) = @_;
346            
347             # expand the parameters of the message
348 0           my($addr, $function, $type, $content) = @{ $msg };
  0            
349            
350 0           _debug("append_message: addr $addr function $function type $type content $content");
351            
352             # the starting frame is selected based on the three least
353             # significant bits
354 0           my $frame_addr = $addr & 7;
355 0           my $frame_addr_cw = $frame_addr * 2;
356            
357 0           _debug(" frame_addr is $frame_addr, current position $startpos");
358            
359             # append idle codewords, until we're in the right frame for this
360             # address
361 0           my $tx = '';
362 0           my $pos = 0;
363 0           while (($startpos + $pos) % 16 != $frame_addr_cw) {
364 0           _debug(" inserting IDLE codewords in position " . ($startpos+$pos) . " (" . (($startpos + $pos) % 16) . ")");
365 0           $tx .= $POCSAG_IDLE_CW;
366 0           $pos++;
367             }
368            
369             # Then, append the address codeword, containing the function and the address
370             # (sans 3 least significant bits, which are indicated by the starting frame,
371             # which the receiver is waiting for)
372 0           $tx .= pack('N', _address_codeword($addr, $function));
373 0           $pos++;
374            
375             # Next, append the message contents
376 0           my($content_enc_len, $content_enc) = _append_content($type, $content);
377 0           $tx .= $content_enc;
378 0           $pos += $content_enc_len;
379            
380             # Return the current frame position and the binary string to be appended
381 0           return ($pos, $tx);
382             }
383              
384             #
385             # Given a binary message string, insert Synchronisation Codeword
386             # before every 8 POCSAG frames (frame is SC+ 64 bytes of address
387             # and message codewords)
388             #
389              
390             sub _insert_scs($)
391             {
392 0     0     my($tx) = @_;
393            
394 0           my $out = '';
395 0           _debug("insert_scs");
396            
397             # each batch is SC + 8 frames, each frame is 2 codewords,
398             # each codeword is 32 bits, so we must insert an SC
399             # every (8*2*32) bits == 64 bytes
400 0           my $tx_len = length($tx);
401 0           for (my $i = 0; $i < $tx_len; $i += 64) {
402             # put in the CW and 64 the next 64 bytes
403 0           $out .= $POCSAG_FS_CW . substr($tx, $i, 64);
404             }
405            
406 0           return $out;
407             }
408              
409             #
410             # Select the optimal next message to be appended, trying to
411             # minimize the amount of idle codewords transmitted
412             #
413              
414             sub _select_msg($$)
415             {
416 0     0     my($pos, $msglistref) = @_;
417            
418 0           my $current_pick;
419             my $current_dist;
420 0           my $pos_frame = int($pos/2) % 8;
421            
422 0           _debug("select_msg pos $pos: $pos_frame");
423            
424 0           my $i;
425 0           for ($i = 0; $i <= $#{ $msglistref }; $i++) {
  0            
426 0           my $addr = $msglistref->[$i]->[0];
427 0           my $frame_addr = $addr & 7;
428 0           my $distance = $frame_addr - $pos_frame;
429 0 0         $distance += 8 if ($distance < 0);
430            
431 0           _debug(" considering list item $i: $addr - frame addr $frame_addr distance $distance");
432            
433 0 0         if ($frame_addr == $pos_frame) {
434 0           _debug(" exact match $i: $addr - frame addr $frame_addr");
435 0           return $i;
436             }
437            
438            
439 0 0         if (!defined $current_pick) {
440 0           _debug(" first option $i: $addr - frame addr $frame_addr distance $distance");
441 0           $current_pick = $i;
442 0           $current_dist = $distance;
443 0           next;
444             }
445            
446 0 0         if ($distance < $current_dist) {
447 0           _debug(" better option $i: $addr - frame addr $frame_addr distance $distance");
448 0           $current_pick = $i;
449 0           $current_dist = $distance;
450             }
451             }
452            
453 0           return $current_pick;
454             }
455              
456             =over
457              
458             =item generate()
459              
460             Generates a transmission binary string.
461              
462             # list of messages to send
463             my @msgs = (
464             # address, function, type, message
465             [ '12345', 0, 'a', 'Hello, world!' ]
466             );
467              
468             my($encoded, @left) = POCSAG::Encode::generate($piss_max_len, @msgs);
469              
470             The function returns the binary string to be keyed over the air in FSK, and
471             any messages which did not fit in the transmission, given the maximum
472             transmission length (in bytes) given in the first parameter. They can be passed
473             in the next generate() call and sent in the next brrraaaap.
474              
475             =back
476              
477             =cut
478              
479             sub generate($@)
480             {
481 0     0 1   my $tx_without_scs = '';
482 0           my $scs_len = length($POCSAG_PREAMBLE);
483            
484 0           my $maxlen = shift;
485 0           my @msgs = @_;
486            
487 0           _debug("generate_transmission, maxlen: $maxlen");
488            
489 0           my($pos) = 0; # number of codewords appended currently
490 0           while (@msgs) {
491             # figure out an optimal next message to minimize the amount of required idle codewords
492             # TODO: do a deeper search, considering the length of the message and a possible
493             # optimal next recipient
494 0           my $optimal_next_msg = _select_msg($pos, \@msgs);
495 0           my $msg = splice(@msgs, $optimal_next_msg, 1);
496 0           my($append_len, $append) = _append_message($pos, $msg);
497            
498 0           my $next_len = $pos + $append_len + 2; # two extra idle codewords in end
499             # initial sync codeword + one for every 16 codewords
500 0           $next_len += 1 + int(($next_len-1)/16);
501 0           my $next_len_bytes = $next_len * 4;
502 0           _debug("after this message of $append_len codewords, burst will be $next_len codewords and $next_len_bytes bytes long");
503            
504 0 0         if ($next_len_bytes > $maxlen) {
505 0 0         if ($pos == 0) {
506 0           _debug("burst would become too large ($next_len_bytes > $maxlen) with first message alone - discarding!");
507             } else {
508 0           _debug("burst would become too large ($next_len_bytes > $maxlen) - returning msg in queue");
509 0           unshift @msgs, $msg;
510 0           last;
511             }
512             } else {
513 0           $tx_without_scs .= $append;
514 0           $pos += $append_len;
515             }
516             }
517            
518             # if the burst is empty, return it as completely empty
519 0 0         if ($pos == 0) {
520 0           return ('', @msgs);
521             }
522            
523             # append a couple of IDLE codewords, otherwise many pagers will
524             # happily decode the junk in the end and show it to the recipient
525 0           $tx_without_scs .= $POCSAG_IDLE_CW x 2;
526            
527 0           my $burst_len = length($tx_without_scs);
528 0           _debug("transmission without SCs: $burst_len bytes, " . int($burst_len/4) . " codewords\n" . _hex_dump($tx_without_scs));
529            
530             # put SC every 8 frames
531 0           my $burst = _insert_scs($tx_without_scs);
532            
533 0           $burst_len = length($burst);
534 0           _debug("transmission with SCs: $burst_len bytes, " . int($burst_len/4) . " codewords\n" . _hex_dump($burst));
535            
536 0           return ($burst, @msgs);
537             }
538              
539             =over
540              
541             =item set_debug($enable)
542              
543             Enables or disables debug printout in the module. Debug output goes to the standard error.
544              
545             =back
546              
547             =cut
548              
549             sub set_debug($)
550             {
551 0     0 1   $debug = ($_[0]);
552             }
553              
554             1;
555