#! /usr/bin/perl -w
use strict;

# Make warnings fatal
local $SIG{__WARN__} = sub { die @_ };

#
# Written by Oron Peled <oron@actcom.co.il>
# Copyright (C) 2006, Xorcom
#
# All rights reserved.
#
# 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.
#
# See the file LICENSE in the top level of this tarball.
#

#
# $Id$
#
# Data format:
#	- A comment start with ';' or '#' until the end of line
#	- Blank lines are ignored
#	- Fields are whitespace separated (spaces or tabs)
#
# The fields are (in command line order):
#	1. SLIC select in decimal (range 0-7).
#	   * is a special value which means ALL SLICS (only some registers
#	   accept settings for ALL SLICS).
#	2. Command word:
#		- RD	Read Direct register.
#		- RS	Read Sub-register.
#		- WD	Write Direct register.
#		- WS	Write Sub-register.
#	3. Register number in hexadecimal.
#	4. Low data byte in hexadecimal. (for WD and WS commands).
#	5. High data byte in hexadecimal. (for WS command only).
#
#

package main;
use File::Basename;
use Getopt::Std;

my $program = basename("$0");
my $init_dir = dirname("$0");
BEGIN { $init_dir = dirname($0); unshift(@INC, "$init_dir"); }
use XppConfig $init_dir;
my $unit_id;
my %opts;
my $eeprom_release_201 = 0;

getopts('o:', \%opts);

my %settings;
$settings{debug} = 0;
$settings{fxs_skip_calib} = 0;
my $chipregs;
my $ring_registers;

sub logit {
	print STDERR "$unit_id: @_\n";
}

sub debug {
	logit @_ if $settings{debug};
}

# Arrange for error logging
if (-t STDERR) {
	$unit_id = 'Interactive';
	debug "Interactive startup";
} else {
	$unit_id = "$ENV{XBUS_NAME}/UNIT-$ENV{UNIT_NUMBER}";
	open (STDERR, "| logger -t $program -p kern.info") || die;
	debug "Non Interactive startup";
	foreach my $k (qw(
			XBUS_NAME
			XBUS_NUMBER
			XBUS_MODEL_STRING
			UNIT_NUMBER
			UNIT_TYPE
			UNIT_SUBUNITS
			UNIT_SUBUNITS_DIR
			XBUS_REVISION
			XBUS_CONNECTOR
			XBUS_LABEL)) {
		unless(defined $ENV{$k}) {
			logit "Missing ENV{$k}\n";
			die;
		}
	}
	logit "XBUS_MODEL_STRING='$ENV{XBUS_MODEL_STRING}'";
	if ($ENV{XBUS_MODEL_STRING} =~ m{.*/.*/20.}) {
		$eeprom_release_201 = 1;
	}
	$chipregs = sprintf "/sys/bus/xpds/devices/%02d:%1d:0/chipregs",
		$ENV{XBUS_NUMBER}, $ENV{UNIT_NUMBER};
	if(! -f $chipregs) {
		my $xpd_name = sprintf("XPD-%1d0", $ENV{UNIT_NUMBER});
		$chipregs = "/proc/xpp/$ENV{XBUS_NAME}/$xpd_name/chipregs";
		logit "OLD DRIVER: does not use /sys chipregs. Falling back to /proc"
			if -f $chipregs;
	}
	$ring_registers = sprintf "/sys/bus/xpds/devices/%02d:%1d:0/fxs_ring_registers",
		$ENV{XBUS_NUMBER}, $ENV{UNIT_NUMBER};
	logit "OLD DRIVER: missing '$ring_registers' -- fallback to hard-coded defaults"
		unless -f $ring_registers;
}

sub set_output() {
	my $output;

	if($opts{o}) {
		$output = $opts{o};
	} else {
		# No subunits in FXS (everything is subunit 0)
		$output = $chipregs;
	}
	open(REG, ">$output") || die "Failed to open '$output': $!\n";
	my $oldfh = select REG;
	main::logit "# Setting output" if $opts{o};
	return $oldfh;
}

sub mysleep($) {
	my $timeout = shift;
	select(undef,undef,undef,$timeout);
}

package FXS;

sub gen {
	my $fmt = shift;
	$| = 1;
	printf "$fmt\n", @_;
}

my @SlicNums = (0 .. 7);

sub write_to_slic_file($) {
	my $write_str = shift;

	open(SLICS,">$chipregs") or 
		die("Failed writing to chipregs file $chipregs");
	print SLICS $write_str;
	close(SLICS) or die "Failed writing '$write_str' to '$chipregs': $!";
	main::mysleep(0.001);
	
}

sub write_to_ring_register($) {
	my $write_str = shift;

	open(SLICS,">$ring_registers") or
		die("Failed writing to ring_registers file $ring_registers");
	print SLICS $write_str;
	close(SLICS) or die "Failed writing '$write_str' to '$ring_registers': $!";
	main::mysleep(0.001);
}

sub read_reg($$$) {
	my $read_slic = shift;
	my $read_reg = shift;
	my $direct = shift;
	
	write_to_slic_file(
		sprintf("%s R%s %02X", $read_slic, $direct, $read_reg));
	my $retries = 10;
	my @reply;
	# If the command queue is long, we may need to wait...
WAIT_RESULTS:
	{
		my @results;

		# The time to sleep is a tradeoff:
		#   - Too long is a waste of time.
		#   - Too short will cause many retries, wastes time.
		# So the current value (after trial and error) is...
		main::mysleep(0.013);
		open(SLICS,$chipregs) or 
			die("Failed reading from chipregs file $chipregs");
		while(<SLICS>){
			s/#.*//;
			next unless /\S/;
			@results = /^\s*(\d+)\s+[RW][DI]\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]*)/;
			if(@results != 4) {
				main::logit "Failed reading from '$chipregs' ($read_slic,$read_reg,$direct)";
				die;
			}
		}
		close(SLICS);
		my $reg = hex($results[1]);
		if($results[0] ne $read_slic || $reg ne $read_reg) {
			# We read obsolete values, need to wait some more
			if(--$retries) {
				main::debug "$read_slic RD $read_reg -- retry ($results[0], $reg)";
				redo WAIT_RESULTS;
			} else {
				main::logit "Failed: $read_slic RD $read_reg returned $results[0], $reg";
				die;
			}
		}
		# Good.
		@reply = (hex($results[2]), hex($results[3]));

	}
	if ($direct eq 'S') {
		return @reply;
	} else {
		return $reply[0];
	}
}

# TODO: rearange arguments
sub write_reg{#($$$$$) {
	my $read_slic = shift;
	my $read_reg = shift;
	my $direct = shift;
	my $reg_val_low = shift;
	my $reg_val_hi = shift;
	
	my $str  = sprintf "%s W%s %02X %02X", 
		$read_slic, $direct, $read_reg, $reg_val_low;
	if ($direct eq 'S') {
		$str .= sprintf " %02X", $reg_val_hi;
	}
	write_to_slic_file($str);
}

sub log_calib_params() {
	for my $i (100 .. 107) {
		my $line="Calib Reg $i:  ";
		for my $slic (@SlicNums) {
			$line .= " ".read_reg($slic, $i, 'D');
		}
		main::debug($line);
	}
}

sub init_indirect_registers() {
	return write_to_slic_file("#
*	WS	1E	00	C2	55
*	WS	1E	01	E6	51
*	WS	1E	02	85	4B
*	WS	1E	03	37	49
                          
*	WS	1E	04	33	33
*	WS	1E	05	02	02
*	WS	1E	06	02	02
*	WS	1E	07	98	01
                          
*	WS	1E	08	98	01
*	WS	1E	09	11	06
*	WS	1E	0A	02	02
*	WS	1E	0B	E5	00
                          
*	WS	1E	0C	1C	0A
*	WS	1E	0D	30	7B
*	WS	1E	0E	63	00
*	WS	1E	0F	00	00
                          
*	WS	1E	10	70	78
*	WS	1E	11	7D	00
*	WS	1E	12	00	00
*	WS	1E	13	00	00
                          
*	WS	1E	14	FD	7E
*	WS	1E	15	77	01
*	WS	1E	16	00	00
*	WS	1E	17	00	20
                          
*	WS	1E	18	00	20
*	WS	1E	19	00	00
*	WS	1E	1A	00	20
*	WS	1E	1B	00	40
                          
*	WS	1E	1C	00	10
*	WS	1E	1D	00	36
*	WS	1E	1E	00	10
*	WS	1E	1F	00	02
                          
*	WS	1E	20	C0	07
*	WS	1E	21	6F	37
*	WS	1E	22	80	1B
*	WS	1E	23	00	80
                          
*	WS	1E	24	00	08
*	WS	1E	25	00	08
*	WS	1E	26	00	08
*	WS	1E	27	00	08
                          
*	WS	1E	28	00	00
*	WS	1E	2B	00	08 # LCRTL = 5.08 mA
                          
*	WS	1E	63	DA	00
*	WS	1E	64	60	6B
*	WS	1E	65	74	00
*	WS	1E	66	C0	79
                          
*	WS	1E	67	20	11
*	WS	1E	68	E0	3B	
#");
}

sub init_early_direct_regs() {
	my $lbv = ($eeprom_release_201) ? "20" : "10";
	my $vcm = ($eeprom_release_201) ? "02" : "03";

	return write_to_slic_file("#
*	WD	08	00	# Audio Path Loopback Control
*	WD	6C	01
*	WD	4A	34	# High Battery Voltage
*	WD	4B	$lbv	# Low Battery Voltage
*	WD	49	$vcm	# Common Mode Voltage (VCM)
*	WD	40	00	# Line Feed Control
#")
}

my @FilterParams = ();

sub save_indirect_filter_params() {
	for my $slic (@SlicNums) {
		for my $reg (35 .. 39) {
			$FilterParams[$slic][$reg] = 
				[read_reg($slic, $reg, 'S')];
			write_reg($slic, $reg, 'S', 0, 0x80);
		}
	}
	
}

sub restore_indirect_filter_params() {
	for my $slic (@SlicNums) {
		for my $reg (35 .. 39) {
			write_reg($slic, $reg, 'S', 
				@{$FilterParams[$slic][$reg]});
		}
	}
}

my $ManualCalibrationSleepTime = 0.04; # 40ms

sub manual_calibrate_loop($$) {
	my $write_reg = shift;
	my $read_reg = shift;
	my @curr_slics = @SlicNums;

	# initialize counters
	my @slic_counters = map { 0x1F } @curr_slics;

	# wait until all slics have finished calibration, or for timeout
	while (@curr_slics) {
		my $debug_calib_str = "ManualCalib:: ";
		my @next_slics;

		for my $slic (@curr_slics) {
			write_reg($slic,$write_reg,'D',$slic_counters[$slic]);
		}
		main::mysleep $ManualCalibrationSleepTime;
		for my $slic (@curr_slics) {
			my $value = read_reg($slic, $read_reg, 'D');
			$debug_calib_str .= sprintf " [%d:%d:%X]",
				$slic, $slic_counters[$slic], $value;
			next if $value == 0;	# This one is calibrated.
			if ($slic_counters[$slic] > 0) {
				$slic_counters[$slic]--;
				push(@next_slics, $slic);
			} else {
				main::logit("ERROR: SLIC $slic reached 0 during manual calibration");
			}
		}
		@curr_slics = @next_slics;
		main::debug($debug_calib_str);
	}
	main::debug("No more slics to calibrate");
}

sub manual_calibrate() {
	manual_calibrate_loop(98, 88);
	manual_calibrate_loop(99, 89);
}

sub auto_calibrate($$) {
	my $calib_96 = shift;
	my $calib_97 = shift;

	#log_calib_params();
	# start calibration:
	for my $slic(@SlicNums) {
		write_to_slic_file(
			sprintf
				"$slic WD 61 %02X\n".
				"$slic WD 60 %02X\n".
				"", $calib_97, $calib_96
				
		);
	}

	# wait until all slics have finished calibration, or for timeout
	# time periods in seconds:
	my $sleep_time = 0.001;
	my $timeout_time = 0.600; # Maximum from the spec
	my @curr_slics = @SlicNums;
	my $sleep_cnt = 0;
CALIB_LOOP:
	while(1) {
		main::mysleep($sleep_time);
		my @next_slics;
		for my $slic (@curr_slics) {
			main::debug("checking slic $slic");
			my $val = read_reg($slic, 96, 'D');
			push(@next_slics, $slic) if $val != 0;
		}
		@curr_slics = @next_slics;
		last unless @curr_slics;
		if ($sleep_cnt * $sleep_time > $timeout_time) {
			main::logit("Auto Calibration: Exiting on timeout: $timeout_time.");
			last CALIB_LOOP;
		}
		main::debug("auto_calibrate not done yet($sleep_cnt): @curr_slics");
		$sleep_cnt++;
	}
	#log_calib_params();
}

sub calibrate_slics() {
	main::debug "Calibrating '$0'";
	auto_calibrate(0x40, 0x1E);
	main::debug "after auto_calibrate";
	manual_calibrate();
	main::debug "after manul_calibrate";
	auto_calibrate(0x40, 0x01);
	main::debug "after auto_calibrate 2";
	main::debug "Continue '$0'";
}

sub read_defaults() {
	if(XppConfig::read_config(\%settings)) {
		main::logit "Defaults from $settings{xppconf}";
	} else {
		main::logit "No defaults file, use hard-coded defaults.";
	}
}

# Try to identify which slics are valid
sub check_slics() {
	my @slics;
	foreach my $slic (0 .. 7) {
		my $value = read_reg($slic, 0, 'D');
		push(@slics, $slic) if $value != 0xFF;
	}
	main::logit "Found " . scalar(@slics) . " SLICS (@slics)";
	return @slics;
}

sub overwrite_ring_registers() {
	write_to_ring_register("NEON 0x33 0x12");
}

package main;

main::debug "Starting '$0'";

FXS::read_defaults;
@SlicNums = FXS::check_slics;
main::debug "before init_indirect_registers";
FXS::init_indirect_registers();
main::debug "after init_indirect_registers";
FXS::init_early_direct_regs();
main::debug "after init_early_direct_regs";
if($settings{fxs_skip_calib}) {
	main::logit "==== WARNING: SKIPPED SLIC CALIBRATION =====";
} else {
	FXS::calibrate_slics;
}
set_output;
while(<DATA>) {
	chomp;
	s/[#;].*$//;		# remove comments
	s/^\s+//;		# trim whitespace
	s/\s+$//;		# trim whitespace
	s/\t+/ /g;		# replace tabs with spaces (for logs)
	next unless /\S/;	# Skip empty lines
	main::debug "writing: '$_'";
	print "$_\n";
}
close REG;
FXS::overwrite_ring_registers();

main::debug "Ending '$0'";
close STDERR;
exit 0;

# ----------------------------------==== 8-channel FXS unit initialization ===-----------------------------------------

__DATA__

# Flush out energy accumulators
*	WS	1E	58	00 00
*	WS	1E	59	00 00
*	WS	1E	5A	00 00
*	WS	1E	5B	00 00
*	WS	1E	5C	00 00
*	WS	1E	5D	00 00
*	WS	1E	5E	00 00
*	WS	1E	5F	00 00

*	WS	1E	61	00 00

*	WS	1E	C1	00 00
*	WS	1E	C2	00 00
*	WS	1E	C3	00 00
*	WS	1E	C4	00 00
*	WS	1E	C5	00 00
*	WS	1E	C6	00 00
*	WS	1E	C7	00 00
*	WS	1E	C8	00 00
*	WS	1E	C9	00 00
*	WS	1E	CA	00 00
*	WS	1E	CB	00 00
*	WS	1E	CC	00 00
*	WS	1E	CD	00 00
*	WS	1E	CE	00 00
*	WS	1E	CF	00 00
*	WS	1E	D0	00 00
*	WS	1E	D1	00 00
*	WS	1E	D2	00 00
*	WS	1E	D3	00 00

# Clear and disable interrupts
*	WD	12	FF
*	WD	13	FF
*	WD	14	FF
*	WD	15	00
*	WD	16	00
*	WD	17	00
                
## Mode(8-bit,u-Law,1 PCLK ) 
*	WD	01	08	# Disable PCM transfers

# Setting of SLICs offsets
# New card initialization

*	WD	03	00
*	WD	05	00

0	WD	02	00
0	WD	04	00
0	WD	01	28	# Enable PCM transfers
1	WD	02	08
1	WD	04	08
1	WD	01	28
2	WD	02	10
2	WD	04	10
2	WD	01	28
3	WD	02	18
3	WD	04	18
3	WD	01	28
4	WD	02	20
4	WD	04	20
4	WD	01	28
5	WD	02	28
5	WD	04	28
5	WD	01	28
6	WD	02	30
6	WD	04	30
6	WD	01	28
7	WD	02	38
7	WD	04	38
7	WD	01	28

# Audio path. (also initialize 0A and 0B here if necessary)
*	WD	08 00
*	WD	09 00
*	WD	0A 08
*	WD	0B 33

#------ Metering tone
*	WD	2C	00	# Timer dL
*	WD	2D	03	# Timer dH
*	WS	1E	17	61 15	# Amplitue Ramp-up
*	WS	1E	18	61 15	# Max Amplitude
*	WS	1E	19	FB 30	# Frequency

# Ring regs are set by driver

# Automatic/Manual Control: defaults but:
#	Cancel AOPN - Power Alarm
#	Cancel ABAT - Battery Feed Automatic Select
*	WD	43 16

# Loop Closure Debounce Interval
*	WD	45 0A

# Ring Detect Debounce Interval
*	WD	46 47

# Battery Feed Control: Battery low (DCSW low)
*	WD	42 00

# Loop Current Limit
*	WD	47 00

# On-Hook Line Voltage (VOC)
*	WD	48 20

*	WS	1E	23	00 80
*	WS	1E	24	20 03
*	WS	1E	25	8C 00
*	WS	1E	26	00 00
*	WS	1E	27	10 00

*	WD	0E 00
