#!/usr/bin/perl -w
#######################################################################
# Program name: alcatel_readserial
# Written by: Jason Balicki, kodak@frontierhomemortgage.com
# Date: 1/21/2005
# This revision: 1-26-2005 @ 3:42 PM CST
# 
# 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 constants:
#
# serial port
my $tty = '/dev/ttyS1';
#
# processed (csv) call log file
my $csv_log_file='/var/log/alcatel.csv';
#
# log raw data?
my $log_raw="yes";
#
# raw log (probably only for testing)
my $raw_log_file='/var/log/alcatel.raw';
#
# log errors?
my $log_errors="yes";
#
# error log: where we put anything other than call records
my $error_log_file='/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_log_files($csv_log_file);
if ($log_raw eq "yes"){
	setup_log_files($raw_log_file);
}
if ($log_errors eq "yes"){
	setup_log_files($error_log_file);
}
#
# if empty, write headers to csv log file
#
setup_log_files($csv_log_file);
if ( -z $csv_log_file){
	open (LOG, ">>$csv_log_file") or die "Can't open $csv_log_file 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.";
#
#
#######################################################################

#######################################################################
#
# main loop
#
while(<FILE>) {
	
	#FIXME: why can't I use my $CD_record=shift; here?
	# is it better to just continue using $_ in main?
	# why do we park on a driveway and drive on a parkway?
	# CD= Call Data
	my $CD_record = $_;

	# 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 (($log_raw eq "yes") && ($CD_record ne "\n")) {
		open ( RAW_LOG, ">>$raw_log_file") or die qq(Can't open "$raw_log_file": $!);
		print RAW_LOG $CD_record;
		close ( RAW_LOG );
	}

	# if it is a call record (as best as we can determine)
	# we print the formatted record to the log
	# I know I could do: if ((my $line_number = which_line($_)) != 3) {
	# but I want it this way for clarity;
	# creating $CDR_line_number, which is passed to format_call_record()
	# CDR= Call Data Record
	
	my $CDR_line_number = which_line($CD_record);
	if (($CDR_line_number == 1) || ($CDR_line_number == 2)) {
		open ( LOG, ">>$csv_log_file" ) or die qq(Can't open "$csv_log_file": $!);
		print LOG format_call_record($CD_record,$CDR_line_number);
		close ( LOG );
	}
	else {
		# it's not a call record, so it's probably an error.
		if (($log_errors eq "yes") && ($CD_record ne "\n")){
			open ( ERROR_LOG, ">>$error_log_file" ) or die qq(Can't open "$error_log_file": $!);
			print ERROR_LOG $CD_record;
			close ( ERROR_LOG );
		}
	}
}
#
#######################################################################

#######################################################################
#
# Subroutine: which_line()
#
# determines if the passed line is line 1 or line 2 of the call record
# by looking for specific strings in the record.
#
# Line 1 will contain |X| where X could be 0 (zero) or any capital letter A-Z
# at the end of the line and will have | symbols at the start of the line,
# and at positions  26 and 38, among others.
#
# Line 2 will contain |X| where X=B or N or P or G, starting at position 30.
# and will also have | symbols at the start and end of the line, and
# at the specific positions 46 and 51.  I could add some more, but I'm 
# not convinced I need to at this point.  Although it wouldn't hurt to
# catch all of the pipes.
#
# returns: 1 or 2, depending on line number determined or will return 3 if
# no matches are found -- this allows us to remove an unnecessary function
# that I was using to determine if the line is a record or not.
#
sub which_line {
	my $record = shift;
	return 1 if $record =~ /^\|.{25}\|.{10}\|.*\|[0A-Z]\|$/;
	return 2 if $record =~ /^\|.{29}\|[BNPG]\|.{12}\|.{4}\|.*\|$/;
	return 3;
}
#
#######################################################################

#######################################################################
#
# Subroutine: setup_log_files() 
#
# 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.)
#
# returns: nothing

sub setup_log_files {
	my $log_file = shift;
	if (! -e $log_file) {
		open (CREATE_FILE, ">$log_file") or die qq(Can't create "$log_file": $!);
		print CREATE_FILE "";
		close ( CREATE_FILE );
	}
}
#
#######################################################################

#######################################################################
#
# Subroutine format_date()
#
# 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 format_date {
	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: format_call_record()
#
# arguments: $line_data (the string that contains the line data) 
# $line_number, which is which line of the call record are we
# operating on in this pass
# explaination of "line_one_start" etc.:
# line 1 starts at 1 instead of 0 because the first |
# comes at the start of the line, and perl wants to assign
# the field before that to a value, but we don't need it
#
# line 1 ends at 8 because field 9 contains a \n, and we don't
# want that either
#
# line 2 starts at 0 because, hell, I don't know.  but for
# whatever reason it works out.
#
# line 2 ends at 11 because that's the last record we're interested
# in.  If we ended it at 12 we'd get a \n, but we'd be left with
# an extra "," at the end, which we don't want.
#
# returns: the csv formatted line

sub format_call_record {
	# the line data and line number are passed in by main
	my ($record, $line_number) = @_;
	# self documenting....
	my $line_one_start=1;
	my $line_one_end=8;
	my $line_two_start=0;
	my $line_two_end=11;
	my $date_field=3;

	if ( $line_number == 1 ) {
		my @fields =  (split /\|/, $record) [ $line_one_start .. $line_one_end ];
		$fields[$date_field] = format_date($fields[$date_field]);
		return join ',', @fields;
	} elsif ( $line_number == 2 ) {
		my @fields = ( split /\|/, $record ) [ $line_two_start .. $line_two_end ];
		$fields[$line_two_end] = $fields[$line_two_end] . "\n";
		return join ',', @fields;
	} else  {
	
		return
			"\nWARNING: Invalid line number ($line_number)".
			" detected in format_call_record().\n";
	}
}
#
#######################################################################
