## Copyright (C) 2009, 2010 Thomas Weidenfeller
## 
## This file is part of gds2000tools/gds-octave.
## 
## gds2000tools/gds-octave is free software: you can redistribute it
## and/or modify it under the terms of the GNU General Public License
## version 3 as published by the Free Software Foundation.
## 
## gds2000tools/gds-octave 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 gds2000tools/gds-octave.  If not,
## see <http://www.gnu.org/licenses/>.
##

## usage: gds_analyse()
## usage: gds_analyse(DIRECTORY)
##
## Interactive tool to select GDS oscilloscope .CSV files and apply
## selected analysis functions to the data.
##
## NOTE: This is a highly experimental implementation with a rather
##	 inconvenient user interface.
##
## BUGS
##
## The ASCII pick-a-number menu system is rather limited.
##

## Author: Thomas Weidenfeller
## Created: 2009-07-19
## Version: $Id: gds_analyse.m,v 1.22 2010/12/31 02:28:33 tw Exp $
## Keywords: Oscilloscope, GDS-2000, gds2000tools, plot

function gds_analyse(start_dir)

	if(nargin > 1)
		print_usage();
	endif

	##
	## Check terminal window size
	##
	## TODO: take terminal height into account when building menu
	##
	ts = terminal_size();
	if(ts(1) < 24)
		error("Terminal window too small. At least 24 character height required.");
	endif
	if(ts(2) < 80)
		error("Terminal window too small. At least 80 character width required.");
	endif

	##
	## Reset plot window
	##
	hold off;
	subplot(1,1,1);
	plot([],[]);

	##
	## Load config variables
	##
	context = config_load();
	if(nargin > 0)
		context.current_dir = start_dir; # From function parameter
	endif

	##
	## We have an old-school ASCII pick-a-number interface
	## So lets do some old-school ASCII art first
	##
	## TODO: Use tput instead of hard-coded escape sequences.
	##
	system("clear; echo '\\033]0;gds2000tools/gds-octave gds_analyse\\007'");
	disp("");
disp("                   _                             _");
disp("                  | |                           | |");
disp("          __ _  __| |___ ______ __ _ _ __   __ _| |_   _ ___  ___");
disp("         / _` |/ _` / __|______/ _` | '_ \\ / _` | | | | / __|/ _ \\");
disp("        | (_| | (_| \\__ \\     | (_| | | | | (_| | | |_| \\__ \\  __/");
disp("         \\__, |\\__,_|___/      \\__,_|_| |_|\\__,_|_|\\__, |___/\\___|");
disp("          __/ |                                     __/ |");
disp("         |___/                                     |___/");

	first = true;


	##
	## Initialize file and directory
	##
	cwd = pwd();
	ok = true;
	try
		cd(context.current_dir);
	catch
		warning("%s not a directory, ignored.", context.current_dir);
	end_try_catch
	context.current_dir = pwd();	# to get full pathname
	last_dir = context.current_dir;
	cd(cwd);

	##
	## Menu states
	##
	S.SEL_FILE 		= 1;
	S.ANALYSE_SEL		= 2;
	S.FREQUENCY		= 3;
	S.DECODE		= 4;
	S.FILTER		= 5;
	S.FILTER_PARAMSEL	= 6;
	S.QUIT			= 99;

	context.state = S.SEL_FILE;
	while(context.state != S.QUIT)
		switch context.state
		case S.SEL_FILE
			files = glob([[ context.current_dir "/*.CSV" ];
				[ context.current_dir "/*.csv"] ]);
			f = @(x) x(:,[rindex(x, "/") + 1:length(x)]);
			files = cellfun(f, files, "UniformOutput", false);

			if(!first)
				system("clear");
			else
				first = false;
			endif
			selection = __gds_menu2([ "\n--- Select input file ---\nDirectory: " context.current_dir ],
			"-QUIT-",
			"-CHANGE DIRECTORY-",
			files{:});

			switch selection
			case 1
				context.state = S.QUIT;

			case 2	# Change dir
				fflush(stdout);
				d = deblank(input("\nNew directory [.|..|~|-|?|<name>]: ", "s"));
				switch d
				case ""
					continue;
				case "-"
					d = last_dir;
				case "?"
					printf("%s:\n", context.current_dir);
					ls(context.current_dir)
					prompt();
					continue;
				endswitch

				##
				## Normalize dir name
				##
				try
					cd(context.current_dir);
					cd(d);
					last_dir = context.current_dir;
					context.current_dir = pwd();
				catch
					warning("%s not a directory, ignored.", d);
					fflush(stderr);
					prompt();
				end_try_catch
				cd(cwd);

			otherwise
				current_file = files{selection - 2};
				printf("Loading %s ... \n", current_file);
				fflush(stdout);
				try
					current_gs = gds_csvread([ context.current_dir "/" current_file ]);
					subplot(1,1,1);
					gds_plot(current_gs);
					context.state = S.ANALYSE_SEL;
				catch
					clf();
					warning("Failed loading %s, ignored.", current_file);
					fflush(stderr);
					prompt();
				end_try_catch
			endswitch

		case S.ANALYSE_SEL
			context = State_Analyse_Sel(context,
				S,
				current_file,
				current_gs);

		case S.FREQUENCY
			context = State_Analyse_Frequency(context,
				S,
				current_file,
				current_gs);

		case S.DECODE
			context = State_Analyse_Dec(context,
				S,
				current_file,
				current_gs);

		case S.FILTER
			context = State_Analyse_Filter(context,
				S,
				current_file,
				current_gs);

		case S.FILTER_PARAMSEL
			context = State_Analyse_Filter_Paramsel(context,
				S,
				current_file,
				current_gs);
		otherwise
			break;
		endswitch
	endwhile

	config_save(context);

	system("echo '\\033]0;octave\\007'");	## TODO: use tput
endfunction

###############################################################################
## Load/store gds-analyse config variables (currently unused)
###############################################################################

##
## Initialize gds-analyse global status variables, then try to override
## them by loading from the rc file
##
function data = config_load() 
	try
		load("-text", "~/.gds-analyserc", "data");
	catch
		disp("No configuration file.");
		data = struct();
	end_try_catch

	defaults = {
		"current_dir",		pwd();
		"flt_type",		"Lowpass";
		"flt_char",		"Chebyshev II";
		"flt_order",		10;
		"flt_f1",		44.8e3;
		"flt_f2",		100e3;
		"flt_ripple_pass",	1;
		"flt_ripple_stop",	10;
	};
	
	for i = 1:rows(cellfun("size", defaults, 1))
		key = defaults{i,1};
		if(!isfield(data, key))
			data = setfield(data, key, defaults{i, 2});
		endif
	endfor
endfunction

##
## Store the current values of the gds-analyse global variables
##
function config_save(data)
	data = rmfield(data, { "state" });
	try
		save("-text", "~/.gds-analyserc", "data");
	catch
		disp("Could not write configuration file.");
	end_try_catch
endfunction




###############################################################################
## STATE: Analysis Selection (and execution)
###############################################################################
function context = State_Analyse_Sel(context, S, fn, gs)
	system("clear");
	selection = __gds_fmenu2( [ "\n--- Select Analysis function for '", fn "' ---" ],
	    {	"-QUIT-",			S.QUIT;
		"-BACK (file selection)-",	S.SEL_FILE;
		"Plot",				@fplot
		#"Spectrum",			@fplotspectrum;
		#"Power Spectral Density (Welch)",@fpsd;
		"AC Component only",		@fac;
		"DC value",			@fdc;
		"Eye Diagram",			@gds_eye;
		"Histogram",			@fhistogram;
		"Inverse",			@finv;
		"Sign Function",		@fsig;
		"Sign Function AC",		@fsigac;
		"Square wave",			@fsqwave;
		"Square wave AC",		@fsqwaveac;
		"Statistics",			@fstat; ## TODO: own submenu with histogram, too?
		"Zero Crossings",		@fzcross;
		"Zero Crossings AC",		@fzcrossac;
		"Raw data"			@fdisp;
		"Decode, Demodulate ..."	S.DECODE;
		"Filter ..."			S.FILTER;
		"Frequency Analysis ..."	S.FREQUENCY }
	);

	if(isnumeric(selection))
		context.state = selection;
	else
		##
		## octave 3.0.1 has no is...() test for function handles, so
		## just try our luck
		##
		selection(gs);
	endif
endfunction

###############################################################################
## Analysis Functions
###############################################################################

## Plot signal
function fplot(gs)
	subplot(1,1,1);
	gds_plot(gs);
endfunction

## Plot AC component only
function fac(gs)
	analyse_plot(gs, @gds_ac);
endfunction

## DC value
function fdc(gs)
	printf("\n=> VDC = %G V\n", gds_dc(gs));
	fflush(stdout);
	prompt;
endfunction

## Histogram
function fhistogram(gs)
	subplot(2,1,1);
	gds_plot(gs);
	subplot(2,1,2);
	hist(gds_struct2val(gs), 64)
	title([ "Histogram " gs.Source " " gs.Horizontal_Mode " " gs.Time ]);
	xlabel([ "Amplitude / " gs.Vertical_Units ]);
	ylabel("Nbr. Samples");
	axis("on");
	grid("on");
endfunction

## Plot inverse signal
function finv(gs)
	analyse_plot(gs, @gds_inv);
endfunction

## Plot signum function
function fsig(gs)
	analyse_plot(gs, @gds_sign);
endfunction

## Plot signum function, AC component only
function fsigac(gs)
	sign_ac = @(x) gds_sign(gds_ac(x));
	analyse_plot(gs, sign_ac);
endfunction

##
## Convert signal into a square wave.
## Simulates the output of a comparator.
##
function fsqwave(gs)
	subplot(2,1,1);
	gds_plot(gs);

	one_volt = gds_val2wd(1, gs.Vertical_Scale);
	L = __gds_memory_length(gs);
	wd = zeros(1, L);
	wd(find(gs.Waveform_Data > 0)) = one_volt;
	wd(find(gs.Waveform_Data < 0)) = -one_volt;
	gs.Waveform_Data = wd;
	gs.Source = [ gs.Source " Square wave" ];
	subplot(2,1,2);
	gds_plot(gs);
endfunction

##
## Convert signal into a square wave.
## Simulates the output of a comparator. AC component only.
##
function fsqwaveac(gs)
	subplot(2,1,1);
	gds_plot(gs);

	one_volt = gds_val2wd(1, gs.Vertical_Scale);
	L = __gds_memory_length(gs);
	wd = zeros(1, L);
	th = mean(gs.Waveform_Data);
	wd(find(gs.Waveform_Data > th)) = one_volt;
	wd(find(gs.Waveform_Data < th)) = -one_volt;
	gs.Waveform_Data = wd;
	gs.Source = [ gs.Source " Square wave AC" ];
	subplot(2,1,2);
	gds_plot(gs);
endfunction

##
## Some Statistic values
##
function fstat(gs)
	v = gds_struct2val(gs);

	unit = gs.Vertical_Units;
	
	printf("\nNbr. of Samples    = %g\n",    __gds_memory_length(gs));
	printf("Standard Deviation = %g %s\n",   std(v),    unit);
	printf("Mean (DC)          = %g %s\n",   mean(v),   unit);
	printf("Variance           = %g %s^2\n", var(v),    unit);
	printf("Median             = %g %s\n",   median(v), unit);
	n = min(v);
	x = max(v);
	printf("Minimum            = %g %s\n", n,      unit);
	printf("Maximum            = %g %s\n", x,      unit);
	printf("Peak to Peak       = %g %s\n", x .- n, unit);
	fflush(stdout);
	prompt;
endfunction

## Zero Crossings
function fzcross(gs)
	subplot(1,1,1);
	gds_zcross(gs);
endfunction

## Zero Crossings, AC component only
function fzcrossac(gs)
	subplot(1,1,1);
	gds_zcross(gds_ac(gs));
endfunction

## Display raw data
function fdisp(gs)
	old_psc = page_screen_output(1);
	gs
	page_screen_output(old_psc);
	prompt;
endfunction

###############################################################################
## STATE: Filtering Analysis Selection and execution
###############################################################################
function context = State_Analyse_Filter(context, S, fn, gs)
	system("clear");
	printf("  Filter: %s %s\n", context.flt_char, context.flt_type);
	printf("  Filter Order: %d\n", context.flt_order);
	printf("  (Low) Corner Frequency 1: %g Hz   High Corner Frequency 2: %g Hz\n", context.flt_f1, context.flt_f2);
	printf("  Passband Ripple: %.1f dB   Stopband Ripple: %.1f dB\n", context.flt_ripple_pass, context.flt_ripple_stop);
	printf("  [not all parameters are used by all filters]\n");

	selection = __gds_fmenu2( [ "\n--- Select Filter function for '", fn "' ---" ],
	    {	"-QUIT-",			S.QUIT;
		"-BACK (Analysis)-",		S.ANALYSE_SEL;
		"APPLY FILTER",			@fapplfilt;
		#"Bessel",			"cBessel"; Requires a later signal package
		"Butterworth",			"cButterworth";
		"Chebyshev I",			"cChebyshev I";
		"Chebyshev II",			"cChebyshev II";
		"Elliptic",			"cElliptic";
		"Lowpass",			"tLowpass";
		"Highpass",			"tHighpass";
		"Bandpass",			"tBandpass";
		"Bandstop",			"tBandstop";
		"Filter parameter ...",		S.FILTER_PARAMSEL;
	    }
	);


	if(isnumeric(selection))
		context.state = selection;
	elseif(ischar(selection))
		if(selection(1) == "c")
			context.flt_char = substr(selection, 2);
		else
			context.flt_type = substr(selection, 2);
		endif
	else
		selection(gs, context);
	endif
endfunction

function fapplfilt(gs, context)
	dt = __gds_sampling_period(gs);

	Wc = 2 * context.flt_f1 * dt;
	if(Wc > 1) 
		printf("Filter corner frequency %g exceeds 1/2 sampling frequency %g\n", context.flt_f1, 1/dt);
		prompt();
		return;
	endif

	Wh = 2 * context.flt_f2 * dt;

	switch context.flt_type
	case "Lowpass"
		type = "low";	# Undocumented
		W = Wc;
	case "Highpass"
		type = "high";
		W = Wc;
	otherwise
		if(Wh > 1) 
			printf("Upper filter corner frequency %g exceeds 1/2 sampling frequency %g\n", context.flt_f2, 1/dt);
			prompt();
			return;
		endif
		if(Wh <= Wc)
			printf("Lower filter corner frequency %g exceeds upper filter corner frequency %g\n", context.flt_f1, context.flt_f2);
			prompt();
			return;
		endif
		switch context.flt_type
		case "Bandpass"
			type = "pass";	# Undocumented
			W = [ Wc, Wh ];
		case "Bandstop"
			type = "stop";
			W = [ Wc, Wh ];
		otherwise
			error("Unknown filter type %s", context.flt_type);
		endswitch
	endswitch

	switch context.flt_char
	case "Butterworth"
		[b, a] = butter(context.flt_order, W, type);
	case "Bessel"		# not in signal 1.0.7
		[b, a] = besself(context.flt_order, W, type);
	case "Chebyshev I"
		[b, a] = cheby1(context.flt_order, context.flt_ripple_pass, W, type);
	case "Chebyshev II"
		[b, a] = cheby2(context.flt_order, context.flt_ripple_stop, W, type);
	case "Elliptic"
		[b, a] = ellip(context.flt_order, context.flt_ripple_pass, context.flt_ripple_stop, W, type);
	otherwise
		error("Unknown filter %s", context.flt_char);
	endswitch

	gsfilt = gs;
	gsfilt.Waveform_Data = filter(b, a, gs.Waveform_Data);
	gsfilt.Source = [ gsfilt.Source " - filtered " context.flt_char " " context.flt_type ];

	gds_plot(gs, gsfilt);

endfunction


###############################################################################
## STATE: Filtering Parameter Selection
###############################################################################
function context = State_Analyse_Filter_Paramsel(context, S, fn, gs)
	fflush(stdout);
	s = input([ "\nFilter Order [" num2str(context.flt_order) "]: " ], "s");
	n = str2num(s);
	if(n >= 1 && n <= 20)
		context.flt_order = int8(n);
	endif;
	s = input([ "(Low) Corner frequency [" num2str(context.flt_f1) " Hz]: " ], "s");
	n = str2num(s);
	if(!isempty(n))
		context.flt_f1 = n;
	endif
	s = input([ "High Corner frequency [" num2str(context.flt_f2) " Hz]: " ], "s");
	n = str2num(s);
	if(!isempty(n))
		context.flt_f2 = n;
	endif
	s = input([ "Passband Ripple [" num2str(context.flt_ripple_pass) " dB]: " ], "s");
	n = str2num(s);
	if(n >= 0 && n <= 100)
		context.flt_ripple_pass = n;
	endif;
	s = input([ "Stopband Ripple [" num2str(context.flt_ripple_stop) " dB]: " ], "s");
	n = str2num(s);
	if(n >= 0 && n <= 100)
		context.flt_ripple_stop = n;
	endif;

	context.state    = S.FILTER;
endfunction



###############################################################################
## STATE: Frequency Analysis
###############################################################################
function context = State_Analyse_Frequency(context, S, fn, gs)
	system("clear");
	selection = __gds_fmenu2( [ "\n--- Select Frequency Analysis function for '", fn "' ---" ],
	    {	"-QUIT-",			S.QUIT;
		"-BACK (Analysis)-",		S.ANALYSE_SEL;
		"Main Frequency",		@fmainfreq;
		"Power Spectral Density (Welch)",@fpsd;
		"Spectrum",			@fplotspectrum;
	    }
	);

	if(isnumeric(selection))
		context.state = selection;
	else
		##
		## octave 3.0.1 has no is...() test for function handles, so
		## just try our luck
		##
		selection(gs);
	endif
endfunction

## Calculate and plot main frequency
function fmainfreq(gs)
	gds_plot(gs, gds_mainfreq(gs));
endfunction

## Plot spectrum
function fplotspectrum(gs)
	analyse_plot(gs, @gds_plotspectrum);
endfunction

## Power Spectral Density
function fpsd(gs)
	subplot(2,1,1);
	gds_plot(gs);
	subplot(2,1,2);
	pwelch(gds_struct2val(gs),[],[],[],1/gs.Sampling_Period,"dB");
endfunction

###############################################################################
## STATE: Decoding / Demodulating Analysis Selection (and execution)
###############################################################################
function context = State_Analyse_Dec(context, S, fn, gs)
	system("clear");
	selection = __gds_fmenu2( [ "\n--- Select Decoding, Demodulating function for '", fn "' ---" ],
	    {	"-QUIT-",			S.QUIT;
		"-BACK (Analysis)-",		S.ANALYSE_SEL;
		"AM",				@fam;
		"FM (estimate, 44.8k LP)",	@ffm;
		"Estimate binary bitrate",	@fbitrate;
#		"FSK",				S.ANALYSE_SEL;
	    }
	);

	if(isnumeric(selection))
		context.state = selection;
	else
		##
		## octave 3.0.1 has no is...() test for function handles, so
		## just try our luck
		##
		selection(gs);
	endif
endfunction

## Plot AM demodulated signal
function fam(gs)
	analyse_plot(gs, @gds_am);
endfunction

## Plot FM demodulated signal
function ffm(gs)
	analyse_plot(gs, @gds_fm);
endfunction

## Print estimated bitrate
function fbitrate(gs)
	printf("\nTs = %.0f Bd\n", gds_bitrate(gs));
	fflush(stdout);
	prompt;
endfunction

###############################################################################
## Perform analysis and plot
###############################################################################
function analyse_plot(gs, p_plotfunc)
	subplot(2,1,1);
	gds_plot(gs);
	subplot(2,1,2);
	p_plotfunc(gs);
endfunction
