#!/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 ">$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