#!/usr/bin/gawk -f
###############################################################################
# $Id: gds-csv-to-rigol.awk,v 1.15 2010/09/26 21:53:58 tw Exp $
#
# Script to convert to Rigol DG function generator formats.
#
# Copyright (C) 2009, 2010 Thomas Weidenfeller
# 
# This file is part of gds2000tools/gds-funcgen.
# 
# gds2000tools/gds-funcgen 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-funcgen 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-funcgen.
# If not, see <http://www.gnu.org/licenses/>.
###############################################################################


BEGIN {
	#
	# Tables
	#
	TXT = 0; CSV = 1; RDF = 2; SCPI = 3
	FORMAT_NAME[TXT]  	= "txt"
	FORMAT_NAME[CSV]  	= "csv"
	FORMAT_NAME[RDF]  	= "rdf"
	FORMAT_NAME[SCPI] 	= "scpi"

	N_TYPES = 0;
	DG1000    = N_TYPES++
	DG1000CH2 = N_TYPES++
	DG2000    = N_TYPES++
	DG3000    = N_TYPES++
	DG5000    = N_TYPES++

	DEV_TYPE_NAME[DG1000] 	= "DG1000"
	DEV_TYPE_NAME[DG1000CH2]= "DG1000CH2"
	DEV_TYPE_NAME[DG2000] 	= "DG2000"
	DEV_TYPE_NAME[DG2000] 	= "DG2000A"
	DEV_TYPE_NAME[DG3000] 	= "DG3000"
	DEV_TYPE_NAME[DG5000] 	= "DG5000"

	MIN_POINTS		= 2
	MAX_POINTS[DG1000]	= 4096		# CH1
	MAX_POINTS[DG1000CH2]	= 1024
	MAX_POINTS[DG2000]	= 524288
	MAX_POINTS[DG2000A]	= 524288
	MAX_POINTS[DG3000]	= 1048576
	# We are close to come into trouble with awk's internal usage of
	# floating point numbers for integers ...
	MAX_POINTS[DG5000]	= 128 * 1024 * 1024;

	#
	# Max Arb. frequencies. NOTE: These are lower than the specified
	# maximum sin frequencies.
	#
	MAX_F[DG1000]		=  5e6		#  5 MHz
	MAX_F[DG1000CH2]	=  5e6		#  5 MHz
	MAX_F[DG2000]		= 12e6		# 12 MHz (old 2021A only 8 MHz)
	MAX_F[DG3000]		= 25e6		# 25 MHz
	MAX_F[DG5000]		= 50e6		# 50 MHz (TODO: guess)

	#
	# Indices into meta-data array
	#
	NDX_VMIN0    = 0	# Ndx. original minimum voltage in input
	NDX_VMAX0    = 1	# Ndx. original maximum voltage in input
	NDX_V_DC_OFF = 2	# Ndx. DC offset of original data
	NDX_VMIN     = 3	# Ndx. minimum voltage after DC offset shift
	NDX_VMAX     = 4	# Ndx. maximum voltage after DC offset shift
	NDX_V2       = 5	# Ndx. Vpp/2 after DC offset shift
	NDX_VPP      = 6	# Ndx. Vpp after DC offset shift
	NDX_T        = 7	# Ndx. signal period of output data
	NDX_NTOT     = 8	# Ndx. number of output samples to generate

	#
	# Animation sequence
	#
	N_ANIM = 0
	ANIM[N_ANIM++] = "|"
	ANIM[N_ANIM++] = "/"
	ANIM[N_ANIM++] = "-"
	ANIM[N_ANIM++] = "\\"

	#
	# Parameters and defaults
	#

	# Application name in messages
	if(MY_NAME == "")	{ MY_NAME = "gds-csv-to-rigol" }

	# Default output format
	if(FORMAT == "")	{ FORMAT = TXT }

	# Total nbr of generated samples equals nbr of input samples
	if(MINIMIZE == "")	{ MINIMIZE = 1 }

	# No DC component adjustment
	if(DC == "")		{ DC = 0 }

	# No verbose messages
	if(VERBOSE == "")	{ VERBOSE = 0 }

	# Generator device type
	if(DEV_TYPE == "")	{ DEV_TYPE = DG1000 }

	# Bit resolution
	if(RES == "")		{ 
		if(DEV_TYPE == DG1000CH2) {
			RES = 10
		} else { 
			RES = 14
		}
	}

	#
	# I/O Setup
	#
	FS = ","	# Good enough for reading GW Instek CSV
	OLD_ORS = ORS
	ORS = "\r\n"	# Rigol uses DOS line endings

	#
	# Notes:
	# 1. Rigol happens to have a superfluous space appended to
	#    each lines in .txt format. This in done here, too.
	#
	# 2. Rigol seems to use the local decimal separator when
	#    writing and reading floating point data. This is
	#    bad. Because it essentially making the Rigol data format
	#    non-portable. 
	#
	#    To be "compatible" the same is done here, too. See
	#    the >>'<< in the floating point formats. However, the
	#    locale can be controlled by the wrapper running this
	#    AWK script.
	#
	# 3. UltraWave refuses to read the data if the "time" in the .txt
	#    file format is given with an exponent. Therefore, a separate
	#    format is used for the exponent.
	#
	# 4. A ridicules floating point resolution is used, because Rigol
	#    does so, too.
	#
	# 5. Rigol might have changed the data format for the VISA version
	#    of UltraWave. Since the VISA version is even more broken than
	#    the old VisualBasic UltraWave, this can't be verified, and
	#    the generated data might not work for UltraWave VISA at all.
	#
	INTFMT = "% d " ORS
	DATFMT = "% '.15G " ORS
	TPEFMT = "% '.15f " ORS	# frequency must not have exponent

	#
	# Misc. constants
	#
	SAMP_DIV = 25		# GDS samples per div
	PI = 4 * atan2(1,1)

	#
	# Global data initialization
	#
	NData = 0
	Datmin = 3.40282347e+38
	Datmax = -Datmax
	ExitCode = 0

	#
	# Initial messages
	#
	info0("--- Converting GDS CSV to Rigol UltraWave DG Data ---")
	info0("Configuration:")
	info0("   Device Type:             " DEV_TYPE_NAME[DEV_TYPE])
	info0("   Maximum nbr. of samples: " MAX_POINTS[DEV_TYPE])
	info0("   Format:                  Rigol UltraWave DG ."	\
	      				     FORMAT_NAME[FORMAT])
	info0("   DC adjustment:           " (DC ? "yes" : "no"))
	info0("   Minimum nbr. of samples: " (MINIMIZE ? "yes" : "no"))
}

#
# Print information for TXT and CSV output
# @param v	Array containing the sampling meta-data.
#
function printInfoTxtCsv(v) {
	if(!VERBOSE) { return }

	info0("   Maximum frequency:       " engNum(MAX_F[DEV_TYPE]) \
								"Hz")
	info0("   Sampling resolution:     " RES " bits (nominally)")
	info0("Data:")
	info0("   Min. Input Value         " Datmin)
	info0("   Max. Input Value         " Datmax)
	if(DC) {
		info0("   Original Vmin:           " engNum(v[NDX_VMIN0]) "V")
		info0("   Original Vmax:           " engNum(v[NDX_VMAX0]) "V")
		info0("   DC offsets adjustment    " engNum(-v[NDX_V_DC_OFF])\
								   "V")
		info0("   Vmin adjusted:           " engNum(v[NDX_VMIN]) "V")
		info0("   Vmax adjusted:           " engNum(v[NDX_VMAX]) "V")
	} else {
		info0("   Vmin:                    " engNum(v[NDX_VMIN]) "V")
		info0("   Vmax:                    " engNum(v[NDX_VMAX]) "V")
		info0("   Selected ampl. range:    -" engNum(v[NDX_V2])	\
		 		     	    "V ... +" engNum(v[NDX_V2]) "V")
	}
	info0("   Frequency:               " engNum(1 / v[NDX_T]) "Hz")
	info0("   Period:                  " engNum(v[NDX_T]) "s")
	info0("   Total nbr. of samples:   " v[NDX_NTOT] " of "	\
						MAX_POINTS[DEV_TYPE])
	info0("Data format " FORMAT_NAME[FORMAT] ":")
	info0("   Nbr. of samples used:    " MemLen " of " v[NDX_NTOT])
	info0("   Numeric Locale:          "			\
		 (ENVIRON["LC_ALL"] != "" ? ENVIRON["LC_ALL"]	\
		 : (ENVIRON["LC_NUMERIC"] != "" ?		\
		 ENVIRON["LC_NUMERIC"] : ENVIRON["LANG"])))
	info0("   Locale number format:    "			\
	      sprintf("%'.15G", 0.12345678901234567890))
}


#
# Calculate all the meta-data that is needed at one point or another
# from the input data read.
# @param v	[out] Array holding the meta-data.
#
function calcValues(v) {

	if(SampPeriod == 0) {
		# sampling period in seconds
		SampPeriod = HoriScale / SAMP_DIV
	}
	v[NDX_T] = SampPeriod * MemLen	
	if(1/v[NDX_T] > MAX_F[DEV_TYPE]) {
		die("Frequency (" engNum(1/v[NDX_T]) "Hz) "	\
		       "exceeds maximum frequency ("	\
		       engNum(MAX_F[DEV_TYPE]) "Hz)")
	}

	v[NDX_NTOT] = MINIMIZE ? MemLen : MAX_POINTS[DEV_TYPE]
	if(v[NDX_NTOT] < MIN_POINTS) {
		die("Not enough data (min. "MIN_POINTS" data points required)")
	}

	v[NDX_VMIN0]    = Datmin * VertScale / SAMP_DIV # + VertPos
	v[NDX_VMAX0]    = Datmax * VertScale / SAMP_DIV # + VertPos
	v[NDX_V_DC_OFF] = DC ? (v[NDX_VMIN0] + v[NDX_VMAX0]) / 2  : 0
	v[NDX_VMIN]     = v[NDX_VMIN0] - v[NDX_V_DC_OFF]
	v[NDX_VMAX]     = v[NDX_VMAX0] - v[NDX_V_DC_OFF]
	# Amplitude Voltage
	v[NDX_V2]       = max(abs(v[NDX_VMIN]), abs(v[NDX_VMAX]))
	if(v[NDX_V2] == 0) {
		v[NDX_V2] = 1			# Probably a constant DC
	}
	v[NDX_VPP]      = 2 * v[NDX_V2]		# Peak-to-peak Voltage
}


###############################################################################
# Main data printing functions
# 


#
# Print footer as present in .txt and .csv format
#
# UltraWave refuses to load a file if not at least one such set
# (usually used to specify which predefined waveform fragments were
# used in the file) is present.
#
# The footer written here, however, doesn't describe a predefined waveform
# fragment, but the data as such and possibly UltraWave default settings.
#
function printFooterTxtCsv() {
	printf INTFMT, 1		# waveform fragment or defaults?
					# 0: waveform fragment, 1: defaults?
	printf INTFMT, 0		# start of fragment?
	printf INTFMT, 0		# start of fragment?
	printf INTFMT, MemLen - 1	# last used position?
	printf INTFMT, 0		# waveform fragment type?
					# 0: sin, 1: rect, 2: ramp, 3: pulse
					# 4: expRise, 5: expFall, 6: noise
					# 7: sinc, 8: DC
	printf INTFMT, MemLen 		# dots used by this fragment?
	printf INTFMT, 0		# amplitude?
	printf INTFMT, 0		# offset?
	printf INTFMT, 0		# cycles?
	printf INTFMT, 0		# phase?
	printf INTFMT, 0		# duty cycle?
	printf INTFMT, 0		# pulse width?
	printf INTFMT, 0		# slope?
	printf INTFMT, 0		# symmetry?
	printf INTFMT, 0		# overzero?
	printf INTFMT, 0		# dampening?
}


###############################################################################
# Print TXT format
#
function printTxt(	i, v) {

	v[0] = 0
	calcValues(v)
	printInfoTxtCsv(v)

	#
	# original Rigol file header
	#
	print "Rigol Technologies,Inc. Save analog waveform to text files."
	printf INTFMT, v[NDX_NTOT]
	printf INTFMT, MemLen
	printf DATFMT, v[NDX_VPP]
	printf TPEFMT, T
	printf INTFMT, RES
	printf INTFMT, MemLen -1
	printf INTFMT, 1	# Nbr of predefined waveform fragments
				# used to construct the waveform
				# NOTE: UltraWave refuses to load a file
				#	if set to 0

	#
	# Formated data. Two lines per data point
	# Line 1: data point number
	# Line 2: value
	#
	for(i = 0; i < MemLen; i++) {
		printf INTFMT, i
		printf DATFMT, (Data[i] * VertScale / SAMP_DIV + VertPos - \
		       				v[NDX_V_DC_OFF]) / v[NDX_V2]
	}
	printFooterTxtCsv()
}


###############################################################################
# Print CSV format
#
function printCsv(	i, v) {

	nTot = MINIMIZE ? MemLen : MAX_POINTS[DEV_TYPE]

	v[0] = 0
	calcValues(v)
	printInfoTxtCsv(v)

	#
	# Original Rigol CSV header
	#
	print "Rigol Technologies,Inc. Save analog waveform to excel files."
				# original Rigol file header in excel
	printf "\"Vpp[V]:%'.15G\",\"Period[S]:%'.15f\",", v[NDX_VPP], v[NDX_T]
	printf "Quantify:%u,Data Dots:%u,Max Dots:%u,",RES, MemLen, v[NDX_NTOT]
	printf "End:%u,Wave:%u" ORS ORS "X,Y[V]" ORS,  MemLen -1, 1

	#
	# Write data
	# Two comma-separated values
	# Number, value
	#
	for(i = 0; i < MemLen; i++) {
		printf "%u,\"%'.15G\"" ORS,	\
		       i + 1, Data[i] * VertScale / SAMP_DIV + VertPos - \
		       						v[NDX_V_DC_OFF]
		if(VERBOSE) printf("%c\b", ANIM[i % N_ANIM]) > "/dev/stderr"
	}

	printFooterTxtCsv()
}


###############################################################################
#
# Print (write) RDF binary data or SCPI file
#
# RDF:
#
# Independent of the chosen resolution the raw data format consists of
# 16-bit big-endian unsigned binary numbers, where 14 bits of the
# 16 bits are used for values.
#
# Values indicate relative voltage levels. The format does not allow
# to encode absolute voltage levels. The voltage range is symmetrically
# centered around 0 V.
#
# The interpretation of the data differs between UltraWave and the
# actual function generator:
#
# UltraWave: A value of 0x0000 indicates the end of the used data.
# UltraWave doesn't load any data beyond the first 0x0000 in the file.
# So valid numbers are [0x0001 ... 0x2000 ... 0x3FFF].
#
# Generator: A value of 0x000 is interpreted as normal data (maximum
# negative voltage) and loaded. However, the following function always
# uses the range [0x0001 ... 0x2000 ... 0x3FFF], since this is a symmetric
# range around 0x2000, and in both cases 0x2000 indicates zero Volt.
#
# SCPI:
#
# SCPI uses the same value range as RDF, but as decimal values.
#
function printRdfOrScpi(	\
  nTot, dataprime, i, zero, s, datmini, datmaxi, tmp, binmin, binmax, v, \
  origMemLen) {

	nTot = MAX_POINTS[DEV_TYPE]
	if(RES != 14) {
		info0("   Configured Resolution:   " RES " bits (ignored)")
	}
	info0("   Used Resolution:         14 bits")
	info0("Data:")
	info0("   Min. Input Value         " Datmin)
	info0("   Max. Input Value         " Datmax)
	info0("   Nbr. of input samples:   " MemLen)
	origMemLen = MemLen
	if(!MINIMIZE) {
		#
		# Interpolate sampling values so the whole
		# data set is filled.
		#
		info0("Interpolating ...")
		dataprime[0] = 0 # make sure dataprime is an array
		sinter(Data, MemLen, dataprime, nTot)
		MemLen = nTot
		datmini = Datmin
		datmaxi = Datmax
		for(i = 0; i < nTot; i++) {
			tmp = dataprime[i]
			Data[i] = tmp
			datmini = min(datmini, tmp)
			datmaxi = max(datmaxi, tmp)
		}
		info0("   Min. Interpolated Value  " datmini)
		info0("   Max. Interpolated Value  " datmaxi)
		Datmin = datmini
		Datmax = datmaxi
	}

	info0("Data format " FORMAT_NAME[FORMAT] ":")
	info0("   Nbr. of samples used:    " MemLen " of " nTot)

	#
	# scale symmetrically around zero, to avoid shifting DC of 0 V
	#
	if(DC || FORMAT == SCPI) {
		zero = (Datmax + Datmin) / 2 
		tmp = Datmax - zero
	} else {
		zero = 0
		tmp = max(abs(Datmax), abs(Datmin))
	}
	s = tmp == 0 ? 0 : 0x1FFF / tmp

	binmin = 32767
	binmax = 0

	if(FORMAT == RDF) {
		for(i = 0; i < MemLen; i++) {
			tmp = int((Data[i] - zero) * s) + 0x2000
			binmin = min(binmin, tmp)
			binmax = max(binmax, tmp)
			printf "%c%c", tmp % 256, int(tmp / 256)
			if(VERBOSE) printf("%c\b", ANIM[i % N_ANIM]) > "/dev/stderr"
		}

		for(i = MemLen; i < MAX_POINTS[DEV_TYPE]; i++) {
			printf "%c%c", 0, 0
			if(VERBOSE) printf("%c\b", ANIM[i % N_ANIM]) > "/dev/stderr"
		}
		info0("   Minimum value written:   "	\
		      sprintf("0x%.4X", binmin) " (14 bits)")
		info0("   Maximum value written:   "	\
		      sprintf("0x%.4X", binmax) " (14 bits)")
      } else { # SCPI
      		printf "DATA:DAC VOLATILE"
		for(i = 0; i < MemLen; i++) {
			tmp = int((Data[i] - zero) * s) + 0x2000
			binmin = min(binmin, tmp)
			binmax = max(binmax, tmp)
			printf ",%d", tmp
			if(VERBOSE) printf("%c\b", ANIM[i % N_ANIM]) > "/dev/stderr"
		}
		print ""
		v[0] = 0
		MemLen = origMemLen
		calcValues(v)
		#printInfoTxtCsv(v)
		printf "FREQUENCY %.11f" ORS, 1 / v[NDX_T] # CH1 frequency
		print  "VOLTAGE:UNIT VPP"	      # Unit for amplitude data
		printf "VOLTAGE:LOW %G"  ORS, v[NDX_VMIN0]  # Min. Voltage.
		printf "VOLTAGE:HIGH %G" ORS, v[NDX_VMAX0]  # Max. Voltage.

		info0("   Minimum value written:   "	\
		      sprintf("%d", binmin) " (14 bits)")
		info0("   Maximum value written:   "	\
		      sprintf("%d", binmax) " (14 bits)")
      }
}


###############################################################################
# Output generation
#
END {
	if(ExitCode) {
		exit ExitCode # Had a fatal error during data reading
	}

	if(!WDFlag) {
		die("No waveform data found in input. Not a GDS CSV file?")
	}

	if(NData < 2) {
		die("No enough waveform samples found in input.")
	}

	if(HoriScale == 0 && SampPeriod == 0) {
		die("No sampling interval information in input.")
	}

	if(VertScale == 0) {
		die("No vertical scaling information in input.")
	}

	if(NData != MemLen) {
		die("Number of samples found (" NData ") " 		\
		    "doesn't match expected Waveform Length (" MemLen ").")
	}

	if(MemLen > MAX_POINTS[DEV_TYPE]) {
		die("Number of input samples (" MemLen ") exceeds number of "\
		       "possible samples in output waveform ("		     \
		       MAX_POINTS[DEV_TYPE] ") for " DEV_TYPE_NAME[DEV_TYPE] \
		       ".")
	}

	if(FORMAT == TXT) {
		printTxt()
	} else if(FORMAT == CSV) {
		printCsv()
	} else if(FORMAT == RDF) {
		printRdfOrScpi()
	} else if(FORMAT == SCPI) {
		printRdfOrScpi()
	}

	ORS = "\n"
}
