#!/usr/bin/perl
#---------------
#
#    Sambru* - a free phonebook synch utility for Samsung
#       SCH-6100, SCH-8500 and SCH-850 cellular phones
#
#		(*Samsung Backup and Restore Utility)
#
#    Copyright (C) 2000 Eric Sandeen (eric_sandeen@bigfoot.com)
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
#---------------

use FileHandle;
use IPC::Open2;
use Getopt::Long;

#-----------------------
# Constants - this is all you should need to edit
$cu_path	= "/usr/bin/cu";
$port		= "/dev/ttyS0";	# Default
$baud_rate	= "19200";	# Can we run at 57600?
#-----------------------

# Other constants
$version = 0.23;
# Max values the phone can handle
# Could get these from AT#PBOKR=? - maybe?
$max_entries	   = 229;
$max_ringer_number = 12;
$max_name_length   = 12;
$max_number_length = 32;
$max_label	   = 6;
$max_fields        = 15; # loc, name, ringer + (6 numbers * 2 fields/number)
$max_phone_nums    = 6;
@num_to_label=("", "HOME", "WORK", "CELL", "PAGER", "FAX", "");
# Hash for entry types
%label_to_num = ("HOME", 1,
		"WORK", 2,
		"CELL", 3,
		"PAGER", 4,
		"FAX", 5);

#-----------------------
# Begin code
#-----------------------

GetOptions	("format:s",	# Records format (default RAW, or vcard)
		"get",		# Get information from phone
		"put",		# Put information into phone
		"file:s",	# File to read or write from (else STDIN/STDOUT)
		"port:s",	# Serial device (default /dev/ttyS0)
		"help"		# Show help usage
		)

or Show_Usage();

if ((!$opt_put && !$opt_get) || ($opt_put && $opt_get)) {
	print "\nERROR - must specify either --get OR --put\n";
	Show_Usage();
}

if ($opt_help) {
	Show_Usage();
}

if ($opt_port) {
	$port = $opt_port;
}

# Open port and put the phone in data mode
open_phone();

if ($opt_get) {
	if ($opt_file) {
		# Open file & Redirect STDOUT
		open(FILE, ">$opt_file");
		*STDOUTSAVE = *STDOUT;
		*STDOUT = *FILE;
	}
	# Read each entry from phone, format, and print
	for ( $entry_num = 1; $entry_num <= $max_entries; $entry_num++) {
		print STDERR "Reading location $entry_num/$max_entries\r";
		$entry = do_command("AT#PBOKR=$entry_num");
		if ($entry) {
			if ($opt_format eq "vcard") {
				$entry = convert_to_vcard($entry);
			}
			print STDERR "                             \r";
			print "$entry\n";
		}	
	}
	print STDERR "\nDone                         \n";
	if ($opt_file) {
		# Close file & Restore STDOUT
		*FILE = *STDOUT;
		close(FILE);
		*STDOUT = *STDOUTSAVE;
	}
}

if ($opt_put) {
	if ($opt_file) {
		# Open file & Redirect STDOUT
		open(FILE, "<$opt_file");
		*STDINSAVE = *STDIN;
		*STDIN = *FILE;
	}
	# Read each entry from file, and load into phone
	# WARNING - no checking for duplicate locations, so may overwrite
	# some previously loaded entries.
	while ( !eof(STDIN) ) {
		if ($opt_format eq "vcard") {
			$entry = convert_from_vcard();
		} else {
			$entry = <STDIN>;
		}
		chomp ($entry);
		@fields = split(/,/,$entry);	# Split entry into array
		# Sanity check what we're writing to the phone	
		if (!check_entry(@fields)) {;
			do_command("AT#PBOKW=$entry");
		}
	}
	
	if ($opt_file) {
		# Close file & Restore STDOUT
		*FILE = *STDIN;
		close(FILE);
		*STDIN = *STDINSAVE;
	}
}


close_phone();

################################################
# Subroutines
################################################
# Get phone's attention, set echo off, set data mode
sub open_phone {
	# Start the 'cu' program and get handles to in, out
	# NEED TO DO SOME ERROR CHECKING HERE!
	open2(*FROM_PHONE, *TO_PHONE, "$cu_path -l$port -s$baud_rate 2>&1");
	# Init phone, turn off command echo
	do_command("AT");
	do_command("ATE0");
	# Make sure this is a phone we can talk to...
	$model = do_command("AT+GMM");
	if ($model =~ /6100|8500|850/) {
		print STDERR "Found phone model $model\n";
	} else {
		die ("Whoa, buddy, can't talk to this $model phone!");
	}
	$battery = do_command("AT+CBC?");
	print STDERR "Battery level at $battery\n";
	# Put it into data mode
	do_command("AT#PMODE=1");
}

# end data mode, set echo on, kill cu
sub close_phone {
	do_command("AT#PMODE=0");
	# #PMODE=0 gives us 2 "OK" lines for some reason, flush it...
	read_line();

	# Turn command echoes back on
	do_command("AT#E1");

	# end cu with "~."
	print TO_PHONE "\r\n~.\r\n";
	close FROM_PHONE;
	close TO_PHONE;
	die ("\n");
}

# Sends a command to the phone, and returns result
# (result is any information before "OK", with original
# command stripped out of the line)
sub do_command {
	my ($command) = @_;

	print TO_PHONE "$command\r\n";
	$result = get_result();
	die ("Got ERROR from phone on command $command") if ($result =~ /ERROR/);
	# Strip down to result
	# Remove original command, error, ok, etc.
	#$result =~ s/^.+:\s//;
	$result =~ s/.*:\s|OK|ERROR//g;
	return $result;
}

# Retrieves lines until it gets an "OK" or an "ERROR"
# and then returns all the lines in one string...
sub get_result {
	$lines = "";
	do {
		$line = read_line();
		$lines .= $line;
	} while ( ( $line ne "OK" ) && ( $line ne "ERROR" ) );
	return $lines;
}

# Gets a single line from the phone, dies if it's empty
# Strips any newline monkey business as well
sub read_line {
    $_ = <FROM_PHONE>;
    $_ || die("got eof on serial - are you using the correct port ($port)?\n");
    s/[\r\n]+$//;
    return $_;
}

##############################
# Record conversion routines #
##############################

sub convert_to_vcard {

	my ($entry) = @_;
	
	@fields = split(/,/,$entry);	# Split entry into array
	$fields[1] =~ s/"//g;		# Strip quote marks from name
	$record =  "BEGIN:VCARD\n";
	$record .= "TITLE:$fields[0]\n";# (speed dial)
	$record .= "FN:$fields[1]\n";	# (name)
	$record .= "ORG:$fields[2]\n";	# (ringer)

	for ($x=3; $x<=14; $x += 2) {	# Process each phone number
		if ($fields[$x]) {	# If we have an entry
			$type = $num_to_label[$fields[$x]];
			$number = $fields[$x+1];
			$number = beautify_number($number);
			if ($type) {	# If there is a type
				$record = $record . "TEL;$type:$number\n";
			}
			else {
				$record = $record . "TEL:$number\n";
			}
		}
	}
	$record = $record . "END:VCARD\n\n";
	
	return $record;
}

sub convert_from_vcard {
	# This is the cheesy bit: We use "Title" for the storage location,
	# and "ORG" for the ringer type (may be blank), for now.
	# Also, speed dial is set by first phone number encountered, no
	# way to control that for now...
	#
	# This assumes a decently formatted vcard entry...
	
	# Suck in the whole vcard record
	# NOTE: [\r\n] to take care of DOS format file (double check this...)
	$vcard = "";
	while ( !eof(STDIN) && $vcard !~ /END:VCARD/i ) {
		$vcard .= <STDIN>;
	}
	$entry = "";
	# Get location
	$vcard =~ /TITLE:(.+)[\r\n]$/mi;
	$entry .= "$1";
	# Get name
	$vcard =~ /FN:(.+)[\r\n]$/mi;
	$name = $1;
	$entry .= ",\"$1\"";
	# Get ringer
	# Save us from no ringer type...
	if ($vcard =~ /ORG:(.+)[\r\n]$/mi) {
		$entry .= ",$1";
	} else {
		$entry .= ",0";
	}
	
	# Now we need to get up to 6 phone numbers
	# In the vcard, these look like:  TEL;WORK:<number>
	for ($slot_num = 1; $slot_num <= $max_phone_nums; $slot_num++) {
		$vcard =~ /TEL\;*(.*):(.+)[\r\n]$/mgci;
		$label = $1;
		$phone_number = $2;
		# Strip "( ) -"
		$phone_number =~ s/[()-]//g;
		# If we didn't find another label & number, we're done.
		if ($label eq "" && $phone_number eq "") {
			last;
		}
		# If there's a label, convert label to upper case
		# and get the right number.
		if ($label eq "") {
			$label = 6;	# No label (6 == none)
		} else {
			$label =~ s/[a-z]/[A-Z]/g;
			$label = $label_to_num{$label};
		}
		$entry .= ",$label,$phone_number";
	}

	# suck up trailing empty lines
	while (<STDIN> eq "\n") {}
	return $entry;
}


# Turns XXXXXXXXXX into (XXX)XXX-XXXX
#   and XXXXXXX into XXX-XXXX
sub beautify_number {
	my ($number) = @_;
	$length = length($number);
	if ($length > 4) {
		substr($number, $length-4, 0)  = "-";
	}
	if ($length >= 10) {
		substr($number, $length-7, 0)  = ")";
		substr($number, $length-10, 0) = "(";
	}
	return $number;
}

sub Show_Usage {
	print "\nSamBRU v.$version - Phone book utility for Samsung cell phones\n";
	print "               models SCH-6100, SCH-8500 and SCH-850\n\n";
	print "Usage: sambru <options>:\n";
	print "\n";
	print "options:\n";
	print "  --get                 get data from phone\n";
	print "  --put                 load data into phone\n";
	print "  --format <raw,vcard>  format of input or output data    (default raw)\n";
	print "  --file <filename>     filename for data input/output    (default STDIN/STDOUT)\n";
	print "  --port <device>       device to which phone is attached (default /dev/ttyS0)\n";
	print "  --help                show usage\n";
	die "\n";

}

sub check_entry {
	my (@fields) = @_;
	
	# Ok, this is really messy.  Should be cleaned up...
	# Basic rules:
	#	first field must be between 1 and max_entries
	#	Second field must be quoted chars, up to 12 chars
	#	Third field must be between 0 and max_ringers
	#	remaining fields are label/number pairs, up to 6 of them
	#		labels must be between 1 and max_lable
	#		numbers must be max 32 chars, of [0123456789*#pT]
	
	# Check entry number
	if ($fields[0] > $max_entries) {
		print "Error on entry $fields[0]:\tInvalid location ($fields[0])\n";
		return 1;
	}
	# Check number of fields
	if (@fields > $max_fields) {
		print "Error on entry $fields[0]:\tToo many fields (@fields)\n";
		return 1;
	}
	# Check name entry for quotes
	if ($fields[1] !~ /^\".*\"$/) {
		print "Error on entry $fields[0]:\tName must be quoted\n";
		return 1;
	}
	# Check name entry for length - SHOULDN'T WE JUST SHORTEN IT?
	if (length($fields[1]) > $max_name_length+2) {
		print "Error on entry $fields[0]:\tName too long ($fields[1])\n";
		return 1;
	}
	# Check ringer type
	if (($fields[2] > $max_ringer_number) 
	|| ($fields[2] !~ /^\d+$/ )) {
		print "Error on entry $fields[0]:\tInvalid ringer number ($fields[2])\n";
		return 1;
	}
	# Check phone numbers & labels
	for ($slot_num = 1; $slot_num <= $max_phone_nums; $slot_num++) {
		# Location label (i.e. 0-6 = work, home, pager, etc)
		$label = $fields[2*$slot_num+1];
		$phone_number = $fields[2*$slot_num+2];
		# If no label and no phone number, check next slot
		if ( !$label && !$phone_number) {
			next;
		}
		# If label & no number, or number & no label, error
		if (!$label && $phone_number) {
			print "Error on entry $fields[0]:\tnumber $phone_number has no label\n";
			return 1;
		}
		if ($label && !$phone_number) {
			print "Error on entry $fields[0]:\tlabel $label has no phone number\n";
			return 1;
		}
		# Check that location label is valid, and a number
		if ( ($label > $max_label) || ($label !~ /^\d$/) )
		{
			print "Error on entry $fields[0]:\tInvalid location label ($label)\n";
			return 1;
		}
		# Check phone number is valid
		if ( (length($phone_number) > $max_number_length) 
		|| ($phone_number !~ /^[\d\*\#pT]+$/) )
		{
			print "Error on entry $fields[0]:\tInvalid phone number ($phone_number)\n";
			return 1;
		}			
	}
	return 0;
}






