iax2ana.rb
#-------------------------------------------------------------------------
# IAX2 Call Analyzer
#    IAX2 call analysis (draw bandwidth, jitter, delay, loss, plots)
#
# You may use this code freely in your commercial and non-commercial work.
# 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.  
#
# Copyright (c) Unleash Networks 2005, All rights reserved
#----------------------------------------------------------------------------
# Tim.V  2/2/06 Unleash Networks http://www.unleashnetworks.com
 
require 'rubygems'
require 'win32ole'
require 'fox16'
include Fox
require 'UnleashCharts'
include UnleashCharts
 
 
# global constants
USEC_PER_SEC=1000000
MSEC_PER_SEC=1000
USEC_PER_MSEC=1000
IAX_COL_COUNT=9
DEBUG_MODE=false
 
class ChartWindow < FXMainWindow
 
	@tabBook
	@table
	@analysisIndex
	@chartsFwd
	@chartsRev
 
	def initialize(theapp)
 
			# base class
			super(theapp, "IAX2 Call Analysis (c) Unleash Networks", nil, nil, DECOR_ALL,
						0,0,700,600)
 
 
			labelfont = FXFont.new(getApp(), "verdana", 10, FONTWEIGHT_BOLD)
 
			# main parent frame
			@main = FXVerticalFrame.new(self, FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y)
			@label = FXLabel.new(@main, "IAX2 Calls (double click on a call to analyze)" , nil, LAYOUT_FILL_X)
			@label.justify = JUSTIFY_LEFT
			@label.font=labelfont
			@label2 = FXLabel.new(@main, "Currently showing" , nil, LAYOUT_FILL_X)
 
			# Main window interior
			@splitter = FXSplitter.new(@main, (LAYOUT_SIDE_TOP|LAYOUT_FILL_X| LAYOUT_FILL_Y|SPLITTER_VERTICAL|SPLITTER_TRACKING))
 
 
 
			@tableFrame = FXVerticalFrame.new(@splitter,
			FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y)
 
			@tabBook = FXTabBook.new(@splitter, nil, 0,
				      LAYOUT_FILL_X|LAYOUT_FILL_Y)
 
			# raw data 
			@table = FXTable.new(@tableFrame,nil,0,
				TABLE_COL_SIZABLE|LAYOUT_FILL_X|LAYOUT_FILL_Y,
				0,0,0,0, 10,10,10,10)
 
 
			@table.setTableSize(0, 4)
			@table.setBackColor(FXRGB(255, 255, 255))
			@table.setCellColor(0, 0, FXRGB(255, 255, 255))
			@table.setCellColor(0, 1, FXRGB(255, 255, 255))
			@table.setCellColor(1, 0, FXRGB(210, 210, 210))
			@table.setCellColor(1, 1, FXRGB(210, 210, 215))
 
			@splitter.setSplit(0, 130)
 
			@table.connect(SEL_DOUBLECLICKED,method(:onDblclicked))
 
			@chartsFwd=Hash.new			
			@chartsRev=Hash.new
 
	end
 
 
	def create
		super
		show(PLACEMENT_SCREEN)
	end	
 
 
	def setModel (mod)
		@canvas.setModel( mod)
	end
 
	def addChart (name, model)
 
			tab = FXTabItem.new(@tabBook, name, nil) 
 
			canvasFrame = FXVerticalFrame.new(@tabBook,
				FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y, 0,0,0,0, 0,0,0,0)
 
			canvas = UnTimeSeriesChart.new(canvasFrame)
			canvas.setModel( model)
			canvas.title="Both Directions"
 
			@chartsFwd[name]=canvas
 
	end
 
	def addChartBoth (name, modelFwd, modelRev)
 
			tab = FXTabItem.new(@tabBook, name, nil) 
 
			canvasFrame = FXVerticalFrame.new(@tabBook,
				FRAME_SUNKEN|FRAME_THICK|LAYOUT_FILL_X|LAYOUT_FILL_Y, 0,0,0,0, 0,0,0,0)
 
			canvas1 = UnTimeSeriesChart.new(canvasFrame)
#			canvas1.backColor = FXRGB(255,255,255)
#			canvas1.drawColor = FXRGB(0,0,0)
			canvas1.setModel( modelFwd)
			canvas1.title="Forward Direction"
 
			canvas2 = UnTimeSeriesChart.new(canvasFrame)
#			canvas2.backColor = FXRGB(255,255,255)
#			canvas2.drawColor = FXRGB(0,0,0)
			canvas2.setModel( modelRev)
			canvas2.title="Reverse Direction"
 
			@chartsFwd[name]=canvas1
			@chartsRev[name]=canvas2
 
 
	end
 
	def initTable
			@table.rowHeaderMode=LAYOUT_NORMAL
 
			@table.setColumnText(0,"From->To IP, Call#")
			@table.setColumnText(1,"Duration")
			@table.setColumnText(2,"Calling #")
			@table.setColumnText(3,"Clng Nm")
			@table.setColumnText(4,"Calld #")
			@table.setColumnText(5,"DNID")
			@table.setColumnText(6,"User")
			@table.setColumnText(7,"Codec")
			@table.setColumnText(8,"Started")
 
			@table.setColumnWidth(0,280)
			@table.setColumnWidth(1,50)
			@table.setColumnWidth(2,80)
			@table.setColumnWidth(3,80)
			@table.setColumnWidth(4,80)
			@table.setColumnWidth(5,40)
			@table.setColumnWidth(6,60)
			@table.setColumnWidth(7,100)
			@table.setColumnWidth(8,120)
 
	end
 
	def loadCalls(ana)
		tableSize(ana.numValidCalls)
		initTable
		ana.loadCalls(@table)	
		@analysisIndex=ana
	end
 
	def tableSize(rows)
		@table.setTableSize(rows,IAX_COL_COUNT)
	end
 
 
	def onDblclicked(sender,sel,pos)
		showCall(pos.row)
 
	end
 
	def showCall (callnum)
 
		call = @analysisIndex.getCompletedCall(callnum)
 
		print "showing call #{callnum}\n"
		@label2.text = "Currently Showing " + call.callDescr + " (# #{callnum + 1} in list)"
 
		if ( @chartsFwd.size()==0)
			addChartBoth("Call Bandwidth",CallBandwidthModel.new(call,0,InputFile),CallBandwidthModel.new(call,1,InputFile))
			addChartBoth("Delay",DelayModel.new(call,0,InputFile),DelayModel.new(call,1,InputFile))
			addChartBoth("Jitter",JitterModel.new(call,0,InputFile),JitterModel.new(call,1,InputFile))
			addChartBoth("Loss",LossModel.new(call,0,InputFile),LossModel.new(call,1,InputFile))
			addChart("IAX2 Events",CallEventModel.new(call,0,InputFile))
		else
			@chartsFwd["Call Bandwidth"].setModel(CallBandwidthModel.new(call,0,InputFile))
			@chartsFwd["Delay"].setModel(DelayModel.new(call,0,InputFile))
			@chartsFwd["Jitter"].setModel(JitterModel.new(call,0,InputFile))
			@chartsFwd["Loss"].setModel(LossModel.new(call,0,InputFile))
			@chartsFwd["IAX2 Events"].setModel(CallEventModel.new(call,0,InputFile))
			@chartsRev["Call Bandwidth"].setModel(CallBandwidthModel.new(call,1,InputFile))
			@chartsRev["Delay"].setModel(DelayModel.new(call,1,InputFile))
			@chartsRev["Jitter"].setModel(JitterModel.new(call,1,InputFile))
			@chartsRev["Loss"].setModel(LossModel.new(call,1,InputFile))
 
		end
	end
 
end
 
#-----------------------------------------
# IAXCallLeg class
#-----------------------------------------
class IAXCallLeg
	attr_accessor  :src_ip
	attr_accessor  :src_call_no
 
 
end
 
#------------------------------------------
# IAXCall class 
#------------------------------------------ 
class IAXCall
 
	@callLegs
 
	attr_accessor :calling_no, :calling_name
	attr_accessor :called_no, :dnid
	attr_accessor :username
	attr_accessor :codec
	attr_accessor :startTimeSecs, :startTimeUSecs
	attr_accessor :endTimeSecs,:endTimeUSecs
	attr_accessor :startPacketID, :endPacketID
	attr_accessor :fHungUp
 
	def initialize
		@callLegs = Array.new
		@fHungUp = false
	end
 
	def match (ip , callno)
		@callLegs.each do |c|
			if (c.src_ip == ip && c.src_call_no==callno)
				return true
			end
		end
		return false	
	end
 
	def createCallLeg(sip, scall)
		leg = IAXCallLeg.new
		leg.src_ip = sip
		leg.src_call_no = scall
 
		@callLegs << leg
	end
 
	def isCompleted
		@callLegs.size == 2
	end
 
	def isActive?
		return !fHungUp
	end
 
	def matchLeg (legno, ip, call)
		if legno >= @callLegs.size
			return false
		end
		leg = @callLegs[legno]
		return leg.src_ip == ip && leg.src_call_no = call	
	end
 
	def matchAnyLeg (ip, call)
		if @callLegs.size == 2
			return matchLeg(0,ip,call) || matchLeg(1,ip,call)
		else
			return matchLeg(0,ipcall)
		end
		return false
	end
 
	def printCall
 
 
		print "----- IAX Call --------\n"
		if isCompleted
			print "Start Time: #{Time.at(@startTimeSecs,@startTimeUSecs)}\n"
		end
 
		print "Calling No: #{@calling_no}  Calling Name: #{@calling_name}\n"
		print "Called No: #{@called_no}  DNID=#{@dnid}\n"
		print "Username: #{@username}\n"
		print "Codec used: #{@codec}\n"
 
		@callLegs.each do |leg|
			print "\tCall Leg "
			print "\tIP Address: #{leg.src_ip}  Call No: #{leg.src_call_no}\n"
		end
		print "------------------------\n\n"
	end
 
 
	def addToTable(tab,row)
			i=row
 
			#call description
			calldescr = callDescr
			duration_s = (@endTimeSecs-@startTimeSecs).to_s + " secs"
			tab.setRowText(i,(row+1).to_s)
			tab.setItemText(i,0,calldescr)	  # ipfrom
			tab.setItemText(i,1,duration_s)	  	  # analysis
			tab.setItemText(i,2,@calling_no)	  # callingname
			tab.setItemText(i,3,@calling_name) 	  # payload bytes 
			tab.setItemText(i,4,@called_no) 	  # was RTT updated due to this segment
			tab.setItemText(i,5,@dnid)	  	  # analysis
			tab.setItemText(i,6,@username)	  # analysis
			tab.setItemText(i,7,@codec)	  	  					# analysis
			tab.setItemText(i,8,Time.at(@startTimeSecs,@startTimeUSecs).to_s)	 # analysis
 
			(0..8).each do|col|
				tab.getItem(i,col).justify = FXTableItem::LEFT
			end
 
	end
 
	def callDescr
		leg1 = @callLegs[0]
		leg2 = @callLegs[1]
 
		"#{leg1.src_ip} -> #{leg2.src_ip} [#{leg1.src_call_no} -> #{leg2.src_call_no}]"
	end
 
	def sampleInterval
 
		codecProps = { 	"G.723.1" 		=> 30,
					"GSM" 		=> 20,
					"G.711u" 		=> 20,
					"G.711a" 		=> 20,
					"G.726" 		=> 20,
					"IMA ADPCM" 	=> 20,
					"16bit LLE" 	=> 20,
					"LPC10"		=> 20,
					"G.729" 		=> 10,
					"SPEEX" 		=> 20,
					"ILBC" 		=> 30,
				  }
 
 
		codecProps[codec]
	end
 
 
end
 
#----------------------------------
# IAXAnalyzer class
#----------------------------------
class IAXAnalyzer
 
	@capfile		# capture file name
	@calls		# all calls found in capture file
 
	def initialize (filename)
 
		@capfile = filename
		@calls = Array.new
		@callCacheCheck=nil 	# to speed up matching calls
 
	end
 
	# - scan the capture file looking for completed calls (IAX messages NEW and ACCEPT 
	def extract_calls
		unsniffDB = WIN32OLE.new("Unsniff.Database")
		unsniffDB.OpenForRead(@capfile)
 
		allPackets = unsniffDB.PacketIndex
		count = allPackets.Count
		(0..count-1).each do |i|
			packet = allPackets.Item(i)
			iaxlayer = packet.FindLayer("IAX2")
			if (iaxlayer)
				findIAXCall(packet)
			end
		end
 
		# need to end incomplete calls
		endIncompleteCalls(allPackets.Item(count-1))
 
		unsniffDB.Close
 
	end
 
 
	# - use the incoming iax2 packet for call information
	def findIAXCall(iaxpacket)
 
 
		iaxlayer = iaxpacket.FindLayer("IAX2")
 
		# only IAX control messages make sense to us
		frametype = iaxlayer.FindField("Frametype")
		if (!frametype)
			# we dont care about mini frames at the moment 
			return
		end
 
		if  frametype.value =~ /IAX Control/
 
			sclass = iaxlayer.FindField(">FULL FRAME>SC_IAX>IAX Subclass")
			if (!sclass) 
				print "Error in capture file, expecting subclass value\n"
				return
			end
 
			case sclass.value
				when /NEW/
					procIAXCtlNew(iaxpacket)
					print "Found New Frame #{iaxpacket.ID}\n" if DEBUG_MODE
 
				when /ACCEPT/
					procIAXCtlAccept(iaxpacket)
					print "Found ACCEPT Frame #{iaxpacket.ID}\n" if DEBUG_MODE
 
				when /HANGUP/
					procIAXCtlHangup(iaxpacket)
					print "Found HANGUP Frame #{iaxpacket.ID}\n" if DEBUG_MODE
 
				else
					return
			end
		end
 
	end
 
 
	# a new call is being initiated, check if one already exists if not insert call 
	def procIAXCtlNew(iaxpacket)
 
		iplayer = iaxpacket.FindLayer("IP")
		iaxlayer = iaxpacket.FindLayer("IAX2")
 
		ipfrom = iplayer.FindField("Source IP")
		iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number")
 
		if (! findActiveCall(ipfrom.value,iaxscall.value) )
			initiateNewCall(iplayer, iaxlayer)
		end	
	end
 
	# a new call was initiated as a result of the IAX control NEW message
	def initiateNewCall(iplayer,iaxlayer)
 
		c = IAXCall.new
 
		f_calling_no = iaxlayer.FindField("CALLING NUMBER")
		f_calling_name = iaxlayer.FindField("CALLING NAME")
		f_called_no = iaxlayer.FindField("CALLED NUMBER")
		f_dnid = iaxlayer.FindField("DNID")
		f_username = iaxlayer.FindField("USERNAME")
 
		c.calling_no = f_calling_no.value if f_calling_no
		c.called_no = f_called_no.value if f_called_no
		c.dnid = f_dnid.value if f_dnid
		c.username = f_username.value if f_username
		c.calling_name = f_calling_name.value if f_calling_name
 
		ipfrom = iplayer.FindField("Source IP")
		iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number")
		c.createCallLeg(ipfrom.value, iaxscall.value)
 
		@calls << c
	end
 
 
	# a call was completed via an ACCEPT message
	def procIAXCtlAccept(iaxpacket)
 
		iplayer = iaxpacket.FindLayer("IP")
		iaxlayer = iaxpacket.FindLayer("IAX2")
 
		ipfrom = iplayer.FindField("Source IP")
		iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number")
 
		ipto= iplayer.FindField("Dest IP")
		iaxdcall = iaxlayer.FindField(">FULL FRAME>Dest Call Number>Destination Call Number")
 
		c = findActiveCall(ipto.value,iaxdcall.value) 
		if (!c)
			# found an accept with no corresponding new
			return
		end
 
		if (! c.isCompleted() )
			completeCall(c,iaxpacket)
		end	
 
		c.startPacketID = iaxpacket.ID
	end
 
 
	# complete call as a result of IAX ctrl ACCEPT message
	def completeCall(c,iaxpacket)
 
		iplayer = iaxpacket.FindLayer("IP")
		iaxlayer = iaxpacket.FindLayer("IAX2")
 
		f_format = iaxlayer.FindField("FORMAT")
		f_flags = f_format.SubFields
		f_flags.each do |f|
			if f.value =~ /1/
				c.codec = f.name
			end
		end
 
		c.startTimeSecs  = iaxpacket.TimestampSecs
		c.startTimeUSecs = iaxpacket.TimestampUSecs
 
 
		ipfrom = iplayer.FindField("Source IP")
		iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number")
		c.createCallLeg(ipfrom.value, iaxscall.value)
 
	end
 
 
	# hangup call as a result of IAX ctrl HANGUP message
	def procIAXCtlHangup(iaxpacket)
 
		iplayer = iaxpacket.FindLayer("IP")
		iaxlayer = iaxpacket.FindLayer("IAX2")
 
		ipfrom = iplayer.FindField("Source IP")
		iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number")
 
		c = findActiveCall(ipfrom.value,iaxscall.value) 
		if ( c )
			c.endTimeSecs  = iaxpacket.TimestampSecs
			c.endTimeUSecs = iaxpacket.TimestampUSecs
			c.endPacketID = iaxpacket.ID
			c.fHungUp=true
		end	
 
	end
 
	# callExists (with perfomance enhancement cache)
	def findActiveCall(sourceip, sourcecall)
 
		# check the last checked call first (mostly we will find a hit here)
		if @callCacheCheck
			if @callCacheCheck.isActive? && @callCacheCheck.match(sourceip,sourcecall)
				return @callCacheCheck
			end
		end
 
		# search the call list
		@calls.each do |c|
 
			if (c.isActive? && c.match(sourceip,sourcecall))
				@callCacheCheck=c
				return c
			end
 
		end
 
		@callCacheCheck=nil
		return nil
	end
 
 
	# print all calls
	def printCalls
		@calls.each do|c|
			c.printCall
		end
	end
 
 
	# load all calls into the table
	def loadCalls(tab)
		r=0
		@calls.each do |c|
			if (c.isCompleted())
				c.addToTable(tab,r)
				r=r+1
			end
		end
 
	end
 
	# get completed call by index
	def getCompletedCall(idx)
		r=0
		@calls.each do |c|
			if c.isCompleted() 
				if r==idx
					return c
				end
				r=r+1
			end
 
		end
		print "getCompletedCall: cannot find call number #{idx}\n"
		nil
	end
 
	# num valid calls
	def numValidCalls
		r=0
		@calls.each do |c|
			r=r+1  if c.isCompleted()
		end
		r
	end
 
	# first call
	def demoCall
		@calls[6]
	end
 
	# set end time to last packet if we cant find Hangup message
	def endIncompleteCalls (lastpacket)
		@calls.each do|c|
			if ! c.endTimeSecs
				c.endTimeSecs = lastpacket.TimestampSecs
				c.endTimeUSecs = lastpacket.TimestampUSecs
				c.endPacketID = lastpacket.ID
			end
		end
	end
 
end
 
# ---------------------------------
# Statistics 
# ---------------------------------
class ModelBase
 
	CHART_FLAG_NORMAL=0
	CHART_FLAG_VOICE=1
	CHART_FLAG_CONTROL=2
 
	@captureFile
	@activeCall
	@sliceus
	@activeCallLeg
	@chartflag
 
	attr_reader :startTime,:endTime
 
	def initialize(call, legno, filename)
		@captureFile=filename
		@activeCall=call
		@sliceus = 200000
		@activeCallLeg = legno		
 
		# set start timestamp
		@startTime = Time.at(@activeCall.startTimeSecs,@activeCall.startTimeUSecs)
 
		# set end timestamp
		@endTime = Time.at(@activeCall.endTimeSecs,@activeCall.endTimeUSecs)
 
		@chartflag = CHART_FLAG_NORMAL
	end
 
	def totalTimeUSecs 
		secdiff = @endTime.tv_sec - @startTime.tv_sec 
		usdiff = @endTime.tv_usec - @startTime.tv_usec
		if usdiff < 0
			usdiff =  usdiff + ::USECPERSEC
			secdiff = secdiff-1
		end
 
		totalTimeUSecs = (secdiff * ::USECPERSEC) + usdiff
	end
 
 
	def yscalelabel
		"Bandwidth"
	end
 
	def xscalelabel
		"Time"
	end
 
	def valunits
		"bps"
	end
 
	def matchCall (iaxpacket,iaxlayer)
 
		# ignore all retransmissions
		f_retran = iaxlayer.FindField(">FULL FRAME>Dest Call Number>R")
		if f_retran
			return f_retran.value == 1
		end
 
 
		# ignore non voice full frames
		iplayer = iaxpacket.FindLayer("IP")
		ipfrom = iplayer.FindField("Source IP")
		iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number")
		if (iaxscall)
 
			f_frametype = iaxlayer.FindField("Frametype")
			if f_frametype.value =~ /Voice/
				@chartFlag = CHART_FLAG_VOICE
			else
				@chartFlag = CHART_FLAG_CONTROL
				# print "skipping non voice control frame #{iaxpacket.Description}\n"
				return false
			end
		else
			iaxscall = iaxlayer.FindField(">MINI FRAME>Source Call Number>Source Call Number")
		end
 
		return  nil if iaxscall.nil?
 
		return  @activeCall.matchLeg(@activeCallLeg, ipfrom.value, iaxscall.value)
 
	end
 
end
 
#-------------------------------
# CallBandwidth
# bandwidth used by the codec of this call in a direction
class CallBandwidthModel < ModelBase
 
 
	def initialize(call, legno, filename)
		super(call,legno,filename)
 
	end
 
 
	def each_val
		unsniffDB = WIN32OLE.new("Unsniff.Database")
		unsniffDB.OpenForRead(@captureFile)
		allPackets = unsniffDB.PacketIndex
 
		print "pid start = #{@activeCall.startPacketID} to #{@activeCall.endPacketID}\n"
 
		byteCount =0
		currSecs = Time.at(@activeCall.startTimeSecs,@activeCall.startTimeUSecs)
		(@activeCall.startPacketID..@activeCall.endPacketID-1).each do |pid|
 
			packet = allPackets.Item(pid)
			@chartFlag = CHART_FLAG_NORMAL
 
			iaxlayer = packet.FindLayer("IAX2")
			if iaxlayer && matchCall(packet,iaxlayer)
				pkttime = Time.at(packet.TimestampSecs, packet.TimestampUSecs)
				difft = pkttime - currSecs
				if difft*USEC_PER_SEC < @sliceus
					byteCount += packet.Length
				else
					case @chartFlag
						when CHART_FLAG_NORMAL
							yield pkttime, 8*(byteCount / difft),  DataPointStyle.new(DataPointStyle::LINEHEIGHT,"o")
 
						when CHART_FLAG_VOICE
							yield pkttime, 8*(byteCount / difft),  DataPointStyle.new(DataPointStyle::POINTCHAR,"V",FXRGB(0,0,255))
 
						when CHART_FLAG_CONTROL
							yield pkttime, 8*(byteCount / difft),  DataPointStyle.new(DataPointStyle::POINTCHAR,"C",FXRGB(255,0,255))
					end
					byteCount = 0
					currSecs = pkttime
				end
			end
 
		end
 
		unsniffDB.Close
	end
 
	def maxval
		128000
	end
 
	def minval
		0
	end
 
	def yscalelabel
		"Bandwidth"
	end
 
	def xscalelabel
		"Time"
	end
 
	def valunits
		"bps"
	end
 
end
 
# --------------
# Delay Model
# calculate delay how are recieving packets spaced wrt to IAX2 timestamps
class DelayModel < ModelBase
 
	@Curr_IAX_Msecs
	@Curr_Capt_Msecs
 
 
	def initialize(call, legno, filename)
		super(call,legno,filename)
 
		@Curr_IAX_Msecs=0
		@Curr_Capt_Msecs=0
 
	end
 
 
	def each_val
		unsniffDB = WIN32OLE.new("Unsniff.Database")
		unsniffDB.OpenForRead(@captureFile)
		allPackets = unsniffDB.PacketIndex
 
		print "pid start = #{@activeCall.startPacketID} to #{@activeCall.endPacketID}\n"
 
		byteCount =0
		currSecs = Time.at(@activeCall.startTimeSecs,@activeCall.startTimeUSecs)
		(@activeCall.startPacketID..@activeCall.endPacketID-1).each do |pid|
 
			packet = allPackets.Item(pid)
			@chartFlag = CHART_FLAG_NORMAL
 
			iaxlayer = packet.FindLayer("IAX2")
			if iaxlayer && matchCall(packet,iaxlayer)
				if @Curr_Capt_Msecs == 0
					@Curr_IAX_Msecs = getIAXTimestamp(iaxlayer)
					@Curr_Capt_Msecs = packet.TimestampSecs*MSEC_PER_SEC + packet.TimestampUSecs/USEC_PER_MSEC
					print "First frame = #{@Curr_IAX_Msecs}\n"
				else
					newIAXMs = getIAXTimestamp(iaxlayer)
					newCaptMs = packet.TimestampSecs*MSEC_PER_SEC + packet.TimestampUSecs/USEC_PER_MSEC
 
					deltaIAX = newIAXMs - @Curr_IAX_Msecs
					deltaCapt = newCaptMs - @Curr_Capt_Msecs
					delayMs = deltaCapt - deltaIAX
 
					delayMs = -delayMs if delayMs < 0
 
					@Curr_IAX_Msecs = newIAXMs
					@Curr_Capt_Msecs = newCaptMs
 
					print "Delay calculated = #{delayMs}\n" if DEBUG_MODE
 
					yield Time.at(packet.TimestampSecs,packet.TimestampUSecs), delayMs ,  DataPointStyle.new(DataPointStyle::LINECONNECT,"o")
				end
			end
 
		end
 
		unsniffDB.Close
	end
 
	# given a full or mini frame calculate the 32 - bit timestamp (in milliseconds)
	def getIAXTimestamp(iaxlayer)
 
		# full frame timestamp
		f_timestamp = iaxlayer.FindField(">FULL FRAME>Timestamp")
		if (f_timestamp)
			return f_timestamp.value.to_i
		else
			f_timestamp = iaxlayer.FindField(">MINI FRAME>Timestamp")
			if (f_timestamp)
				return (@Curr_IAX_Msecs  & 0x00000000 ) + f_timestamp.value.to_i
			end
		end
 
		print "IAXTimestamp Error: Cannot find timestamp field in full or mini frames\n"
		return nil
	end
 
	def maxval
		50
	end
 
	def minval
		0
	end
 
	def yscalelabel
		"Delay (ms)"
	end
 
	def xscalelabel
		"Time"
	end
 
	def valunits
		"ms"
	end
 
end
 
# --------------
# Jitter Model
# calculate jitter as per RFC3550 (is this ok for IAX vs RTP ?)
class JitterModel  < ModelBase
 
	@Curr_IAX_Msecs
	@Curr_Capt_Msecs
	@Curr_Jitter
 
 
	def initialize(call, legno, filename)
		super(call,legno,filename)
 
		@Curr_IAX_Msecs=0
		@Curr_Capt_Msecs=0
		@Curr_Jitter=0
 
	end
 
 
	def each_val
		unsniffDB = WIN32OLE.new("Unsniff.Database")
		unsniffDB.OpenForRead(@captureFile)
		allPackets = unsniffDB.PacketIndex
 
		print "pid start = #{@activeCall.startPacketID} to #{@activeCall.endPacketID}\n"
 
		byteCount =0
		currSecs = Time.at(@activeCall.startTimeSecs,@activeCall.startTimeUSecs)
		(@activeCall.startPacketID..@activeCall.endPacketID-1).each do |pid|
 
			packet = allPackets.Item(pid)
			@chartFlag = CHART_FLAG_NORMAL
 
			iaxlayer = packet.FindLayer("IAX2")
			if iaxlayer && matchCall(packet,iaxlayer)
				if @Curr_Capt_Msecs == 0
					@Curr_IAX_Msecs = getIAXTimestamp(iaxlayer)
					@Curr_Capt_Msecs = packet.TimestampSecs*MSEC_PER_SEC + packet.TimestampUSecs/USEC_PER_MSEC
					print "First frame = #{@Curr_IAX_Msecs}\n"
				else
					newIAXMs = getIAXTimestamp(iaxlayer)
					newCaptMs = packet.TimestampSecs*MSEC_PER_SEC + packet.TimestampUSecs/USEC_PER_MSEC
 
					deltaIAX = newIAXMs - @Curr_IAX_Msecs
					deltaCapt = newCaptMs - @Curr_Capt_Msecs
					delayMs = deltaCapt - deltaIAX
 
					delayMs = -delayMs if delayMs < 0
 
					@Curr_Jitter += 1.0/16.0*(delayMs - @Curr_Jitter)
 
					@Curr_IAX_Msecs = newIAXMs
					@Curr_Capt_Msecs = newCaptMs
 
					print "Jitter calculated = #{@Curr_Jitter}\n" if DEBUG_MODE
 
					yield Time.at(packet.TimestampSecs,packet.TimestampUSecs), @Curr_Jitter ,  DataPointStyle.new(DataPointStyle::LINECONNECT,"o")
				end
			end
 
		end
 
		unsniffDB.Close
	end
 
	# given a full or mini frame calculate the 32 - bit timestamp (in milliseconds)
	def getIAXTimestamp(iaxlayer)
 
		# full frame timestamp
		f_timestamp = iaxlayer.FindField(">FULL FRAME>Timestamp")
		if (f_timestamp)
			return f_timestamp.value.to_i
		else
			f_timestamp = iaxlayer.FindField(">MINI FRAME>Timestamp")
			if (f_timestamp)
				return (@Curr_IAX_Msecs  & 0x00000000 ) + f_timestamp.value.to_i
			end
		end
 
		print "IAXTimestamp Error: Cannot find timestamp field in full or mini frames\n"
		return nil
	end
 
	def maxval
		50
	end
 
	def minval
		0
	end
 
	def yscalelabel
		"Jitter (ms)"
	end
 
	def xscalelabel
		"Time"
	end
 
	def valunits
		"ms"
	end
 
end
 
# --------------
# Loss Model
# calculate packet loss (packets expected - packet received)
class LossModel < ModelBase
 
	@Curr_IAX_Msecs
 
 
	def initialize(call, legno, filename)
		super(call,legno,filename)
 
		@Curr_IAX_Msecs=0
 
	end
 
 
	def each_val
		unsniffDB = WIN32OLE.new("Unsniff.Database")
		unsniffDB.OpenForRead(@captureFile)
		allPackets = unsniffDB.PacketIndex
 
		print "pid start = #{@activeCall.startPacketID} to #{@activeCall.endPacketID}\n"
 
		packetCount  =0
		sampleInterval = @activeCall.sampleInterval
 
		if !sampleInterval
			print "Sorry the codec is not supported for packet loss calculation #{@activeCall.codec}\n"
			return
		end
 
		# calculate packetization interval (depends on the codec used)
 
		currSecs = Time.at(@activeCall.startTimeSecs,@activeCall.startTimeUSecs)
		(@activeCall.startPacketID..@activeCall.endPacketID-1).each do |pid|
 
			packet = allPackets.Item(pid)
			@chartFlag = CHART_FLAG_NORMAL
 
			iaxlayer = packet.FindLayer("IAX2")
			if iaxlayer && matchCall(packet,iaxlayer)
 
				@Curr_IAX_Msecs = getIAXTimestamp(iaxlayer)
				packetCount = packetCount + 1
				packetCountExpected = @Curr_IAX_Msecs / sampleInterval
 
				lossPercent = (100.0 * (packetCountExpected -packetCount )) / (1.0 * packetCountExpected)
				#lossAbsolute =  (packetCountExpected -packetCount )
 
				print "expected = #{packetCountExpected}  got = #{packetCount}\n" if DEBUG_MODE
 
				yield Time.at(packet.TimestampSecs,packet.TimestampUSecs), lossPercent,  DataPointStyle.new(DataPointStyle::LINECONNECT,"o")
 
			end
 
		end
 
		unsniffDB.Close
	end
 
	# given a full or mini frame calculate the 32 - bit timestamp (in milliseconds)
	def getIAXTimestamp(iaxlayer)
 
		# full frame timestamp
		f_timestamp = iaxlayer.FindField(">FULL FRAME>Timestamp")
		if (f_timestamp)
			return f_timestamp.value.to_i
		else
			f_timestamp = iaxlayer.FindField(">MINI FRAME>Timestamp")
			if (f_timestamp)
				return (@Curr_IAX_Msecs  & 0x00000000 ) + f_timestamp.value.to_i
			end
		end
 
		print "IAXTimestamp Error: Cannot find timestamp field in full or mini frames\n"
		return nil
	end
 
	def maxval
		100
	end
 
	def minval
		0
	end
 
	def yscalelabel
		"Packet Loss (percent)"
	end
 
	def xscalelabel
		"Time"
	end
 
	def valunits
		"%"
	end
 
end
 
 
#---------------------
# Call Event Model
# plot call events ,mini frames, full frames, control commands, ping, pong , DTMF and everything else
class CallEventModel < ModelBase
 
	@dataChar
	@dataColor
	@dataHeight
	@oppositeLeg
	@retran
 
	def initialize(call, legno, filename)
		super(call,legno,filename)
 
		@dataChar = "."
		@dataColor=FXRGB(255,255,255)
		@dataHeight=50
		@oppositeLeg=false
		@retran=false
 
	end
 
	def matchCall (iaxpacket,iaxlayer)
		# ignore non voice full frames
		iplayer = iaxpacket.FindLayer("IP")
		ipfrom = iplayer.FindField("Source IP")
		iaxscall = iaxlayer.FindField(">FULL FRAME>Source Call Number>Source Call Number")
		if (!iaxscall)
			iaxscall = iaxlayer.FindField(">MINI FRAME>Source Call Number>Source Call Number")
		end
 
		return false if iaxscall.nil?
 
		@retran=false
		@oppositeLeg=false
		bMatch =   @activeCall.matchLeg(@activeCallLeg, ipfrom.value, iaxscall.value)
		if (! bMatch)
			bMatch = @activeCall.matchLeg(1-@activeCallLeg,ipfrom.value,iaxscall.value)
			if (!bMatch)
				return false	
			end
			@oppositeLeg=true
		end
 
		# retransmission (R) - ref
		f_retran = iaxlayer.FindField(">FULL FRAME>Dest Call Number>R")
		if f_retran
			if (f_retran.value == "1")
				@retran=true
			end
		else
			# mini frame
			@dataChar = "."
 
			@dataHeight=55
 
			if @oppositeLeg
				# yellow color lower line
				@dataColor = FXRGB(255,255,0)
			else
				# white color upper line
				@dataColor = FXRGB(255,255,255)
			end
			return true
		end
 
		# voice,video,dtmf frame types
		f_frametype = iaxlayer.FindField("Frametype")
		if (f_frametype)
			@dataColor = FXRGB(255,255,0)
			@dataHeight = 60
			case f_frametype.value 
				when /Voice/
					@dataChar = "V"
					@dataHeight = @dataHeight + 0
					return true
 
				when /DTMF/
					@dataChar="#"
					@dataHeight = @dataHeight + 5
					return true
			end
		end
 
		# IAX control frame type
		sclass = iaxlayer.FindField(">FULL FRAME>SC_IAX>IAX Subclass")
		if (sclass) 
			@dataColor = FXRGB(255,0,255)
			@dataHeight = 70
 
			case sclass.value
				when /NEW/
					@dataChar="N"
					@dataHeight = @dataHeight + 0
 
				when /ACCEPT/
					@dataChar="A"
					@dataHeight = @dataHeight + 3
 
				when /HANGUP/
					@dataChar="H"
					@dataHeight = @dataHeight + 6
 
				when /REG/
					@dataChar="R"
					@dataHeight = @dataHeight + 8
 
				when /ACK/
					@dataChar="+"
					@dataHeight = @dataHeight + 12
 
				when /PING/
					@dataChar=">"
					@dataHeight = @dataHeight + 16
 
				when /PONG/
					@dataChar="<"
					@dataHeight = @dataHeight + 16
 
				when /LAG/
					@dataChar="L"
					@dataHeight = @dataHeight + 19
 
				else
					@dataChar="O"
					@dataHeight = @dataHeight + 0
			end
 
			return true
		end
 
		# control frame types
		ctrltype  = iaxlayer.FindField(">FULL FRAME>SC_Control>Control Subclass")
		if (ctrltype) 
			@dataColor = FXRGB(255,128,255)
			@dataHeight=90
 
			case ctrltype.value
				when /Ringing/
					@dataChar="*"
					@dataHeight = @dataHeight + 0
 
				when /Answer/
					@dataChar="@"
					@dataHeight = @dataHeight + 3
 
				else
					@dataChar="U"
					@dataHeight = @dataHeight + 6
			end
		end
 
 
		return true
 
 
	end
 
	def each_val
		unsniffDB = WIN32OLE.new("Unsniff.Database")
		unsniffDB.OpenForRead(@captureFile)
		allPackets = unsniffDB.PacketIndex
 
		print "pid start = #{@activeCall.startPacketID} to #{@activeCall.endPacketID}\n"
		(@activeCall.startPacketID..@activeCall.endPacketID-1).each do |pid|
 
			packet = allPackets.Item(pid)
 
			iaxlayer = packet.FindLayer("IAX2")
			if iaxlayer && matchCall(packet,iaxlayer)
				pkttime = Time.at(packet.TimestampSecs, packet.TimestampUSecs)
				@dataColor = FXRGB(255,0,0) if @retran==true
				if @oppositeLeg
					@dataHeight = 100-@dataHeight 
				end
				yield pkttime, @dataHeight,  DataPointStyle.new(DataPointStyle::POINTCHAR,@dataChar,@dataColor)
			end
 
		end
 
		unsniffDB.Close
	end
 
	def maxval
		90
	end
 
	def minval
		0
	end
 
	def yscalelabel
		"Event"
	end
 
	def xscalelabel
		"Time"
	end
 
	def valunits
		"-"
	end
 
end
 
#---------------------------------------------------------
# Drive the IAX2 analysis
#---------------------------------------------------------
USAGE = "iax2ana <capture-filename> "
if ARGV.length != 1
	puts USAGE
	exit 1
end
 
# Scan the capture and build an index of all IAX2 calls
InputFile = ARGV[0]
ana = IAXAnalyzer.new(InputFile)
ana.extract_calls
ana.printCalls if DEBUG_MODE
 
# A new Fox Application and MainWindow object
theApp = FXApp.new
theMainWindow = ChartWindow.new(theApp)
theMainWindow.loadCalls(ana)
theMainWindow.showCall(0)
 
# Run application
theApp.create
theMainWindow.show
theApp.run