#!/usr/bin/perl -w ####################################################################### # Program name: alcatel_readserial # Written by: Jason Balicki, kodak@frontierhomemortgage.com # Date: 1/21/2005 # # The Alcatel OmniPCX Office phone system will output call records to # what they refer to as the "V24" port, which is /dev/ttyS0 on # the PCX itself (the PCX is a Linux based phone system.) # # The problem is that call records are not the only thing # output on the line. That port is also the serial console # and serial login maintenance port (you can still get # in via other means.) Usually, Alcatel will provide # (er, well, sell you for a lot of money) a network serial # port and then the call records would be sent to that # port instead of /dev/ttyS0. However, the cost for doing # that is prohibitive. Almost as much as purchasing their # network-based call record system, the price of which is # why I'm bothering with the serial port at all. Otherwise # I'd have bought the network license and just read that # directly, it'd be cleaner and easier. # # This program makes the assumption that you are using the # extended call records. Alcatel can provide "reduced" # and "extended" call records. Extended records are on # two lines and reduced are on one. Since I'm using # extended I have to allow for multiple lines and identify # which line I'm dealing with at a time. I also have to # determine which lines are call records and which are # other messages from the phone system. # # The following is the format of and an example of one call # record. Please see your system documentation for field # definitionis. # # |Subscr |Name |CCN |EndCalTime|Duration |Cu/Cost |VSACMP |O| # |Trf.Sub |Called Number |P|Code |PNI |SBNode|TKNode|TGN |Trunk|C|A| # # |6643 |Jason Balicki | |0501211243|000:00:00| 0| S |0| # | |13145551212 |N| | 0|001001|001001| 100| 10|B|A| # # BTW: I disabled getty on the phone system on that port # and I cut all lines on the physical cable except for # signal ground and receive data, just to make things # easy on myself. I used an adaptor to do it, so I # can just remove the adaptor if I ever need to send # data to the phone system (such as log in on the serial # console or something. # # I'm over documenting this file because I'm brand spanking # new at perl (5 days, as of when I'm writing this note # (1/21/2005) and I've found when looking at examples on # the intar-web that a lot of people don't document "simple" # things that may or may not be simple to others. # ####################################################################### ####################################################################### # # Perl Options use strict; use warnings; # ####################################################################### ####################################################################### # # Define vars and contants: # # serial port my $tty = '/dev/ttyS1'; # # processed (csv) call log file my $csvlog='/var/log/alcatel.csv'; # # log raw data? my $lograw="yes"; # # raw log (probably only for testing) my $rawlog='/var/log/alcatel.raw'; # # log errors? my $logerrors="yes"; # # error log: where we put anything other than call records my $errorlog='/var/log/alcatel.err'; # # the csv headers my $headers="Subscriber,Name,CCN,EndCallDate,EndCallTime,Duration,Cost,VSACMF,O,Transfer Subscriber,Called Number,P,Code,PNI,SBNode,TKNode,TGN,Trunk,C,A\n"; # ####################################################################### ####################################################################### # # Initialize: # # create the files if they don't exist (and if we have specified to # do so above.) setup($csvlog); if ($lograw eq "yes"){ setup($rawlog); } if ($logerrors eq "yes"){ setup($errorlog); } # # if empty, write headers to csv log file if ( -z $csvlog){ open (LOG, ">>$csvlog") or die "Can't open $csvlog for writing!"; print LOG $headers; close ( LOG ); } # # open the serial port. The phone system sends \r\n (crlf -- it's expecting # a printer, really) so we just convert on the fly with "<:crlf" # open (FILE, "<:crlf", "$tty") or die "Can't open $tty, please make sure the correct serial port is selected."; # #open the main log file # open (LOG, ">>$csvlog") or die "Can't open $csvlog for writing!"; # ####################################################################### ####################################################################### # # main loop # while() { # send the raw data to the raw log file to compare with later # and make sure nothing is missing. If it's ok we'll remove later. if ($lograw eq "yes") { writeraw($_); } # is it a call record? if (iscallrec($_)) { # yes, ok which line? if (whichline($_)==1){ printcallrec($_, 1); } else { if (whichline($_)==2){ printcallrec($_, 2); } } } else { # it's not a call record, so it's probably an error. if ($logerrors eq "yes"){ if (!iscallrec($_)){ printerror($_); } } } } close ( LOG ); # ####################################################################### ####################################################################### # # Subroutine: writeraw() # # writes the input buffer to a raw log file. This may be removed # after troubleshooting and making sure that no records (error or # call records) are lost. # # returns: nothing # ####################################################################### sub writeraw{ open (RAW, ">>$rawlog") or die "Can't open $rawlog for writing!"; print RAW $_; close ( RAW ); } ####################################################################### # # Subroutine: iscallrec() # # checks to see if the line is a call record. # # it does this by a funky regex that I won't be able to read in # a year, but it looks for a pipe ("|") character as the first # and last character on the line, and also looks for distinguishing # character strings that might be on line one or two. # # Special thanks to John Krahn for the tips on finding |[BNPG]| at # a specific location on the line. # # The regex breaks down to: # #if # (((/^\|/): There is a | as the first character on the line # and (/\|$/): There is a | as the last character on the line # and (/^.{30}\|[BNPG]\|/): There is a |[BNPG]| starting at position 30 # or ((/^\|/): | as first char. # and (/\|$/): | as last char. # and (/\|[0A-Z]\|/) There is a |[0A-Z]| at the end of the line # # I figure this may or may not match line noise at some point. # If it does, I'm buying a lottery ticket the next day. # # FIXME: Add more checks for "|" at specific locations # # returns: boolean, 0 or 1 # ####################################################################### sub iscallrec { # HFS, batman. See the call record example above. if (((/^\|/) && (/\|$/) && (/^.{30}\|[BNPG]\|/)) || ((/^\|/) && (/\|$/) && (/\|[0A-Z]\|/))){ return 1; } else { return 0; } } ####################################################################### # # Subroutine: whichline() # # determines if the buffer is line 1 or line 2 of the call record # by looking for specific strings in the record. # Line 2 will contain |X| where X=B or N or P or G, starting at position 30. # Line 1 will contain |X| where X could be 0 (zero) or any capital letter A-Z # at the end of the line. # # returns: 1 or 2. (should I make this "one" or "two"?) # ####################################################################### sub whichline { if (/^.{30}\|[BNPG]\|/){ return 2; } else { if (/\|[0A-Z]\|$/) { return 1; } } } ####################################################################### # # Subroutine: setup() # # checks to see if the passed logs exist, if not it creates them. # # funny story: at first I had the file handle as "FILE" here. Yeah. # That was fun. (Spoiler: The main loop is looking at "FILE", so # when I closed FILE here, the main loop ended.) # # SUF="Set Up Files" -- My creativity was waning at this point. # # returns: nothing # ####################################################################### sub setup { my ($log) = $_[0]; if (! -e $log) { open (SUF, ">$log") or die "Can't create $log file."; print SUF ""; close (SUF); } } ####################################################################### # # Subroutine printerror() # # prints the line to an error log after it has been determined that the line # isn't a call record AERR stands for "Alcatel Error". # #returns: nothing # ####################################################################### sub printerror { my($locbuff) = $_; open ( AERR, ">>$errorlog" ) or die "Can't open $errorlog for writing!"; print AERR "$locbuff"; close ( AERR ); } ####################################################################### # # Subroutine logdate() # # converts alcatels date format to something more readable # # Thanks very much to Charles K. Clarkson (in the perl-beginners list) # for the sub. The one I had here sucked. A lot. # # returns: the formatted date string # ####################################################################### sub logdate { my $date = shift; my( $year, $month, $day, $hour, $minute ) = $date =~ /../g; return sprintf '%s/%s/20%s,%s:%s', $month, $day, $year, $hour, $minute; } ####################################################################### # # Subroutine: printcallrec() # # prints the formatted call record to the csv log file. # it performs differently depending on which line number it is (1 or 2) # this is really the meat, the part that performs the conversion # from the "printed" records on the serial port to the useable csv # in the logs. # Take note that we need to ignore the line feed at the end of # line one, and not print a "," in the csv log file at the end # of line 2. Also, call date conversion. # # arguments: $locbuff (the string that contains the line data) and # $linenum (the line number we have determined the string to be.) # # returns: 2 if $linenum is not 1 or 2, otherwise nothing. # ####################################################################### sub printcallrec { my ($locbuff) = $_[0]; my ($linenum) = $_[1]; # the split function will split a string into an array using the # specified characters as a field seperator. In this case, the "|" # symbol is the seperator (and has to be quoted), $locbuff is the # string to be split and @infos (couldn't think of a better name) # is the array we'll be working with. my (@infos) = split(/\|/, $locbuff); # FIXME: date field (could we detect this with a regex? I don't know.) # FIXME: should these be global so I can put them up top? my ($df) = 4; # the first carriage return my ($crf) = 9; # the second carriage return my ($crf2) = 12; # the last field we care about (the carriage returns get split into array members too my ($eol2) = 11; # if the line number is not 1 or 2, something's fucked up bad, kitty. # This has yet to happen. if (($linenum != 1) && ($linenum != 2)){ print LOG "\nWARNING: Invalid line number detected in printcallrec() expect 1 or 2 got $linenum\n"; return 2; } if($linenum == 1) { # line one. Special conditions: date field and carriage return for (my $i = 1; $i <= $#infos; $i++) { # if it's not the date field and it's not the first carriage return (two # lines, remember) then go ahead and start printing the fields to the # log file. if (($i != $df) && ($i != $crf)){ print LOG "$infos[$i]"; print LOG ","; } else { # Ok, we're at the date field, so we're going to change it into # something more readable and print it to the log. if ($i == $df) { print LOG logdate($infos[$i]); print LOG ","; } } } } else { if($linenum==2){ # line 2. Special conditions: we don't want a "," on the last record for (my $i = 1; $i <= $#infos; $i++) { print LOG "$infos[$i]"; # as long as we're not printing the last record, print the "," if ($i < $eol2){ print LOG ","; } } } } } =head1 NAME alcatel_readserial` =head1 DESCRIPTION Reads data on the searial port that has been provided by an Alcatel OmniPCX phone system. The data is then processed and re-written into a csv file for later processing. =head1 README Reads data on the searial port that has been provided by an Alcatel OmniPCX phone system. The data is then processed and re-written into a csv file for later processing. =pod OSNAMES any =pod SCRIPT CATEGORIES Unix/System_administration =cut