#!/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(<FILE>) {

	# 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 ",";
				}
			}
		}
	}
}