Library "autoplugins.brs"

'region Main
Sub Main()
    autorunVersion$ = "8.0.12" 'BA 5.0.1.2
    customAutorunVersion$ = "8.0.12"
	baVersion$ = "5.0.1.2"

    debugParams = EnableDebugging()
	
    sysFlags = CreateObject("roAssociativeArray")
    sysFlags.debugOn = debugParams.serialDebugOn    
	sysFlags.systemLogDebugOn = debugParams.systemLogDebugOn
    
    modelObject = CreateObject("roDeviceInfo")
    sysInfo = CreateObject("roAssociativeArray")
    sysInfo.autorunVersion$ = autorunVersion$
    sysInfo.customAutorunVersion$ = customAutorunVersion$
	sysInfo.baVersion$ = baVersion$
    sysInfo.deviceUniqueID$ = modelObject.GetDeviceUniqueId()
    sysInfo.deviceFWVersion$ = modelObject.GetVersion()
	sysInfo.deviceFWVersionNumber% = modelObject.GetVersionNumber()

    sysInfo.deviceModel$ = modelObject.GetModel()
    sysInfo.deviceFamily$ = modelObject.GetFamily()
    sysInfo.enableLogDeletion = true

	sysInfo.ipAddressWired$ = "Invalid"
	nc = CreateObject("roNetworkConfiguration", 0)
	if type(nc) = "roNetworkConfiguration" then
        currentConfig = nc.GetCurrentConfig()
        if type(currentConfig) = "roAssociativeArray" then
			if currentConfig.ip4_address <> "" then
				sysInfo.ipAddressWired$ = currentConfig.ip4_address
			endif
        endif
	endif
	nc = invalid

    sysInfo.modelSupportsWifi = false
	sysInfo.ipAddressWireless$ = "Invalid"
    nc = CreateObject("roNetworkConfiguration", 1)
    if type(nc) = "roNetworkConfiguration" then
        currentConfig = nc.GetCurrentConfig()
        if type(currentConfig) = "roAssociativeArray" then
            sysInfo.modelSupportsWifi = true
			if currentConfig.ip4_address <> "" then
				sysInfo.ipAddressWireless$ = currentConfig.ip4_address
			endif
        endif
    endif
    nc = invalid

	videoMode = CreateObject("roVideoMode")
	edid = videoMode.GetEdidIdentity(true)
	UpdateEdidValues(edid, sysInfo)
	edid = invalid
	videoMode = invalid

	' determine if the storage device is writable
	du = CreateObject("roStorageInfo", "./")
	if du.IsReadOnly() then
		sysInfo.storageIsWriteProtected = true
	else
		sysInfo.storageIsWriteProtected = false
	endif

	systemTime = CreateObject("roSystemTime")

	InitRemoteSnapshots(sysInfo)

' check to see whether or not the current firmware meets the minimum compatibility requirements
	versionNumber% = modelObject.GetVersionNumber()

	if sysInfo.deviceFamily$ = "pantera" then
		minVersionNumber% = 393761
		minVersion$ = "6.2.33"
	else if sysInfo.deviceFamily$ = "pagani" then
		minVersionNumber% = 459275
		minVersion$ = "7.2.11"
	else if sysInfo.deviceFamily$ = "impala" then
		minVersionNumber% = 393761
		minVersion$ = "6.2.33"
	else if sysInfo.deviceFamily$ = "malibu" then
		minVersionNumber% = 459041
		minVersion$ = "7.1.33"
	else if sysInfo.deviceFamily$ = "panther" then
		minVersionNumber% = 327952
		minVersion$ = "5.1.16"
	else if sysInfo.deviceFamily$ = "cheetah" then
		minVersionNumber% = 327952
		minVersion$ = "5.1.16"
	else if sysInfo.deviceFamily$ = "tiger" then
		minVersionNumber% = 327952
		minVersion$ = "5.1.16"
	else if sysInfo.deviceFamily$ = "puma" then
		minVersionNumber% = 327952
		minVersion$ = "5.1.16"
	else if sysInfo.deviceFamily$ = "bobcat" then
		minVersionNumber% = 327952
		minVersion$ = "5.1.16"
	else if sysInfo.deviceFamily$ = "lynx" then
		minVersionNumber% = 327952
		minVersion$ = "5.1.16"
	else	' no supported devices should hit this else
		minVersionNumber% = 199435
		minVersion$ = "3.11.11"
	endif

	if versionNumber% < minVersionNumber% then
        videoMode = CreateObject("roVideoMode")
        resX = videoMode.GetResX()
        resY = videoMode.GetResY()
        videoMode = invalid
        r=CreateObject("roRectangle",0,resY/2-resY/64,resX,resY/32)
        twParams = CreateObject("roAssociativeArray")
        twParams.LineCount = 1
        twParams.TextMode = 2
        twParams.Rotation = 0
        twParams.Alignment = 1
        tw=CreateObject("roTextWidget",r,1,2,twParams)
        tw.PushString("Firmware needs to be upgraded to " + minVersion$ + " or greater")
        tw.Show()

		globalAA = GetGlobalAA()
		if globalAA.enableRemoteSnapshot then
			sleep(1000)	' sleep here ensures that graphics makes it to the screen before the snapshot is taken
			TakeSnapshot(systemTime, "")
		endif

		sleep(120000)
        RebootSystem()
	endif
	
    diagnosticCodes = newDiagnosticCodes() 

    RunBsp(sysFlags, sysInfo, diagnosticCodes, systemTime)
    
End Sub


Sub InitRemoteSnapshots(sysInfo As Object)

	ok = CreateDirectory("snapshots")		' fails if storage is write protected or formatted as ntfs, but ignore return value at this point

	' setup snapshot capability as early as possible
    registrySection = CreateObject("roRegistrySection", "networking")
    if type(registrySection)<>"roRegistrySection" then print "Error: Unable to create roRegistrySection":stop

	globalAA = GetGlobalAA()
	globalAA.enableRemoteSnapshot = false
	if lcase(registrySection.Read("enableRemoteSnapshot")) = "yes" then
		if sysInfo.storageIsWriteProtected then DisplayStorageDeviceLockedMessage()
		globalAA.enableRemoteSnapshot = true
		globalAA.remoteSnapshotInterval = int(val(registrySection.Read("remoteSnapshotInterval")))
		globalAA.remoteSnapshotMaxImages = int(val(registrySection.Read("remoteSnapshotMaxImages")))
		globalAA.remoteSnapshotJpegQualityLevel = int(val(registrySection.Read("remoteSnapshotJpegQualityLevel")))
		globalAA.remoteSnapshotDisplayPortrait = GetBoolFromString("remoteSnapshotDisplayPortrait", false)
	endif

	registrySection = invalid

	' generate list of snapshots currently on card
	globalAA.listOfSnapshotFiles = MatchFiles("/snapshots/", "*.jpg")
	BubbleSortFileNames(globalAA.listOfSnapshotFiles)

End Sub


' sort in ascending order from oldest to newest
Sub BubbleSortFileNames(fileNames As Object)

	if type(fileNames) = "roList" then
	
		n = fileNames.Count()

		while n <> 0

			newn = 0
			for i = 1 to (n - 1)
				if fileNames[i-1] > fileNames[i] then
					k = fileNames[i]
					fileNames[i] = fileNames[i-1]
					fileNames[i-1] = k
					newn = i
				endif
			next
			n = newn

		end while

	endif

End Sub



Sub UpdateEdidValues(edid As Object, sysInfo As Object)

	if type(edid) = "roAssociativeArray" then
		sysInfo.edidMonitorSerialNumber$ = edid.serial_number_string
		sysInfo.edidYearOfManufacture$ = edid.year_of_manufacture
		sysInfo.edidMonitorName$ = edid.monitor_name
		sysInfo.edidManufacturer$ = edid.manufacturer
		sysInfo.edidUnspecifiedText$ = edid.text_string
		sysInfo.edidSerialNumber$ = StripLeadingSpaces(stri(edid.serial_number))
		sysInfo.edidManufacturerProductCode$ = edid.product
		sysInfo.edidWeekOfManufacture$ = edid.week_of_manufacture
	else
		sysInfo.edidMonitorSerialNumber$ = ""
		sysInfo.edidYearOfManufacture$ = ""
		sysInfo.edidMonitorName$ = ""
		sysInfo.edidManufacturer$ = ""
		sysInfo.edidUnspecifiedText$ = ""
		sysInfo.edidSerialNumber$ = ""
		sysInfo.edidManufacturerProductCode$ = ""
		sysInfo.edidWeekOfManufacture$ = ""
	endif

End Sub


Sub DisplayErrorScreen(msg1$ As String, msg2$ As String, msToWaitBeforeRebooting As Integer)

	videoMode = CreateObject("roVideoMode")
	resX = videoMode.GetResX()
	resY = videoMode.GetResY()
	videoMode = invalid

	r = CreateObject("roRectangle", 0, 0, resX, resY)
	twParams = CreateObject("roAssociativeArray")
	twParams.LineCount = 1
	twParams.TextMode = 2
	twParams.Rotation = 0
	twParams.Alignment = 1
	tw=CreateObject("roTextWidget",r,1,2,twParams)
	tw.PushString("")
	tw.Show()

	r=CreateObject("roRectangle",0,resY/2-resY/32,resX,resY/32)
	tw=CreateObject("roTextWidget",r,1,2,twParams)
	tw.PushString(msg1$)
	tw.Show()

	r2=CreateObject("roRectangle",0,resY/2,resX,resY/32)
	tw2=CreateObject("roTextWidget",r2,1,2,twParams)
	tw2.PushString(msg2$)
	tw2.Show()

	globalAA = GetGlobalAA()

	if type(globalAA.bsp) = "roAssociativeArray" then
	
		if type(globalAA.bsp.msgPort) = "roMessagePort" then
			globalAA.bsp.msgPort.SetWatchdogTimeout(0)
		endif

		if globalAA.enableRemoteSnapshot then
			sleep(1000)	' sleep here ensures that graphics makes it to the screen before the snapshot is taken
			TakeSnapshot(globalAA.bsp.systemTime, globalAA.bsp.activePresentation$)
		endif

	endif

    msgPort = CreateObject("roMessagePort")
    msg = wait(msToWaitBeforeRebooting, msgPort)

    RebootSystem()

End Sub


Sub DisplayStorageDeviceLockedMessage()
	DisplayErrorScreen("The attached storage device is write protected.", "Remove it, enable writing, and reboot the device.", 0)
End Sub


Sub DisplayObsoleteModelMessage(bsp As Object, publishedModel$ As String)
	DisplayErrorScreen("This presentation targets a BrightSign " + publishedModel$ + " and is not supported by this version of the script.", "Remove the card, publish with a version of BrightAuthor that supports this model, and reboot the device.", 120000)
End Sub


Function EnableDebugging() As Object

	debugParams = CreateObject("roAssociativeArray")
	
	debugParams.serialDebugOn = false
	debugParams.systemLogDebugOn = false
	
	syncSpec = CreateObject("roSyncSpec")
	if syncSpec.ReadFromFile("current-sync.xml") or syncSpec.ReadFromFile("local-sync.xml") or syncSpec.ReadFromFile("localToBSN-sync.xml") or syncSpec.ReadFromFile("localSetupToStandalone-sync.xml") then
		if syncSpec.LookupMetadata("client", "enableSerialDebugging") = "True" then
			debugParams.serialDebugOn = true
		endif
		if syncSpec.LookupMetadata("client", "enableSystemLogDebugging") = "True" then
			debugParams.systemLogDebugOn = true
		endif
		syncSpec = invalid
	endif

	return debugParams
	
End Function


Sub WriteRegistrySetting(key$ As String, value$ As String)
	
	m.registrySection.Write(key$, value$)

End Sub


Function GetRegistrySettingValue(newRegistryKey$ As String, oldRegistryKey$ As String) As String
    
    value$ = m.registrySection.Read(newRegistryKey$)
    if value$ = "" then
        value$ = m.registrySection.Read(oldRegistryKey$)
    endif
    return value$

End Function


Sub ReadCachedRegistrySettings()

	m.registrySettings = CreateObject("roAssociativeArray")
	
    m.registrySettings.lwsConfig$ = m.registrySection.Read("nlws")
    m.registrySettings.lwsUserName$ = m.registrySection.Read("nlwsu")
    m.registrySettings.lwsPassword$ = m.registrySection.Read("nlwsp")        
    m.registrySettings.lwsEnableUpdateNotifications$ = m.registrySection.Read("nlwseun")        
	if m.registrySettings.lwsEnableUpdateNotifications$ = "" then
		m.registrySettings.lwsEnableUpdateNotifications$ = "yes"
	endif

    m.registrySettings.unitName$ = m.GetRegistrySettingValue("un", "unitName")
    m.registrySettings.unitNamingMethod$ = m.GetRegistrySettingValue("unm", "unitNamingMethod")
    m.registrySettings.unitDescription$ = m.GetRegistrySettingValue("ud", "unitDescription")
	
	m.registrySettings.playbackLoggingEnabled = m.registrySection.Read("ple")
	m.registrySettings.eventLoggingEnabled = m.registrySection.Read("ele")
	m.registrySettings.diagnosticLoggingEnabled = m.registrySection.Read("dle")
	m.registrySettings.stateLoggingEnabled = m.registrySection.Read("sle")
	m.registrySettings.variableLoggingEnabled = m.registrySection.Read("vle")
	m.registrySettings.uploadLogFilesAtBoot = m.registrySection.Read("uab")
	m.registrySettings.uploadLogFilesAtSpecificTime = m.registrySection.Read("uat")
	m.registrySettings.uploadLogFilesTime$ = m.registrySection.Read("ut")

	m.registrySettings.OnlyDownloadIfCached$ = m.registrySection.Read("OnlyDownloadIfCached")

    m.registrySettings.timeBetweenNetConnects$ = m.GetRegistrySettingValue("tbnc", "timeBetweenNetConnects")
    m.registrySettings.contentDownloadsRestricted = m.GetRegistrySettingValue("cdr", "contentDownloadsRestricted")
    m.registrySettings.contentDownloadRangeStart = m.GetRegistrySettingValue("cdrs", "contentDownloadRangeStart")
    m.registrySettings.contentDownloadRangeLength = m.GetRegistrySettingValue("cdrl", "contentDownloadRangeLength")

    m.registrySettings.timeBetweenHeartbeats$ = m.GetRegistrySettingValue("tbh", "tbh")
    m.registrySettings.heartbeatsRestricted = m.GetRegistrySettingValue("hr", "hr")
    m.registrySettings.heartbeatsRangeStart = m.GetRegistrySettingValue("hrs", "hrs")
    m.registrySettings.heartbeatsRangeLength = m.GetRegistrySettingValue("hdrl", "hrl")

	m.registrySettings.rateLimitModeOutsideWindow$ = m.registrySection.Read("rlmow")
	m.registrySettings.rateLimitRateOutsideWindow$ = m.registrySection.Read("rlrow")
	m.registrySettings.rateLimitModeInWindow$ = m.registrySection.Read("rlmiw")
	m.registrySettings.rateLimitRateInWindow$ = m.registrySection.Read("rlriw")
	m.registrySettings.rateLimitModeInitialDownloads$ = m.registrySection.Read("rlmid")
	m.registrySettings.rateLimitRateInitialDownloads$ = m.registrySection.Read("rlrid")

	m.registrySettings.rateLimitModeOutsideWindow_2$ = m.registrySection.Read("rlmow2")
	if m.registrySettings.rateLimitModeOutsideWindow_2$ = "" then m.registrySettings.rateLimitModeOutsideWindow_2$ = "default"
	m.registrySettings.rateLimitRateOutsideWindow_2$ = m.registrySection.Read("rlrow2")
	if m.registrySettings.rateLimitRateOutsideWindow_2$ = "" then m.registrySettings.rateLimitRateOutsideWindow_2$ = "0"
	m.registrySettings.rateLimitModeInWindow_2$ = m.registrySection.Read("rlmiw2")
	if m.registrySettings.rateLimitModeInWindow_2$ = "" then m.registrySettings.rateLimitModeInWindow_2$ = "default"
	m.registrySettings.rateLimitRateInWindow_2$ = m.registrySection.Read("rlriw2")
	if m.registrySettings.rateLimitRateInWindow_2$ = "" then m.registrySettings.rateLimitRateInWindow_2$ = "0"
	m.registrySettings.rateLimitModeInitialDownloads_2$ = m.registrySection.Read("rlmid2")
	if m.registrySettings.rateLimitModeInitialDownloads_2$ = "" then m.registrySettings.rateLimitModeInitialDownloads_2$ = "default"
	m.registrySettings.rateLimitRateInitialDownloads_2$ = m.registrySection.Read("rlrid2")
	if m.registrySettings.rateLimitRateInitialDownloads_2$ = "" then m.registrySettings.rateLimitRateInitialDownloads_2$ = "0"

	' wired parameters
	m.registrySettings.networkConnectionPriorityWired$ = m.registrySection.Read("ncp")
	if m.registrySettings.networkConnectionPriorityWired$ = "" then m.registrySettings.networkConnectionPriorityWired$ = "0"

	' wireless parameters
	m.registrySettings.networkConnectionPriorityWireless$ = m.registrySection.Read("ncp2")
	if m.registrySettings.networkConnectionPriorityWireless$ = "" then m.registrySettings.networkConnectionPriorityWireless$ = "0"

    m.registrySettings.tbnco$ = m.registrySection.Read("tbnco")

	m.registrySettings.useProxy = m.registrySection.Read("up")
	if m.registrySettings.useProxy = "yes" then
		m.registrySettings.proxy$ = m.registrySection.Read("ps")
		m.registrySettings.networkHosts$ = m.registrySection.Read("bph")
	else
		m.registrySettings.proxy$ = ""
		m.registrySettings.networkHosts$ = ""
	endif

    m.registrySettings.useWireless$ = m.registrySection.Read("wifi")
    m.registrySettings.ssid$ = m.registrySection.Read("ss")
    m.registrySettings.passphrase$ = m.registrySection.Read("pp")
    m.registrySettings.timeServer$ = m.GetRegistrySettingValue("ts", "timeServer")

	m.registrySettings.wiredNetworkingParameters = {}
	m.registrySettings.wiredNetworkingParameters.networkConnectionPriority$ = m.registrySection.Read("ncp")

	m.registrySettings.wirelessNetworkingParameters = {}
	m.registrySettings.wirelessNetworkingParameters.networkConnectionPriority$ = m.registrySection.Read("ncp2")

	if m.registrySettings.useWireless$ = "yes" then
		m.registrySettings.wirelessNetworkingParameters.useDHCP$ = m.registrySection.Read("dhcp")
		m.registrySettings.wirelessNetworkingParameters.staticIPAddress$ = m.registrySection.Read("sip")
		m.registrySettings.wirelessNetworkingParameters.subnetMask$ = m.registrySection.Read("sm")
		m.registrySettings.wirelessNetworkingParameters.gateway$ = m.registrySection.Read("gw")
		m.registrySettings.wirelessNetworkingParameters.dns1$ = m.registrySection.Read("d1")
		m.registrySettings.wirelessNetworkingParameters.dns2$ = m.registrySection.Read("d2")
		m.registrySettings.wirelessNetworkingParameters.dns3$ = m.registrySection.Read("d3")
 		m.registrySettings.wirelessNetworkingParameters.enableWPAEnterpriseAuthentication = GetBoolFromString(m.registrySection.Read("enableWPAEnterpriseAuthentication"), false)
		m.registrySettings.wirelessNetworkingParameters.enableWPAEnterpriseAuthentication2 = GetBoolFromString(m.registrySection.Read("enableWPAEnterpriseAuthentication2"), false)

		m.registrySettings.wiredNetworkingParameters.useDHCP$ = m.registrySection.Read("dhcp2")
		m.registrySettings.wiredNetworkingParameters.staticIPAddress$ = m.registrySection.Read("sip2")
		m.registrySettings.wiredNetworkingParameters.subnetMask$ = m.registrySection.Read("sm2")
		m.registrySettings.wiredNetworkingParameters.gateway$ = m.registrySection.Read("gw2")
		m.registrySettings.wiredNetworkingParameters.dns1$ = m.registrySection.Read("d12")
		m.registrySettings.wiredNetworkingParameters.dns2$ = m.registrySection.Read("d22")
		m.registrySettings.wiredNetworkingParameters.dns3$ = m.registrySection.Read("d32")
	else
		m.registrySettings.wiredNetworkingParameters.useDHCP$ = m.registrySection.Read("dhcp")
		m.registrySettings.wiredNetworkingParameters.staticIPAddress$ = m.registrySection.Read("sip")
		m.registrySettings.wiredNetworkingParameters.subnetMask$ = m.registrySection.Read("sm")
		m.registrySettings.wiredNetworkingParameters.gateway$ = m.registrySection.Read("gw")
		m.registrySettings.wiredNetworkingParameters.dns1$ = m.registrySection.Read("d1")
		m.registrySettings.wiredNetworkingParameters.dns2$ = m.registrySection.Read("d2")
		m.registrySettings.wiredNetworkingParameters.dns3$ = m.registrySection.Read("d3")
	endif

	m.registrySettings.contentXfersEnabledWired$ = m.registrySection.Read("cwr")
	m.registrySettings.textFeedsXfersEnabledWired$ = m.registrySection.Read("twr")
	m.registrySettings.healthXfersEnabledWired$ = m.registrySection.Read("hwr")
	m.registrySettings.mediaFeedsXfersEnabledWired$ = m.registrySection.Read("mwr")
	m.registrySettings.logUploadsXfersEnabledWired$ = m.registrySection.Read("lwr")
    
	m.registrySettings.contentXfersEnabledWireless$ = m.registrySection.Read("cwf")
	m.registrySettings.textFeedsXfersEnabledWireless$ = m.registrySection.Read("twf")
	m.registrySettings.healthXfersEnabledWireless$ = m.registrySection.Read("hwf")
	m.registrySettings.mediaFeedsXfersEnabledWireless$ = m.registrySection.Read("mwf")
	m.registrySettings.logUploadsXfersEnabledWireless$ = m.registrySection.Read("lwf")

    m.registrySettings.logDate$ = m.registrySection.Read("ld")
    m.registrySettings.logCounter$ = m.registrySection.Read("lc")

    m.registrySettings.dwsEnabled$ = m.registrySection.Read("dwse")
    m.registrySettings.dwsPassword$ = m.registrySection.Read("dwsp")

	m.registrySettings.idleScreenColor$ = m.registrySection.Read("isc")
	if m.registrySettings.idleScreenColor$ = "" then
		m.registrySettings.idleScreenColor$ = "FF000000"
	endif

	m.registrySettings.usbContentUpdatePassword$ = m.registrySection.Read("uup")

	m.registrySettings.brightWallName$ = m.registrySection.Read("brightWallName")
	m.registrySettings.brightWallScreenNumber$ = m.registrySection.Read("brightWallScreenNumber")

    m.registrySettings.lastBSNConnectionTime = m.registrySection.Read("lastBSNConnectionTime")

	'Read persistent beacon data
	m.registrySettings.beacon1 = m.registrySection.Read("beacon1")
	m.registrySettings.beacon2 = m.registrySection.Read("beacon2")
	
End Sub


Sub RunBsp(sysFlags As Object, sysInfo As Object, diagnosticCodes As Object, systemTime As Object)

    msgPort = CreateObject("roMessagePort")
    msgPort.SetWatchdogTimeout(60)

    BSP = newBSP(sysFlags, msgPort, systemTime)
		
	BSP.GetRegistrySettingValue = GetRegistrySettingValue
	BSP.ReadCachedRegistrySettings = ReadCachedRegistrySettings
	BSP.WriteRegistrySetting = WriteRegistrySetting
	BSP.ReadCachedRegistrySettings()

    BSP.globalVariables = NewGlobalVariables()

	BSP.svcPort = CreateObject("roControlPort", "BrightSign")
	BSP.svcPort.SetUserData("BrightSign")
	BSP.svcPort.SetPort(msgPort)
	BSP.svcPortIdentity = stri(BSP.svcPort.GetIdentity())

	di = CreateObject("roDeviceInfo")
	if di.HasFeature("GPIO Connector") then
    BSP.controlPort = BSP.svcPort
    BSP.controlPortIdentity = BSP.svcPortIdentity
	else
    BSP.controlPort = CreateObject("roControlPort", "Expander-0-GPIO")
		if IsControlPort(BSP.controlPort) then
        BSP.controlPort.SetUserData("Expander-0-GPIO")
  			BSP.controlPortIdentity = stri(BSP.controlPort.GetIdentity())
			BSP.controlPort.SetPort(msgPort)
		else
			BSP.controlPort = {}
			BSP.controlPortIdentity = ""
		endif
	endif

	BSP.sh = CreateObject("roStorageHotplug")
    BSP.sh.SetPort(msgPort)

	BSP.nh = CreateObject("roNetworkHotplug")
	BSP.nh.SetPort(msgPort)

	BSP.diskMonitor = CreateObject("roDiskMonitor")
	BSP.diskMonitor.SetPort(msgPort)

	BSP.videoMode = CreateObject("roVideoMode")
	BSP.videoMode.SetPort(msgPort)

' create objects for lighting controllers
	BSP.blcs = CreateObject("roArray", 3, true)
	BSP.blcs[0] = CreateObject("roControlPort", "LightController-0-CONTROL")
	BSP.blcs[1] = CreateObject("roControlPort", "LightController-1-CONTROL")
	BSP.blcs[2] = CreateObject("roControlPort", "LightController-2-CONTROL")

' create objects for blc diagnostics
	BSP.blcDiagnostics = CreateObject("roArray", 3, true)
	
	BSP.blcDiagnostics[0] = CreateObject("roControlPort", "LightController-0-DIAGNOSTICS")
	if type(BSP.blcDiagnostics[0]) = "roControlPort" then
    BSP.blcDiagnostics[0].SetUserData("LightController-0-DIAGNOSTICS")
		BSP.blcDiagnostics[0].SetPort(msgPort)
	endif

	BSP.blcDiagnostics[1] = CreateObject("roControlPort", "LightController-1-DIAGNOSTICS")
	if type(BSP.blcDiagnostics[1]) = "roControlPort" then
    BSP.blcDiagnostics[1].SetUserData("LightController-1-DIAGNOSTICS")
		BSP.blcDiagnostics[1].SetPort(msgPort)
	endif

	BSP.blcDiagnostics[2] = CreateObject("roControlPort", "LightController-2-DIAGNOSTICS")
	if type(BSP.blcDiagnostics[2]) = "roControlPort" then
    BSP.blcDiagnostics[2].SetUserData("LightController-2-DIAGNOSTICS")
		BSP.blcDiagnostics[2].SetPort(msgPort)
	endif

' create objects for all attached button panels

	BSP.bpInputPorts = CreateObject("roArray", 4, true)
	BSP.bpInputPortIdentities = CreateObject("roArray", 4, true)
	BSP.bpInputPortHardware = CreateObject("roArray", 4, true)
	BSP.bpInputPortConfigurations = CreateObject("roArray", 4, true)

	BSP.bpInputPorts[0] = CreateObject("roControlPort", "TouchBoard-0-GPIO")
	if type(BSP.bpInputPorts[0]) = "roControlPort" then
    BSP.bpInputPorts[0].SetUserData("TouchBoard-0-GPIO")
		BSP.bpInputPortIdentities[0] = stri(BSP.bpInputPorts[0].GetIdentity())
    BSP.bpInputPorts[0].SetPort(msgPort)
		properties = BSP.bpInputPorts[0].GetProperties()
		BSP.bpInputPortHardware[0] = properties.hardware
		BSP.bpInputPortConfigurations[0] = 0
    endif
    
	BSP.bpInputPorts[1] = CreateObject("roControlPort", "TouchBoard-1-GPIO")
	if type(BSP.bpInputPorts[1]) = "roControlPort" then
    BSP.bpInputPorts[1].SetUserData("TouchBoard-1-GPIO")
		BSP.bpInputPortIdentities[1] = stri(BSP.bpInputPorts[1].GetIdentity())
	    BSP.bpInputPorts[1].SetPort(msgPort)
		properties = BSP.bpInputPorts[1].GetProperties()
		BSP.bpInputPortHardware[1] = properties.hardware
		BSP.bpInputPortConfigurations[1] = 0
    endif
    
	BSP.bpInputPorts[2] = CreateObject("roControlPort", "TouchBoard-2-GPIO")
	if type(BSP.bpInputPorts[2]) = "roControlPort" then
    BSP.bpInputPorts[2].SetUserData("TouchBoard-2-GPIO")
		BSP.bpInputPortIdentities[2] = stri(BSP.bpInputPorts[2].GetIdentity())
	    BSP.bpInputPorts[2].SetPort(msgPort)
		properties = BSP.bpInputPorts[2].GetProperties()
		BSP.bpInputPortHardware[2] = properties.hardware
		BSP.bpInputPortConfigurations[2] = 0
    endif
    
	BSP.bpInputPorts[3] = CreateObject("roControlPort", "TouchBoard-3-GPIO")
	if type(BSP.bpInputPorts[3]) = "roControlPort" then
    BSP.bpInputPorts[3].SetUserData("TouchBoard-3-GPIO")
		BSP.bpInputPortIdentities[3] = stri(BSP.bpInputPorts[3].GetIdentity())
	    BSP.bpInputPorts[3].SetPort(msgPort)
		properties = BSP.bpInputPorts[3].GetProperties()
		BSP.bpInputPortHardware[3] = properties.hardware
		BSP.bpInputPortConfigurations[3] = 0
    endif
    
    BSP.sysInfo = sysInfo
    BSP.diagnosticCodes = diagnosticCodes

    BSP.diagnostics.SetSystemInfo(sysInfo, diagnosticCodes)
    BSP.logging.SetSystemInfo(sysInfo, diagnosticCodes)

    ' Create device specific User-Agent string
    BSP.userAgent$ = "BrightSign/" + sysInfo.deviceUniqueID$ + "/" + sysInfo.deviceFWVersion$ + " (" + sysInfo.deviceModel$ + ")"

	' if the device is configured for local file networking with content transfers, require that the storage is writable
	if BSP.registrySettings.lwsConfig$ = "c" and BSP.sysInfo.storageIsWriteProtected then DisplayStorageDeviceLockedMessage()

	lwsEnabled = false
	if BSP.registrySettings.lwsConfig$ = "c" or BSP.registrySettings.lwsConfig$ = "s" then
		lwsEnabled = true
	endif

	if lwsEnabled then

        BSP.localServer = CreateObject("roHttpServer", { port: 8080 })
        BSP.localServer.SetPort(msgPort)

		if lwsEnabled then

			lwsUserName$ = BSP.registrySettings.lwsUserName$
			lwsPassword$ = BSP.registrySettings.lwsPassword$
        
			if (len(lwsUserName$) + len(lwsPassword$)) > 0 then
				lwsCredentials = CreateObject("roAssociativeArray")
				lwsCredentials.AddReplace(lwsUserName$, lwsPassword$)
			else
				lwsCredentials = invalid
			end if
        
		endif

        BSP.GetIDAA =							{ HandleEvent: GetID, mVar: BSP }
        BSP.GetUDPEventsAA =					{ HandleEvent: GetUDPEvents, mVar: BSP }
        BSP.GetRemoteDataAA =					{ HandleEvent: GetRemoteData, mVar: BSP }
		BSP.GetUserVarsAA =						{ HandleEvent: GetUserVars, mVar: BSP }
        BSP.GetCurrentStatusAA =				{ HandleEvent: GetCurrentStatus, mVar: BSP }
		BSP.GetIDInfoPageAA =					{ HandleEvent: GetIDInfoPage, mVar: BSP }
        BSP.FilePostedAA =						{ HandleEvent: FilePosted, mVar: BSP }
        BSP.SyncSpecPostedAA =					{ HandleEvent: SyncSpecPosted, mVar: BSP }
        BSP.PrepareForTransferAA =				{ HandleEvent: PrepareForTransfer, mVar: BSP }
        BSP.SpecifyCardSizeLimitsAA =			{ HandleEvent: SpecifyCardSizeLimits, mVar: BSP }

		BSP.SendUdpRestAA =						{ HandleEvent: SendUdpRest, mVar: BSP }

		BSP.GetUserVariableCategoriesAA	=		{ HandleEvent: GetUserVariableCategories, mVar: BSP }
		BSP.GetUserVariablesByCategoryAA =		{ HandleEvent: GetUserVariablesByCategory, mVar: BSP }

		BSP.GetSnapshotConfigurationAA =		{ HandleEvent: GetSnapshotConfiguration, mVar: BSP }
		BSP.SetSnapshotConfigurationAA =		{ HandleEvent: SetSnapshotConfiguration, mVar: BSP }
		BSP.GetSnapshotAA =						{ HandleEvent: GetSnapshot, mVar: BSP }

		BSP.GetBSNStatusAA =					{ HandleEvent: GetBSNStatus, mVar: BSP }
		BSP.SetBSNOverrideAA =					{ HandleEvent: SetBSNOverride, mVar: BSP }
		BSP.CancelBSNOverrideAA	=				{ HandleEvent: CancelBSNOverride, mVar: BSP }

		BSP.localServer.AddGetFromFile({ url_path: "/GetAutorun", content_type: "text/plain; charset=utf-8", filename: "autorun.brs"})

        BSP.localServer.AddGetFromEvent({ url_path: "/GetID", user_data: BSP.GetIDAA })
        BSP.localServer.AddGetFromEvent({ url_path: "/GetUDPEvents", user_data: BSP.GetUDPEventsAA })
        BSP.localServer.AddGetFromEvent({ url_path: "/GetRemoteData", user_data: BSP.GetRemoteDataAA })
		BSP.localServer.AddGetFromEvent({ url_path: "/GetUserVars", user_data: BSP.GetUserVarsAA})
'		BSP.localServer.AddGetFromEvent({ url_path: "/", user_data: BSP.GetIDInfoPageAA})

		BSP.localServer.AddGetFromEvent({ url_path: "/GetUserVariableCategories", user_data: BSP.GetUserVariableCategoriesAA})
		BSP.localServer.AddGetFromEvent({ url_path: "/GetUserVariablesByCategory", user_data: BSP.GetUserVariablesByCategoryAA})

	    BSP.localServer.AddGetFromEvent({ url_path: "/GetCurrentStatus", user_data: BSP.GetCurrentStatusAA, passwords: lwsCredentials })        
		BSP.localServer.AddPostToFile({ url_path: "/UploadFile", destination_directory: GetDefaultDrive(), user_data: BSP.FilePostedAA, passwords: lwsCredentials })
		BSP.localServer.AddPostToFile({ url_path: "/UploadSyncSpec", destination_directory: GetDefaultDrive(), user_data: BSP.SyncSpecPostedAA, passwords: lwsCredentials })
		BSP.localServer.AddPostToFile({ url_path: "/PrepareForTransfer", destination_directory: GetDefaultDrive(), user_data: BSP.PrepareForTransferAA, passwords: lwsCredentials })
		BSP.localServer.AddGetFromEvent({ url_path: "/SpecifyCardSizeLimits", user_data: BSP.SpecifyCardSizeLimitsAA, passwords: lwsCredentials})
		
		BSP.localServer.AddGetFromEvent({ url_path: "/GetSnapshotConfiguration", user_data: BSP.GetSnapshotConfigurationAA})
		BSP.localServer.AddPostToFormData({ url_path: "/SetSnapshotConfiguration", user_data: BSP.SetSnapshotConfigurationAA, passwords: lwsCredentials})
		BSP.localServer.AddGetFromEvent({ url_path: "/GetSnapshot", user_data: BSP.GetSnapshotAA, passwords: lwsCredentials})
		BSP.localServer.AddGetFromEvent({ url_path: "/bsn/status", user_data: BSP.GetBSNStatusAA, passwords: lwsCredentials})		
		BSP.localServer.AddPostToFormData({ url_path: "/bsn/override", user_data: BSP.SetBSNOverrideAA, passwords: lwsCredentials})
		BSP.localServer.AddGetFromEvent({ url_path: "/bsn/override", user_data: BSP.CancelBSNOverrideAA, passwords: lwsCredentials})
'		BSP.localServer.AddMethodFromEvent({ method: "DELETE", url_path: "/bsn/override", user_data: BSP.CancelBSNOverrideAA, passwords: lwsCredentials})

		BSP.localServer.AddPostToFormData({ url_path: "/SendUDP", user_data: BSP.SendUdpRestAA })

        unitName$ = BSP.registrySettings.unitName$
        unitNamingMethod$ = BSP.registrySettings.unitNamingMethod$
        unitDescription$ = BSP.registrySettings.unitDescription$
        
        if BSP.registrySettings.lwsConfig$ = "c" then
            BSP.lwsConfig$ = "content"
        else
            BSP.lwsConfig$ = "status"
        endif
        
        service = { name: "BrightSign Web Service", type: "_http._tcp", port: 8080, _functionality: BSP.lwsConfig$, _serialNumber: sysInfo.deviceUniqueID$, _unitName: unitName$, _unitNamingMethod: unitNamingMethod$, _unitDescription: unitDescription$ }
        BSP.advert = CreateObject("roNetworkAdvertisement", service)
        if BSP.advert = invalid then
            stop
        end if
    
    else
    
        BSP.lwsConfig$ = "none"
    
	endif
	
	' create will fail if storage device is formatted as NTFS
	ok = CreateDirectory("pool")
	ok = CreateDirectory("feedPool")
	ok = CreateDirectory("feed_cache")
	ok = CreateDirectory("htmlWidgets")
'	ok = CreateDirectory("snapshots")

	BSP.assetPool = CreateObject("roAssetPool", "pool")
	BSP.feedPool = CreateObject("roAssetPool", "feedPool")

	activeSyncSpec = invalid

	BSP.contentEncrypted = false

    localCurrentSync = CreateObject("roSyncSpec")
    if localCurrentSync.ReadFromFile("local-sync.xml") or localCurrentSync.ReadFromFile("localSetupToStandalone-sync.xml") then

		activeSyncSpec = localCurrentSync

		BSP.assetCollection = localCurrentSync.GetAssets("download")
		BSP.assetPoolFiles = CreateObject("roAssetPoolFiles", BSP.assetPool, BSP.assetCollection)

		' update registry setting for USB content updates if necessary
		metadata = localCurrentSync.GetMetadata("client")
		if metadata.DoesExist("usbUpdatePassword") then
			usbUpdatePassphrase$ = localCurrentSync.LookupMetadata("client", "usbUpdatePassword")

			if usbUpdatePassphrase$ <> BSP.registrySettings.usbContentUpdatePassword$ then
				BSP.registrySettings.usbContentUpdatePassword$ = usbUpdatePassphrase$
				BSP.WriteRegistrySetting("uup", usbUpdatePassphrase$)
			endif
		endif

		BSP.GetSupportedFeatures()

		if metadata.DoesExist("obfuscatedPassphrase") and BSP.contentEncryptionSupported then
			obfuscatedPassphrase$ = localCurrentSync.LookupMetadata("client", "obfuscatedPassphrase")
			deviceCustomization = CreateObject("roDeviceCustomization")
			deviceCustomization.StoreObfuscatedEncryptionKey("AesCtrHmac", obfuscatedPassphrase$)
			BSP.contentEncrypted = true
		endif

    endif

' networking is considered active if current-sync.xml is present.
    networkedCurrentSyncValid = false
	networkedCurrentSync = CreateObject("roSyncSpec")
	
	if networkedCurrentSync.ReadFromFile("current-sync.xml") or networkedCurrentSync.ReadFromFile("localToBSN-sync.xml") then
	
		activeSyncSpec = networkedCurrentSync

		' if the device is configured for networking, require that the storage is writable
		if BSP.sysInfo.storageIsWriteProtected then DisplayStorageDeviceLockedMessage()

        networkedCurrentSyncValid = true

		BSP.contentXfersEnabledWired = GetDataTransferEnabled(networkedCurrentSync, "contentXfersEnabledWired")
		BSP.textFeedsXfersEnabledWired = GetDataTransferEnabled(networkedCurrentSync, "textFeedsXfersEnabledWired")
		BSP.healthXfersEnabledWired = GetDataTransferEnabled(networkedCurrentSync, "healthXfersEnabledWired")
		BSP.mediaFeedsXfersEnabledWired = GetDataTransferEnabled(networkedCurrentSync, "mediaFeedsXfersEnabledWired")
		BSP.logUploadsXfersEnabledWired = GetDataTransferEnabled(networkedCurrentSync, "logUploadsXfersEnabledWired")
    
		BSP.contentXfersEnabledWireless = GetDataTransferEnabled(networkedCurrentSync, "contentXfersEnabledWireless")
		BSP.textFeedsXfersEnabledWireless = GetDataTransferEnabled(networkedCurrentSync, "textFeedsXfersEnabledWireless")
		BSP.healthXfersEnabledWireless = GetDataTransferEnabled(networkedCurrentSync, "healthXfersEnabledWireless")
		BSP.mediaFeedsXfersEnabledWireless = GetDataTransferEnabled(networkedCurrentSync, "mediaFeedsXfersEnabledWireless")
		BSP.logUploadsXfersEnabledWireless = GetDataTransferEnabled(networkedCurrentSync, "logUploadsXfersEnabledWireless")
    
		BSP.networkingHSM = newNetworkingStateMachine(BSP, BSP.msgPort)
		
		BSP.networkingHSM.SetSystemInfo(sysInfo, diagnosticCodes)
		BSP.logging.networking = BSP.networkingHSM

		BSP.assetCollection = networkedCurrentSync.GetAssets("download")
		BSP.assetPoolFiles = CreateObject("roAssetPoolFiles", BSP.assetPool, BSP.assetCollection)

        BSP.downloadFiles = networkedCurrentSync.GetFileList("download")
        
		BSP.networkingActive = true
		
		BSP.GetSupportedFeatures()

		deviceCustomization = CreateObject("roDeviceCustomization")
		if BSP.contentEncryptionSupported and deviceCustomization.IsEncryptionKeyPresent("AesCtrHmac") then
			BSP.contentEncrypted = true
		endif

		BSP.SetPerFileEncryptionStatus(networkedCurrentSync)

		BSP.networkingHSM.Initialize()
		
    else
    
		BSP.networkingHSM = invalid
		
        BSP.networkingActive = false
        
    endif

' determine and set file paths for global files
	globalAA = GetGlobalAA()
    globalAA.autoscheduleFilePath$ = GetPoolFilePath(BSP.assetPoolFiles, "autoschedule.xml")
    globalAA.resourcesFilePath$ = GetPoolFilePath(BSP.assetPoolFiles, "resources.txt")
    globalAA.boseProductsFilePath$ = GetPoolFilePath(BSP.assetPoolFiles, "BoseProducts.xml")
    if globalAA.autoscheduleFilePath$ = "" then stop
    
' initialize logging parameters
    playbackLoggingEnabled = false
    eventLoggingEnabled = false
    diagnosticLoggingEnabled = false
    stateLoggingEnabled = false
    variableLoggingEnabled = false
    uploadLogFilesAtBoot = false
    uploadLogFilesAtSpecificTime = false
    uploadLogFilesTime% = 0

    if networkedCurrentSyncValid then
        if networkedCurrentSync.LookupMetadata("client", "playbackLoggingEnabled") = "yes" then playbackLoggingEnabled = true
        if networkedCurrentSync.LookupMetadata("client", "eventLoggingEnabled") = "yes" then eventLoggingEnabled = true
        if networkedCurrentSync.LookupMetadata("client", "diagnosticLoggingEnabled") = "yes" then diagnosticLoggingEnabled = true
        if networkedCurrentSync.LookupMetadata("client", "stateLoggingEnabled") = "yes" then stateLoggingEnabled = true
        if networkedCurrentSync.LookupMetadata("client", "variableLoggingEnabled") = "yes" then variableLoggingEnabled = true
        if networkedCurrentSync.LookupMetadata("client", "uploadLogFilesAtBoot") = "yes" then uploadLogFilesAtBoot = true
        if networkedCurrentSync.LookupMetadata("client", "uploadLogFilesAtSpecificTime") = "yes" then uploadLogFilesAtSpecificTime = true
        uploadLogFilesTime$ = networkedCurrentSync.LookupMetadata("client", "uploadLogFilesTime")

	' as of BrightAuthor 3.0, logging parameters are set when the user performs a standalone publish - no uploads for a standalone unit, so leave those values false
	else if type(localCurrentSync) = "roSyncSpec" then
        
		if localCurrentSync.LookupMetadata("client", "playbackLoggingEnabled") = "yes" then
			playbackLoggingEnabled = true
		else if localCurrentSync.LookupMetadata("client", "playbackLoggingEnabled") = "no" then
			playbackLoggingEnabled = false
        else if BSP.registrySettings.playbackLoggingEnabled = "yes" then
			playbackLoggingEnabled = true
		endif

		if localCurrentSync.LookupMetadata("client", "eventLoggingEnabled") = "yes" then
			eventLoggingEnabled = true
		else if localCurrentSync.LookupMetadata("client", "eventLoggingEnabled") = "no" then
			eventLoggingEnabled = false
        else if BSP.registrySettings.eventLoggingEnabled = "yes" then
			eventLoggingEnabled = true
		endif

		if localCurrentSync.LookupMetadata("client", "diagnosticLoggingEnabled") = "yes" then
			diagnosticLoggingEnabled = true
		else if localCurrentSync.LookupMetadata("client", "diagnosticLoggingEnabled") = "no" then
			diagnosticLoggingEnabled = false
        else if BSP.registrySettings.diagnosticLoggingEnabled = "yes" then
			diagnosticLoggingEnabled = true
		endif

		if localCurrentSync.LookupMetadata("client", "stateLoggingEnabled") = "yes" then
			stateLoggingEnabled = true
		else if localCurrentSync.LookupMetadata("client", "stateLoggingEnabled") = "no" then
			stateLoggingEnabled = false
        else if BSP.registrySettings.stateLoggingEnabled = "yes" then
			stateLoggingEnabled = true
		endif

		if localCurrentSync.LookupMetadata("client", "variableLoggingEnabled") = "yes" then
			variableLoggingEnabled = true
		else if localCurrentSync.LookupMetadata("client", "variableLoggingEnabled") = "no" then
			variableLoggingEnabled = false
        else if BSP.registrySettings.variableLoggingEnabled = "yes" then
			variableLoggingEnabled = true
		endif

		uploadLogFilesTime$ = ""

		BSP.GetDataTransferEnabledFromRegistry = GetDataTransferEnabledFromRegistry

		BSP.contentXfersEnabledWired = BSP.GetDataTransferEnabledFromRegistry("contentXfersEnabledWired$")
		BSP.textFeedsXfersEnabledWired = BSP.GetDataTransferEnabledFromRegistry("textFeedsXfersEnabledWired$")
		BSP.healthXfersEnabledWired = BSP.GetDataTransferEnabledFromRegistry("healthXfersEnabledWired$")
		BSP.mediaFeedsXfersEnabledWired = BSP.GetDataTransferEnabledFromRegistry("mediaFeedsXfersEnabledWired$")
		BSP.logUploadsXfersEnabledWired = BSP.GetDataTransferEnabledFromRegistry("logUploadsXfersEnabledWired$")

		BSP.contentXfersEnabledWireless = BSP.GetDataTransferEnabledFromRegistry("contentXfersEnabledWireless$")
		BSP.textFeedsXfersEnabledWireless = BSP.GetDataTransferEnabledFromRegistry("textFeedsXfersEnabledWireless$")
		BSP.healthXfersEnabledWireless = BSP.GetDataTransferEnabledFromRegistry("healthXfersEnabledWireless$")
		BSP.mediaFeedsXfersEnabledWireless = BSP.GetDataTransferEnabledFromRegistry("mediaFeedsXfersEnabledWireless$")
		BSP.logUploadsXfersEnabledWireless = BSP.GetDataTransferEnabledFromRegistry("logUploadsXfersEnabledWireless$")

	else
        if BSP.registrySettings.playbackLoggingEnabled = "yes" then playbackLoggingEnabled = true
        if BSP.registrySettings.eventLoggingEnabled = "yes" then eventLoggingEnabled = true
        if BSP.registrySettings.stateLoggingEnabled = "yes" then stateLoggingEnabled = true
        if BSP.registrySettings.diagnosticLoggingEnabled = "yes" then diagnosticLoggingEnabled = true
        if BSP.registrySettings.variableLoggingEnabled = "yes" then variableLoggingEnabled = true
        if BSP.registrySettings.uploadLogFilesAtBoot = "yes" then uploadLogFilesAtBoot = true
        if BSP.registrySettings.uploadLogFilesAtSpecificTime = "yes" then uploadLogFilesAtSpecificTime = true
        uploadLogFilesTime$ = BSP.registrySettings.uploadLogFilesTime$
    endif
    if uploadLogFilesTime$ <> "" then uploadLogFilesTime% = int(val(uploadLogFilesTime$))
    
' if the device is configured for logging, require that the storage is writable
	if (playbackLoggingEnabled or eventLoggingEnabled or stateLoggingEnabled or diagnosticLoggingEnabled or variableLoggingEnabled) and BSP.sysInfo.storageIsWriteProtected then DisplayStorageDeviceLockedMessage()

	BSP.variablesDBExists = false

' setup logging
    BSP.logging.InitializeLogging(playbackLoggingEnabled, eventLoggingEnabled, stateLoggingEnabled, diagnosticLoggingEnabled, variableLoggingEnabled, uploadLogFilesAtBoot, uploadLogFilesAtSpecificTime, uploadLogFilesTime%)
    
    BSP.logging.WriteDiagnosticLogEntry(diagnosticCodes.EVENT_STARTUP, BSP.sysInfo.deviceFWVersion$ + chr(9) + BSP.sysInfo.autorunVersion$ + chr(9) + BSP.sysInfo.customAutorunVersion$)
        
    BSP.InitializeNonPrintableKeyboardCodeList()

' ensure roAssetPool objects were created properly
	if type(BSP.assetPool) <> "roAssetPool" then
		BSP.diagnostics.PrintDebug("Unable to create roAssetPool for directory pool.")
		BSP.logging.WriteDiagnosticLogEntry(BSP.diagnosticCodes.EVENT_UNABLE_TO_CREATE_ASSET_POOL, "pool")
		BSP.logging.FlushLogFile()
	endif

	if type(BSP.feedPool) <> "roAssetPool" then
		BSP.diagnostics.PrintDebug("Unable to create roAssetPool for directory feedPool.")
		BSP.logging.WriteDiagnosticLogEntry(BSP.diagnosticCodes.EVENT_UNABLE_TO_CREATE_ASSET_POOL, "feedPool")
		BSP.logging.FlushLogFile()
	endif

' protect assets
	if not BSP.assetPool.ProtectAssets( "current", BSP.assetCollection ) then
		BSP.logging.WriteDiagnosticLogEntry(BSP.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, "AssetPool Protect Failure")
		BSP.logging.FlushLogFile()
		BSP.diagnostics.PrintDebug("### ProtectFiles failed: " + "AssetPool Protect Failure")
		stop
	endif

' limit pool sizes
	BSP.limitStorageSpace = false
	BSP.SetPoolSizes( activeSyncSpec )

' setup BrightSign application server
	if lwsEnabled then

		BSP.localServer.SetupDWSLink("Application")

		' retrieve the files that are part of the BrightSign application server
		prefix$ = "BrightSignApplicationServer-"
		allFiles = activeSyncSpec.GetFileList("download")
		for each fileNameAA in allFiles
			syncSpecFileName$ = fileNameAA.name
			if instr(1, syncSpecFileName$, prefix$) = 1 then
				fileName$ = mid(syncSpecFileName$, len(prefix$) + 1)
				ext = GetFileExtension(fileName$)
				if ext <> invalid then
					contentType$ = GetMimeTypeByExtension(ext)
					if contentType$ <> invalid then
						url$ = "/" + fileName$
						filePath$ = GetPoolFilePath(BSP.assetPoolFiles, syncSpecFileName$)

						BSP.localServer.AddGetFromFile({ url_path: url$, filename: filePath$, content_type: contentType$, passwords: lwsCredentials })
						if fileName$ = "index.html" then
							BSP.localServer.AddGetFromFile({ url_path: "/", filename: filePath$, content_type: contentType$, passwords: lwsCredentials })
						endif
					endif
				endif
			endif
		next
	endif

' Read and parse BoseProducts.xml
	BSP.boseProductSpecs = ReadBoseProductsFile()

' Get tuner data
	BSP.scannedChannels = GetScannedChannels()
	BSP.tunerScanPercentageComplete$ = "0"

' Set up Bluetooth manager for players that support Bluetooth
	BSP.btManager = BSP.newBtManager()

' GPIO state machines and associated data structures
	dim gpioStateMachineRequired[8]
	dim gpioSM[8]

	BSP.gpioStateMachineRequired = gpioStateMachineRequired
	BSP.gpioSM = gpioSM

' BP state machines and associated data structures
	dim bpStateMachineRequired[4,11]
	dim bpInputUsed[4,11]
	dim bpOutputUsed[4,11]
	dim bpSM[4,11]
	
	BSP.bpStateMachineRequired = bpStateMachineRequired
	BSP.bpInputUsed = bpInputUsed
	BSP.bpOutputUsed = bpOutputUsed
	BSP.bpSM = bpSM

' Create state machines

	' Player state machine
	BSP.playerHSM = newPlayerStateMachine(BSP)
	BSP.playerHSM.SetSystemInfo(sysInfo, diagnosticCodes)
	
	' Zone state machines are created by the Player state machine when it parses the schedule and autoplay files
    BSP.playerHSM.Initialize()
    
	BSP.CheckBLCsStatus()

    BSP.EventLoop()
    
End Sub


Function IsControlPort(controlPort As Object) As Boolean
	return type(controlPort) = "roControlPort"
End Function

Sub GetSupportedFeatures()

	featureMinRevsFilePath$ = GetPoolFilePath(m.assetPoolFiles, "featureMinRevs.xml")
	m.featureMinRevs = ParseFeatureMinRevs(featureMinRevsFilePath$)

' set booleans for each feature where autorun needs to know whether this version of FW supports it
	m.useInitiatorAddressFromPacketSupported = IsFeatureSupported("UseInitiatorAddressFromPacket", m.sysInfo.deviceFWVersion$, m.featureMinRevs)	
	m.setSyncDomainSupported = IsFeatureSupported("SetSyncDomain", m.sysInfo.deviceFWVersion$, m.featureMinRevs)	
	m.setDNSServersSupported = IsFeatureSupported("ResetInterfaceSettings", m.sysInfo.deviceFWVersion$, m.featureMinRevs)	
	m.forceResolutionSupported = IsFeatureSupported("ForceResolution", m.sysInfo.deviceFWVersion$, m.featureMinRevs)	
	m.showHideVideoZoneSupported = IsFeatureSupported("ShowHideVideoZone", m.sysInfo.deviceFWVersion$, m.featureMinRevs)
	m.bypassProxySupported = IsFeatureSupported("BypassProxy", m.sysInfo.deviceFWVersion$, m.featureMinRevs)
	m.contentEncryptionSupported = IsFeatureSupported("ContentEncryption", m.sysInfo.deviceFWVersion$, m.featureMinRevs)
	m.htmlSetTransformSupported = IsFeatureSupported("HtmlSetTransform", m.sysInfo.deviceFWVersion$, m.featureMinRevs)
	m.httpWidgetGetUserAgentSupported = IsFeatureSupported("HttpWidgetGetUserAgent", m.sysInfo.deviceFWVersion$, m.featureMinRevs)
	m.mosaicModeSupported = IsFeatureSupported("MosaicMode", m.sysInfo.deviceFWVersion$, m.featureMinRevs)
	m.beaconsSupported = IsFeatureSupported("Beacons", m.sysInfo.deviceFWVersion$, m.featureMinRevs)
	m.btleSupported = IsFeatureSupported("Btle", m.sysInfo.deviceFWVersion$, m.featureMinRevs)
	m.fullResolutionGraphicsSupported = IsFeatureSupported("FullResolutionGraphics", m.sysInfo.deviceFWVersion$, m.featureMinRevs)

End Sub


Sub SetPerFileEncryptionStatus(syncSpec As Object)

' get per file encryption status
	m.encryptionByFile = {}
	listOfDownloads = syncSpec.GetFileList("download")
	for each download in listOfDownloads
		if download.DoesExist("encryption") then
			m.encryptionByFile.AddReplace(download.name, download.encryption)
		endif
	next

End Sub


Function ParseFeatureMinRevs(path$ As String)

	featureMinRevs = {}

    featureMinRevs$ = ReadAsciiFile(path$)
    if len(featureMinRevs$) > 0 then
    
		featureMinRevsXML = CreateObject("roXMLElement")
		featureMinRevsXML.Parse(featureMinRevs$)

		' verify that this is a valid FeatureMinRevs XML file
		if featureMinRevsXML.GetName() <> "FeatureMinRevs" then print "Invalid FeatureMinRevs XML file - name not FeatureMinRevs" : stop
		if not IsString(featureMinRevsXML@version) then print "Invalid FeatureMinRevs XML file - version not found" : stop    

		featuresXML = featureMinRevsXML.feature

		for each featureXML in featuresXML
			featureName$ = featureXML.name.GetText()
			minRev$ = featureXML.minFWRev.GetText()
			
			featureMinRevs.AddReplace(featureName$, minRev$)
		next
	endif

	return featureMinRevs

End Function


Sub SetPoolSizes( syncSpec As Object ) As Object

	limitStorageSpace$ = syncSpec.LookupMetadata("client", "limitStorageSpace")
	if lcase(limitStorageSpace$) = "true" then
		limitStorageSpace = true
		spaceLimitedByAbsoluteSize = syncSpec.LookupMetadata("client", "spaceLimitedByAbsoluteSize")
		publishedDataSizeLimitMB = syncSpec.LookupMetadata("client", "publishedDataSizeLimitMB")
		publishedDataSizeLimitPercentage = syncSpec.LookupMetadata("client", "publishedDataSizeLimitPercentage")
		dynamicDataSizeLimitMB = syncSpec.LookupMetadata("client", "dynamicDataSizeLimitMB")
		dynamicDataSizeLimitPercentage = syncSpec.LookupMetadata("client", "dynamicDataSizeLimitPercentage")
		htmlDataSizeLimitPercentage = syncSpec.LookupMetadata("client", "htmlDataSizeLimitPercentage")
		htmlDataSizeLimitMB = syncSpec.LookupMetadata("client", "htmlDataSizeLimitMB")
		htmlLocalStorageSizeLimitPercentage = syncSpec.LookupMetadata("client", "htmlLocalStorageSizeLimitPercentage")
		htmlLocalStorageSizeLimitMB = syncSpec.LookupMetadata("client", "htmlLocalStorageSizeLimitMB")
		htmlIndexedDBSizeLimitPercentage = syncSpec.LookupMetadata("client", "htmlIndexedDBSizeLimitPercentage")
		htmlIndexedDBSizeLimitMB = syncSpec.LookupMetadata("client", "htmlIndexedDBSizeLimitMB")
	else
		limitStorageSpace = false
	endif

	if limitStorageSpace then

		if lcase(spaceLimitedByAbsoluteSize) <> "true" then

			' convert from percentage settings to absolute settings
			du = CreateObject("roStorageInfo", "./")
			totalCardSizeMB% = du.GetSizeInMegabytes()

			' pool size for published data
			publishedDataSizeLimitPercentage% = int(val(publishedDataSizeLimitPercentage))
			maximumPublishedDataPoolSizeMB% = publishedDataSizeLimitPercentage% * totalCardSizeMB% / 100

			' pool size for dynamic data
			dynamicDataSizeLimitPercentage% = int(val(dynamicDataSizeLimitPercentage))
			maximumDynamicDataPoolSizeMB% = dynamicDataSizeLimitPercentage% * totalCardSizeMB% / 100

			' size for html data
			htmlDataSizeLimitPercentage% = int(val(htmlDataSizeLimitPercentage))
			maximumHTMLDataPoolSizeMB% = htmlDataSizeLimitPercentage% * totalCardSizeMB% / 100

			' size for html local storage
			htmlLocalStorageSizeLimitPercentage% = 0
			maximumHTMLLocalStoragePoolSizeMB% = 0
			if htmlLocalStorageSizeLimitPercentage <> "" then 
				htmlLocalStorageSizeLimitPercentage% = int(val(htmlLocalStorageSizeLimitPercentage))
				maximumHTMLLocalStoragePoolSizeMB% = htmlLocalStorageSizeLimitPercentage% * totalCardSizeMB% / 100
			endif
			
			' size for html indexed db
			htmlIndexedDBSizeLimitPercentage% = 0
			maximumHTMLIndexedDBPoolSizeMB% = 0
			if htmlIndexedDBSizeLimitPercentage <> "" then 
				htmlIndexedDBSizeLimitPercentage% = int(val(htmlIndexedDBSizeLimitPercentage))
				maximumHTMLIndexedDBPoolSizeMB% = htmlIndexedDBSizeLimitPercentage% * totalCardSizeMB% / 100
			endif

		else

			maximumPublishedDataPoolSizeMB% = int(val(publishedDataSizeLimitMB))
			maximumDynamicDataPoolSizeMB% = int(val(dynamicDataSizeLimitMB))
			maximumHTMLDataPoolSizeMB% = int(val(htmlDataSizeLimitMB))

			maximumHTMLLocalStoragePoolSizeMB% = 0
			if htmlLocalStorageSizeLimitMB <> "" then
				maximumHTMLLocalStoragePoolSizeMB% = int(val(htmlLocalStorageSizeLimitMB))
			endif

			maximumHTMLIndexedDBPoolSizeMB% = 0
			if htmlIndexedDBSizeLimitMB <> "" then
				maximumHTMLIndexedDBPoolSizeMB% = int(val(htmlIndexedDBSizeLimitMB))
			endif

		endif

		ok = m.assetPool.SetMaximumPoolSizeMegabytes( maximumPublishedDataPoolSizeMB% )
		' if not ok ??

		ok = m.feedPool.SetMaximumPoolSizeMegabytes( maximumDynamicDataPoolSizeMB% )
		' if not ok ??

		if maximumHTMLDataPoolSizeMB% > 0 then
			' max int is  2147483647 => <2048 MB
			if maximumHTMLDataPoolSizeMB% >= 2048 then
				maximumHTMLDataPoolSizeMB% = 2047
			endif
	        r=CreateObject("roRectangle",0,0,0,0)
			htmlWidget = CreateObject("roHtmlWidget", r)
			if type(htmlWidget) = "roHtmlWidget" then
				ok = htmlWidget.SetAppCacheSize( maximumHTMLDataPoolSizeMB% * 1024.0 * 1024.0 )
				ok = htmlWidget.SetAppCacheDir("/HTMLAppCache")
				htmlWidget = invalid
			endif
		endif

		if maximumHTMLLocalStoragePoolSizeMB% > 0 then

			' max int is  2147483647 => <2048 MB
			if maximumHTMLLocalStoragePoolSizeMB% >= 2048 then
				maximumHTMLLocalStoragePoolSizeMB% = 2047
			endif
	        r=CreateObject("roRectangle",0,0,0,0)
			htmlWidget = CreateObject("roHtmlWidget", r)
			if type(htmlWidget) = "roHtmlWidget" then
				ok = htmlWidget.SetLocalStorageDir("localstorage")
				ok = htmlWidget.SetLocalStorageQuota(maximumHTMLLocalStoragePoolSizeMB% * 1024 * 1024)
				htmlWidget = invalid
			endif
		endif

		if maximumHTMLIndexedDBPoolSizeMB% > 0 then

			' max int is  2147483647 => <2048 MB
			if maximumHTMLIndexedDBPoolSizeMB% >= 2048 then
				maximumHTMLIndexedDBPoolSizeMB% = 2047
			endif
	        r=CreateObject("roRectangle",0,0,0,0)
			htmlWidget = CreateObject("roHtmlWidget", r)
			if type(htmlWidget) = "roHtmlWidget" then
				ok = htmlWidget.SetWebDatabaseDir("IndexedDB")
				ok = htmlWidget.SetWebDatabaseQuota(maximumHTMLIndexedDBPoolSizeMB% * 1024 * 1024)
				htmlWidget = invalid
			endif
		endif

	else
		' clear prior settings
		ok = m.assetPool.SetMaximumPoolSizeMegabytes( -1 )
		' if not ok ??
		ok = m.feedPool.SetMaximumPoolSizeMegabytes( -1 )
		' if not ok ??

	endif

End Sub


Sub CheckBLCsStatus()

	CheckBLCStatus(m.blcs[0], 0)
	CheckBLCStatus(m.blcs[1], 0)
	CheckBLCStatus(m.blcs[2], 0)

End Sub


Sub CheckBLCStatus(controlPort As Object, channel% As Integer)

	if type(controlPort) <> "roControlPort" return

	control_cmd = CreateObject("roArray", 4, false)

	CHANNEL_CMD_STATUS%    = &h1700

	control_cmd[ 0 ] = CHANNEL_CMD_STATUS%
	control_cmd[ 1 ] = channel%				' Channel to check status for (note use 0 for main power)
	control_cmd[ 2 ] = 0                    ' unused
	control_cmd[ 3 ] = 0                    ' unused

	controlPort.SetOutputValues(control_cmd)

End Sub


Function GetDataTransferEnabled(syncSpec As Object, syncSpecEntry$ As String) As Boolean

	spec$ = syncSpec.LookupMetadata("client", syncSpecEntry$)
	dataTransferEnabled = true
	if lcase(spec$) = "false" then dataTransferEnabled = false
	return dataTransferEnabled

End Function


Function GetDataTransferEnabledFromRegistry(registryKey$ As String) As Boolean

	if m.registrySettings.Lookup(registryKey$) = "False" then
		registryValue = false
	else
		registryValue = true
	endif

	return registryValue

End Function


Function GetBinding(wiredTransferEnabled As Boolean, wirelessTransferEnabled As Boolean) As Integer

	binding% = -1
	if wiredTransferEnabled <> wirelessTransferEnabled then
		if wiredTransferEnabled then
			binding% = 0
		else
			binding% = 1
		endif
	endif

	return binding%

End Function


Function newBSP(sysFlags As Object, msgPort As Object, systemTime As Object) As Object

    BSP = {}
	globalAA = GetGlobalAA()
    globalAA.bsp = BSP
	BSP.globalAA = globalAA

    BSP.msgPort = msgPort

    registrySection = CreateObject("roRegistrySection", "networking")
    if type(registrySection)<>"roRegistrySection" then print "Error: Unable to create roRegistrySection":stop
    BSP.registrySection = registrySection    

    BSP.systemTime = systemTime

    BSP.diagnostics = newDiagnostics(sysFlags)

    BSP.Restart = Restart
    BSP.StartPlayback = StartPlayback

	BSP.ClearImageBuffers = ClearImageBuffers
	
    BSP.newLogging = newLogging
    BSP.logging = BSP.newLogging()
	BSP.LogActivePresentation = LogActivePresentation

    BSP.SetTouchRegions = SetTouchRegions
    BSP.InitializeTouchScreen = InitializeTouchScreen
    BSP.AddRectangularTouchRegion = AddRectangularTouchRegion

	BSP.TuneToChannel = TuneToChannel
	BSP.SendTuneFailureMessage = SendTuneFailureMessage

    BSP.ExecuteMediaStateCommands = ExecuteMediaStateCommands
    BSP.ExecuteTransitionCommands = ExecuteTransitionCommands
    BSP.ExecuteCmd = ExecuteCmd
	BSP.ExecuteSwitchPresentationCommand = ExecuteSwitchPresentationCommand
	BSP.ChangeRFChannel = ChangeRFChannel

    BSP.EventLoop = EventLoop

    BSP.MapDigitalOutput = MapDigitalOutput
    BSP.SetAudioOutput = SetAudioOutput
    BSP.SetAudioMode = SetAudioMode
    BSP.MapStereoOutput = MapStereoOutput
    BSP.SetSpdifMute = SetSpdifMute
    BSP.SetAnalogMute = SetAnalogMute
    BSP.SetHDMIMute = SetHDMIMute
    
	BSP.UnmuteAllAudio = UnmuteAllAudio
	BSP.UnmuteAudioConnector = UnmuteAudioConnector
	BSP.SetAllAudioOutputs = SetAllAudioOutputs
	BSP.SetAudioMode1 = SetAudioMode1
	BSP.MuteAudioOutput = MuteAudioOutput
	BSP.MuteAudioOutputs = MuteAudioOutputs
	BSP.SetConnectorVolume = SetConnectorVolume
	BSP.ChangeConnectorVolume = ChangeConnectorVolume
	BSP.SetZoneVolume = SetZoneVolume
	BSP.ChangeZoneVolume = ChangeZoneVolume
	BSP.SetZoneChannelVolume = SetZoneChannelVolume
	BSP.ChangeZoneChannelVolume = ChangeZoneChannelVolume
	BSP.SetUSBAudioOutput = SetUSBAudioOutput

    BSP.SetVideoVolume = SetVideoVolume
	BSP.SetVideoVolumeByConnector = SetVideoVolumeByConnector
	BSP.ChangeVideoVolumeByConnector = ChangeVideoVolumeByConnector
	BSP.IncrementVideoVolumeByConnector = IncrementVideoVolumeByConnector
	BSP.DecrementVideoVolumeByConnector = DecrementVideoVolumeByConnector
    BSP.ChangeVideoVolume = ChangeVideoVolume
    BSP.IncrementVideoVolume = IncrementVideoVolume
    BSP.DecrementVideoVolume = DecrementVideoVolume
    BSP.SetVideoChannnelVolume = SetVideoChannnelVolume
    BSP.IncrementVideoChannnelVolumes = IncrementVideoChannnelVolumes
    BSP.DecrementVideoChannnelVolumes = DecrementVideoChannnelVolumes
    
    BSP.SetAudioVolume = SetAudioVolume
	BSP.SetAudioVolumeByConnector = SetAudioVolumeByConnector
	BSP.ChangeAudioVolumeByConnector = ChangeAudioVolumeByConnector
	BSP.IncrementAudioVolumeByConnector = IncrementAudioVolumeByConnector
	BSP.DecrementAudioVolumeByConnector = DecrementAudioVolumeByConnector
    BSP.ChangeAudioVolume = ChangeAudioVolume
    BSP.IncrementAudioVolume = IncrementAudioVolume
    BSP.DecrementAudioVolume = DecrementAudioVolume
    BSP.SetAudioChannnelVolume = SetAudioChannnelVolume
    BSP.IncrementAudioChannelVolumes = IncrementAudioChannelVolumes
    BSP.DecrementAudioChannelVolumes = DecrementAudioChannelVolumes
    
	BSP.SetAudioVolumeLimits = SetAudioVolumeLimits
    
	BSP.GetZone = GetZone
	BSP.GetVideoZone = GetVideoZone

    BSP.ChangeChannelVolumes = ChangeChannelVolumes
    BSP.SetChannelVolumes = SetChannelVolumes
    
    BSP.PauseVideo = PauseVideo
    BSP.ResumeVideo = ResumeVideo
    BSP.SetPowerSaveMode = SetPowerSaveMode
    
    BSP.CecDisplayOn = CecDisplayOn
    BSP.CecDisplayOff = CecDisplayOff
    BSP.CecPhilipsSetVolume = CecPhilipsSetVolume
    BSP.SendCecCommand = SendCecCommand
	BSP.CecSetSourceBrightSign = CecSetSourceBrightSign
    
    BSP.WaitForSyncResponse = WaitForSyncResponse
    
    BSP.XMLAutoschedule = XMLAutoSchedule

    BSP.GetNonPrintableKeyboardCode = GetNonPrintableKeyboardCode
    BSP.InitializeNonPrintableKeyboardCodeList = InitializeNonPrintableKeyboardCodeList

	BSP.ConfigureBPs = ConfigureBPs
	BSP.ConfigureBP = ConfigureBP
	BSP.ConfigureBPButton = ConfigureBPButton
	BSP.ConfigureBPInput = ConfigureBPInput
	
	BSP.ConfigureGPIOButton = ConfigureGPIOButton
	BSP.ConfigureGPIOInput = ConfigureGPIOInput

    BSP.GetID = GetID
    BSP.GetCurrentStatus = GetCurrentStatus
	BSP.GetUDPEvents = GetUDPEvents
	BSP.GetRemoteData = GetRemoteData
	BSP.GetIDInfoPage = GetIDInfoPage
    BSP.FilePosted = FilePosted
    BSP.SyncSpecPosted = SyncSpecPosted
    BSP.PrepareForTransfer = PrepareForTransfer
    BSP.FreeSpaceOnDrive = FreeSpaceOnDrive
	
	BSP.GetBoseProductSpec = GetBoseProductSpec
	
	BSP.CreateSerial = CreateSerial
	BSP.ScheduleRetryCreateSerial = ScheduleRetryCreateSerial
	BSP.RetryCreateSerial = RetryCreateSerial
	BSP.AttemptOpenSerial = AttemptOpenSerial
	BSP.CreateUDPSender = CreateUDPSender
	BSP.SendUDPNotification = SendUDPNotification
	BSP.udpNotificationAddress$ = "224.0.200.200"
	BSP.udpNotificationPort% = 5000

	BSP.rssFileIndex% = 0
	BSP.GetRSSTempFilename = GetRSSTempFilename
		
	BSP.ReadVariablesDB = ReadVariablesDB
	BSP.DBIsValid = DBIsValid
 	BSP.DBTablesExist = DBTablesExist
	BSP.ReadVariables = ReadVariables
	BSP.ReadSchema1Tables = ReadSchema1Tables
	BSP.CreateSchema2Tables = CreateSchema2Tables
	BSP.CreateDBTable = CreateDBTable
	BSP.DeleteDBTable = DeleteDBTable
	BSP.DropSchema1Tables = DropSchema1Tables
	BSP.AddDBSection = AddDBSection
	BSP.AddDBCategory = AddDBCategory
	BSP.AddDBVariable = AddDBVariable
	BSP.DeleteDBVariable = DeleteDBVariable
	BSP.UpdateDBVariable = UpdateDBVariable
	BSP.UpdateDBVariableDefaultValue = UpdateDBVariableDefaultValue
	BSP.UpdateDBVariableMediaUrl = UpdateDBVariableMediaUrl
	BSP.UpdateDBVariablePosition = UpdateDBVariablePosition
	BSP.GetDBVersion = GetDBVersion
	BSP.SetDBVersion = SetDBVersion
	BSP.UpdateDBVersion = UpdateDBVersion
	BSP.GetDBTableNames = GetDBTableNames
	BSP.GetDBSectionNames = GetDBSectionNames
	BSP.GetDBSectionId = GetDBSectionId
	BSP.GetDBCategoryId = GetDBCategoryId
	BSP.GetDBCategoryNames = GetDBCategoryNames
	BSP.DoGetCategories = DoGetCategories
	BSP.GetOrderedVariables = GetOrderedVariables
	BSP.GetUserVariableCategoryList = GetUserVariableCategoryList
	BSP.GetUserVariablesByCategoryList = GetUserVariablesByCategoryList
	BSP.GetUserVariablesGivenCategory = GetUserVariablesGivenCategory
	BSP.GetCategoryIdFromAccess = GetCategoryIdFromAccess
	BSP.GetCategoryFromSection = GetCategoryFromSection
	BSP.ExecuteDBInsert = ExecuteDBInsert
	BSP.ExecuteDBSelect	= ExecuteDBSelect
	BSP.GetDBVersionCallback = GetDBVersionCallback
	BSP.GetDBCategoryIdCallback = GetDBCategoryIdCallback
	BSP.GetDBTableNamesCallback = GetDBTableNamesCallback
	BSP.GetDBSectionNamesCallback = GetDBSectionNamesCallback
	BSP.GetDBSectionIdCallback = GetDBSectionIdCallback
	BSP.ReadSchema1TablesCallback = ReadSchema1TablesCallback
	BSP.GetUserVariablesGivenCategoryCallback = GetUserVariablesGivenCategoryCallback
	BSP.ReadVariablesCallback = ReadVariablesCallback

	BSP.ExportVariablesDBToAsciiFile = ExportVariablesDBToAsciiFile
	BSP.GetUserVariable = GetUserVariable
	BSP.DeleteVariable = DeleteVariable
	BSP.ResetVariables = ResetVariables
	BSP.ResetVariable = ResetVariable
	BSP.ChangeUserVariableValue = ChangeUserVariableValue
	BSP.AssignSystemVariablesToUserVariables = AssignSystemVariablesToUserVariables
	BSP.AssignSystemVariableToUserVariables = AssignSystemVariableToUserVariables

	BSP.UpdateDataFeed = UpdateDataFeed
	BSP.CreateUserVariablesFromDataFeed = CreateUserVariablesFromDataFeed

	BSP.CheckBLCsStatus = CheckBLCsStatus
	BSP.CheckBLCStatus = CheckBLCStatus

	BSP.UpdateIPAddressUserVariables = UpdateIPAddressUserVariables
	BSP.UpdateRFChannelCountUserVariables = UpdateRFChannelCountUserVariables
	BSP.UpdateRFChannelUserVariables = UpdateRFChannelUserVariables
	BSP.UpdateTunerScanPercentageCompleteUserVariables = UpdateTunerScanPercentageCompleteUserVariables
	BSP.UpdateEdidUserVariables = UpdateEdidUserVariables

	BSP.GetAttachedFiles = GetAttachedFiles

	BSP.PostponeRestart = PostponeRestart
	BSP.ProcessMediaEndEvent = ProcessMediaEndEvent

	BSP.SetPoolSizes = SetPoolSizes

	BSP.InitiateRemoteSnapshotTimer = InitiateRemoteSnapshotTimer
	BSP.RemoveRemoteSnapshotTimer = RemoveRemoteSnapshotTimer

	BSP.NetworkingIsActive = NetworkingIsActive

	BSP.DeviceSupportsRotation = DeviceSupportsRotation
	
	BSP.GetSupportedFeatures = GetSupportedFeatures

	BSP.encryptionByFile = {}
	BSP.SetPerFileEncryptionStatus = SetPerFileEncryptionStatus
	BSP.SetEncryptionAttributes = SetEncryptionAttributes

	BSP.QueueRetrieveLiveDataFeed = QueueRetrieveLiveDataFeed
	BSP.RetrieveLiveDataFeed = RetrieveLiveDataFeed
	BSP.RetrievePendingLiveDataFeed = RetrievePendingLiveDataFeed
	BSP.AdvanceToNextLiveDataFeedInQueue = AdvanceToNextLiveDataFeedInQueue
	BSP.RemoveFailedFeedFromQueue = RemoveFailedFeedFromQueue

	BSP.newBtManager = newBtManager

    return BSP
    
End Function

'endregion

'region Local Web server
Sub GetConfigurationPage(userData as Object, e as Object)

    mVar = userData.mVar

'	print "respond to GetConfigurationPage request"
	e.AddResponseHeader("Content-type", "text/html; charset=utf-8")

	if type(mVar.sign) = "roAssociativeArray" and mVar.sign.deviceWebPageDisplay$ = "None" then

		e.SetResponseBodyString("")
		if not e.SendResponse(403) then stop

	else if mVar.deviceWebPageFilePath$ <> ""

		webPageContents$ = ReadAsciiFile(mVar.deviceWebPageFilePath$)
		e.SetResponseBodyString(webPageContents$)
		if not e.SendResponse(200) then stop

	else
	
		e.SetResponseBodyString("")
		if not e.SendResponse(404) then stop

	endif

End Sub


Sub SendUdpRest(userData as Object, e as Object)

  mVar = userData.mVar 
  args = e.GetFormData()
  CreateUDPSender(mVar)
  for each key in args
     value=args[key]
     mVar.udpSender.Send(value)
''     print "sendUDP key: "+key+" value: "+value
  next
  if not e.SendResponse(200) then stop

end Sub


Function GetCategoryFromSection(sectionName$ as String, categoryName$ as String) As Integer

	sectionId% = m.GetDBSectionId(sectionName$)
	if sectionId% < 0 return -1
	return m.GetDBCategoryId(sectionId%, categoryName$)

End Function


Sub SetValuesByCategory(userData as Object, e as Object)

    mVar = userData.mVar

    args = e.GetFormData()

	if args.DoesExist("category") then

		categoryName$ = args.Lookup("category")

		categoryId% = mVar.GetCategoryFromSection(mVar.activePresentation$, categoryName$)
		if categoryId% < 0 then
			categoryId% = mVar.GetCategoryFromSection("Shared", categoryName$)
		endif

		if categoryId% > 0 then
			' is there a way to check that userVariableName exists in the db?
			for each userVariableName in args
				mVar.UpdateDBVariable(categoryId%, userVariableName, args.Lookup(userVariableName))
			next
		endif

	endif

	e.AddResponseHeader("Location", e.GetRequestHeader("Referer"))
	if not e.SendResponse(302) then stop

End Sub


Sub SetValues(userData as Object, e as Object)

'	print "respond to SetValues request"

    mVar = userData.mVar

    args = e.GetFormData()
'   print args

	userVariables = mVar.currentUserVariables

	userVariablesUpdated = false

	if type(userVariables) = "roAssociativeArray" then
		for each userVariableName in args
			if userVariables.DoesExist(userVariableName) then
				userVariable = userVariables.Lookup(userVariableName)
				userVariable.SetCurrentValue(args.Lookup(userVariableName), false)
				userVariablesUpdated = true
			endif
		next
	endif

	e.AddResponseHeader("Location", e.GetRequestHeader("Referer"))
	if not e.SendResponse(302) then stop

	if userVariablesUpdated then
		userVariablesChanged = CreateObject("roAssociativeArray")
		userVariablesChanged["EventType"] = "USER_VARIABLES_UPDATED"
		mVar.msgPort.PostMessage(userVariablesChanged)

		' Notify controlling devices to refresh
		mVar.SendUDPNotification("refresh")
	endif

End Sub


Sub PopulateIDData(mVar As Object, root As Object)

    unitName$ = mVar.registrySettings.unitName$
    unitNamingMethod$ = mVar.registrySettings.unitNamingMethod$
    unitDescription$ = mVar.registrySettings.unitDescription$

    elem = root.AddElement("unitName")
    elem.SetBody(unitName$)

    elem = root.AddElement("unitNamingMethod")
    elem.SetBody(unitNamingMethod$)

    elem = root.AddElement("unitDescription")
    elem.SetBody(unitDescription$)

    elem = root.AddElement("serialNumber")
    elem.SetBody(mVar.sysInfo.deviceUniqueID$)

    elem = root.AddElement("functionality")
    elem.SetBody(mVar.lwsConfig$)
    
    elem = root.AddElement("autorunVersion")
    elem.SetBody(mVar.sysInfo.autorunVersion$)
	
    elem = root.AddElement("firmwareVersion")
    elem.SetBody(mVar.sysInfo.deviceFWVersion$)
	
	elem = root.AddElement("bsnActive")
	if mVar.NetworkingIsActive() then
		elem.SetBody("yes")
	else
		elem.SetBody("no")
	endif
	
	elem = root.AddElement("snapshotsAvailable")
	globalAA = GetGlobalAA()
	if globalAA.listOfSnapshotFiles.Count() > 0 then
		elem.SetBody("yes")
	else
		elem.SetBody("no")
	endif
	
End Sub


Function newUDPItem(udpLabel As Object, udpEventName As Object) As Object

	udpItem = {}
	udpItem.action$ = udpEventName
	udpItem.label$ = udpLabel
	return udpItem

End Function


Sub PopulateUDPData(mVar As Object, root As Object)

	sign = mVar.sign

	elem = root.AddElement("udpNotificationAddress")
	elem.SetBody(mVar.udpNotificationAddress$)

	elem = root.AddElement("notificationPort")
	elem.SetBody(StripLeadingSpaces(stri(mVar.udpNotificationPort%)))

	elem = root.AddElement("destinationPort")
	elem.SetBody(StripLeadingSpaces(stri(mVar.udpNotificationPort%)))

	if type(sign) = "roAssociativeArray" then

		elem = root.AddElement("receivePort")
		elem.SetBody(StripLeadingSpaces(stri(sign.udpReceivePort)))

		udpEvents = { }
		for each zoneHSM in sign.zonesHSM
			for each stateName in zoneHSM.stateTable

				state = zoneHSM.stateTable[stateName]

				udpEventsInState = state.udpEvents
				if type(udpEventsInState) = "roAssociativeArray" then

					for each udpEventName in udpEventsInState

						transition = udpEventsInState.Lookup(udpEventName)

						if type(transition.udpExport) = "roBoolean" and transition.udpExport then

							if type(transition.udpLabel$) = "roString" then
								udpLabel$ = transition.udpLabel$
							else
								udpLabel$ = udpEventName
							endif

							if not udpEvents.DoesExist(udpLabel$) then
								udpEvents.AddReplace(udpLabel$, newUDPItem(udpLabel$, udpEventName))
							endif

						endif

						if udpEventName = "<any>" or udpEventName = "(.*)" then
							targetMediaState$ = udpEventsInState.Lookup(udpEventName).targetMediaState$
							if targetMediaState$ <> "" then
								targetState = zoneHSM.stateTable[targetMediaState$]
								if targetState.type$ = "playFile" then
									filesTable = targetState.filesTable
									for each playFileKey in filesTable
										entry = filesTable[playFileKey]
										export = true
										if entry.DoesExist("export") and (not entry["export"]) then
											export = false
										endif
										if export then
											udpLabel$ = playFileKey
											if entry.DoesExist("label$") then
												udpLabel$ = entry["label$"]
											endif
											if not udpEvents.DoesExist(udpLabel$) then
												udpEvents.AddReplace(udpLabel$, newUDPItem(udpLabel$, playFileKey))
											endif
										endif
									next
								endif
							endif
						endif

					next

				endif

				if state.type$ = "rfInputChannel" then
					if type(state.channelUpEvent) = "roAssociativeArray" and IsString(state.channelUpEvent.udpUserEvent$) and type(state.channelUpEvent.udpExport) = "roBoolean" and state.channelUpEvent.udpExport then
						udpLabel$ = state.channelUpEvent.udpLabel$
						if not udpEvents.DoesExist(udpLabel$) then
							udpEvents.AddReplace(udpLabel$, newUDPItem(udpLabel$, state.channelUpEvent.udpUserEvent$))
						endif
					endif
					if type(state.channelDownEvent) = "roAssociativeArray" and IsString(state.channelDownEvent.udpUserEvent$) and type(state.channelDownEvent.udpExport) = "roBoolean" and state.channelDownEvent.udpExport then
						udpLabel$ = state.channelDownEvent.udpLabel$
						if not udpEvents.DoesExist(udpLabel$) then
							udpEvents.AddReplace(udpLabel$, newUDPItem(udpLabel$, state.channelDownEvent.udpUserEvent$))
						endif
					endif
				endif

			next
		next

		udpEventsElem = root.AddElement("udpEvents")
		udpEventsElem.AddAttribute("useLabel","true")

		for each udpEvent in udpEvents

			udpItem = udpEvents.Lookup(udpEvent)

			udpEventElem = udpEventsElem.AddElement("udpEvent")
			udpEventLabel = udpEventElem.AddElement("label")
			udpEventLabel.SetBody(udpItem.label$)
			udpEventAction = udpEventElem.AddElement("action")
			udpEventAction.SetBody(udpItem.action$)

		next

	endif

End Sub


Sub GetID(userData as Object, e as Object)

    mVar = userData.mVar
    
'    print "respond to GetID request"

    root = CreateObject("roXMLElement")
    root.SetName("BrightSignID")

	PopulateIDData(mVar, root)

    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

    e.AddResponseHeader("Content-type", "text/xml")
    e.SetResponseBodyString(xml)
    e.SendResponse(200)
      
End Sub


Sub GetUDPEvents(userData as Object, e as Object)

    mVar = userData.mVar

    root = CreateObject("roXMLElement")
    root.SetName("BrightSignUDPEvents")

	PopulateUDPData(mVar, root)

    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

    e.AddResponseHeader("Content-type", "text/xml")
    e.SetResponseBodyString(xml)
    e.SendResponse(200)

End Sub


Sub GetRemoteData(userData as Object, e as Object)

    mVar = userData.mVar
    
'    print "respond to GetRemoteData request"

    root = CreateObject("roXMLElement")
    root.SetName("BrightSignRemoteData")

	PopulateIDData(mVar, root)

	PopulateUDPData(mVar, root)

    elem = root.AddElement("contentPort")
    elem.SetBody("8008")
	
    elem = root.AddElement("activePresentation")
	if mVar.activePresentation$ <> invalid then
		elem.SetBody(mVar.activePresentation$)
	endif

    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

    e.AddResponseHeader("Content-type", "text/xml")
    e.SetResponseBodyString(xml)
    e.SendResponse(200)

End Sub


Function GetUserVariables(mVar As Object) As Object

	userVariablesList = mVar.GetOrderedVariables(mVar.activePresentation$)

	' restrict returned user variables to those in the presentation
	filteredUserVariables = []
	for each userVariable in userVariablesList
		if mVar.currentUserVariables.DoesExist(userVariable.name$) then
			uv = mVar.currentUserVariables.Lookup(userVariable.name$)
			if uv.position% <> -1 then
				userVariable.position% = uv.position%
				filteredUserVariables.push(userVariable)
			endif
		endif
	next

	if mVar.sign.alphabetizeVariableNames then
		BubbleSortUserVariables(filteredUserVariables, "name$")
	else
		BubbleSortUserVariables(filteredUserVariables, "position%")
	endif

	return filteredUserVariables

End Function


Sub BubbleSortUserVariables(userVariables As Object, sortKey$ As String)

	if type(userVariables) = "roArray" then
	
		n = userVariables.Count()

		while n <> 0

			newn = 0
			for i = 1 to (n - 1)
				if userVariables[i-1].Lookup(sortKey$) > userVariables[i].Lookup(sortKey$) then
					k = userVariables[i]
					userVariables[i] = userVariables[i-1]
					userVariables[i-1] = k
					newn = i
				endif
			next
			n = newn

		end while

	endif

End Sub


Sub PopulateUserVarData(mVar As Object, root As Object)

	userVariables = GetUserVariables(mVar)
	for each userVariable in userVariables
		variableName = userVariable.name$
		elem = root.AddElement("BrightSignVar")
		elem.AddAttribute("name",variableName)
		elem.SetBody(userVariable.GetCurrentValue())
	next

End Sub


Sub GetUserVars(userData as Object, e as Object)

    mVar = userData.mVar

    root = CreateObject("roXMLElement")
    root.SetName("BrightSignUserVariables")

	PopulateUserVarData(mVar, root)

    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

	e.AddResponseHeader("Content-type", "text/xml; charset=utf-8")
    e.SetResponseBodyString(xml)
    e.SendResponse(200)
      
End Sub


Sub GetUserVariableCategories(userData as Object, e as Object)

    mVar = userData.mVar

    root = CreateObject("roXMLElement")
    root.SetName("BrightSignUserVariableCategories")

	userVariableCategoryList = mVar.GetUserVariableCategoryList(mVar.activePresentation$)

	for each userVariableCategory in userVariableCategoryList
		elem = root.AddElement("BrightSignUserVariableCategory")
		elem.SetBody(userVariableCategory)
	next

    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

	e.AddResponseHeader("Content-type", "text/xml; charset=utf-8")
    e.SetResponseBodyString(xml)
    e.SendResponse(200)

End Sub


Sub UpdateFeedByCategory(userData as Object, e as Object)

    mVar = userData.mVar

	categoryName$ = e.GetRequestParam("CategoryName")

	updateDataFeedByCategoryMsg = CreateObject("roAssociativeArray")
	updateDataFeedByCategoryMsg["EventType"] = "UPDATE_DATA_FEED_BY_CATEGORY"
	updateDataFeedByCategoryMsg["Name"] = categoryName$
	mVar.msgPort.PostMessage(updateDataFeedByCategoryMsg)

    e.SendResponse(200)

End Sub


Sub UpdateAllFeeds(userData as Object, e as Object)

    mVar = userData.mVar

	updateDataFeedByCategoryMsg = CreateObject("roAssociativeArray")
	updateDataFeedByCategoryMsg["EventType"] = "UPDATE_ALL_DATA_FEEDS"
	mVar.msgPort.PostMessage(updateDataFeedByCategoryMsg)

    e.SendResponse(200)

End Sub


Sub GetUserVariablesByCategory(userData as Object, e as Object)

    mVar = userData.mVar

    root = CreateObject("roXMLElement")
    root.SetName("BrightSignUserVariablesByCategory")

	categoryName$ = e.GetRequestParam("CategoryName")
	if categoryName$ <> "" then
		userVariablesByCategoryList = mVar.GetUserVariablesByCategoryList(categoryName$)
		for each userVariable in userVariablesByCategoryList
			variableName = userVariable.name$
			elem = root.AddElement("BrightSignVar")
			elem.AddAttribute("name",variableName)
			elem.AddAttribute("mediaUrl", userVariable.mediaUrl$)
			elem.SetBody(userVariable.GetCurrentValue())
		next
	endif

    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

	e.AddResponseHeader("Content-type", "text/xml; charset=utf-8")
    e.SetResponseBodyString(xml)
    e.SendResponse(200)

End Sub


Sub GetCurrentStatus(userData as Object, e as Object)
    
    mVar = userData.mVar
    
'    print "respond to GetCurrentStatus request"
  
    root = CreateObject("roXMLElement")
    root.SetName("BrightSignStatus")

    autorunVersion$ = mVar.sysInfo.autorunVersion$

    unitName$ = mVar.registrySettings.unitName$
    unitNamingMethod$ = mVar.registrySettings.unitNamingMethod$
    unitDescription$ = mVar.registrySettings.unitDescription$
    
    elem = root.AddElement("unitName")
    elem.AddAttribute("label", "Unit Name")
    elem.SetBody(unitName$)

    elem = root.AddElement("unitNamingMethod")
    elem.AddAttribute("label", "Unit Naming Method")
    elem.SetBody(unitNamingMethod$)

    elem = root.AddElement("unitDescription")
    elem.AddAttribute("label", "Unit Description")
    elem.SetBody(unitDescription$)

    modelObject = CreateObject("roDeviceInfo")
    
    elem = root.AddElement("model")
    elem.AddAttribute("label", "Model")
    elem.SetBody(modelObject.GetModel())

    elem = root.AddElement("firmware")
    elem.AddAttribute("label", "Firmware")
    elem.SetBody(modelObject.GetVersion())

    elem = root.AddElement("autorun")
    elem.AddAttribute("label", "Autorun")
    elem.SetBody(mVar.sysInfo.autorunVersion$)

    elem = root.AddElement("serialNumber")
    elem.AddAttribute("label", "Serial Number")
    elem.SetBody(modelObject.GetDeviceUniqueId())

    elem = root.AddElement("functionality")
    elem.AddAttribute("label", "Functionality")
    elem.SetBody(mVar.lwsConfig$)
    
' 86400 seconds per day
    deviceUptime% = modelObject.GetDeviceUptime()
    numDays% = deviceUptime% / 86400
    numHours% = (deviceUptime% - (numDays% * 86400)) / 3600
    numMinutes% = (deviceUptime% - (numDays% * 86400) - (numHours% * 3600)) / 60
    numSeconds% = deviceUptime% - (numDays% * 86400) - (numHours% * 3600) - (numMinutes% * 60)
    deviceUptime$ = ""
    if numDays% > 0 then deviceUptime$ = stri(numDays%) + " days "
    if numHours% > 0 then deviceUptime$ = deviceUptime$ + stri(numHours%) + " hours "
    if numMinutes% > 0 then deviceUptime$ = deviceUptime$ + stri(numMinutes%) + " minutes "
    if numSeconds% > 0 then deviceUptime$ = deviceUptime$ + stri(numSeconds%) + " seconds"
            
    elem = root.AddElement("deviceUptime")
    elem.AddAttribute("label", "Device Uptime")
    elem.SetBody(deviceUptime$)
    
    elem = root.AddElement("activePresentation")
    elem.AddAttribute("label", "Active Presentation")
    elem.SetBody(mVar.activePresentation$)
    
	if mVar.globalAA.enableRemoteSnapshot then
		numSnapshots% = mVar.globalAA.listOfSnapshotFiles.Count()
		if numSnapshots% > 0 then
			latestSnapshot = mVar.globalAA.listOfSnapshotFiles[numSnapshots%-1]
			index% = instr(1, latestSnapshot, ".jpg")
			if index% > 0 then
				id$ = mid(latestSnapshot, 1, index% - 1)
				elem = root.AddElement("snapshotId")
				elem.AddAttribute("label", "Snapshot ID")
				elem.SetBody(id$)

				elem = root.AddElement("snapshotDisplayPortrait")
				elem.AddAttribute("label", "Snapshot Display Portrait")
				if mVar.globalAA.remoteSnapshotDisplayPortrait then
					elem.SetBody("true")
				else
					elem.SetBody("false")
				endif
			endif
		endif
	endif

'    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })
    xml = root.GenXML({ header: true })

    e.AddResponseHeader("Content-type", "text/xml")
    e.SetResponseBodyString(xml)
    e.SendResponse(200)
  
End Sub


Sub GetIDInfoPage(userData as Object, e as Object)

    mVar = userData.mVar

'	print "respond to GetIDInfoPage request"
	e.AddResponseHeader("Content-type", "text/html; charset=utf-8")

	deviceIdWebPageFilePath$ = GetPoolFilePath(mVar.assetPoolFiles, "_deviceIdWebPage.html")
	webPageContents$ = ReadAsciiFile(deviceIdWebPageFilePath$)
	e.SetResponseBodyString(webPageContents$)
	if not e.SendResponse(200) then stop

End Sub


Sub FilePosted(userData as Object, e as Object)

'    print "respond to FilePosted request"

	destinationFilename = e.GetRequestHeader("Destination-Filename")

	currentDir$ = "pool/"
	poolDepth% = 2
	while poolDepth% > 0
		newDir$ = Left(Right(destinationFilename, poolDepth%), 1)
		currentDir$ = currentDir$ + newDir$ + "/"
		CreateDirectory(currentDir$)
		poolDepth% = poolDepth% - 1
	end while

	regex = CreateObject("roRegEx","/","i")
	fileParts = regex.Split(destinationFilename)

	fullFilePath$ = currentDir$ + fileParts[1]

	MoveFile(e.GetRequestBodyFile(), fullFilePath$)

	e.SetResponseBodyString("RECEIVED")
    e.SendResponse(200)

End Sub


Function GetContentFiles(topDir$ As String) As Object

	allFiles = { }

	firstLevelDirs = MatchFiles(topDir$, "*")
	for each firstLevelDir in firstLevelDirs
		firstLevelDirSpec$ = topDir$ + firstLevelDir + "/"
		secondLevelDirs = MatchFiles(firstLevelDirSpec$, "*")
		for each secondLevelDir in secondLevelDirs
			secondLevelDirSpec$ = firstLevelDirSpec$ + secondLevelDir + "/"
			files = MatchFiles(secondLevelDirSpec$, "*")
			for each file in files
				allFiles.AddReplace(file, secondLevelDirSpec$)
			next
		next
	next

	return allFiles

End Function


Sub PopulateSnapshotData(mVar As Object, root As Object, startTimeSpecified As Boolean, startTime As Object)

	globalAA = GetGlobalAA()
	for each snapshotFile in globalAA.listOfSnapshotFiles	

		' get timestamp, id from file name
		index% = instr(1, snapshotFile, ".jpg")
		if index% > 0 then

			time$ = mid(snapshotFile, 1, index% - 1)
			snapshotTime = CreateObject("roDateTime")
			snapshotTime.FromIsoString(time$)

			if not startTimeSpecified or snapshotTime.GetString() >= startTime.GetString() then
				
				itemElem = root.AddElement("Item")
		
				timeElem = itemElem.AddElement("Time")
				timeElem.SetBody(time$)
		
				idElem = itemElem.AddElement("ID")
				idElem.SetBody(time$)

	'			imagePathElem = itemElem.AddElement("ImagePath")
	'			imagePathElem.SetBody("/snapshots/" + snapshotFile)
			endif

		endif

	next

End Sub


Function GetSnapshotConfiguration(userData as Object, e as Object) As Object

    mVar = userData.mVar

	globalAA = GetGlobalAA()

    root = CreateObject("roXMLElement")
    root.SetName("BrightSignSnapshots")
	root.AddAttribute("Count", StripLeadingSpaces(stri(globalAA.listOfSnapshotFiles.Count())))
	if globalAA.enableRemoteSnapshot <> invalid then
		if globalAA.enableRemoteSnapshot then
			strVal = "true"
		else
			strVal = "false"
		end if
		root.AddAttribute("Enabled", strVal)
	endif
	if globalAA.remoteSnapshotInterval <> invalid then
		root.AddAttribute("Interval", StripLeadingSpaces(stri(globalAA.remoteSnapshotInterval)))
	endif
	if globalAA.remoteSnapshotMaxImages <> invalid then
		root.AddAttribute("MaxImages", StripLeadingSpaces(stri(globalAA.remoteSnapshotMaxImages)))
	endif
	if globalAA.remoteSnapshotJpegQualityLevel <> invalid then
		root.AddAttribute("Quality", StripLeadingSpaces(stri(globalAA.remoteSnapshotJpegQualityLevel)))
	endif
	if mVar.videoMode <> invalid then
		root.AddAttribute("ResX", StripLeadingSpaces(stri(mVar.videoMode.GetOutputResX())))
		root.AddAttribute("ResY", StripLeadingSpaces(stri(mVar.videoMode.GetOutputResY())))
	endif

	if globalAA.remoteSnapshotDisplayPortrait <> invalid then
		remoteSnapshotDisplayPortrait = "false"
		if globalAA.remoteSnapshotDisplayPortrait then
			remoteSnapshotDisplayPortrait = "true"
		endif
		root.AddAttribute("DisplayPortraitMode", remoteSnapshotDisplayPortrait)
	endif
	' CanConfigure attribute notifies clients if snapshot configuration can be managed by this unit
	root.AddAttribute("CanConfigure","true")

	startTimeSpecified = false
	startDT = CreateObject("roDateTime")
	startTime$ = e.GetRequestParam("starttime")

	if startTime$ <> "" then
		startTimeSpecified = startDT.FromIsoString(startTime$ + ".000")
	endif

	PopulateSnapshotData(mVar, root, startTimeSpecified, startDT)

    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

    e.AddResponseHeader("Content-type", "text/xml")
    e.SetResponseBodyString(xml)
    e.SendResponse(200)

End Function


Function SetSnapshotConfiguration(userData as Object, e as Object) As Object

    mVar = userData.mVar

	' only set the override timer if appropriate (networking configured / active)
	overrideModeActive = false
	if mVar.NetworkingIsActive() then
		args = e.GetFormData()
		if type(args) = "roAssociativeArray" and type(args.remoteSnapshotBsnTimeout) = "roString" then
			timeout% = int(val(args.remoteSnapshotBsnTimeout))

			mVar.overrideBSNTimer = CreateObject("roTimer")
			mVar.overrideBSNTimer.SetPort(mvar.msgPort)
			mVar.overrideBSNTimer.SetElapsed(timeout%, 0)
			mVar.overrideBSNTimer.Start()

			mVar.diagnostics.PrintDebug("### SetBSNOverride for " + args.remoteSnapshotBsnTimeout + " seconds.")
			mVar.logging.WriteDiagnosticLogEntry(mVar.diagnosticCodes.EVENT_SET_BSN_OVERRIDE, "### SetBSNOverride for " + "69" + " seconds.")

			overrideModeActive = true
		endif
	endif

    mVar.diagnostics.PrintDebug("### SetSnapshotConfiguration")
	mVar.logging.WriteDiagnosticLogEntry(mVar.diagnosticCodes.EVENT_SET_SNAPSHOT_CONFIGURATION, "")

	e.AddResponseHeader("Content-type", "text/plain")

	' don't allow the user to set the configuration is bsn is connected and not in override mode
	if mVar.NetworkingIsActive() and not overrideModeActive then
		e.SetResponseBodyString("")
		if not e.SendResponse(403) then stop
		return 0
	endif

	globalAA = GetGlobalAA()

	' remember current settings
	currentEnableRemoteSnapshot = globalAA.enableRemoteSnapshot
	currentRemoteSnapshotInterval = globalAA.remoteSnapshotInterval
	currentRemoteSnapshotMaxImages = globalAA.remoteSnapshotMaxImages
	currentRemoteSnapshotJpegQualityLevel = globalAA.remoteSnapshotJpegQualityLevel
	currentRemoteSnapshotDisplayPortrait = globalAA.remoteSnapshotDisplayPortrait

	' set global values from form data
	args = e.GetFormData()
	if args.enableRemoteSnapshot <> invalid then
		globalAA.enableRemoteSnapshot = GetBoolFromString(args.enableRemoteSnapshot, false)
	endif
	if args.remoteSnapshotInterval <> invalid then
		globalAA.remoteSnapshotInterval = int(val(args.remoteSnapshotInterval))
	endif
	if args.remoteSnapshotMaxImages <> invalid then
		globalAA.remoteSnapshotMaxImages = int(val(args.remoteSnapshotMaxImages))
	endif
	if args.remoteSnapshotJpegQualityLevel <> invalid then
		globalAA.remoteSnapshotJpegQualityLevel = int(val(args.remoteSnapshotJpegQualityLevel))
	endif
	if args.remoteSnapshotDisplayPortrait <> invalid then
		globalAA.remoteSnapshotDisplayPortrait = GetBoolFromString(args.remoteSnapshotDisplayPortrait, false)
	endif

	if not overrideModeActive then		' set registry values if not in override mode

		registrySection = CreateObject("roRegistrySection", "networking")
		if type(registrySection)<>"roRegistrySection" then print "Error: Unable to create roRegistrySection":stop
	
		registrySection.Write("enableRemoteSnapshot", GetYesNoFromBool(globalAA.enableRemoteSnapshot))
		registrySection.Write("remoteSnapshotInterval", stri(globalAA.remoteSnapshotInterval))
		registrySection.Write("remoteSnapshotMaxImages", stri(globalAA.remoteSnapshotMaxImages))
		registrySection.Write("remoteSnapshotJpegQualityLevel", stri(globalAA.remoteSnapshotJpegQualityLevel))
		registrySection.Write("remoteSnapshotDisplayPortrait", GetYesNoFromBool(globalAA.remoteSnapshotDisplayPortrait))
	
	endif

	if globalAA.enableRemoteSnapshot then

		DeleteExcessSnapshots(globalAA)

		if not currentEnableRemoteSnapshot or globalAA.remoteSnapshotInterval <> currentRemoteSnapshotInterval then
			mVar.InitiateRemoteSnapshotTimer()
		endif

	else if currentEnableRemoteSnapshot then

		mVar.RemoveRemoteSnapshotTimer()
	
	endif

	e.SetResponseBodyString("OK")
	e.SendResponse(200)

End Function


Function GetSnapshot(userData as Object, e as Object) As Object

    mVar = userData.mVar

	globalAA = GetGlobalAA()
	listOfSnapshotFiles = globalAA.listOfSnapshotFiles

	snapshotID$ = e.GetRequestParam("ID")

	if snapshotID$ <> "" then
		' perform linear search to find image
		for each snapshotFile in listOfSnapshotFiles
			if snapshotFile = snapshotID$ + ".jpg" then
				e.AddResponseHeader("Content-type", "image/jpeg")
				e.SetResponseBodyFile("snapshots/" + snapshotID$ + ".jpg")
				e.SendResponse(200)
				return 0
			endif
		next
	endif

	e.AddResponseHeader("Content-type", "text/plain; charset=utf-8")

	if snapshotID$ <> "" then
		e.SetResponseBodyString("Snapshot file corresponding to ID " + snapshotID$ + " not found")
    else
		e.SetResponseBodyString("Snapshot ID not specified.")
	endif

	e.SendResponse(404)

End Function


Function GetBSNStatus(userData as Object, e as Object) As Object

    print "respond to GetBSNStatus request"

    mVar = userData.mVar

    root = CreateObject("roXMLElement")
    root.SetName("BrightSignID")

	elem = root.AddElement("bsnActive")
	if mVar.NetworkingIsActive() then
		elem.SetBody("yes")
	else
		elem.SetBody("no")
	endif

    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

    e.AddResponseHeader("Content-type", "text/xml")
    e.SetResponseBodyString(xml)
    e.SendResponse(200)

End Function


Function SetBSNOverride(userData as Object, e as Object) As Object

    print "respond to SetBSNOverride request"

    mVar = userData.mVar

	' only set the override timer if appropriate (networking configured / active)
	if mVar.NetworkingIsActive() then
		args = e.GetFormData()
		timeout% = int(val(args.timeout))

		mVar.overrideBSNTimer = CreateObject("roTimer")
		mVar.overrideBSNTimer.SetPort(mvar.msgPort)
		mVar.overrideBSNTimer.SetElapsed(timeout%, 0)
		mVar.overrideBSNTimer.Start()

		mVar.diagnostics.PrintDebug("### SetBSNOverride for " + args.timeout + " seconds.")
		mVar.logging.WriteDiagnosticLogEntry(mVar.diagnosticCodes.EVENT_SET_BSN_OVERRIDE, "### SetBSNOverride for " + args.timeout + " seconds.")
	endif

	e.AddResponseHeader("Content-type", "text/plain")
	e.SetResponseBodyString("OK")
	e.SendResponse(200)

End Function


Function CancelBSNOverride(userData as Object, e as Object) As Object

    print "respond to CancelBSNOverride request"

    mVar = userData.mVar

    mVar.diagnostics.PrintDebug("### CancelBSNOverride")
	mVar.logging.WriteDiagnosticLogEntry(mVar.diagnosticCodes.EVENT_CANCEL_BSN_OVERRIDE, "")

	if type(mVar.overrideBSNTimer) = "roTimer" then
		mVar.overrideBSNTimer = invalid
		mVar.networkingHSM.SetBASParameters(mVar.networkingHSM.currentSync, false, false, true, true)
	endif

	e.AddResponseHeader("Content-type", "text/plain")
	e.SetResponseBodyString("OK")
	e.SendResponse(200)

End Function

'endregion

'region Sync
Function FreeSpaceOnDrive() As Object

    filesToPublish$ = ReadAsciiFile("filesToPublish.xml")
    if filesToPublish$ = "" then stop

' files that need to be copied by BrightAuthor
    filesToCopy = CreateObject("roAssociativeArray")
    
' files that can be deleted to make room for more content    
    deletionCandidates = CreateObject("roAssociativeArray")
    oldLocationDeletionCandidates = CreateObject("roAssociativeArray")

' create list of files already on the card
    listOfOldPoolFiles = MatchFiles("/pool", "*")
    for each file in listOfOldPoolFiles
        oldLocationDeletionCandidates.AddReplace(file, file)
    next

    listOfPoolFiles = GetContentFiles("/pool/")
    for each file in listOfPoolFiles
        deletionCandidates.AddReplace(file, listOfPoolFiles.Lookup(file))
    next
        
' create the list of files that need to be copied. this is the list of files in filesToPublish that are not in listOfPoolFiles
    filesToPublish = CreateObject("roXMLElement")
    filesToPublish.Parse(filesToPublish$)

' determine total space required
    totalSpaceRequired! = 0    
    for each fileXML in filesToPublish.file
        fullFileName$ = fileXML.fullFileName.GetText()
        o = deletionCandidates.Lookup(fullFileName$)
        if not IsString(o) then		' file is not already on the card
        
            fileItem = CreateObject("roAssociativeArray")
            fileItem.fileName$ = fileXML.fileName.GetText()
            fileItem.filePath$ = fileXML.filePath.GetText()
            fileItem.hashValue$ = fileXML.hashValue.GetText()
            fileItem.fileSize$ = fileXML.fileSize.GetText()

            filesToCopy.AddReplace(fullFileName$, fileItem)		' files that need to be copied to the card

            fileSize% = val(fileItem.fileSize$)
            totalSpaceRequired! = totalSpaceRequired! + fileSize%
            
        endif
    next
    filesToPublish = invalid

' determine if additional space is required
	du = CreateObject("roStorageInfo", "./")
    freeInMegabytes! = du.GetFreeInMegabytes()
    totalFreeSpace! = freeInMegabytes! * 1048576
    
' print "totalFreeSpace = "; totalFreeSpace!;", totalSpaceRequired = ";totalSpaceRequired!

	if m.limitStorageSpace then

		budgetedMaximumPoolSize = 0

		if m.spaceLimitedByAbsoluteSize = "true" then
			budgetedMaximumPoolSize = m.publishedDataSizeLimitMB * (1024.0 * 1024.0)
		else
			totalCardSize = du.GetSizeInMegabytes() * (1024.0 * 1024.0)
			publishedDataSizeLimitPercentage% = m.publishedDataSizeLimitPercentage
			budgetedMaximumPoolSize = publishedDataSizeLimitPercentage% * totalCardSize / 100.0
		endif

		' calculate the space that will be used after the files are copied over (size of existing pool + size of files that are getting copied over)
		' totalSpaceRequired! = size of files that are getting copied over
		totalSizeOfPoolAfterCopy! = totalSpaceRequired!		' units are bytes
	    for each file in listOfPoolFiles
			relativePath$ = listOfPoolFiles.Lookup(file)
			fullPath$ = relativePath$ + file
			' get size of file
			size% = GetFileSize( fullPath$ )
			totalSizeOfPoolAfterCopy! = totalSizeOfPoolAfterCopy! + size%
		next

	endif

	deleteUnneededFiles = false
	if totalFreeSpace! < totalSpaceRequired! then
		deleteUnneededFiles = true
	endif
	if m.limitStorageSpace then
		if totalSizeOfPoolAfterCopy! > budgetedMaximumPoolSize then
			deleteUnneededFiles = true
		endif
	endif
    
	if deleteUnneededFiles then

' parse local-sync.xml - remove its files from deletionCandidates
        localSync$ = ReadAsciiFile("local-sync.xml")
        if localSync$ <> "" then
            localSync = CreateObject("roXMLElement")
            localSync.Parse(localSync$)

            for each fileXML in localSync.files.download
                hashValue$ = fileXML.hash.GetText()
                hashMethod$ = fileXML.hash@method
                fileName$ = hashMethod$ + "-" + hashValue$
                fileExisted = deletionCandidates.Delete(fileName$)
				if not fileExisted then
	                fileExisted = oldLocationDeletionCandidates.Delete(fileName$)
				endif
	        next
        endif

' parse filesToPublish.xml - remove its files from deletionCandidates
        
        filesToPublish = CreateObject("roXMLElement")
        filesToPublish.Parse(filesToPublish$)
        
        for each fileXML in filesToPublish.file
            fullFileName$ = fileXML.fullFileName.GetText()
            fileExisted = deletionCandidates.Delete(fullFileName$)
        next

' delete all files that used the old style pool strategy that aren't currently in use
		for each fileToDelete in oldLocationDeletionCandidates
            pathOnCard$ = "/pool/" + fileToDelete
            oldLocationDeletionCandidates.Delete(fileToDelete)
            ok = DeleteFile(pathOnCard$)
		next

' delete files from deletionCandidates until totalFreeSpace! > totalSpaceRequired!
' if the user has limited storage space for pool files, delete content until that limitation is reached

        for each fileToDelete in deletionCandidates
        
			path$ = deletionCandidates.Lookup(fileToDelete)
            pathOnCard$ = path$ + fileToDelete

			if m.limitStorageSpace then
				size% = GetFileSize( pathOnCard$ )
				totalSizeOfPoolAfterCopy! = totalSizeOfPoolAfterCopy! - size%
			endif

            deletionCandidates.Delete(fileToDelete)
            DeleteFile(pathOnCard$)

			continueDeleting = false
			
			if m.limitStorageSpace then

				if totalSizeOfPoolAfterCopy! > budgetedMaximumPoolSize then
					continueDeleting = true
				endif
			
			else
            
				du = invalid
				du = CreateObject("roStorageInfo", "./")
				freeInMegabytes! = du.GetFreeInMegabytes()
				totalFreeSpace! = freeInMegabytes! * 1048576
            
' print "Delete file ";pathOnCard$        
' print "totalFreeSpace = "; totalFreeSpace!;", totalSpaceRequired = ";totalSpaceRequired!

				if totalFreeSpace! <= totalSpaceRequired! then
					continueDeleting = true
				endif

			endif

			if not continueDeleting then
				return filesToCopy
			endif

        next
        
		' the way this code is currently written, this method will return 'fail' if we can't delete enough files to get to the budgeted amount

        return "fail"
            
    endif
    
    return filesToCopy
    
End Function


Function GetFileSize( filePath$ As String ) As Integer

	size = 0
	checkFile = CreateObject("roReadFile", filePath$)
	if (checkFile <> invalid) then
		checkFile.SeekToEnd()
		size = checkFile.CurrentPosition()
		checkFile = invalid
	endif

	return size

End Function


Sub SpecifyCardSizeLimits(userData as Object, e as Object)

	mVar = userData.mVar ' = bsp

	limitStorageSpace = lcase(e.GetRequestParam("limitStorageSpace"))
	if limitStorageSpace = "true" then
		mVar.limitStorageSpace = true
		mVar.spaceLimitedByAbsoluteSize = lcase(e.GetRequestParam("spaceLimitedByAbsoluteSize"))
		mVar.publishedDataSizeLimitMB = ConvertToInt(e.GetRequestParam("publishedDataSizeLimitMB"))
		mVar.publishedDataSizeLimitPercentage = ConvertToInt(e.GetRequestParam("publishedDataSizeLimitPercentage"))
		mVar.dynamicDataSizeLimitMB = ConvertToInt(e.GetRequestParam("dynamicDataSizeLimitMB"))
		mVar.dynamicDataSizeLimitPercentage = ConvertToInt(e.GetRequestParam("dynamicDataSizeLimitPercentage"))
		mVar.htmlDataSizeLimitPercentage = ConvertToInt(e.GetRequestParam("htmlDataSizeLimitPercentage"))
		mVar.htmlDataSizeLimitMB = ConvertToInt(e.GetRequestParam("htmlDataSizeLimitMB"))

		mVar.htmlLocalStorageSizeLimitPercentage = 0
		mVar.htmlLocalStorageSizeLimitMB = 0
		if e.GetRequestParam("htmlDataSizeLimitPercentage") <> "" then
			mVar.htmlLocalStorageSizeLimitPercentage = ConvertToInt(e.GetRequestParam("htmlLocalStorageSizeLimitPercentage"))
			mVar.htmlLocalStorageSizeLimitMB = ConvertToInt(e.GetRequestParam("htmlLocalStorageSizeLimitMB"))
		endif

		mVar.htmlIndexedDBSizeLimitPercentage = 0
		mVar.htmlIndexedDBSizeLimitMB = 0
		if e.GetRequestParam("htmlIndexedDBSizeLimitPercentage") <> "" then
			mVar.htmlIndexedDBSizeLimitPercentage = ConvertToInt(e.GetRequestParam("htmlIndexedDBSizeLimitPercentage"))
			mVar.htmlIndexedDBSizeLimitMB = ConvertToInt(e.GetRequestParam("htmlIndexedDBSizeLimitMB"))
		endif
	else
		mVar.limitStorageSpace = false
	endif

	e.SetResponseBodyString("")
	if not e.SendResponse(200) then stop

End Sub


Function ConvertToInt(str As string) As Integer

	if str = "" then
		return 0
	endif

	return int(val(str))

End Function


Sub PrepareForTransfer(userData as Object, e as Object)

    mVar = userData.mVar
    
'    print "respond to PrepareForTransfer request"

    MoveFile(e.GetRequestBodyFile(), "filesToPublish.xml")
    
    filesToCopy = mVar.FreeSpaceOnDrive()
    if type(filesToCopy) = "roAssociativeArray" then

        root = CreateObject("roXMLElement")
        
        root.SetName("filesToCopy")

		root.AddAttribute("Family", mVar.sysInfo.deviceFamily$)
		root.AddAttribute("Model", mVar.sysInfo.deviceModel$)
		root.AddAttribute("FWVersion", mVar.sysInfo.deviceFWVersion$)
		root.AddAttribute("FWVersionNumber", StripLeadingSpaces(stri(mVar.sysInfo.deviceFWVersionNumber%)))

        for each key in filesToCopy
        
            fileItem = filesToCopy[key]
            
            item = root.AddBodyElement()
            item.SetName("file")

            elem = item.AddElement("fileName")
            elem.SetBody(fileItem.fileName$)
        
            elem = item.AddElement("filePath")
            elem.SetBody(fileItem.filePath$)
        
            elem = item.AddElement("hashValue")
            elem.SetBody(fileItem.hashValue$)
        
            elem = item.AddElement("fileSize")
            elem.SetBody(fileItem.fileSize$)
        
        next

        xml = root.GenXML({ header: true })

		e.SetResponseBodyString(xml)
		e.SendResponse(200)
            
    else
' the following call is ignored on a post
'		e.SetResponseBodyString("413")
		e.SendResponse(413)
    endif
        
End Sub


Sub SyncSpecPosted(userData as Object, e as Object)

    EVENT_REALIZE_SUCCESS = 101

    mVar = userData.mVar
    
'    print "respond to SyncSpecPosted request"

'    MoveFile(e.GetRequestBodyFile(), "tmp:new-sync.xml")
    MoveFile(e.GetRequestBodyFile(), "new-sync.xml")
    e.SetResponseBodyString("RECEIVED")
    e.SendResponse(200)
    
    oldSync = CreateObject("roSyncSpec")
    ok = oldSync.ReadFromFile("local-sync.xml")
    if not ok then stop
    
	newSync = CreateObject("roSyncSpec")
	ok = newSync.ReadFromFile("new-sync.xml")
    if not ok then stop

	oldSyncSpecScriptsOnly  = oldSync.FilterFiles("download", { group: "script" } )
	newSyncSpecScriptsOnly  = newSync.FilterFiles("download", { group: "script" } )

'	listOfDownloadFiles = newSyncSpecScriptsOnly.GetFileList("download")
'    for each downloadFile in listOfDownloadFiles
'		print "name = ";downloadFile.name
'		print "size = ";downloadFile.size
'		print "hash = ";downloadFile.hash
'	next

    mVar.diagnostics.PrintTimestamp()
    mVar.diagnostics.PrintDebug("### LWS DOWNLOAD COMPLETE")

	mVar.assetCollection = newSync.GetAssets("download")
	mVar.assetPoolFiles = CreateObject("roAssetPoolFiles", mVar.assetPool, mVar.assetCollection)
    if type(mVar.assetPoolFiles) <> "roAssetPoolFiles" then stop

	rebootRequired = false

	if not oldSyncSpecScriptsOnly.FilesEqualTo(newSyncSpecScriptsOnly) then

		' Protect all the media files that the current sync spec is using in case we fail part way 
		' through and need to continue using it. 
		if not (mVar.assetPool.ProtectAssets("current", oldSync) and mVar.assetPool.ProtectAssets("new", newSync)) then
			print "Failed to protect files that we need in the pool"
			stop	
		endif   

		realizer = CreateObject("roAssetRealizer", mVar.assetPool, "/")
		globalAA = GetGlobalAA()
		globalAA.bsp.msgPort.DeferWatchdog(120)
		event = realizer.Realize(newSyncSpecScriptsOnly)
		realizer = invalid

		if event.GetEvent() <> EVENT_REALIZE_SUCCESS then
	        mVar.logging.WriteDiagnosticLogEntry(mVar.diagnosticCodes.EVENT_REALIZE_FAILURE, stri(event.GetEvent()) + chr(9) + event.GetName() + chr(9) + event.GetFailureReason())
			mVar.diagnostics.PrintDebug("### Realize failed " + stri(event.GetEvent()) + chr(9) + event.GetName() + chr(9) + event.GetFailureReason() )
			DeleteFile("new-sync.xml")
			newSync = invalid
			return
		else
			rebootRequired = true
		endif
	
	endif

	if not newSync.WriteToFile("local-sync.xml") then stop

	' cause fsync
	CreateObject("roReadFile", "local-sync.xml")

    if rebootRequired then
        mVar.diagnostics.PrintDebug("### new script or upgrade found - reboot")
        RebootSystem()
    endif

	globalAA = GetGlobalAA()
    globalAA.autoscheduleFilePath$ = GetPoolFilePath(mVar.assetPoolFiles, "autoschedule.xml")
    globalAA.resourcesFilePath$ = GetPoolFilePath(mVar.assetPoolFiles, "resources.txt")
    globalAA.boseProductsFilePath$ = GetPoolFilePath(mVar.assetPoolFiles, "BoseProducts.xml")
    if globalAA.autoscheduleFilePath$ = "" then stop

	metadata = newSync.GetMetadata("client")
	if metadata.DoesExist("obfuscatedPassphrase") and mVar.contentEncryptionSupported then
		obfuscatedPassphrase$ = newSync.LookupMetadata("client", "obfuscatedPassphrase")
		deviceCustomization = CreateObject("roDeviceCustomization")
		deviceCustomization.StoreObfuscatedEncryptionKey("AesCtrHmac", obfuscatedPassphrase$)
		mVar.contentEncrypted = true
	endif

	mVar.SetPoolSizes(newSync)
	                
	DeleteFile("new-sync.xml")
	newSync = invalid

' send internal message to prepare for restart
    prepareForRestartEvent = CreateObject("roAssociativeArray")
    prepareForRestartEvent["EventType"] = "PREPARE_FOR_RESTART"
    mVar.msgPort.PostMessage(prepareForRestartEvent)

' send internal message indicating that new content is available
    contentUpdatedEvent = CreateObject("roAssociativeArray")
    contentUpdatedEvent["EventType"] = "CONTENT_UPDATED"
    mVar.msgPort.PostMessage(contentUpdatedEvent)
    
End Sub

'endregion

'region Presentation
Sub ConfigureGPIOInput(buttonNumber$ As String)

	buttonNumber% = int(val(buttonNumber$))
	m.gpioStateMachineRequired[buttonNumber%] = true

End Sub


Sub ConfigureGPIOButton(buttonNumber$ As String, gpioSpec As Object)

	buttonNumber% = int(val(buttonNumber$))
	if type(m.gpioSM[buttonNumber%]) = "roAssociativeArray" then
		m.gpioSM[buttonNumber%].ConfigureButton(gpioSpec)
	endif
	
End Sub


Sub ConfigureBPInput(buttonPanelIndex% As Integer, buttonNumber$ As String)

	if buttonNumber$ = "-1" then
		for i% = 0 to 10
			m.bpStateMachineRequired[buttonPanelIndex%, i%] = true
		next
	else
	    buttonNumber% = int(val(buttonNumber$))
	    m.bpStateMachineRequired[buttonPanelIndex%, buttonNumber%] = true
	    m.bpInputUsed[buttonPanelIndex%, buttonNumber%] = true
	endif

End Sub


Sub ConfigureBPButton(buttonPanelIndex% As Integer, buttonNumber$ As String, bpSpec As Object)

	if buttonNumber$ = "-1" then
	    for i% = 0 to 10
			if type(m.bpSM[buttonPanelIndex%, i%]) = "roAssociativeArray" then
				m.bpSM[buttonPanelIndex%, i%].ConfigureButton(bpSpec)
			endif
		next
	else
		buttonNumber% = int(val(buttonNumber$))
		if type(m.bpSM[buttonPanelIndex%, buttonNumber%]) = "roAssociativeArray" then
			m.bpSM[buttonPanelIndex%, buttonNumber%].ConfigureButton(bpSpec)
		endif
	endif
	
End Sub


Function NewGlobalVariables() As Object

    globalVariables = CreateObject("roAssociativeArray")
    globalVariables.language$ = "eng"
    
    return globalVariables
    
End Function


Function newPresentation(bsp As Object, presentationXML As Object) As Object

	presentation = {}
	presentation.name$ = presentationXML.name.GetText()
	presentation.presentationName$ = presentationXML.presentationName.GetText()
	presentation.path$ = presentationXML.path.GetText()

	' for earlier 3.7 presentations
	if presentation.presentationName$ = "" then
		presentation.presentationName$ = presentation.path$
	endif

	return presentation

End Function


Function newScriptPlugin(scriptPluginXML As Object) As Object

	scriptPlugin = {}
	scriptPlugin.name$ = scriptPluginXML.name.GetText()
	scriptPlugin.plugin = invalid

	return scriptPlugin

End Function


Function newVideoModePlugin(videoModePluginXML As Object) As Object

	videoModePlugin = {}
	videoModePlugin.fileName$ = videoModePluginXML.fileName.GetText()
	videoModePlugin.functionName$ = videoModePluginXML.functionName.GetText()
	videoModePlugin.plugin = invalid

	return videoModePlugin

End Function


Function newParserPlugin(parserPluginXML As Object) As Object

	parserPlugin = {}
	parserPlugin.name$ = parserPluginXML.name.GetText()
	parserPlugin.parseFeedFunction$ = parserPluginXML.parseFeedFunctionName.GetText()
	parserPlugin.parseUVFunction$ = parserPluginXML.parseUVFunctionName.GetText()
	parserPlugin.userAgentFunction$ = parserPluginXML.userAgentFunctionName.GetText()

	return parserPlugin

End Function


Function getParserPlugin(bsp As Object, parserPluginName As Object) As Object

	if Len(parserPluginName) > 0 then
		for each plugin in bsp.parserPlugins
			if plugin.name$ = parserPluginName then
				return plugin
			endif
		next
	endif
	return invalid

End Function


Function newHTMLSite(bsp As Object, htmlSiteXML As Object) As Object

	htmlSite = {}

	htmlSite.name$ = htmlSiteXML.name.GetText()
	htmlSite.queryString = newParameterValue(bsp, htmlSiteXML.queryString.parameterValue)
	
	if htmlSiteXML.GetName() = "localHTMLSite" or htmlSiteXML.GetName() = "brightPlateHTMLSite" then
		htmlSite.prefix$ = htmlSiteXML.prefix.GetText()
		htmlSite.filePath$ = htmlSiteXML.filePath.GetText()
		htmlSite.contentIsLocal = true
	else if htmlSiteXML.GetName() = "remoteHTMLSite" then
		htmlSite.url = newParameterValue(bsp, htmlSiteXML.url.parameterValue)
		htmlSite.contentIsLocal = false
	endif

	return htmlSite

End Function


Sub ParseProxyBypass(bsp, xml)

	bypassProxy$ = xml.bypassProxy.GetText()
	if bypassProxy$ <> "" then
		if lcase(bypassProxy$) = "true" then
			hostName$ = xml.hostName.GetText()
			if hostName$ <> "" then
				bsp.bypassProxyHosts.push(hostName$)
			endif
		endif
	endif

End Sub


Function newLiveDataFeed(bsp As Object, liveDataFeedXML As Object) As Object

	liveDataFeed = InitializeLiveDataFeedParameters( bsp )

	liveDataFeed.name$ = CleanName( liveDataFeedXML.name.GetText() )

	liveBSNDataFeedXMLList = liveDataFeedXML.GetNamedElements("liveBSNDataFeed")
	liveBSNMediaFeedXMLList = liveDataFeedXML.GetNamedElements("liveBSNMediaFeed")
	liveDynamicPlaylistXMLList = liveDataFeedXML.GetNamedElements("liveDynamicPlaylist")
	liveBSNTaggedPlaylistFeedXMLList = liveDataFeedXML.GetNamedElements("liveBSNTaggedPlaylist")

	liveDataFeed.isDynamicPlaylist = false
	liveDataFeed.isLiveMediaFeed = false
	if liveBSNDataFeedXMLList.Count() = 1 then
		urlSpec$ = liveDataFeedXML.liveBSNDataFeed.url.GetText()
		liveDataFeed.url = newTextParameterValue(urlSpec$)
	else if liveBSNMediaFeedXMLList.Count() = 1 then
		urlSpec$ = liveDataFeedXML.liveBSNMediaFeed.url.GetText()
		liveDataFeed.url = newTextParameterValue(urlSpec$)
		liveDataFeed.isLiveMediaFeed = true
	else if liveDynamicPlaylistXMLList.Count() = 1 then
		urlSpec$ = liveDataFeedXML.liveDynamicPlaylist.url.GetText()
		liveDataFeed.url = newTextParameterValue(urlSpec$)
		liveDataFeed.isDynamicPlaylist = true
	else if liveBSNTaggedPlaylistFeedXMLList.Count() = 1 then
		urlSpec$ = liveDataFeedXML.liveBSNTaggedPlaylist.url.GetText()
		liveDataFeed.url = newTextParameterValue(urlSpec$)
' Set isLiveMediaFeed to true for tagged playlists as for all uses of this variable, a tagged playlist is the
' same as a live media feed. 
		liveDataFeed.isLiveMediaFeed = true
	else
		liveDataFeed.url = newParameterValue(bsp, liveDataFeedXML.url.parameterValue)
	endif

	' get parser functions
	parserPluginName = liveDataFeedXML.parserPluginName.GetText()
	uvParserPluginName = liveDataFeedXML.uvParserPluginName.GetText()

	' look at main parser for feed first - all functions "should" be in that one
	' older versions of BA were able to specify a second parser for User Vars, however, so we check for that if necessary
	parser = getParserPlugin(bsp, parserPluginName)
	if parser <> invalid then
		liveDataFeed.parser$ = parser.parseFeedFunction$
		liveDataFeed.uvParser$ = parser.parseUVFunction$
		liveDataFeed.customUserAgent$ = parser.userAgentFunction$
		if liveDataFeed.uvParser$ = "" then
			' Backward compatibility check - see if UV parser plugin separately defined
			parser = getParserPlugin(bsp, uvParserPluginName)
			if parser <> invalid then
				liveDataFeed.uvParser$ = parser.parseUVFunction$
			endif
		endif
	endif

	liveDataFeed.updateInterval% = int(val(liveDataFeedXML.updateInterval.GetText()))
	if LCase(liveDataFeedXML.useHeadRequest.GetText()) = "true" then
		liveDataFeed.useHeadRequest = true
	else
		liveDataFeed.useHeadRequest = false
	endif
	
	liveDataFeed.usage$ = "undefined"
	if LCase(liveDataFeedXML.downloadContent.GetText()) = "true" then
		liveDataFeed.usage$ = "content"
	else if liveDataFeedXML.dataFeedUse.GetText() <> "" then
		liveDataFeed.usage$ = LCase(liveDataFeedXML.dataFeedUse.GetText())
	endif

	liveDataFeed.autoGenerateUserVariables = false
	if liveDataFeedXML.autoGenerateUserVariables.Count() = 1 and LCase(liveDataFeedXML.autoGenerateUserVariables.GetText()) = "true" then
		liveDataFeed.autoGenerateUserVariables = true
	endif

	liveDataFeed.userVariableAccess$ = "private"
	if liveDataFeedXML.userVariableAccess.Count() = 1 and LCase(liveDataFeedXML.userVariableAccess.GetText()) = "shared" then
		liveDataFeed.userVariableAccess$ = "shared"
	endif

	SetLiveDataFeedHandlers( liveDataFeed )

	return liveDataFeed

End Function


Function newLiveDataFeedFromMRSSFormat( bsp As Object, liveDataFeedName$ As String, url$ As String, isDynamicPlaylist As Boolean ) As Object

	liveDataFeed = InitializeLiveDataFeedParameters( bsp )

	liveDataFeed.name$ = liveDataFeedName$

	liveDataFeed.url = newTextParameterValue(url$)
	liveDataFeed.updateInterval% = 900	' default to 15 minutes. If the feed has a TTL value that is lower, use that.
	liveDataFeed.isDynamicPlaylist = isDynamicPlaylist

	liveDataFeed.isMRSSFeed	= true
	liveDataFeed.usage$ = "mrss"

	SetLiveDataFeedHandlers( liveDataFeed )

	return liveDataFeed

End Function


Function newLiveDataFeedFromOldDataFormat(bsp As Object, url As Object, updateInterval% As Integer) As Object

	liveDataFeed = InitializeLiveDataFeedParameters( bsp )

	liveDataFeed.name$ = url.GetCurrentParameterValue()
	liveDataFeed.url = url
	liveDataFeed.updateInterval% = updateInterval%
	
	SetLiveDataFeedHandlers( liveDataFeed )

	return liveDataFeed

End Function


Function newLiveDataFeedWithAuthData(bsp As Object, url As Object, authData As Object, updateInterval% As Integer) As Object

	liveDataFeed = InitializeLiveDataFeedParameters( bsp )

	liveDataFeed.name$ = CleanName( url.GetCurrentParameterValue() )
	liveDataFeed.url = url
	liveDataFeed.authenticationData = authData
	liveDataFeed.updateInterval% = updateInterval%
	liveDataFeed.useHeadRequest = false
	
	SetLiveDataFeedHandlers( liveDataFeed )

	return liveDataFeed

End Function


Function InitializeLiveDataFeedParameters( bsp As Object )

	liveDataFeed = {}
	liveDataFeed.bsp = bsp
	liveDataFeed.name$ = ""
	liveDataFeed.title$ = ""
	liveDataFeed.url = ""
	liveDataFeed.parser$ = ""
	liveDataFeed.uvParser$ = ""
	liveDataFeed.customUserAgent$ = ""
	liveDataFeed.updateInterval% = 0
	liveDataFeed.usage$ = "text"
	liveDataFeed.isMRSSFeed = false
	liveDataFeed.isDynamicPlaylist = false
	liveDataFeed.isLiveMediaFeed = false
	liveDataFeed.headRequest = false
	liveDataFeed.useHeadRequest = false
	
	liveDataFeed.autoGenerateUserVariables = false
	liveDataFeed.forceUpdate = false

	liveDataFeed.restrictNumberOfItems = false
	liveDataFeed.numberOfItemsToDisplay = -1

	liveDataFeed.feedContentFilesToDownload = {}

	return liveDataFeed

End Function


Sub SetLiveDataFeedHandlers( liveDataFeed As Object )

	liveDataFeed.ReadFeedContent = ReadFeedContent
	liveDataFeed.ReadLiveFeedContent = ReadLiveFeedContent
	liveDataFeed.ParseSimpleRSSFeed = ParseSimpleRSSFeed
	liveDataFeed.ReadMRSSContent = ReadMRSSContent
	liveDataFeed.DownloadLiveFeedContent = DownloadLiveFeedContent
	liveDataFeed.DownloadMRSSContent = DownloadMRSSContent
	liveDataFeed.ParseMRSSFeed = ParseMRSSFeed
	liveDataFeed.FeedIsMRSS = FeedIsMRSS
	liveDataFeed.RestartLiveDataFeedDownloadTimer = RestartLiveDataFeedDownloadTimer
	liveDataFeed.HandleLiveDataFeedContentDownloadAssetFetcherEvent = HandleLiveDataFeedContentDownloadAssetFetcherEvent
	liveDataFeed.HandleLiveDataFeedContentDownloadAssetFetcherProgressEvent = HandleLiveDataFeedContentDownloadAssetFetcherProgressEvent
	liveDataFeed.ConvertMRSSFormatToContent = ConvertMRSSFormatToContent

End Sub


Function CleanName(input As String) As String
    charsToReplace = [ "/", ":", ",", ".", "&", "=", "?"]
    output = input
    for each charToReplace in charsToReplace
        index = 1
        while index <> 0
            index = instr(1, output, charToReplace)
            if index <> 0 then
                part1 = ""
                if (index - 1) > 0 then
                    part1 = mid(output, 0, index - 1)
                endif
                part2 = ""
                if (len(output) - index) > 0 then
                    part2 = mid(output, index + 1, len(output) - index)
                endif
                output = part1 + "-" + part2
            endif
        end while
    next
    return output
End Function


Sub Restart(presentationName$ As String)

	m.restartPendingMediaEnd = false
	m.dontChangePresentationUntilMediaEndEventReceived = false

	m.currentUserVariables = {}
	m.liveDataFeeds = { }
	m.presentations = { }
	m.htmlSites = { }
	m.beacons = { }
	m.scriptPlugins = CreateObject("roArray", 1, true)
	m.videoModePlugins = CreateObject("roArray", 1, true)
	m.parserPlugins = CreateObject("roArray", 1, true)
	m.additionalPublishedFiles = CreateObject("roArray", 1, true)

	m.bypassProxyHosts = []

	m.IsSyncMaster = false
	m.IsSyncSlave = false

	m.rfChannelName = ""
	m.rfVirtualChannel = ""
	m.tunerScanPercentageComplete$ = "0"

	for n% = 0 to 3
		for i% = 0 to 10
			m.bpStateMachineRequired[n%, i%] = false
			m.bpInputUsed[n%, i%] = false
			m.bpOutputUsed[n%, i%] = false
		next
	next

	for i% = 0 to 7
		m.gpioStateMachineRequired[i%] = false
	next

	if presentationName$ = "" then

		globalAA = GetGlobalAA()
		xmlAutoscheduleFile$ = ReadAsciiFile(globalAA.autoscheduleFilePath$)
		if xmlAutoscheduleFile$ <> "" then
			schedule = m.XMLAutoschedule(globalAA.autoscheduleFilePath$)
		else
			stop
		endif
         
		if type(schedule.activeScheduledEvent) = "roAssociativeArray" then
			xmlFileName$ = schedule.autoplayPoolFile$
		else
			xmlFileName$ = ""
		endif
        
		m.schedule = schedule
	
	    if (xmlFileName$ <> "") then
			presentationName$ = schedule.activeScheduledEvent.presentationName$
		endif

	else

		autoplayFileName$ = "autoplay-" + presentationName$ + ".xml"
		xmlFileName$ = m.assetPoolFiles.GetPoolFilePath(autoplayFileName$)
        m.activePresentation$ = presentationName$

	endif

    if (xmlFileName$ <> "") then
        BrightAuthor = CreateObject("roXMLElement")
        BrightAuthor.Parse(ReadAsciiFile(xmlFileName$))
        
        ' verify that this is a valid BrightAuthor XML file
        if BrightAuthor.GetName() <> "BrightAuthor" then print "Invalid XML file - name not BrightAuthor" : stop
        if not IsString(BrightAuthor@version) then print "Invalid XML file - version not found" : stop    

        m.diagnostics.PrintTimestamp()
        m.diagnostics.PrintDebug("### create sign object")

        version% = int(val(BrightAuthor@version))
        
	    sign = newSign(BrightAuthor, m.globalVariables, m, m.msgPort, m.controlPort, version%)

		' send a heartbeat when starting a new presentation so that BA->Manage has the latest information as quickly as possible
		sendHeartbeatMsg = CreateObject("roAssociativeArray")
		sendHeartbeatMsg["EventType"] = "SEND_HEARTBEAT"
		m.msgPort.PostMessage(sendHeartbeatMsg)

		m.LogActivePresentation()

    else
        sign = invalid

		' set idle screen color
		b = CreateObject("roByteArray")
		b.FromHexString(m.registrySettings.idleScreenColor$)
		color_spec% = (255*256*256*256) + (b[1]*256*256) + (b[2]*256) + b[3]

		m.diagnostics.PrintDebug("Set idle screen color: red = " + stri(b[1]) + ", green = " + stri(b[2]) + ", blue = " + stri(b[3]))

		videoMode = CreateObject("roVideoMode")
		videoMode.SetBackgroundColor(color_spec%)
		videoMode = invalid
    endif
    
    if type(m.sign) = "roAssociativeArray" and type(m.sign.zonesHSM) = "roArray" then
		for each zoneHSM in m.sign.zonesHSM
			if IsAudioPlayer(zoneHSM.audioPlayer) then
				zoneHSM.audioPlayer.Stop()
				zoneHSM.audioPlayer = invalid
			endif
			if type(zoneHSM.videoPlayer) = "roVideoPlayer" then
				zoneHSM.videoPlayer.Stop()
				zoneHSM.videoPlayer = invalid
			endif
		next
    endif

	zoneHSM = invalid
	m.dispatchingZone=invalid
	m.sign = invalid
	RunGarbageCollector()
    
    m.ClearImageBuffers()
    
    m.sign = sign	

	if type(m.sign) = "roAssociativeArray" then
	' initialize audio configuration
		audioConfiguration = CreateObject("roAudioConfiguration")
		if type(audioConfiguration) = "roAudioConfiguration" then
			audioConfiguration$ = lcase(m.sign.audioConfiguration$)
			audioAutoLevel = m.sign.audioAutoLevel
			if audioAutoLevel = true then
				m.diagnostics.PrintDebug("Debug: audioAutoLevel is set to true")
			else if audioAutoLevel = false then
				m.diagnostics.PrintDebug("Debug: audioAutoLevel is set to false")
			endif
			if audioConfiguration$ = "mixedaudiopcmonly" then
				if audioAutoLevel = true then
					audioRouting = { mode: "prerouted", autolevel: "on", pcmonly: "true", srcrate: 48000 }
				else
					audioRouting = { mode: "prerouted", autolevel: "off", pcmonly: "true", srcrate: 48000 }
				endif
			else if audioConfiguration$ = "mixedaudiopcmcompressed" then
				if audioAutoLevel = true then
					audioRouting = { mode: "prerouted", autolevel: "on", pcmonly: "false", srcrate: 48000 }
				else
					audioRouting = { mode: "prerouted", autolevel: "off", pcmonly: "false", srcrate: 48000 }
				endif
			else
				audioRouting = { mode : "dynamic" }
			endif

			ok = audioConfiguration.ConfigureAudio(audioRouting)
			if not ok then
				m.diagnostics.PrintDebug("Configure audio failure: " + audioConfiguration$)		
			endif
		endif
	endif
	
' create required GPIO state machines
	for i% = 0 to 7
		if m.gpioStateMachineRequired[i%] then
			m.gpioSM[i%] = newGPIOStateMachine(m, m.controlPort, m.controlPortIdentity, i%)
		else
			m.gpioSM[i%] = invalid
		endif
	next

	' create, initialize, and configure required BP state machines and BP's
	for buttonPanelIndex% = 0 to 3
		if type(m.bpInputPorts[buttonPanelIndex%]) = "roControlPort" then
			configuration% = m.bpInputPortConfigurations[buttonPanelIndex%]
		else
			configuration% = 0
		endif
		for i% = 0 to 10
			if (configuration% and (2 ^ i%)) <> 0 then
				forceUsed = true
			else
				forceUsed = false
			endif
'			if m.bpStateMachineRequired[buttonPanelIndex%, i%] then
			if (m.bpInputUsed[buttonPanelIndex%, i%] or forceUsed) and IsString(m.bpInputPortIdentities[buttonPanelIndex%]) and type(m.bpInputPorts[buttonPanelIndex%]) = "roControlPort" then
				m.bpSM[buttonPanelIndex%, i%] = newBPStateMachine(m, m.bpInputPortIdentities[buttonPanelIndex%], buttonPanelIndex%, i%)
			else
				m.bpSM[buttonPanelIndex%, i%] = invalid
			endif			
		next
    next

	m.ConfigureBPs()

	for buttonPanelIndex% = 0 to 3
	
		if type(m.bpOutputSetup[buttonPanelIndex%]) = "roControlPort" then
	
			configuration% = m.bpInputPortConfigurations[buttonPanelIndex%]

			' set bits in our mask for buttons we want to disable (not enable!)
			loopFlag% = 1
			buttonFlag% = 0

			for i% = 0 to 10
				if (configuration% and (2 ^ i%)) <> 0 then
					forceUsed = true
				else
					forceUsed = false
				endif
	'			if not (m.bpInputUsed[buttonPanelIndex%, i%] or m.bpOutputUsed[buttonPanelIndex%, i%]) then
	'			if not m.bpStateMachineRequired[buttonPanelIndex%, i%] then
				if not (m.bpInputUsed[buttonPanelIndex%, i%] or forceUsed) then
					buttonFlag% = buttonFlag% + loopFlag%
				endif

				loopFlag% = loopFlag% * 2
			next

			' the 1 here is the position of the mask for disabling buttons
			m.bpOutputSetup[buttonPanelIndex%].SetOutputValue(1, buttonFlag%)
		
			loopFlag% = 1
			ledFlag% = 0

			for i% = 0 to 10
				if (configuration% and (2 ^ i%)) <> 0 then
					forceUsed = true
				else
					forceUsed = false
				endif
			    if not (m.bpInputUsed[buttonPanelIndex%, i%] or m.bpOutputUsed[buttonPanelIndex%, i%] or forceUsed) then
				   ledFlag% = ledFlag% + loopFlag%
			    endif

			    loopFlag% = loopFlag% * 2
			next

			' the 2 here signifies the mask position for LED disabling
			m.bpOutputSetup[buttonPanelIndex%].SetOutputValue(2, ledFlag%)	
	
		endif
	
	next

	' reset connector volumes
	m.analogVolume% = 100
	m.analog2Volume% = 100
	m.analog3Volume% = 100
	m.hdmiVolume% = 100
	m.spdifVolume% = 100
	m.usbVolumeA% = 100
	m.usbVolumeB% = 100
	m.usbVolumeC% = 100
	m.usbVolumeD% = 100
	m.usbVolumeTypeA% = 100
	m.usbVolumeTypeC% = 100
	m.usbVolumeA1% = 100
	m.usbVolumeA2% = 100
	m.usbVolumeA3% = 100
	m.usbVolumeA4% = 100
	m.usbVolumeA5% = 100
	m.usbVolumeA6% = 100
	m.usbVolumeA7% = 100

	' reclaim memory
	RunGarbageCollector()
	
	' user variable web server
    if type(m.sign) = "roAssociativeArray" and (m.registrySettings.lwsConfig$ = "c" or m.registrySettings.lwsConfig$ = "s") then

		lwsUserName$ = m.registrySettings.lwsUserName$
		lwsPassword$ = m.registrySettings.lwsPassword$
        
        if (len(lwsUserName$) + len(lwsPassword$)) > 0 then
            credentials = CreateObject("roAssociativeArray")
            credentials.AddReplace(lwsUserName$, lwsPassword$)
        else
            credentials = invalid
        end if
                        
        m.sign.localServer = CreateObject("roHttpServer", { port: 8008 })
        m.sign.localServer.SetPort(m.msgPort)

		m.sign.GetUserVarsAA =			{ HandleEvent: GetUserVars, mVar: m }
        m.sign.GetConfigurationPageAA =	{ HandleEvent: GetConfigurationPage, mVar: m }
		m.sign.GetUDPEventsAA =			{ HandleEvent: GetUDPEvents, mVar: m}
		m.sign.SendUdpRestAA =			{ HandleEvent: SendUdpRest, mVar: m }

		m.sign.SetValuesAA =			{ HandleEvent: SetValues, mVar: m }
		m.sign.SetValuesByCategoryAA =	{ HandleEvent: SetValuesByCategory, mVar: m }

		m.sign.GetUserVariableCategoriesAA =	{ HandleEvent: GetUserVariableCategories, mVar: m }
		m.sign.GetUserVariablesByCategoryAA =	{ HandleEvent: GetUserVariablesByCategory, mVar: m }

		m.sign.UpdateFeedByCategoryAA		=	{ HandleEvent: UpdateFeedByCategory, mVar: m }
		m.sign.UpdateAllFeedsAA				=	{ HandleEvent: UpdateAllFeeds, mVar: m }

		m.sign.localServer.AddGetFromFile({ url_path: "/GetAutorun", content_type: "text/plain; charset=utf-8", filename: "autorun.brs"})

		m.sign.localServer.AddGetFromEvent({ url_path: "/GetUserVars", user_data: m.sign.GetUserVarsAA})
		m.sign.localServer.AddGetFromEvent({ url_path: "/", user_data: m.sign.GetConfigurationPageAA, passwords: credentials})
		m.sign.localServer.AddGetFromEvent({ url_path: "/GetUDPEvents", user_data: m.sign.GetUDPEventsAA })

		m.sign.localServer.AddGetFromEvent({ url_path: "/GetUserVariableCategories", user_data: m.sign.GetUserVariableCategoriesAA})
		m.sign.localServer.AddGetFromEvent({ url_path: "/GetUserVariablesByCategory", user_data: m.sign.GetUserVariablesByCategoryAA})

		m.sign.localServer.AddGetFromEvent({ url_path: "/UpdateFeedByCategory", user_data: m.sign.UpdateFeedByCategoryAA})
		m.sign.localServer.AddGetFromEvent({ url_path: "/UpdateAllFeeds", user_data: m.sign.UpdateAllFeedsAA})

        m.sign.localServer.AddPostToFormData({ url_path: "/SetValues", user_data: m.sign.SetValuesAA, passwords: credentials})
        m.sign.localServer.AddPostToFormData({ url_path: "/SetValuesByCategory", user_data: m.sign.SetValuesByCategoryAA, passwords: credentials})
		m.sign.localServer.AddPostToFormData({ url_path: "/SendUDP", user_data: m.sign.SendUdpRestAA })

    endif

	userVariables = m.currentUserVariables

	' if there are script plugins associated with this sign, initialize them here

	ERR_NORMAL_END = &hFC

	for each scriptPlugin in m.scriptPlugins
		initializeFunction$ = "result = " + scriptPlugin.name$ + "_Initialize(m.msgPort, userVariables, m)"
		retVal = Eval(initializeFunction$)

		if type(retVal) = "roList" then
			' log the failure
		    m.diagnostics.PrintDebug("Failure executing Eval to initialize script plugin file: error string was " + retVal[0].ERRSTR + ", line number was " + stri(retVal[0].LINENO) + ", call was " + initializeFunction$)
			m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_SCRIPT_PLUGIN_FAILURE, retVal[0].ERRSTR + chr(9) + stri(retVal[0].LINENO) + chr(9) + scriptPlugin.name$)
		else if retVal <> ERR_NORMAL_END then
			' log the failure
		    m.diagnostics.PrintDebug("Failure executing Eval to initialize script plugin file: return value = " + stri(retVal) + ", call was " + initializeFunction$)
			m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_SCRIPT_PLUGIN_FAILURE, stri(retVal) + chr(9) + scriptPlugin.name$)
		else
			scriptPlugin.plugin = result
		endif
	next

	' device web page
	m.deviceWebPageFilePath$ = ""

	if type(m.sign) = "roAssociativeArray" and m.sign.deviceWebPageDisplay$ <> "None" and type(m.sign.localServer) = "roHttpServer" then

		' get all the files in the device web page
        
		' FIX ME SOMEDAY - need a single object that represents the current sync spec
		currentSync = ReadSyncSpec()
		if not type(currentSync) = "roSyncSpec" stop
		
		if m.sign.deviceWebPageDisplay$ = "Custom" then
		
			' files in the custom device web page start with either
			' <presentation name>-customDeviceWebPage-
			' or
			' <site name>-customDeviceWebPage-
			customDeviceWebPageMarker$ = "-customDeviceWebPage-"

			downloadFiles = currentSync.GetFileList("download")
			for each downloadFile in downloadFiles
				fileName$ = downloadFile.name
			
				indexOfMarker% = instr(1, fileName$, customDeviceWebPageMarker$)
				if indexOfMarker% > 1 then
					strippedFileName$ = mid(fileName$, indexOfMarker% + len(customDeviceWebPageMarker$))

					' the main web page is
					' <presentation name>-<customDeviceWebPage>-<custom device web page file name> or
					' <site name>-customDeviceWebPage-<custom device web page file name>
					if strippedFileName$ = m.sign.customDeviceWebPage$ then

						m.deviceWebPageFilePath$ = GetPoolFilePath(m.assetPoolFiles, fileName$)

					else

						' other asset
						ext = GetFileExtension(strippedFileName$)
						if ext <> invalid then
							contentType$ = GetMimeTypeByExtension(ext)
							if contentType$ <> invalid then
								url$ = "/" + strippedFileName$
								filePath$ = GetPoolFilePath(m.assetPoolFiles, fileName$)
								m.sign.localServer.AddGetFromFile({ url_path: url$, filename: filePath$, content_type: contentType$ })
							endif
						endif

					endif
				endif
			
			next

		else

			m.deviceWebPageFilePath$ = GetPoolFilePath(m.assetPoolFiles, "_deviceWebPage.html")

		endif

	endif

	' Notify controlling devices we have started playback
	m.SendUDPNotification("startPlayback")

End Sub


Function GetFileExtension(file as String) as Object
  s=file.tokenize(".")
  if s.Count()>1
    ext=s.pop()
    return ext
  end if
  return invalid
end Function

Function GetMimeTypeByExtension(ext as String) as String

  ' start with audio types '
  if ext="mp3"
    return "audio/mpeg"
  else if ext="ogg"
	return "audio/ogg"
  else if ext="flac"
	return "audio/flac"
  else if ext="aac"
    return "audio/aac"
  else if ext="m4a"
    return "audio/mp4"

  ' now image types '
  else if ext="gif"
    return "image/gif"
  else if ext="jpeg"
    return "image/jpeg"
  else if ext="jpg"
    return "image/jpeg"
  else if ext="png"
    return "image/png"
  else if ext="svg"
    return "image/svg+xml"

  ' now text types'
  else if ext="css"
    return "text/css"
  else if ext="js"
    return "application/JavaScript"
  else if ext="csv"
    return "text/csv"
  else if ext="html"
    return "text/html"
  else if ext="htm"
    return "text/html"
  else if ext="txt"
    return "text/plain"
  else if ext="xml"
    return "text/xml"

  ' now some video types'
  else if ext="mpeg"
    return "video/mpeg"
  else if ext="mp4"
    return "video/mp4"
  else if ext="ts"
    return "video/mpeg"
  else if ext="mov"
	return "video/quicktime"
  else if ext="mkv"
	return "video/x-matroska"
  end if
  return ""
end Function


Sub LogActivePresentation()

	if type(m.activePresentation$) = "roString" then
		activePresentation$ = m.activePresentation$
	else
		activePresentation$ = ""
	endif

	m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_START_PRESENTATION, activePresentation$)

End Sub


Sub UpdateEdidUserVariables(postMsg As Boolean)

	userVariables = m.currentUserVariables

	for each userVariableKey in userVariables
		userVariable = userVariables.Lookup(userVariableKey)
		if userVariable.systemVariable$ = "edidMonitorSerialNumber" then
			userVariable.SetCurrentValue(m.sysInfo.edidMonitorSerialNumber$, postMsg)
		else if userVariable.systemVariable$ = "edidYearOfManufacture" then
			userVariable.SetCurrentValue(m.sysInfo.edidYearOfManufacture$, postMsg)
		else if userVariable.systemVariable$ = "edidMonitorName" then
			userVariable.SetCurrentValue(m.sysInfo.edidMonitorName$, postMsg)
		else if userVariable.systemVariable$ = "edidManufacturer" then
			userVariable.SetCurrentValue(m.sysInfo.edidManufacturer$, postMsg)
		else if userVariable.systemVariable$ = "edidUnspecifiedText" then
			userVariable.SetCurrentValue(m.sysInfo.edidUnspecifiedText$, postMsg)
		else if userVariable.systemVariable$ = "edidSerialNumber" then
			userVariable.SetCurrentValue(m.sysInfo.edidSerialNumber$, postMsg)
		else if userVariable.systemVariable$ = "edidManufacturerProductCode" then
			userVariable.SetCurrentValue(m.sysInfo.edidManufacturerProductCode$, postMsg)
		else if userVariable.systemVariable$ = "edidWeekOfManufacture" then
			userVariable.SetCurrentValue(m.sysInfo.edidWeekOfManufacture$, postMsg)
		endif
	next

End Sub


Sub UpdateIPAddressUserVariables(postMsg As Boolean)

	userVariables = m.currentUserVariables

	for each userVariableKey in userVariables
		userVariable = userVariables.Lookup(userVariableKey)
		if userVariable.systemVariable$ = "ipAddressWired" then
			userVariable.SetCurrentValue(m.sysInfo.ipAddressWired$, postMsg)
		else if userVariable.systemVariable$ = "ipAddressWireless" then
			userVariable.SetCurrentValue(m.sysInfo.ipAddressWireless$, postMsg)
		endif
	next

End Sub


Sub UpdateRFChannelCountUserVariables(postMsg As Boolean)

	userVariables = m.currentUserVariables

	for each userVariableKey in userVariables
		userVariable = userVariables.Lookup(userVariableKey)
		if userVariable.systemVariable$ = "rfChannelCount" then
			userVariable.SetCurrentValue(StripLeadingSpaces(stri(m.scannedChannels.Count())), postMsg)
		endif
	next

End Sub


Sub UpdateRFChannelUserVariables(postMsg As Boolean)

	userVariables = m.currentUserVariables

	for each userVariableKey in userVariables
		userVariable = userVariables.Lookup(userVariableKey)
		if userVariable.systemVariable$ = "rfChannelName" then
			userVariable.SetCurrentValue(m.rfChannelName, postMsg)
		else if userVariable.systemVariable$ = "rfVirtualChannel" then
			userVariable.SetCurrentValue(m.rfVirtualChannel, postMsg)
		endif
	next

End Sub


Sub UpdateTunerScanPercentageCompleteUserVariables(postMsg As Boolean)

	userVariables = m.currentUserVariables

	for each userVariableKey in userVariables
		userVariable = userVariables.Lookup(userVariableKey)
		if userVariable.systemVariable$ = "tunerScanPercentageComplete" then
			userVariable.SetCurrentValue(m.tunerScanPercentageComplete$, postMsg)
		endif
	next

End Sub


Function GetScannedChannels() As Object

	channelDescriptors = CreateObject("roArray", 1, true)

	channelManager = CreateObject("roChannelManager")
	if type(channelManager) = "roChannelManager" then

		' Get channel descriptors for all the channels on the device
		channelCount% = channelManager.GetChannelCount()

		if channelCount% > 0 then

			channelInfo  = CreateObject("roAssociativeArray")

			for channelIndex% = 0 to channelCount% - 1
				channelInfo["ChannelIndex"] = channelIndex%
				channelDescriptor = channelManager.CreateChannelDescriptor(channelInfo)
				channelDescriptors.push(channelDescriptor)
			next
		endif
		
	endif

	return channelDescriptors

End Function

'endregion

'region Bose Products
Function ReadBoseProductsFile() As Object

    boseProductsFileXML = CreateObject("roXMLElement")
	globalAA = GetGlobalAA()
    boseProductsFileContents$ = ReadAsciiFile(globalAA.boseProductsFilePath$)
    if len(boseProductsFileContents$) = 0 then return 0
    
    boseProductsFileXML.Parse(boseProductsFileContents$)

    ' verify that this is a valid BoseProducts XML file
    if boseProductsFileXML.GetName() <> "BoseProducts" then print "Invalid BoseProducts XML file - name not BoseProducts" : stop
    if not IsString(boseProductsFileXML@version) then print "Invalid BoseProducts XML file - version not found" : stop    

    boseProductsXML = boseProductsFileXML.product
    numBoseProducts% = boseProductsXML.Count()

	boseProducts = CreateObject("roAssociativeArray")

	rebootRequired = false

    for each boseProductXML in boseProductsXML
		registryKeyWritten = AddBoseProduct(boseProducts, boseProductXML)
		rebootRequired = rebootRequired or registryKeyWritten
    next

	if rebootRequired then
		usbRegistrySection = CreateObject("roRegistrySection", "usb")
		if type(usbRegistrySection)<>"roRegistrySection" then print "Error: Unable to create roRegistrySection":stop
		usbRegistrySection.Flush()
		RebootSystem()
	endif

	return boseProducts
	
End Function


Function AddBoseProduct(boseProducts As Object, boseProductXML As Object)

	rebootRequired = false

	boseProduct = CreateObject("roAssociativeArray")

	productName$ = boseProductXML.productName.GetText()

	tapProtocol$ = boseProductXML.tapProtocol.GetText()
	if lcase(tapProtocol$) = "hid" then
		boseProduct.usbHIDCommunication = true
	else
		boseProduct.usbHIDCommunication = false
	endif

	boseProduct.usbAudioInterfaceIndex$ = boseProductXML.usbAudioInterfaceIndex.GetText()
	if boseProduct.usbAudioInterfaceIndex$ <> "" then
		boseProduct.usbAudio = true
	else
		boseProduct.usbAudio = false
	endif

	boseProduct.usbTapInterfaceIndex$ = boseProductXML.usbTapInterfaceIndex.GetText()

	boseProduct.tapProtocol = boseProductXML.tapProtocol.GetText()

	boseProduct.usbAsyncAudio = false
	usbAsyncAudio$ = boseProductXML.usbAsyncAudio.GetText()
	if lcase(usbAsyncAudio$) = "true" then
		boseProduct.usbAsyncAudio = true
	endif

	if type(boseProductXML.usbToSerial) = "roXMLList" and boseProductXML.usbToSerial.Count() > 0 then

		usbRegistrySection = CreateObject("roRegistrySection", "usb")
		if type(usbRegistrySection)<>"roRegistrySection" then print "Error: Unable to create roRegistrySection":stop

		for each usbToSerialEntry in boseProductXML.usbToSerial
			registryKey$ = usbToSerialEntry.registryKey[0].getText()
			registryValue$ = usbToSerialEntry.registryValue[0].getText()

			existingRegistryValue$ = usbRegistrySection.Read(registryKey$)
			if registryValue$ <> existingRegistryValue$ then
				usbRegistrySection.Write(registryKey$, registryValue$)
				rebootRequired = true
			endif
		next

	endif

	volumeTable = CreateObject("roAssociativeArray")

	volumeTableElements = boseProductXML.GetNamedElements("volumeTable")
    if volumeTableElements.Count() <> 1 then stop
    
	volumeTableElement = volumeTableElements[0]
	
	xval1Elements = volumeTableElement.GetNamedElements("xval1")
	volumeTable.xval1% = int(val(xval1Elements[0].GetText()))
	
	yval1Elements = volumeTableElement.GetNamedElements("yval1")
	volumeTable.yval1% = int(val(yval1Elements[0].GetText()))
	
	xval2Elements = volumeTableElement.GetNamedElements("xval2")
	volumeTable.xval2% = int(val(xval2Elements[0].GetText()))
	
	yval2Elements = volumeTableElement.GetNamedElements("yval2")
	volumeTable.yval2% = int(val(yval2Elements[0].GetText()))
	
	xval3Elements = volumeTableElement.GetNamedElements("xval3")
	volumeTable.xval3% = int(val(xval3Elements[0].GetText()))
	
	yval3Elements = volumeTableElement.GetNamedElements("yval3")
	volumeTable.yval3% = int(val(yval3Elements[0].GetText()))
		
	boseProduct.volumeTable = volumeTable

	transportElements = boseProductXML.GetNamedElements("transport")
	if transportElements.Count() <> 1 then stop
	
	transportElement = transportElements[0]
	transportAttributes = transportElement.GetAttributes()
	transportType$ = transportAttributes.Lookup("type")
	boseProduct.protocol$ = "ASCII"
	if transportType$ = "Serial-Binary" then
		boseProduct.protocol$ = "Binary"
	endif
	
	baudRateElements = transportElement.GetNamedElements("baudRate")
	boseProduct.baudRate% = int(val(baudRateElements[0].GetText()))
	
	dataBitsElements = transportElement.GetNamedElements("dataBits")
	boseProduct.dataBits$ = dataBitsElements[0].GetText()
	
	parityElements = transportElement.GetNamedElements("parity")
	boseProduct.parity$ = parityElements[0].GetText()
	
	stopBitsElements = transportElement.GetNamedElements("stopBits")
	boseProduct.stopBits$ = stopBitsElements[0].GetText()
	
	invertSignalsElements = transportElement.GetNamedElements("invertSignals")
	invertSignals$ = invertSignalsElements[0].GetText()

	if invertSignals$ = "true" then
		boseProduct.invertSignals = true
	else
		boseProduct.invertSignals = false
	endif
	
	sendEol$ = "CR"
	sendEolElements = transportElement.GetNamedElements("sendEOL")
	if sendEolElements.Count() = 1 then
		if sendEolElements[0].GetText() <> "" then
			sendEol$ = sendEolElements[0].GetText()
		endif
	endif
	boseProduct.sendEol$ = sendEol$

	receiveEol$ = "CR"
	receiveEolElements = transportElement.GetNamedElements("receiveEOL")
	if receiveEolElements.Count() = 1 then
		if receiveEolElements[0].GetText() <> "" then
			receiveEol$ = receiveEolElements[0].GetText()
		endif
	endif
	boseProduct.receiveEol$ = receiveEol$
						
	boseProducts.AddReplace(productName$, boseProduct)
	
	return rebootRequired

End Function


Function GetBoseProductSpec(productName$) As Object

	if type(m.boseProductSpecs) = "roAssociativeArray" then
		return m.boseProductSpecs.Lookup(productName$)
	else
		return 0
	endif
	
End Function

'endregion

'region Schedule
Function XMLAutoschedule(xmlFileName$ As String)

    autoScheduleXML = CreateObject("roXMLElement")
    autoScheduleXML.Parse(ReadAsciiFile(xmlFileName$))
    
    ' verify that this is a valid autoschedule XML file
    if autoScheduleXML.GetName() <> "autoschedule" then print "Invalid autoschedule XML file - name not autoschedule" : stop
    if not IsString(autoScheduleXML@version) then print "Invalid autoschedule XML file - version not found" : stop    
'    print "autoschedule xml file - version = "; autoScheduleXML@version

    schedule = newSchedule(autoScheduleXML)

    if type(schedule.activeScheduledEvent) = "roAssociativeArray" then

        presentation$ = schedule.activeScheduledEvent.presentationName$
        m.activePresentation$ = presentation$
        
        autoplayFileName$ = "autoplay-" + presentation$ + ".xml"

		' find the autoplay file in the pool folder
		currentSync = ReadSyncSpec()
		if not type(currentSync) = "roSyncSpec" stop

		assetCollection = currentSync.GetAssets("download")
		apf = CreateObject("roAssetPoolFiles", m.assetPool, assetCollection)
			
        autoplayPoolFile$ = apf.GetPoolFilePath(autoplayFileName$)
        if autoplayPoolFile$ = "" then stop
		schedule.autoplayPoolFile$ = autoplayPoolFile$
        apf = invalid
				
        currentSync = invalid
    
    endif
    
    return schedule
    
End Function


Function newSchedule(autoScheduleXML As Object) As Object

    schedule = CreateObject("roAssociativeArray")

    ' create and read schedules
    scheduledPresentationsXML = autoScheduleXML.scheduledPresentation
    numScheduledPresentations% = scheduledPresentationsXML.Count()
    
    schedule.allScheduledEvents = CreateObject("roArray", numScheduledPresentations%, true)
    schedule.scheduledEvents = CreateObject("roArray", numScheduledPresentations%, true)
    schedule.scheduledInterruptions = CreateObject("roArray", 1, true)
    
    for each scheduledPresentationXML in scheduledPresentationsXML
    
        scheduledPresentationBS = newScheduledEvent(scheduledPresentationXML)

		schedule.allScheduledEvents.push(scheduledPresentationBS)

		if scheduledPresentationBS.interruption then
			schedule.scheduledInterruptions.push(scheduledPresentationBS)
		else
			schedule.scheduledEvents.push(scheduledPresentationBS)
        endif

    next

    ' get starting presentation
    schedule.GetActiveScheduledEvent = GetActiveScheduledEvent    
    schedule.GetNextScheduledEventTime = GetNextScheduledEventTime
        
    schedule.activeScheduledEvent = schedule.GetActiveScheduledEvent(schedule.scheduledInterruptions)
	if type(schedule.activeScheduledEvent) <> "roAssociativeArray" then
		schedule.activeScheduledEvent = schedule.GetActiveScheduledEvent(schedule.scheduledEvents)
	endif

    if type(schedule.activeScheduledEvent) <> "roAssociativeArray" then
        schedule.nextScheduledEventTime = schedule.GetNextScheduledEventTime(schedule.allScheduledEvents)
    else
		' determine when this scheduled event will end
		schedule.activeScheduledEventEndDateTime = invalid
		if schedule.activeScheduledEvent.interruption then
			schedule.activeScheduledEventEndDateTime = CopyDateTime(schedule.activeScheduledEvent.dateTime)
			schedule.activeScheduledEventEndDateTime.AddSeconds(schedule.activeScheduledEvent.duration% * 60)
		else
			endDateTime = invalid

			if not schedule.activeScheduledEvent.allDayEveryDay then				
				endDateTime = CopyDateTime(schedule.activeScheduledEvent.dateTime)
				endDateTime.AddSeconds(schedule.activeScheduledEvent.duration% * 60)
			endif
			
			nextInterruptionStartTime = schedule.GetNextScheduledEventTime(schedule.scheduledInterruptions)
			
			if endDateTime = invalid then
				if nextInterruptionStartTime <> invalid then
					schedule.activeScheduledEventEndDateTime = nextInterruptionStartTime
				endif
			else
				if nextInterruptionStartTime = invalid then
					schedule.activeScheduledEventEndDateTime = endDateTime
				else
					if endDateTime.GetString() < nextInterruptionStartTime.GetString() then
						schedule.activeScheduledEventEndDateTime = endDateTime
					else
						schedule.activeScheduledEventEndDateTime = nextInterruptionStartTime
					endif
				endif
			endif
		endif
	endif

    return schedule
    
End Function


Function newScheduledEvent(scheduledEventXML As Object) As Object

    scheduledEventBS = CreateObject("roAssociativeArray")
        
    if scheduledEventXML.playlist.Count() > 0 then
        scheduledEventBS.playlist$ = scheduledEventXML.playlist.GetText()
    endif
    if scheduledEventXML.presentationToSchedule.Count() > 0 then
        scheduledEventBS.presentationName$ = scheduledEventXML.presentationToSchedule.name.GetText()
    endif
    
    dateTime$ = scheduledEventXML.dateTime.GetText()    
    scheduledEventBS.dateTime = FixDateTime(dateTime$)
    
    scheduledEventBS.duration% = int(val(scheduledEventXML.duration.GetText()))
    
    if lcase(scheduledEventXML.allDayEveryDay.GetText()) = "true" then
        scheduledEventBS.allDayEveryDay = true
    else
        scheduledEventBS.allDayEveryDay = false
    endif
        
    if lcase(scheduledEventXML.recurrence.GetText()) = "true" then
        scheduledEventBS.recurrence = true
    else
        scheduledEventBS.recurrence = false
    endif
        
    scheduledEventBS.recurrencePattern$ = scheduledEventXML.recurrencePattern.GetText()
    
    scheduledEventBS.recurrencePatternDaily$ = scheduledEventXML.recurrencePatternDaily.GetText()

    scheduledEventBS.recurrencePatternDaysOfWeek% = int(val(scheduledEventXML.recurrencePatternDaysOfWeek.GetText()))
    
    dateTime$ = scheduledEventXML.recurrenceStartDate.GetText()    
    scheduledEventBS.recurrenceStartDate = FixDateTime(dateTime$)

    if lcase(scheduledEventXML.recurrenceGoesForever.GetText()) = "true" then
        scheduledEventBS.recurrenceGoesForever = true
    else
        scheduledEventBS.recurrenceGoesForever = false
    endif
    
    dateTime$ = scheduledEventXML.recurrenceEndDate.GetText()    
    recurrenceEndDate = FixDateTime(dateTime$)
    recurrenceEndDate.AddSeconds(60 * 60 * 24) ' adjust the recurrence end date to refer to the beginning of the next day
    scheduledEventBS.recurrenceEndDate = recurrenceEndDate

	if scheduledEventXML.interruption.GetText() <> "" and lcase(scheduledEventXML.interruption.GetText()) = "true" then
        scheduledEventBS.interruption = true
	else
        scheduledEventBS.interruption = false
	endif

    return scheduledEventBS    
    
End Function


' required format looks like
'		2012-10-03T15:49:00
Function FixDateTime(dateTime$ As String) As Object

    dateTime = CreateObject("roDateTime")
    
    ' strip '-' and ':' so that BrightSign can parse the dateTime properly
    index = instr(1, dateTime$, "-")
    while index > 0
        
        a$ = mid(dateTime$, 1, index - 1)
        b$ = mid(dateTime$, index + 1)
        dateTime$ = a$ + b$

        index = instr(1, dateTime$, "-")
        
    end while

    index = instr(1, dateTime$, ":")
    while index > 0
        
        a$ = mid(dateTime$, 1, index - 1)
        b$ = mid(dateTime$, index + 1)
        dateTime$ = a$ + b$

        index = instr(1, dateTime$, ":")
        
    end while

	if not dateTime.FromIsoString(dateTime$) then
		return Invalid
	endif

    return dateTime
        
End Function



Function GetActiveScheduledEvent(scheduledEvents As Object) As Object

'   determine if there is a scheduled event that should be active at this time

    activeScheduledEvent = invalid

    for each scheduledEvent in scheduledEvents
    
'       is there a playlist that should be active now based on the scheduledEvent?

        if scheduledEvent.allDayEveryDay then
        
            activeScheduledEvent = scheduledEvent
            exit for
            
        endif
                         
'       is the current scheduledEvent active today? if no, go to next scheduledEvent

        eventDateTime = scheduledEvent.dateTime
        systemTime = CreateObject("roSystemTime")
        currentDateTime = systemTime.GetLocalDateTime()

        scheduledEventActiveToday = false
        
'       if it's not a recurring event and its start date is today, then it is active today

        if not scheduledEvent.recurrence then
        
            if eventDateTime.GetYear() = currentDateTime.GetYear() and eventDateTime.GetMonth() = currentDateTime.GetMonth() and eventDateTime.GetDay() = currentDateTime.GetDay() then
               scheduledEventActiveToday = true
            endif
            
        endif
        
        if (not scheduledEventActiveToday) and scheduledEvent.recurrence then
        
'           determine if the date represented by the scheduled event is within the recurrence range

            dateWithinRange = false
            if scheduledEvent.recurrenceStartDate.GetString() < currentDateTime.GetString() then
            
                if scheduledEvent.recurrenceGoesForever then
                    dateWithinRange = true
                else if scheduledEvent.recurrenceEndDate.GetString() >= currentDateTime.GetString() then
                    dateWithinRange = true
                endif
                
            endif
                            
'           if it is within the range, check the recurrence pattern

            if dateWithinRange then
            
                if scheduledEvent.recurrencePattern$ = "Daily" then
                
                    if scheduledEvent.recurrencePatternDaily$ = "EveryDay" then
            
                        scheduledEventActiveToday = true
                        
                    else if scheduledEvent.recurrencePatternDaily$ = "EveryWeekday" then
                    
                        if currentDateTime.GetDayOfWeek() > 0 and currentDateTime.GetDayOfWeek() < 6 then
                        
                            scheduledEventActiveToday = true
                        
                        endif
                    
                    else ' EveryWeekend
                    
                        if currentDateTime.GetDayOfWeek() = 0 or currentDateTime.GetDayOfWeek() = 6 then
                        
                            scheduledEventActiveToday = true
                            
                        endif
                        
                    endif
                    
                else ' Weekly
                
                    bitwiseDaysOfWeek% = scheduledEvent.recurrencePatternDaysOfWeek%
                    currentDayOfWeek = currentDateTime.GetDayOfWeek()
                    bitDayOfWeek% = 2 ^ currentDayOfWeek
                    if (bitwiseDaysOfWeek% and bitDayOfWeek%) <> 0 then
                        scheduledEventActiveToday = true
                    endif
                                        
                endif
                                
            endif
            
        endif

'           see if the currentScheduledEvent should be active right now
'               it will be active right now if its start time < current start time and its end time > current start time

        if scheduledEventActiveToday then
        
            eventTodayStartTime = systemTime.GetLocalDateTime()
            eventTodayStartTime.SetHour(scheduledEvent.dateTime.GetHour())
            eventTodayStartTime.SetMinute(scheduledEvent.dateTime.GetMinute())
            eventTodayStartTime.SetSecond(scheduledEvent.dateTime.GetSecond())
            eventTodayStartTime.SetMillisecond(0)
            
            eventTodayEndTime = systemTime.GetLocalDateTime()
            eventTodayEndTime.SetHour(scheduledEvent.dateTime.GetHour())
            eventTodayEndTime.SetMinute(scheduledEvent.dateTime.GetMinute())
            eventTodayEndTime.SetSecond(scheduledEvent.dateTime.GetSecond())
            eventTodayEndTime.SetMillisecond(0)
            eventTodayEndTime.AddSeconds(scheduledEvent.duration% * 60)
            
            if eventTodayStartTime.GetString() <= currentDateTime.GetString() and eventTodayEndTime.GetString() > currentDateTime.GetString() then
                
                activeScheduledEvent = scheduledEvent
                activeScheduledEvent.dateTime = eventTodayStartTime
                
'                print "at end, eventDateTime = ";eventDateTime.GetString()
'                print "at end, currentDateTime = ";currentDateTime.GetString()

                exit for
                
            endif
            
        endif

    next
    
    return activeScheduledEvent
    
End Function    


Function GetNextScheduledEventTime(scheduledEvents) As Object

    dim futureScheduledEvents[10]
    dim futureScheduledEventStartTimes[10]
    
    nextScheduledEventTime = CreateObject("roDateTime")
    
    ' for each scheduled event, see if it could start in the future. If yes, determine the earliest
    ' future start time that is later than now. Store the scheduled event and that start time.
    ' Use the scheduled event in that list with the lowest start time.
    
    for each scheduledEvent in scheduledEvents
    
        if scheduledEvent.allDayEveryDay then
        
            ' an allDayEveryDay event is always active, so by definition, it is not a future event.
            goto endLoop
            
        endif

        eventDateTime = scheduledEvent.dateTime
        systemTime = CreateObject("roSystemTime")
        currentDateTime = systemTime.GetLocalDateTime()

'       if it's not a recurring event and its start date/time is in the future, then it is eligible

        if not scheduledEvent.recurrence then
        
            if eventDateTime.GetString() > currentDateTime.GetString() then
                
                futureScheduledEvents.push(scheduledEvent)
                futureScheduledEventStartTimes.push(eventDateTime)
                goto endLoop
                
            endif
            
        endif
        
'       if it's a recurring event, see if its date range includes the future

        if scheduledEvent.recurrence then
        
            eventToday = CreateObject("roDateTime")
            eventToday.SetYear(currentDateTime.GetYear())
            eventToday.SetMonth(currentDateTime.GetMonth())
            eventToday.SetDay(currentDateTime.GetDay())
            eventToday.SetHour(eventDateTime.GetHour())
            eventToday.SetMinute(eventDateTime.GetMinute())

            if scheduledEvent.recurrenceGoesForever or scheduledEvent.recurrenceEndDate.GetString() > currentDateTime.GetString() then
        
'               find the earliest time > now that this recurring event could start

                if scheduledEvent.recurrencePattern$ = "Daily" then
                
                    if scheduledEvent.recurrencePatternDaily$ = "EveryDay" then
            
                        
                        if eventToday.GetString() > currentDateTime.GetString() then
                        
                            futureScheduledEvents.push(scheduledEvent)
                            futureScheduledEventStartTimes.push(eventToday)
                            goto endLoop
                        
                        else ' use the next day
                        
                            eventToday.AddSeconds(60 * 60 * 24)
                            futureScheduledEvents.push(scheduledEvent)
                            futureScheduledEventStartTimes.push(eventToday)
                            goto endLoop
                            
                        endif
                        
                    else if scheduledEvent.recurrencePatternDaily$ = "EveryWeekday" then
                        ' if today is a weekday, proceed as in the case above, except that instead of using
                        ' the 'next day', use the 'next weekday' (which may or may not be the next day) for the test
                        
                        if currentDateTime.GetDayOfWeek() > 0 and currentDateTime.GetDayOfWeek() < 6 then
                            
                            ' current day is a weekday
                            
                            if eventToday.GetString() > currentDateTime.GetString() then
                            
                                futureScheduledEvents.push(scheduledEvent)
                                futureScheduledEventStartTimes.push(eventToday)
                                goto endLoop
                        
                            else
                                
                                ' if today is Friday, add 3 days
                                daysToAdd% = 1
                                if currentDateTime.GetDayOfWeek() = 5 then daysToAdd% = 3
                                eventToday.AddSeconds(60 * 60 * 24 * daysToAdd%)
                                futureScheduledEvents.push(scheduledEvent)
                                futureScheduledEventStartTimes.push(eventToday)
                                goto endLoop
                                
                            endif
                            
                        else ' current day is a weekend
                            
                            ' if today is not a weekday, the next weekday (Monday) is the future event
                            daysToAdd% = 1
                            if currentDateTime.GetDayOfWeek() = 6 then daysToAdd% = 2
                            eventToday.AddSeconds(60 * 60 * 24 * daysToAdd%)
                            futureScheduledEvents.push(scheduledEvent)
                            futureScheduledEventStartTimes.push(eventToday)
                            goto endLoop
                            
                        endif
                    
                    else ' EveryWeekend
                        ' if today is a weekend, proceed as in the case above, except that instead of using
                        ' the 'next day', use the 'next weekend' (which may or may not be the next day) for the test

                        if currentDateTime.GetDayOfWeek() = 0 or currentDateTime.GetDayOfWeek() = 6 then
                            
                            ' current day is a weekend
                            
                            if eventToday.GetString() > currentDateTime.GetString() then
                            
                                futureScheduledEvents.push(scheduledEvent)
                                futureScheduledEventStartTimes.push(eventToday)
                                goto endLoop
                        
                            else
                                
                                ' if today is Sunday, add 6 days
                                daysToAdd% = 1
                                if currentDateTime.GetDayOfWeek() = 5 then daysToAdd% = 6
                                eventToday.AddSeconds(60 * 60 * 24 * daysToAdd%)
                                futureScheduledEvents.push(scheduledEvent)
                                futureScheduledEventStartTimes.push(eventToday)
                                goto endLoop
                                
                            endif
                            
                        else ' current day is a weekday
                            
                            ' if today is not a weekday, the next weekday (Monday) is the future event
                            daysToAdd% = 6 - currentDateTime.GetDayOfWeek()
                            eventToday.AddSeconds(60 * 60 * 24 * daysToAdd%)
                            futureScheduledEvents.push(scheduledEvent)
                            futureScheduledEventStartTimes.push(eventToday)
                            goto endLoop
                            
                        endif
                        
                    endif        
        
                else ' Weekly
                
                    ' if today is one of the days specified, test against today. if the test fails,
                    ' or today is not one of the days specified, find the next specified day and use it.
                    
                    bitwiseDaysOfWeek% = scheduledEvent.recurrencePatternDaysOfWeek%
                    currentDayOfWeek = currentDateTime.GetDayOfWeek()
                    bitDayOfWeek% = 2 ^ currentDayOfWeek
                    if (bitwiseDaysOfWeek% and bitDayOfWeek%) <> 0 then
                    
                        if eventToday.GetString() > currentDateTime.GetString() then
                        
                            futureScheduledEvents.push(scheduledEvent)
                            futureScheduledEventStartTimes.push(eventToday)
                            goto endLoop
                            
                        endif
                    
                    endif
                        
                    ' find the next specified day and use it    
                    if bitwiseDaysOfWeek% <> 0 then
                    
                        while true
                            currentDayOfWeek = currentDayOfWeek + 1
                            if currentDayOfWeek >= 7 then currentDayOfWeek = 0
                            bitDayOfWeek% = 2 ^ currentDayOfWeek
                            eventToday.AddSeconds(60 * 60 * 24)
                            if (bitwiseDaysOfWeek% and bitDayOfWeek%) <> 0 then
                                futureScheduledEvents.push(scheduledEvent)
                                futureScheduledEventStartTimes.push(eventToday)
                                goto endLoop
                            endif
                        end while        
                        
                    endif

                endif
                
            endif
            
        endif        

endLoop:

    next
    
    ' sort the future events
    dim sortedFutureEventTimes[10]
    if futureScheduledEventStartTimes.Count() > 1 then
        SortFutureScheduledEvents(futureScheduledEventStartTimes, sortedFutureEventTimes)
        nextScheduledEventTime = futureScheduledEventStartTimes[sortedFutureEventTimes[0]]
    else
        nextScheduledEventTime = futureScheduledEventStartTimes[0]
    endif
        
    return nextScheduledEventTime
    
End Function


Sub SortFutureScheduledEvents(futureEventTimes As Object, sortedIndices As Object)

    ' initialize array with indices.
    for i% = 0 to futureEventTimes.Count()-1
        sortedIndices[i%] = i%
    next

    numItemsToSort% = futureEventTimes.Count()

    for i% = numItemsToSort% - 1 to 1 step -1
        for j% = 0 to i%-1
	        index0 = sortedIndices[j%]
	        time0 = futureEventTimes[index0].GetString()
            index1 = sortedIndices[j%+1]
            time1 = futureEventTimes[index1].GetString()
            if time0 > time1 then
                k% = sortedIndices[j%]
                sortedIndices[j%] = sortedIndices[j%+1]
                sortedIndices[j%+1] = k%
            endif
        next
    next

    return
    
End Sub

'endregion

'region StartPlayback and CreateObjects
Sub StartPlayback()

    sign = m.sign
    
    ' set a default udp receive port
    m.udpReceivePort = sign.udpReceivePort
    m.udpSendPort = sign.udpSendPort
    m.udpAddress$ = sign.udpAddress$
    m.udpAddressType$ = sign.udpAddressType$

    ' kick off playback

	m.diagnostics.PrintTimestamp()
	m.diagnostics.PrintDebug("### set background screen color")

	' set background screen color
	if type(sign.backgroundScreenColor%) = "roInt" then
		videoMode = CreateObject("roVideoMode")
		videoMode.SetBackgroundColor(sign.backgroundScreenColor%)                     
		videoMode = invalid
	endif

	' unmute all audio explicitly for Cheetah / Panther / Puma
	m.UnmuteAllAudio()

	numZones% = sign.zonesHSM.Count()
	if numZones% > 0 then

		' construct zones
		for i% = 0 to numZones% - 1
			zoneHSM = sign.zonesHSM[i%]
			if type(zoneHSM.playlist) = "roAssociativeArray" then
				m.diagnostics.PrintTimestamp()
				m.diagnostics.PrintDebug("### Constructor zone")
				zoneHSM.Constructor()
			endif
		next  
		
		' launch the zones      
		for i% = 0 to numZones% - 1
			zoneHSM = sign.zonesHSM[i%]
			if type(zoneHSM.playlist) = "roAssociativeArray" then
				m.diagnostics.PrintTimestamp()
				m.diagnostics.PrintDebug("### Launch playback")
				zoneHSM.Initialize()
			endif
		next        
	endif
	
	' Notify controlling devices we have started playback
	m.SendUDPNotification("startPlayback")
	
End Sub


' m is the zone
Sub CreateObjects()

	zoneHSM = m
	
	' is there any harm in creating a keyboard object even if it is not used?
    if type(m.bsp.keyboard) <> "roKeyboard" then
        m.bsp.keyboard = CreateObject("roKeyboard")
        m.bsp.keyboard.SetPort(m.bsp.msgPort)
    endif
		                        
	for each key in zoneHSM.stateTable
    
		state = zoneHSM.stateTable[key]
		
		if state.type$ = "tripleUSB" then
            m.CreateSerial(m.bsp, state.tripleUSBPort$, false)
            if IsString(state.boseProductPort$) and state.boseProductPort$ <> "" then
	            m.CreateSerial(m.bsp, state.boseProductPort$, true)
	        endif
		endif

		if state.type$ = "xModem" then
			m.CreateSerial(m.bsp, state.port$, false)
		endif
				  
		if state.type$ = "mediaList" then
			for each cmd in state.transitionNextItemCmds
				m.CreateObjectForTransitionCommand(cmd)
			next

			for each cmd in state.transitionPrevItemCmds
				m.CreateObjectForTransitionCommand(cmd)
			next
		endif
		  
        gpioEvents = state.gpioEvents
        for each gpioEventNumber in gpioEvents
            if type(gpioEvents[gpioEventNumber]) = "roAssociativeArray" then
                m.CreateObjectsNeededForTransitionCommands(gpioEvents[gpioEventNumber])
            endif
        next

        gpioUpEvents = state.gpioUpEvents
        for each gpioEventNumber in gpioUpEvents
            if type(gpioUpEvents[gpioEventNumber]) = "roAssociativeArray" then
                m.CreateObjectsNeededForTransitionCommands(gpioUpEvents[gpioEventNumber])
            endif
        next

		for buttonPanelIndex% = 0 to 3
			bpEvents = state.bpEvents[buttonPanelIndex%]
			for each bpEventNumber in bpEvents
				if type(bpEvents[bpEventNumber]) = "roAssociativeArray" then
					m.CreateObjectsNeededForTransitionCommands(bpEvents[bpEventNumber])
				endif
			next
		next                

        if type(state.mstimeoutEvent) = "roAssociativeArray"
            m.CreateObjectsNeededForTransitionCommands(state.mstimeoutEvent)
        endif
        
		if type(state.timeClockEvents) = "roArray" then
			for each timeClockEvent in state.timeClockEvents
	            m.CreateObjectsNeededForTransitionCommands(timeClockEvent.transition)
			next
		endif

        if type(state.videoEndEvent) = "roAssociativeArray"
            m.CreateObjectsNeededForTransitionCommands(state.videoEndEvent)
        endif
                
        if type(state.audioEndEvent) = "roAssociativeArray"
            m.CreateObjectsNeededForTransitionCommands(state.audioEndEvent)
        endif

        if type(state.keyboardEvents) = "roAssociativeArray" or type(state.usbStringEvents) = "roAssociativeArray" then
            
            if type(state.keyboardEvents) = "roAssociativeArray" then
                keyboardEvents = state.keyboardEvents
                for each keyboardEvent in state.keyboardEvents
                    if type(keyboardEvents[keyboardEvent]) = "roAssociativeArray" then
                        m.CreateObjectsNeededForTransitionCommands(keyboardEvents[keyboardEvent])
                    endif
                next
            endif                    

            if type(state.usbStringEvents) = "roAssociativeArray"
                usbEvents = state.usbStringEvents
                for each usbEvent in state.usbEvents
                    if type(usbEvents[usbEvent]) = "roAssociativeArray" then
                        m.CreateObjectsNeededForTransitionCommands(usbEvents[usbEvent])
                    endif
                next                        
            endif
            
        endif
            
        if type(state.remoteEvents) = "roAssociativeArray" then
        
            if type(m.bsp.remote) <> "roIRRemote" then
                m.bsp.remote = CreateObject("roIRRemote")
                m.bsp.remote.SetPort(m.bsp.msgPort)
            endif
        
            remoteEvents = state.remoteEvents
            for each remoteEvent in state.remoteEvents
                m.CreateObjectsNeededForTransitionCommands(remoteEvents[remoteEvent])                      
            next
        
        endif

		if (type(state.gpsEnterRegionEvents) = "roArray" and state.gpsEnterRegionEvents.Count() > 0) or (type(state.gpsExitRegionEvents) = "roArray" and state.gpsExitRegionEvents.Count() > 0) then
			m.CreateSerial(m.bsp, m.bsp.gpsPort$, false)
		endif

        serialEvents = state.serialEvents
        for each serialPort in serialEvents

            m.CreateSerial(m.bsp, serialPort, false)

			protocol$ = ""
			if IsUsbCommunicationPort(serialPort) then
				if GetGlobalAA().usbHIDPortConfigurations.DoesExist(serialPort) then
					usbHIDPortConfiguration = GetGlobalAA().usbHIDPortConfigurations[serialPort]
					protocol$ = usbHIDPortConfiguration.protocol$
				endif
			else
				port% = int(val(serialPort))
				serialPortConfiguration = m.bsp.sign.serialPortConfigurations[port%]
				protocol$ = serialPortConfiguration.protocol$
			endif

			if protocol$ = "Binary" then
				if type(serialEvents[serialPort]) = "roAssociativeArray" then
					if type(serialEvents[serialPort].streamInputTransitionSpecs) = "roArray" then
						for each streamInputTransitionSpec in serialEvents[serialPort].streamInputTransitionSpecs
							m.CreateObjectsNeededForTransitionCommands(streamInputTransitionSpec.transition)
						next
					endif
				endif
			else if protocol$ <> "" then
				for each serialEvent in serialEvents[serialPort]
					m.CreateObjectsNeededForTransitionCommands(serialEvents[serialPort][serialEvent])
				next
            endif
        next

		if type(state.zoneMessageEvents) = "roAssociativeArray" then

            for each zoneMessageEvent in state.zoneMessageEvents
                m.CreateObjectsNeededForTransitionCommands(state.zoneMessageEvents[zoneMessageEvent])                      
            next
		
		endif
		
		if type(state.pluginMessageEvents) = "roAssociativeArray" then

            for each pluginMessageEvent in state.pluginMessageEvents
                m.CreateObjectsNeededForTransitionCommands(state.pluginMessageEvents[pluginMessageEvent])                      
            next
		
		endif
		
		if type(state.internalSynchronizeEvents) = "roAssociativeArray" then

            for each internalSynchronizeEvent in state.internalSynchronizeEvents
                m.CreateObjectsNeededForTransitionCommands(state.internalSynchronizeEvents[internalSynchronizeEvent])                      
            next
		
		endif
		
		createDatagramReceiver = false
		if state.type$ = "rfInputChannel" then
			if type(state.channelUpEvent) = "roAssociativeArray" and IsString(state.channelUpEvent.udpUserEvent$) createDatagramReceiver = true
			if type(state.channelDownEvent) = "roAssociativeArray" and IsString(state.channelDownEvent.udpUserEvent$) createDatagramReceiver = true
		endif

        if type(state.udpEvents) = "roAssociativeArray" or type(state.synchronizeEvents) = "roAssociativeArray" or createDatagramReceiver then
            
			createDatagramReceiver = false

			if type(m.bsp.udpReceiver) <> "roDatagramReceiver" then
				createDatagramReceiver = true
			else
				if type(m.bsp.existingUdpReceivePort) = "roInt" and m.bsp.existingUdpReceivePort <> m.bsp.udpReceivePort then
					createDatagramReceiver = true
				endif
			endif

			if createDatagramReceiver then
                m.bsp.udpReceiver = CreateObject("roDatagramReceiver", m.bsp.udpReceivePort)
                m.bsp.udpReceiver.SetPort(m.bsp.msgPort)
				m.bsp.existingUdpReceivePort = m.bsp.udpReceivePort
            endif
            
            if type(state.udpEvents) = "roAssociativeArray" then
                udpEvents = state.udpEvents
                for each udpEvent in state.udpEvents
                    m.CreateObjectsNeededForTransitionCommands(udpEvents[udpEvent])                      
                next
            endif
            
            if type(state.synchronizeEvents) = "roAssociativeArray" then
                synchronizeEvents = state.synchronizeEvents
                for each synchronizeEvent in state.synchronizeEvents
                    m.CreateObjectsNeededForTransitionCommands(synchronizeEvents[synchronizeEvent])                      
                next
            endif
            
        endif
        
		if state.type$ = "html5" and state.enableMouseEvents then
            m.bsp.InitializeTouchScreen(zoneHSM)
		endif
		                                         
        if type(state.touchEvents) = "roAssociativeArray" then
            m.bsp.InitializeTouchScreen(zoneHSM)
                        
            for each eventNum in state.touchEvents
                m.bsp.AddRectangularTouchRegion(m, state.touchEvents[eventNum], val(eventNum))
                m.CreateObjectsNeededForTransitionCommands(state.touchEvents[eventNum])                      
            next
        endif
                
        if type(state.audioTimeCodeEvents) = "roAssociativeArray" then
            for each eventNum in state.audioTimeCodeEvents
                m.CreateObjectsNeededForTransitionCommands(state.audioTimeCodeEvents[eventNum])                      
            next
        endif
                                
        if type(state.videoTimeCodeEvents) = "roAssociativeArray" then
            for each eventNum in state.videoTimeCodeEvents
                m.CreateObjectsNeededForTransitionCommands(state.videoTimeCodeEvents[eventNum])                      
            next
        endif
                                
		if type(state.cmds) = "roArray" then
			for each cmd in state.cmds
				m.CreateCommunicationObjects(cmd)
			next
		endif
            
		if type(state.exitCmds) = "roArray" then
			for each cmd in state.exitCmds
				m.CreateCommunicationObjects(cmd)
			next
		endif
            
	next

	if m.bsp.sign.enableEnhancedSynchronization and (m.bsp.IsSyncMaster or m.bsp.IsSyncSlave) then
		aa = {}
		aa.Domain = m.bsp.sign.ptpDomain$
		m.bsp.SyncManager = invalid
		m.bsp.SyncManager = CreateObject("roSyncManager", aa)
		m.bsp.diagnostics.PrintDebug("@@@ roSyncManager created. Value of ptp domain:" + aa.Domain)
		m.bsp.SyncManager.SetMasterMode(m.bsp.IsSyncMaster)

		if m.bsp.IsSyncSlave then
			m.bsp.SyncManager.SetPort(m.bsp.msgPort)
			m.bsp.diagnostics.PrintDebug("@@@ Node is a slave")
		endif

		if m.bsp.setSyncDomainSupported then
			if m.bsp.videomode.SetSyncDomain(m.bsp.sign.ptpDomain$)	then
				m.bsp.diagnostics.PrintDebug("@@@ VSYNC Enabled on Domain @@@ : " + m.bsp.sign.ptpDomain$)
			else
				m.bsp.diagnostics.PrintDebug("@@@ VSYNC failed to enable on Domain @@@ : " + m.bsp.sign.ptpDomain$ + " : " + m.bsp.videomode.GetFailureReason())
			end if
		endif
	endif

End Sub


Sub CreateCommunicationObjects(cmd As Object)

	commandName$ = cmd.name$
	if commandName$ = "sendUDPCommand" or commandName$ = "sendUDPBytesCommand" or commandName$ = "synchronize" then
		m.CreateUDPSender(m.bsp)
	else if commandName$ = "sendSerialStringCommand" or commandName$ = "sendSerialBlockCommand" or commandName$ = "sendSerialByteCommand" or commandName$ = "sendSerialBytesCommand" then
		port$ = cmd.parameters["port"].GetCurrentParameterValue()
		m.CreateSerial(m.bsp, port$, true)
	endif

End Sub


Sub CreateObjectsNeededForTransitionCommands(transition As Object)

    if type(transition.transitionCmds) = "roArray" then
        for each cmd in transition.transitionCmds
			m.CreateObjectForTransitionCommand(cmd)
        next
    endif

	if type(transition.conditionalTargets) = "roArray" then
		for each conditionalTarget in transition.conditionalTargets
			if type(conditionalTarget.transitionCmds) = "roArray" then
				for each cmd in conditionalTarget.transitionCmds
					m.CreateObjectForTransitionCommand(cmd)
				next
			endif
		next
	endif

End Sub


Sub CreateObjectForTransitionCommand(cmd As Object)

    commandName$ = cmd.name$

    if commandName$ = "sendUDPCommand" or commandName$ = "sendUDPBytesCommand" or commandName$ = "synchronize" then
        m.CreateUDPSender(m.bsp)
    else if commandName$ = "sendSerialStringCommand" or commandName$ = "sendSerialBlockCommand" or commandName$ = "sendSerialByteCommand" or commandName$ = "sendSerialBytesCommand" then
		port$ = cmd.parameters["port"].GetCurrentParameterValue()
		m.CreateSerial(m.bsp, port$, true)
    endif

	if commandName$ = "synchronize" then
		m.bsp.IsSyncMaster = true
	endif

End Sub


Sub ConfigureBPs()

	m.bpOutput = CreateObject("roArray", 4, true)
	m.bpOutputSetup = CreateObject("roArray", 4, true)
	
	m.ConfigureBP(0, "TouchBoard-0-LED", "TouchBoard-0-LED-SETUP")
	m.ConfigureBP(1, "TouchBoard-1-LED", "TouchBoard-1-LED-SETUP")
	m.ConfigureBP(2, "TouchBoard-2-LED", "TouchBoard-2-LED-SETUP")
	m.ConfigureBP(3, "TouchBoard-3-LED", "TouchBoard-3-LED-SETUP")
	
End Sub


Sub ConfigureBP(buttonPanelIndex% As Integer, touchBoardLED$ As String, touchBoardLEDSetup$ As String)
	if type(m.bpInputPorts[buttonPanelIndex%]) = "roControlPort" then
		if type(m.bpOutput[buttonPanelIndex%]) <> "roControlPort" then
			m.diagnostics.PrintDebug("Creating bpOutput")
			m.bpOutput[buttonPanelIndex%] = CreateObject("roControlPort", touchBoardLED$)
			if type(m.bpOutput[buttonPanelIndex%]) = "roControlPort" then
        m.bpOutput[buttonPanelIndex%].SetUserData(touchBoardLED$)
				m.bpOutputSetup[buttonPanelIndex%] = CreateObject("roControlPort", touchBoardLEDSetup$)
				if type(m.bpOutputSetup[buttonPanelIndex%]) = "roControlPort" then
          m.bpOutputSetup[buttonPanelIndex%].SetUserData(touchBoardLEDSetup$)
					m.bpOutputSetup[buttonPanelIndex%].SetOutputValue(0, 22)
				endif
			endif
		endif
	endif
End Sub


Sub CreateUDPSender(bsp As Object)

	createDatagramSender = false

	if type(bsp.udpSender) <> "roDatagramSender" then
		createDatagramSender = true
	else
		if (type(bsp.existingUdpAddressType$) = "roString" and bsp.existingUdpAddressType$ <> bsp.udpAddressType$) or (type(bsp.existingUdpAddress$) = "roString" and bsp.existingUdpAddress$ <> bsp.udpAddress$) or (type(bsp.existingUdpSendPort) = "roInt" and bsp.existingUdpSendPort <> bsp.udpSendPort) then
			createDatagramSender = true
		endif
	endif

	if createDatagramSender then
        bsp.diagnostics.PrintDebug("Creating roDatagramSender")
        bsp.udpSender = CreateObject("roDatagramSender")
		if bsp.udpAddressType$ = "LocalSubnet" then
			bsp.udpSender.SetDestination("BCAST-LOCAL-SUBNETS", bsp.udpSendPort)
		else if bsp.udpAddressType$ = "Ethernet" then
			bsp.udpSender.SetDestination("BCAST-SUBNET-0", bsp.udpSendPort)
		else if bsp.udpAddressType$ = "Wireless" then
			bsp.udpSender.SetDestination("BCAST-SUBNET-1", bsp.udpSendPort)
        else
			bsp.udpSender.SetDestination(bsp.udpAddress$, bsp.udpSendPort)
		endif

		bsp.existingUdpAddressType$ = bsp.udpAddressType$
		bsp.existingUdpAddress$ = bsp.udpAddress$
		bsp.existingUdpSendPort = bsp.udpSendPort

    endif
    
End Sub


Sub SendUDPNotification(msg As String)

	if m.registrySettings.lwsEnableUpdateNotifications$ <> "yes" then
		return
	endif

	if type(m.udpNotifier) <> "roDatagramSender" then
        m.diagnostics.PrintDebug("Creating roDatagramSender for notifications")
        m.udpNotifier = CreateObject("roDatagramSender")
		m.udpNotifier.SetDestination(m.udpNotificationAddress$, m.udpNotificationPort%)
	endif

	m.udpNotifier.Send(msg)
	m.diagnostics.PrintDebug("UDP notification sent: " + msg)

End Sub


Sub ScheduleRetryCreateSerial(port$ As String, outputOnly As Boolean)

	if type(m.serialPortsToRetry) <> "roAssociativeArray" then
		m.serialPortsToRetry = {}
	endif

	if not m.serialPortsToRetry.DoesExist(port$) then

		aa = {}
		aa.port$ = port$
		aa.outputOnly = outputOnly
	
		timer = CreateObject("roTimer")
		timer.SetPort(m.msgPort)
		timer.SetElapsed(15, 0)
		timer.Start()
		aa.timer = timer

		m.serialPortsToRetry[port$] = aa

		m.diagnostics.PrintDebug("ScheduleRetryCreateSerial on port " + port$)

	endif

End Sub

 
Function RetryCreateSerial(port$ As String, outputOnly As Boolean) As Boolean

	ok = m.AttemptOpenSerial(port$, outputOnly)
    m.diagnostics.PrintDebug("RetryCreateSerial on port " + port$)
	
	if ok and m.serialPortsToRetry.DoesExist(port$) then
		m.serialPortsToRetry.Delete(port$)
	endif

	return ok

End Function


Sub CreateSerial(bsp As Object, port$ As String, outputOnly As Boolean)

	ok = bsp.AttemptOpenSerial(port$, outputOnly)
	if not ok then
		bsp.ScheduleRetryCreateSerial(port$, outputOnly)
	endif

End Sub


Function IsUsbCommunicationPort(port$ As String) As Boolean

	port = val(port$)
	if port = 0 and port$ <> "0" then return true
	return false

End Function


Function AttemptOpenSerial(port$ As String, outputOnly As Boolean) As Boolean

	if type(m.serial) <> "roAssociativeArray" then
		m.serial = CreateObject("roAssociativeArray")
	endif

	if type(m.serialOutputOnlySpec) <> "roAssociativeArray" then
		m.serialOutputOnlySpec = CreateObject("roAssociativeArray")
	endif

	' determine if the desired port refers to a port number or a usb port
	if m.usbDevicesByConnector.DoesExist(port$) then
		port$ = m.usbDevicesByConnector.Lookup(port$)
	endif

	if IsUsbCommunicationPort(port$) then
		
		if GetGlobalAA().usbCDCPortConfigurations.DoesExist(port$) then

			usbCDCPortConfiguration = GetGlobalAA().usbCDCPortConfigurations[port$]

			serialPortSpeed% = usbCDCPortConfiguration.serialPortSpeed
			sendEol$ = usbCDCPortConfiguration.sendEol$
			receiveEol$ = usbCDCPortConfiguration.receiveEol$
			protocol$ = usbCDCPortConfiguration.protocol$

			usbPort$ = "USB:" + right(port$, len(port$) - 4) + "." + usbCDCPortConfiguration.usbTapInterfaceIndex$

			if type(m.serial[port$]) = "roSerialPort" then
				serial = m.serial[port$]
			else
				serial = CreateObject("roSerialPort", usbPort$, serialPortSpeed%)
				if type(serial) <> "roSerialPort" then
					m.diagnostics.PrintDebug("Error creating roSerialPort " + usbPort$)
					return false
				endif

				m.serialOutputOnlySpec[port$] = true
			endif
		else if GetGlobalAA().usbHIDPortConfigurations.DoesExist(port$) then

			usbHIDPortConfiguration = GetGlobalAA().usbHIDPortConfigurations[port$]

			sendEol$ = usbHIDPortConfiguration.sendEol$
			receiveEol$ = usbHIDPortConfiguration.receiveEol$
			protocol$ = usbHIDPortConfiguration.protocol$

			usbPort$ = "USB:" + right(port$, len(port$) - 4) + "." + usbHIDPortConfiguration.usbTapInterfaceIndex$

			if type(m.serial[port$]) = "roUsbTap" then
				serial = m.serial[port$]
			else
				serial = CreateObject("roUsbTap", usbPort$)
				if type(serial) <> "roUsbTap" then
					m.diagnostics.PrintDebug("Error creating roUsbTap " + usbPort$)
					return false
				endif

				m.serialOutputOnlySpec[port$] = true
			endif
		else
			return false
		endif
		
	else

		port% = int(val(port$))
		serialPortConfiguration = m.serialPortConfigurations[port%]
		serialPortSpeed% = serialPortConfiguration.serialPortSpeed%
		serialPortMode$ = serialPortConfiguration.serialPortMode$    
      
		if type(m.serial[port$]) = "roSerialPort" then
			serial = m.serial[port$]
		else
			serial = CreateObject("roSerialPort", port%, serialPortSpeed%)
			if type(serial) <> "roSerialPort" then 
				m.diagnostics.PrintDebug("Error creating roSerialPort " + port$)
				return false
			endif

			m.serialOutputOnlySpec[port$] = true
		endif
    
		ok = serial.SetMode(serialPortMode$)
		if not ok then print "Error setting serial mode" : return false
    
		protocol$ = serialPortConfiguration.protocol$
		sendEol$ = serialPortConfiguration.sendEol$
		receiveEol$ = serialPortConfiguration.receiveEol$
	
		if serialPortConfiguration.invertSignals then
			serial.SetInverted(1)
		else
			serial.SetInverted(0)
		endif
	
	endif

	serial.SetSendEol(sendEol$)

	serial.SetReceiveEol(receiveEol$)

	serial.SetUserData(port$)

	m.serial[port$] = serial
        
	if not outputOnly then

		m.serialOutputOnlySpec[port$] = false

		if protocol$ = "Binary" then
			serial.SetByteEventPort(m.msgPort)
		else
			serial.SetLineEventPort(m.msgPort)
		endif
	endif

	return true

End Function


Function IsNonEmptyString(inputVariable As Object) As Boolean

	if not IsString(inputVariable) then return false
	if len(inputVariable) = 0 then return false
	return true

End Function


Function IsString(inputVariable As Object) As Boolean

	if type(inputVariable) = "roString" or type(inputVariable) = "String" then return true
	return false
	
End Function


Function IsInteger(inputVariable As Object) As Boolean

	if type(inputVariable) = "roInt" or type(inputVariable) = "Integer" then return true
	return false
	
End Function


Function GetEolFromSpec(eolSpec$ As String) As String

    eol$ = chr(13)
    if eolSpec$ = "LF" then
	    eol$ = chr(10)
    else if eolSpec$ = "CR+LF" then
	    eol$ = chr(13) + chr(10)
    endif

	return eol$

End Function

'endregion

Function CanUseScreenModes(videoMode as object) as boolean
  if type(videoMode) <> "roVideoMode" then videoMode = CreateObject("roVideoMode")

  ' GetScreenModes is a function introduced in Series 5 players. 
  ' Therefore we need to check if it's supported before use to avoid errors in lower series.
  if type(videoMode) = "roVideoMode" and findMemberFunction(videoMode, "GetScreenModes") <> invalid then
    screenModes = videoMode.GetScreenModes()
    if type(screenModes) = "roArray" and screenModes.Count() > 0 then return true
  end if
  return false
end function


'region newSign
Function newSign(BrightAuthor As Object, globalVariables As Object, bsp As Object, msgPort As Object, controlPort As Object, version% As Integer) As Object

	publishedModel$ = UCase(BrightAuthor.meta.model.GetText())
	if publishedModel$ = "HD110" or publishedModel$ = "HD210" or publishedModel$ = "HD410" or publishedModel$ = "HD1010" or publishedModel$ = "HD1010W" or publishedModel$ = "TD1012" or publishedModel$ = "A913" or publishedModel$ = "A933" or publishedModel$ = "HD910" or publishedModel$ = "HD912" or publishedModel$ = "HD960" or publishedModel$ = "HD962" then
		DisplayObsoleteModelMessage(bsp, publishedModel$)
	endif

' determine whether this presentation could use new or old parameters (a Panther might use either)
	bsp.currentPresentationUsesRoAudioOutputParameters = true

' reset variable
	bsp.mediaListInactivity = invalid

  Sign = CreateObject("roAssociativeArray")

  Sign.numTouchEvents% = 0
  Sign.numAudioTimeCodeEvents% = 0
  Sign.numVideoTimeCodeEvents% = 0

  ' get sign data

  Sign.name$ = BrightAuthor.meta.name.GetText()
  if not IsString(Sign.name$) then print "Invalid XML file - meta name not found" : stop

' videoMode plugins
	videoModePluginsContainer = BrightAuthor.meta.videoModePlugins
	if videoModePluginsContainer.Count() = 1 then
		videoModePluginsXML = videoModePluginsContainer.GetChildElements()
		for each videoModePluginXML in videoModePluginsXML
			videoModePlugin = newVideoModePlugin(videoModePluginXML)
			bsp.videoModePlugins.push(videoModePlugin)
		next
	endif

' parse script plugins
	scriptPluginsContainer = BrightAuthor.meta.scriptPlugins
	if scriptPluginsContainer.Count() = 1 then
		scriptPluginsXML = scriptPluginsContainer.GetChildElements()
		for each scriptPluginXML in scriptPluginsXML
			scriptPlugin = newScriptPlugin(scriptPluginXML)
			bsp.scriptPlugins.push(scriptPlugin)
		next
	endif

	videoMode = CreateObject("roVideoMode")

	if CanUseScreenModes(videoMode) then
		screenModes = videoMode.GetScreenModes()

		' Update the corresponding settings to the OS video output array
		' BA Classic only supports 1 screen, so we'll update only index 0

		videoOutputOSIndex% = 0

		videoMode$ = BrightAuthor.meta.videoMode.GetText()

		forceResolution = false
		if BrightAuthor.meta.forceResolution.GetText() <> "" then
		  if lcase(BrightAuthor.meta.forceResolution.GetText()) = "true" then
			forceResolution = true
		  endif
		endif

		if not forceResolution and bsp.forceResolutionSupported then
		  videoMode$ = videoMode$ + ":preferred"
		endif

		if BrightAuthor.meta.dolbyVision.GetText() <> "" then			
		  if lcase(BrightAuthor.meta.dolbyVision.GetText()) = "true" then				
				videoMode$ = videoMode$ + ":dbv"
		  endif
		endif

		if BrightAuthor.meta.fullResGraphicsEnabled.GetText() <> "" then
		  if lcase(BrightAuthor.meta.fullResGraphicsEnabled.GetText()) = "true" and bsp.fullResolutionGraphicsSupported then
				videoMode$ = videoMode$ + ":fullres"
		  endif
		endif

		if BrightAuthor.meta.tenBitColorEnabled.GetText() <> "" then
		  if lcase(BrightAuthor.meta.tenBitColorEnabled.GetText()) = "true" then
				videoMode$ = videoMode$ + ":10bit"
		  endif
		endif

		screenModes[videoOutputOSIndex%].video_mode = videoMode$

		' transform has options normal, 90, 180 or 270
		orientation$ = lcase(BrightAuthor.meta.monitorOrientation.GetText())

		if orientation$ = "portrait" then
			screenModes[videoOutputOSIndex%].transform = "90"
		else if orientation$ = "portraitbottomonright" then
			screenModes[videoOutputOSIndex%].transform = "270"
		else
			screenModes[videoOutputOSIndex%].transform = "normal"
		end if

		screenModes[videoOutputOSIndex%].display_x = 0
		screenModes[videoOutputOSIndex%].display_y = 0

		screenModes[videoOutputOSIndex%].enabled = true

		' disable all HDMI ports except 0th index
		for i% = 1 to screenModes.Count() - 1
			screenModes[i%].enabled = false
		next

		' allow videoModePlugin to set multi screens videoMode
		videoModeInputs = {}
		videoModeInputs.screenModes = screenModes
		screenModes = parseVideoModePlugin(videoModeInputs, bsp, "", screenModes)

		ok = videoMode.SetScreenModes(screenModes)
		' error handling in case it fails
		if ok = 0 then
			errMsg$ = "Error: Can't set VIDEOMODE screen array. Resetting to 1920x1080x60i"
			bsp.diagnostics.PrintDebug(errMsg$)
			bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_SET_VIDEO_MODE, errMsg$)
			videoMode.SetMode("1920x1080x60i")
		end if

		' Video walls should be supported on all Series 5 players since only 1 screen is supported in BA Classic
        if BrightAuthor.meta.stretchedVideoWall.Count() = 1 then
            Sign.isVideoWall = true
            Sign.videoWallType$ = "stretched"
            Sign.videoWallNumRows% = int(val(BrightAuthor.meta.stretchedVideoWall.videoWallNumRows.GetText()))
            Sign.videoWallNumColumns% = int(val(BrightAuthor.meta.stretchedVideoWall.videoWallNumColumns.GetText()))
            Sign.videoWallRowPosition% = int(val(BrightAuthor.meta.stretchedVideoWall.videoWallRowPosition.GetText()))
            Sign.videoWallColumnPosition% = int(val(BrightAuthor.meta.stretchedVideoWall.videoWallColumnPosition.GetText()))
            videoMode.SetMultiscreenBezel(int(val(BrightAuthor.meta.stretchedVideoWall.bezelWidthPercentage.GetText())), int(val(BrightAuthor.meta.stretchedVideoWall.bezelHeightPercentage.GetText())))
        else
            videoMode.SetMultiscreenBezel(0, 0)
        endif

  else
      ' players do not support multi screens
      ' players do not support multi screens or SetScreenModes()
		Sign.videoMode$ = BrightAuthor.meta.videoMode.GetText()

		if Sign.videoMode$ <> "not applicable" then
		if not IsString(Sign.videoMode$) then print "Invalid XML file - meta videoMode not found" : stop
		'    print "Video mode is ";Sign.videoMode$

		videoMode$ = Sign.videoMode$

		forceResolution = false
		if BrightAuthor.meta.forceResolution.GetText() <> "" then
			if lcase(BrightAuthor.meta.forceResolution.GetText()) = "true" then
			forceResolution = true
			endif
		endif

		setPreferredResolution = false
		if not forceResolution and bsp.forceResolutionSupported then
			setPreferredResolution = true
			videoMode$ = videoMode$ + ":preferred"
		endif

		dolbyVisionEnabled = false
		if BrightAuthor.meta.dolbyVision.GetText() <> "" then			
			if lcase(BrightAuthor.meta.dolbyVision.GetText()) = "true" then				
			videoMode$ = videoMode$ + ":dbv"
			dolbyVisionEnabled = true
			endif
		endif

		fullResGraphicsEnabled = false
		if BrightAuthor.meta.fullResGraphicsEnabled.GetText() <> "" then
		  if lcase(BrightAuthor.meta.fullResGraphicsEnabled.GetText()) = "true" and bsp.fullResolutionGraphicsSupported then
			fullResGraphicsEnabled = true
			videoMode$ = videoMode$ + ":fullres"
		  endif
		endif

		tenBitColorEnabled = false
		if BrightAuthor.meta.tenBitColorEnabled.GetText() <> "" then
		  if lcase(BrightAuthor.meta.tenBitColorEnabled.GetText()) = "true" then
			tenBitColorEnabled = true
			videoMode$ = videoMode$ + ":10bit"
		  endif
		endif

		' allow videoModePlugin to set videoMode
		videoModeInputs = {}
		videoModeInputs.signVideoMode$ = Sign.videoMode$
		videoModeInputs.setPreferredResolution = setPreferredResolution
		videoModeInputs.fullResGraphicsEnabled = fullResGraphicsEnabled
		videoModeInputs.tenBitColorEnabled = tenBitColorEnabled
		videoModeInputs.dolbyVisionEnabled = dolbyVisionEnabled
		videoModeInputs.videoMode$ = videoMode$

		' allow videoModePlugin to set single screen videoMode
		singleVideoMode = parseVideoModePlugin(videoModeInputs, bsp, videoMode$, {})

		ok = videoMode.SetMode(singleVideoMode)
		if ok = 0 then
		  print "Error: Can't set VIDEOMODE to ::"; Sign.videoMode$; " resetting to 1920x1080x60i"
		  ok = videoMode.SetMode("1920x1080x60i")
		endif

		if bsp.forceResolutionSupported then
		  aa = videoMode.GetConfiguredMode()
		  bsp.configuredResX = aa.graphicsPlaneWidth
		  bsp.configuredResY = aa.graphicsPlaneHeight

		  bsp.diagnostics.PrintDebug("Specified videoMode: " + singleVideoMode + ", actual videoMode: " + videoMode.GetMode())
		  bsp.logging.WriteDiagnosticLogEntry(bsp.diagnosticCodes.EVENT_SET_VIDEO_MODE, "Specified videoMode: " + singleVideoMode + ", actual videoMode: " + videoMode.GetMode())

		else
		  bsp.configuredResX = videoMode.GetResX()
		  bsp.configuredResY = videoMode.GetResY()
		endif

		' if the user specified a video mode other than what the system chose, scale screen items as needed
		bsp.actualResX = videoMode.GetResX()
		bsp.actualResY = videoMode.GetResY()

		Sign.videoConnector$ = BrightAuthor.meta.videoConnector.GetText()
		if not IsString(Sign.videoConnector$) then print "Invalid XML file - meta videoConnector not found" : stop

	  endif

		Sign.isVideoWall = false
		if BrightAuthor.meta.stretchedVideoWall.Count() = 1 then
			Sign.isVideoWall = true
			Sign.videoWallType$ = "stretched"
			Sign.videoWallNumRows% = int(val(BrightAuthor.meta.stretchedVideoWall.videoWallNumRows.GetText()))
			Sign.videoWallNumColumns% = int(val(BrightAuthor.meta.stretchedVideoWall.videoWallNumColumns.GetText()))
			Sign.videoWallRowPosition% = int(val(BrightAuthor.meta.stretchedVideoWall.videoWallRowPosition.GetText()))
			Sign.videoWallColumnPosition% = int(val(BrightAuthor.meta.stretchedVideoWall.videoWallColumnPosition.GetText()))
			videoMode.SetMultiscreenBezel(int(val(BrightAuthor.meta.stretchedVideoWall.bezelWidthPercentage.GetText())), int(val(BrightAuthor.meta.stretchedVideoWall.bezelHeightPercentage.GetText())))
		else
			videoMode.SetMultiscreenBezel(0, 0)
		endif

		videoMode = invalid

	endif

	Sign.monitorOrientation = "landscape"
	if BrightAuthor.meta.monitorOrientation.GetText() <> "" then
		Sign.monitorOrientation = lcase(BrightAuthor.meta.monitorOrientation.GetText())
	endif
	
    Sign.timezone$ = BrightAuthor.meta.timezone.GetText()
    if IsString(Sign.timezone$) then
        bsSystemTime = CreateObject("roSystemTime")
        if Sign.timezone$ <> "" then
            bsSystemTime.SetTimeZone(Sign.timezone$)
        endif
        bsSystemTime = 0
    endif
    
    rssDownloadSpec = BrightAuthor.meta.rssDownloadSpec
	Sign.rssDownloadPeriodicValue% = GetRSSDownloadInterval(rssDownloadSpec)
    
	Sign.deviceWebPageDisplay$ = "Standard"
    if BrightAuthor.meta.deviceWebPageDisplay.GetText() <> "" then
		Sign.deviceWebPageDisplay$ = BrightAuthor.meta.deviceWebPageDisplay.GetText()
	endif
    Sign.customDeviceWebPage$ = BrightAuthor.meta.customDeviceWebPage.GetText()

	Sign.alphabetizeVariableNames = true
	if BrightAuthor.meta.alphabetizeVariableNames.GetText() <> "" and lcase(BrightAuthor.meta.alphabetizeVariableNames.GetText()) = "false" then
		Sign.alphabetizeVariableNames = false
	endif

	Sign.htmlEnableJavascriptConsole = false
	if BrightAuthor.meta.htmlEnableJavascriptConsole.GetText() <> "" and lcase(BrightAuthor.meta.htmlEnableJavascriptConsole.GetText()) = "true" then
		Sign.htmlEnableJavascriptConsole = true
	endif

	' maintain for backwards compatibilities with old projects
	Sign.htmlEnableLocalStorage = false
	if lcase(BrightAuthor.meta.htmlEnableLocalStorage.GetText()) = "true" then
		Sign.htmlEnableLocalStorage = true
		Sign.htmlLocalStorageSize% = int(val(BrightAuthor.meta.htmlLocalStorageSize.GetText()))
	endif

    backgroundScreenColor = BrightAuthor.meta.backgroundScreenColor
    Sign.backgroundScreenColor% = GetColor(backgroundScreenColor.GetAttributes())

	bsp.dontChangePresentationUntilMediaEndEventReceived = false
	delay$ = BrightAuthor.meta.delayScheduleChangeUntilMediaEndEvent.GetText()
	if delay$ <> "" and lcase(delay$) = "true" then
		bsp.dontChangePresentationUntilMediaEndEventReceived = true
	endif
    
    Sign.languageKey$ = BrightAuthor.meta.languageKey.GetText()
    globalVariables.language$ = Sign.languageKey$

    Sign.serialPortConfigurations = CreateObject("roArray", 8, true)
    GetGlobalAA().usbCDCPortConfigurations = {}
    GetGlobalAA().usbHIDPortConfigurations = {}
	GetGlobalAA().usbAudioPortConfigurations = {}

	bp900AConfigureAutomatically = GetBoolFromString(BrightAuthor.meta.BP900AConfigureAutomatically.GetText(), true)
	bp900BConfigureAutomatically = GetBoolFromString(BrightAuthor.meta.BP900BConfigureAutomatically.GetText(), true)
	bp900CConfigureAutomatically = GetBoolFromString(BrightAuthor.meta.BP900CConfigureAutomatically.GetText(), true)
	bp900DConfigureAutomatically = GetBoolFromString(BrightAuthor.meta.BP900DConfigureAutomatically.GetText(), true)
	bp200AConfigureAutomatically = GetBoolFromString(BrightAuthor.meta.BP200AConfigureAutomatically.GetText(), true)
	bp200BConfigureAutomatically = GetBoolFromString(BrightAuthor.meta.BP200BConfigureAutomatically.GetText(), true)
	bp200CConfigureAutomatically = GetBoolFromString(BrightAuthor.meta.BP200CConfigureAutomatically.GetText(), true)
	bp200DConfigureAutomatically = GetBoolFromString(BrightAuthor.meta.BP200DConfigureAutomatically.GetText(), true)

	bp900AConfiguration% = GetIntFromString(BrightAuthor.meta.BP900AConfiguration.GetText())
	bp900BConfiguration% = GetIntFromString(BrightAuthor.meta.BP900BConfiguration.GetText())
	bp900CConfiguration% = GetIntFromString(BrightAuthor.meta.BP900CConfiguration.GetText())
	bp900DConfiguration% = GetIntFromString(BrightAuthor.meta.BP900DConfiguration.GetText())
	bp200AConfiguration% = GetIntFromString(BrightAuthor.meta.BP200AConfiguration.GetText())
	bp200BConfiguration% = GetIntFromString(BrightAuthor.meta.BP200BConfiguration.GetText())
	bp200CConfiguration% = GetIntFromString(BrightAuthor.meta.BP200CConfiguration.GetText())
	bp200DConfiguration% = GetIntFromString(BrightAuthor.meta.BP200DConfiguration.GetText())

	if type(bsp.bpInputPorts[0]) = "roControlPort" then
		bsp.bpInputPortConfigurations[0] = GetBPConfiguration(bsp.bpInputPortHardware[0], bp900AConfigureAutomatically, bp900AConfiguration%, bp200AConfigureAutomatically, bp200AConfiguration%)
	endif

	if type(bsp.bpInputPorts[1]) = "roControlPort" then
		bsp.bpInputPortConfigurations[1] = GetBPConfiguration(bsp.bpInputPortHardware[1], bp900BConfigureAutomatically, bp900BConfiguration%, bp200BConfigureAutomatically, bp200BConfiguration%)
	endif

	if type(bsp.bpInputPorts[2]) = "roControlPort" then
		bsp.bpInputPortConfigurations[2] = GetBPConfiguration(bsp.bpInputPortHardware[2], bp900CConfigureAutomatically, bp900CConfiguration%, bp200CConfigureAutomatically, bp200CConfiguration%)
	endif

	if type(bsp.bpInputPorts[3]) = "roControlPort" then
		bsp.bpInputPortConfigurations[3] = GetBPConfiguration(bsp.bpInputPortHardware[3], bp900DConfigureAutomatically, bp900DConfiguration%, bp200DConfigureAutomatically, bp200DConfiguration%)
	endif

	bsp.gpsConfigured = false
	bsp.gpsLocation = { latitude: invalid, longitude: invalid }
	bsp.gpsPort$ = ""

    if BrightAuthor.meta.baudRate.Count() = 1 then
        ' old format
        serialPortSpeed% = int(val(BrightAuthor.meta.baudRate.GetText()))
        serialPortMode$ = BrightAuthor.meta.dataBits.GetText() + BrightAuthor.meta.parity.GetText() + BrightAuthor.meta.stopBits.GetText()
        serialPortConfiguration = CreateObject("roAssociativeArray")
        serialPortConfiguration.serialPortSpeed% = serialPortSpeed%
        serialPortConfiguration.serialPortMode$ = serialPortMode$
        serialPortConfiguration.sendEol$ = chr(13)
        serialPortConfiguration.receiveEol$ = chr(13)
        serialPortConfiguration.invertSignals = false
		serialPortConfiguration.gps = false
        Sign.serialPortConfigurations[0] = serialPortConfiguration
    else
        serialPortConfigurationsXML = BrightAuthor.meta.SerialPortConfiguration
        for each serialPortConfigurationXML in serialPortConfigurationsXML
            
            serialPortConfiguration = CreateObject("roAssociativeArray")

            serialPortSpeed% = int(val(serialPortConfigurationXML.baudRate.GetText()))
            serialPortConfiguration.serialPortSpeed% = serialPortSpeed%

            dataBits$ = serialPortConfigurationXML.dataBits.GetText()
            parity$ = serialPortConfigurationXML.parity.GetText()
            stopBits$ = serialPortConfigurationXML.stopBits.GetText()
            serialPortMode$ = dataBits$ + parity$ + stopBits$
            serialPortConfiguration.serialPortMode$ = serialPortMode$
            
            protocol$ = serialPortConfigurationXML.protocol.GetText()
            if protocol$ = "" then
	            serialPortConfiguration.protocol$ = "ASCII"
            else
	            serialPortConfiguration.protocol$ = serialPortConfigurationXML.protocol.GetText()
            endif
            
			serialPortConfiguration.sendEol$ = GetEolFromSpec(serialPortConfigurationXML.sendEol.GetText())
            serialPortConfiguration.receiveEol$ = GetEolFromSpec(serialPortConfigurationXML.receiveEol.GetText())
	        serialPortConfiguration.invertSignals = false
            if lcase(serialPortConfigurationXML.invertSignals.GetText()) = "true" then
				serialPortConfiguration.invertSignals = true
			endif

			port$ = serialPortConfigurationXML.port.GetText()
            port% = int(val(port$))

			connectedDevice$ = serialPortConfigurationXML.connectedDevice.GetText()
			if connectedDevice$ = "GPS" then
				serialPortConfiguration.gps = true
				bsp.gpsConfigured = true
				bsp.gpsPort$ = port$
			else
				serialPortConfiguration.gps = false
			endif
			                        
            Sign.serialPortConfigurations[port%] = serialPortConfiguration

        next    
    endif

' parse parser plugins

	parserPluginsContainer = BrightAuthor.meta.parserPlugins

	if parserPluginsContainer.Count() = 1 then

		parserPluginsXML = parserPluginsContainer.GetChildElements()

		for each parserPluginXML in parserPluginsXML
			parserPlugin = newParserPlugin(parserPluginXML)
			bsp.parserPlugins.push(parserPlugin)
		next

	endif


' first pass parse of user variables

'	bsp.autoCreateMediaCounterVariables = GetBoolFromString(BrightAuthor.meta.autoCreateMediaCounterVariables.GetText(), false)

	bsp.networkedVariablesUpdateInterval% = 300
	networkedVariablesUpdateInterval$ = BrightAuthor.meta.networkedVariablesUpdateInterval.GetText()
	if networkedVariablesUpdateInterval$ <> "" then
        bsp.networkedVariablesUpdateInterval% = int(val(networkedVariablesUpdateInterval$))
	endif

	bsp.variablesDBExists = false
	bsp.ReadVariablesDB(bsp.activePresentation$)

	variablePosition% = 0

	userVariableSetXML = BrightAuthor.meta.userVariables
	if userVariableSetXML.Count() = 1 then

		userVariablesXML = userVariableSetXML.GetChildElements()

		if type(userVariablesXML) = "roXMLList" and userVariablesXML.Count() > 0 then

			bsp.privateDBSectionId% = bsp.GetDBSectionId(bsp.activePresentation$)
			if bsp.privateDBSectionId% < 0 then
				bsp.AddDBSection(bsp.activePresentation$)
				bsp.privateDBSectionId% = bsp.GetDBSectionId(bsp.activePresentation$)
			endif

			bsp.privateBrightAuthorCategoryId% = bsp.GetDBCategoryId(bsp.privateDBSectionId%, "BrightAuthor")
			if bsp.privateBrightAuthorCategoryId% < 0 then
				bsp.AddDBCategory(bsp.privateDBSectionId%, "BrightAuthor")
				bsp.privateBrightAuthorCategoryId% = bsp.GetDBCategoryId(bsp.privateDBSectionId%, "BrightAuthor")
			endif

			bsp.sharedDBSectionId% = bsp.GetDBSectionId("Shared")
			if bsp.sharedDBSectionId% < 0 then
				bsp.AddDBSection("Shared")
				bsp.sharedDBSectionId% = bsp.GetDBSectionId("Shared")
			endif

			bsp.sharedBrightAuthorCategoryId% = bsp.GetDBCategoryId(bsp.sharedDBSectionId%, "BrightAuthor")
			if bsp.sharedBrightAuthorCategoryId% < 0 then
				bsp.AddDBCategory(bsp.sharedDBSectionId%, "BrightAuthor")
				bsp.sharedBrightAuthorCategoryId% = bsp.GetDBCategoryId(bsp.sharedDBSectionId%, "BrightAuthor")
			endif

			userVariables = bsp.currentUserVariables

			for each userVariableXML in userVariablesXML

				access$ = userVariableXML.access.GetText()
				if access$ = "Shared" then
					categoryId% = bsp.sharedBrightAuthorCategoryId%
				else
					categoryId% = bsp.privateBrightAuthorCategoryId%
				endif

				name$ = userVariableXML.name.GetText()

				defaultValue$ = userVariableXML.defaultValue.GetText()

				if not userVariables.DoesExist(name$) then
					bsp.AddDBVariable(categoryId%, name$, defaultValue$, "", 0)
					userVariable = newUserVariable(bsp, name$, defaultValue$, defaultValue$, "", access$)
					userVariables.AddReplace(name$, userVariable)
				else
					userVariable = userVariables.Lookup(name$)
					if userVariable.defaultValue$ <> defaultValue$ then
						userVariable.defaultValue$ = defaultValue$
						bsp.UpdateDBVariableDefaultValue(categoryId%, name$, defaultValue$)
					endif
				endif

				userVariable.position% = variablePosition%
				variablePosition% = variablePosition% + 1

				userVariable.systemVariable$ = userVariableXML.systemVariable.GetText()

				' record networked information - parse in 2nd pass
				userVariable.url$ = ""
				userVariable.liveDataFeedName$ = ""

				url$ = userVariableXML.url.GetText()
				if url$ <> "" then
					userVariable.url$ = url$
				else
					if userVariableXML.liveDataFeedName.GetText() <> "" then
						userVariable.liveDataFeedName$ = CleanName(userVariableXML.liveDataFeedName.GetText())
					endif
				endif
			next
		endif
	endif

' parse live data feeds
    
	liveDataFeedsContainer = BrightAuthor.meta.liveDataFeeds

	if liveDataFeedsContainer.Count() = 1 then

		liveDataFeedsXML = liveDataFeedsContainer.GetChildElements()

		for each liveDataFeedXML in liveDataFeedsXML
			liveDataFeed = newLiveDataFeed(bsp, liveDataFeedXML)
			bsp.liveDataFeeds.AddReplace(liveDataFeed.name$, liveDataFeed)
		next

	endif

	if bsp.liveDataFeeds.IsEmpty() then
		if type(bsp.networkingHSM) = "roAssociativeArray" then
			bsp.networkingHSM.UploadDeviceDownloadProgressFileList()
			bsp.networkingHSM.FileListPendingUpload = false
		endif
	endif

' second pass parse of user variables

	if type(userVariables) = "roAssociativeArray" then
		for each userVariableKey in userVariables
			userVariable = userVariables.Lookup(userVariableKey)
			if type(userVariable.url$) <> "Invalid" and userVariable.url$ <> "" then
				' old format
				liveDataFeed = bsp.liveDataFeeds.Lookup(userVariable.url$)
				if type(liveDataFeed) <> "roAssociativeArray" then
					url = newTextParameterValue(userVariable.url$)
					liveDataFeed = newLiveDataFeedFromOldDataFormat(bsp, url, bsp.networkedVariablesUpdateInterval%)
					bsp.liveDataFeeds.AddReplace(liveDataFeed.name$, liveDataFeed)
				endif
				userVariable.liveDataFeed = liveDataFeed
			else if type(userVariable.liveDataFeedName$) <> "Invalid" and userVariable.liveDataFeedName$ <> "" then
				' new format
				userVariable.liveDataFeed = bsp.liveDataFeeds.Lookup(userVariable.liveDataFeedName$)
			endif
		next
	endif

' reset variables if indicated in sign
	if BrightAuthor.meta.resetVariablesOnPresentationStart.GetText() <> "" and lcase(BrightAuthor.meta.resetVariablesOnPresentationStart.GetText()) = "true" then
		bsp.ResetVariables()
	endif

' assign system variables to user variables
	bsp.AssignSystemVariablesToUserVariables()

' parse HTML sites

	htmlSitesContainer = BrightAuthor.meta.htmlSites

	if htmlSitesContainer.Count() = 1 then
	
		htmlSitesXML = htmlSitesContainer.GetChildElements()

		for each htmlSiteXML in htmlSitesXML
			htmlSite = newHTMLSite(bsp, htmlSiteXML)
			bsp.htmlSites.AddReplace(htmlSite.name$, htmlSite)
		next

	endif

' parse presentations

	presentationsContainer = BrightAuthor.meta.presentationIdentifiers

	if presentationsContainer.Count() = 1 then
	
		presentationsXML = presentationsContainer.GetChildElements()

		for each presentationXML in presentationsXML
			presentation = newPresentation(bsp, presentationXML)
			bsp.presentations.AddReplace(presentation.name$, presentation)
		next

	endif

' parse beacons

	beaconsContainer = BrightAuthor.meta.beacons
	if beaconsContainer <> invalid and beaconsContainer.Count() = 1 then
		if bsp.btManager.ParsePresentationBeacons(beaconsContainer.GetChildElements()) then
			' If presentation beacons were defined, set advertising to start any marked for autostart
			bsp.btManager.SetBtAdvertising()
		endif
	endif

' get list of additional files to publish

	additionalPublishedFilesXML = BrightAuthor.meta.additionalFileToPublish
	for each additionalPublishedFileXML in additionalPublishedFilesXML
		additionalPublishedFile = {}
		additionalPublishedFile.fileName$ = additionalPublishedFileXML.GetText()
	    additionalPublishedFile.filePath$ = GetPoolFilePath(bsp.assetPoolFiles, additionalPublishedFile.fileName$)
		bsp.additionalPublishedFiles.push(additionalPublishedFile)
	next

' parse boseProduct section - lists Bose products in use in this presentation

	gaa = GetGlobalAA()
    
	boseProductsXML = BrightAuthor.meta.boseProduct
	if boseProductsXML.Count() > 0 then

		Sign.boseProducts = CreateObject("roAssociativeArray")
		Sign.boseProductsByConnector = {}
		for each boseProductXML in boseProductsXML
			boseProduct = CreateObject("roAssociativeArray")
			boseProduct.productName$ = boseProductXML.productName.GetText()
			boseProduct.port$ = boseProductXML.port.GetText()
			Sign.boseProducts.AddReplace(boseProduct.productName$, boseProduct)
			Sign.boseProductsByConnector.AddReplace(boseProduct.port$, boseProduct)

			boseProductSpec = bsp.GetBoseProductSpec(boseProduct.productName$)

			if type(boseProductSpec) = "roAssociativeArray" then
				if boseProductSpec.tapProtocol = "CDC" then
					usbCDCPortConfiguration = {}
					usbCDCPortConfiguration.sendEol$ = GetEolFromSpec(boseProductSpec.sendEol$)
					usbCDCPortConfiguration.receiveEol$ = GetEolFromSpec(boseProductSpec.receiveEol$)
					usbCDCPortConfiguration.protocol$ = boseProductSpec.protocol$
					usbCDCPortConfiguration.usbTapInterfaceIndex$ = boseProductSpec.usbTapInterfaceIndex$
					usbCDCPortConfiguration.serialPortSpeed = boseProductSpec.baudRate%
					gaa.usbCDCPortConfigurations.AddReplace(boseProduct.port$, usbCDCPortConfiguration)
				else if boseProductSpec.tapProtocol = "HID" then
					usbHIDPortConfiguration = {}
					usbHIDPortConfiguration.sendEol$ = GetEolFromSpec(boseProductSpec.sendEol$)
					usbHIDPortConfiguration.receiveEol$ = GetEolFromSpec(boseProductSpec.receiveEol$)
					usbHIDPortConfiguration.protocol$ = boseProductSpec.protocol$
					usbHIDPortConfiguration.usbTapInterfaceIndex$ = boseProductSpec.usbTapInterfaceIndex$
					gaa.usbHIDPortConfigurations.AddReplace(boseProduct.port$, usbHIDPortConfiguration)
				else
					port% = int(val(boseProduct.port$))
					serialPortConfiguration = Sign.serialPortConfigurations[port%]
					serialPortConfiguration.serialPortSpeed% = boseProductSpec.baudRate%
					serialPortConfiguration.serialPortMode$ = boseProductSpec.dataBits$ + boseProductSpec.parity$ + boseProductSpec.stopBits$
					serialPortConfiguration.sendEol$ = GetEolFromSpec(boseProductSpec.sendEol$)
					serialPortConfiguration.receiveEol$ = GetEolFromSpec(boseProductSpec.receiveEol$)
					serialPortConfiguration.invertSignals = boseProductSpec.invertSignals
				endif

				if boseProductSpec.usbAudioInterfaceIndex$ <> "" then
					usbAudioPortConfiguration = {}
					usbAudioPortConfiguration.usbAudioInterfaceIndex$ = boseProductSpec.usbAudioInterfaceIndex$
					gaa.usbAudioPortConfigurations.AddReplace(boseProduct.port$, usbAudioPortConfiguration)
				endif
			endif
		next
	endif

' initialize USB name mapping
	gaa.baUSBConfigNameToDeviceName = {}
	gaa.baUSBConfigNameToDeviceName["usbA"] = "USB A"
	gaa.baUSBConfigNameToDeviceName["usbB"] = "USB B"
	gaa.baUSBConfigNameToDeviceName["usbC"] = "USB C"
	gaa.baUSBConfigNameToDeviceName["usbD"] = "USB D"
	gaa.baUSBConfigNameToDeviceName["usb type_a"] = "USB Type_A"
	gaa.baUSBConfigNameToDeviceName["usb type_c"] = "USB Type_C"
	gaa.baUSBConfigNameToDeviceName["usb700_1"] = "USB 700_1"
	gaa.baUSBConfigNameToDeviceName["usb700_2"] = "USB 700_2"
	gaa.baUSBConfigNameToDeviceName["usb700_3"] = "USB 700_3"
	gaa.baUSBConfigNameToDeviceName["usb700_4"] = "USB 700_4"
	gaa.baUSBConfigNameToDeviceName["usb700_5"] = "USB 700_5"
	gaa.baUSBConfigNameToDeviceName["usb700_6"] = "USB 700_6"
	gaa.baUSBConfigNameToDeviceName["usb700_7"] = "USB 700_7"

	gaa.baUSBParamNameToDeviceParamName = {}
	gaa.baUSBParamNameToDeviceParamName["usbA"] = "usbA"
	gaa.baUSBParamNameToDeviceParamName["usbB"] = "usbB"
	gaa.baUSBParamNameToDeviceParamName["usbC"] = "usbC"
	gaa.baUSBParamNameToDeviceParamName["usbD"] = "usbD"
	gaa.baUSBParamNameToDeviceParamName["usbTypeA"] = "usbTypeA"
	gaa.baUSBParamNameToDeviceParamName["usbTypeC"] = "usbTypeC"
	gaa.baUSBParamNameToDeviceParamName["usb700_1"] = "usbA1"
	gaa.baUSBParamNameToDeviceParamName["usb700_2"] = "usbA2"
	gaa.baUSBParamNameToDeviceParamName["usb700_3"] = "usbA3"
	gaa.baUSBParamNameToDeviceParamName["usb700_4"] = "usbA4"
	gaa.baUSBParamNameToDeviceParamName["usb700_5"] = "usbA5"
	gaa.baUSBParamNameToDeviceParamName["usb700_6"] = "usbA6"
	gaa.baUSBParamNameToDeviceParamName["usb700_7"] = "usbA7"

	gaa.usbParamNameToDeviceParamName = {}
	gaa.usbParamNameToDeviceParamName["a1"] = "A/1"
	gaa.usbParamNameToDeviceParamName["a2"] = "A/2"
	gaa.usbParamNameToDeviceParamName["a3"] = "A/3"
	gaa.usbParamNameToDeviceParamName["a4"] = "A/4"
	gaa.usbParamNameToDeviceParamName["a5"] = "A/5"
	gaa.usbParamNameToDeviceParamName["a6"] = "A/6"
	gaa.usbParamNameToDeviceParamName["a7"] = "A/7"


' Get the USB topology of the device and create a mapping of Bose port names in the presentation to USB device names
	usbMappings = BuildUSBDevicesByConnector(sign)
	bsp.usbDevicesByConnector =  usbMappings.usbDevicesByConnector
	' TODO - HACK
	gaa.usbDevicesByConnector =  usbMappings.usbDevicesByConnector
	bsp.usbBACommandToDeviceCommandMapping = usbMappings.usbBACommandToDeviceCommandMapping
	bsp.usbConnectorToConnectorParameterMapping = usbMappings.usbConnectorToConnectorParameterMapping

	connectedBoseProducts = {}
	usbCDCPortConfigurations = {}
	usbHIDPortConfigurations = {}
	usbAudioPortConfigurations = {}

	for each connector in Sign.boseProductsByConnector
		
		boseProduct = Sign.boseProductsByConnector[connector]

		if  bsp.boseProductSpecs[boseproduct.productname$].usbAsyncAudio then
			audioConfiguration = CreateObject("roAudioConfiguration")
			audioConfiguration.ConfigureAudio({ usbasync: 1})
		endif

		if bsp.usbDevicesByConnector.DoesExist(connector) then

			boseDeviceName = bsp.usbDevicesByConnector[connector]
			boseProduct.port$ = boseDeviceName

			connectedBoseProducts.AddReplace(boseDeviceName, boseProduct)

			if gaa.usbCDCPortConfigurations.DoesExist(connector) then
				usbCDCPortConfiguration = gaa.usbCDCPortConfigurations.Lookup(connector)
				usbCDCPortConfigurations.AddReplace(boseDeviceName, usbCDCPortConfiguration)
			endif

			if gaa.usbHIDPortConfigurations.DoesExist(connector) then
				usbHIDPortConfiguration = gaa.usbHIDPortConfigurations.Lookup(connector)
				usbHIDPortConfigurations.AddReplace(boseDeviceName, usbHIDPortConfiguration)
			endif

			if gaa.usbAudioPortConfigurations.DoesExist(connector) then
				usbAudioPortConfiguration = gaa.usbAudioPortConfigurations.Lookup(connector)
				usbAudioPortConfigurations.AddReplace(boseDeviceName, usbAudioPortConfiguration)
			endif
		endif

	next

' substitute values in look up tables - change from BA names to actual USB device names
	Sign.boseProductsByConnector = connectedBoseProducts
	gaa.usbHIDPortConfigurations = usbHIDPortConfigurations
	gaa.usbCDCPortConfigurations = usbCDCPortConfigurations
	gaa.usbAudioPortConfigurations = usbAudioPortConfigurations

	bsp.boseUSBAudioDevicesByConnector = BuildBoseUSBAudioDevicesByConnector(Sign)

    Sign.tripleUSBPort$ = BrightAuthor.meta.tripleUSBPort.GetText()
    if BrightAuthor.meta.tripleUSBPort.Count() = 1 then
		port% = int(val(BrightAuthor.meta.tripleUSBPort.GetText()))
        serialPortConfiguration = Sign.serialPortConfigurations[port%]
		serialPortConfiguration.serialPortSpeed% = 9600
		serialPortConfiguration.serialPortMode$ = "8N1"
    endif
        
    ' set default serial port speed, mode
    bsp.serialPortConfigurations = CreateObject("roArray", 8, true)
    for i% = 0 to 7
        if type(Sign.serialPortConfigurations[i%]) = "roAssociativeArray" then
            serialPortConfiguration = CreateObject("roAssociativeArray")
            serialPortConfiguration.serialPortSpeed% = Sign.serialPortConfigurations[i%].serialPortSpeed%
            serialPortConfiguration.serialPortMode$ = Sign.serialPortConfigurations[i%].serialPortMode$
            serialPortConfiguration.protocol$ = Sign.serialPortConfigurations[i%].protocol$
            serialPortConfiguration.sendEol$ = Sign.serialPortConfigurations[i%].sendEol$
            serialPortConfiguration.receiveEol$ = Sign.serialPortConfigurations[i%].receiveEol$
            serialPortConfiguration.invertSignals = Sign.serialPortConfigurations[i%].invertSignals
            bsp.serialPortConfigurations[i%] = serialPortConfiguration
        endif
    next
    
    Sign.udpReceivePort = int(val(BrightAuthor.meta.udpReceiverPort.GetText()))
    Sign.udpSendPort = int(val(BrightAuthor.meta.udpDestinationPort.GetText()))
    Sign.udpAddressType$ = BrightAuthor.meta.udpDestinationAddressType.GetText()
    if Sign.udpAddressType$ = "" then Sign.udpAddressType$ = "IPAddress"
    Sign.udpAddress$ = BrightAuthor.meta.udpDestinationAddress.GetText()

	Sign.enableEnhancedSynchronization = false
	Sign.deviceIsSyncMaster = false
	Sign.ptpDomain$ = 0
    enableEnhancedSynchronization = BrightAuthor.meta.enableEnhancedSynchronization
	if type(enableEnhancedSynchronization) = "roXMLList" and enableEnhancedSynchronization.Count() = 1 then
		Sign.enableEnhancedSynchronization = true
		enableEnhancedSynchronizationAttrs = enableEnhancedSynchronization.GetAttributes()
		deviceIsSyncMasterStr = enableEnhancedSynchronizationAttrs["deviceIsSyncMaster"]
		if lcase(deviceIsSyncMasterStr) = "true" then
			Sign.deviceIsSyncMaster = true
			targetSyncMasterInRegistry = "1"
		else
			targetSyncMasterInRegistry = "0"
		endif
		Sign.ptpDomain$ = enableEnhancedSynchronizationAttrs["ptpDomain"]

		rebootRequired = false
		' check the sync master value in the registry. if it does not exist or is different, set it and reboot.

		' check the domain value in the registry. if it does not exist or is different, set it and reboot.
	    ptpDomainInRegistry$ = bsp.registrySection.Read("ptp_domain")
		if ptpDomainInRegistry$ <> Sign.ptpDomain$ then
			bsp.registrySection.Write("ptp_domain", Sign.ptpDomain$)
			bsp.diagnostics.PrintDebug("@@@ PTP domain value written to registry:" + Sign.ptpDomain$)
			rebootRequired = true
		endif
		
		' check the syncMaster value in the registry. if it does not exist or is different, set it and reboot.
	    syncMasterInRegistry$ = bsp.registrySection.Read("sync_master")
		if syncMasterInRegistry$ <> targetSyncMasterInRegistry then
			bsp.registrySection.Write("sync_master", targetSyncMasterInRegistry)
			rebootRequired = true
		endif

		if rebootRequired then
			bsp.registrySection.Flush()
			RebootSystem()
		endif

	endif

    Sign.flipCoordinates = false
    flipCoordinates$ = BrightAuthor.meta.flipCoordinates.GetText()
    if flipCoordinates$ = "true" then Sign.flipCoordinates = true
    
    Sign.touchCursorDisplayMode$ = BrightAuthor.meta.touchCursorDisplayMode.GetText()

	if IsControlPort(controlPort) then
		Sign.gpio0Config = BrightAuthor.meta.gpio0.GetText()
		if Sign.gpio0Config = "input" then
			controlPort.EnableInput(0)
		else
			controlPort.EnableOutput(0)
		endif
    
		Sign.gpio1Config = BrightAuthor.meta.gpio1.GetText()
		if Sign.gpio1Config = "input" then
			controlPort.EnableInput(1)
		else
			controlPort.EnableOutput(1)
		endif
    
		Sign.gpio2Config = BrightAuthor.meta.gpio2.GetText()
		if Sign.gpio2Config = "input" then
			controlPort.EnableInput(2)
		else
			controlPort.EnableOutput(2)
		endif
    
		Sign.gpio3Config = BrightAuthor.meta.gpio3.GetText()
		if Sign.gpio3Config = "input" then
			controlPort.EnableInput(3)
		else
			controlPort.EnableOutput(3)
		endif
    
		Sign.gpio4Config = BrightAuthor.meta.gpio4.GetText()
		if Sign.gpio4Config = "input" then
			controlPort.EnableInput(4)
		else
			controlPort.EnableOutput(4)
		endif
    
		Sign.gpio5Config = BrightAuthor.meta.gpio5.GetText()
		if Sign.gpio5Config = "input" then
			controlPort.EnableInput(5)
		else
			controlPort.EnableOutput(5)
		endif
    
		Sign.gpio6Config = BrightAuthor.meta.gpio6.GetText()
		if Sign.gpio6Config = "input" then
			controlPort.EnableInput(6)
		else
			controlPort.EnableOutput(6)
		endif
    
		Sign.gpio7Config = BrightAuthor.meta.gpio7.GetText()
		if Sign.gpio7Config = "input" then
			controlPort.EnableInput(7)
		else
			controlPort.EnableOutput(7)
		endif
	endif
            
    audioInSampleRate$ = BrightAuthor.meta.audioInSampleRate.GetText()
    if audioInSampleRate$ <> "" then
		Sign.audioInSampleRate% = int(val(audioInSampleRate$))
    else
		Sign.audioInSampleRate% = 48000
    endif
    
	audioConfiguration$ = BrightAuthor.meta.audioConfiguration.GetText()
	if audioConfiguration$ = "" then
		audioConfiguration$ = "FixedAudio"
	endif
	Sign.audioConfiguration$ = audioConfiguration$

	audioAutoLevel$ = lcase(BrightAuthor.meta.audioAutoLevel.GetText())
	if audioAutoLevel$ = "true" then
		Sign.audioAutoLevel = true
	else
		Sign.audioAutoLevel = false
	endif



    audio1MinVolume$ = BrightAuthor.meta.audio1MinVolume.GetText()
    if audio1MinVolume$ <> "" then
		Sign.audio1MinVolume% = int(val(audio1MinVolume$))
	else
		Sign.audio1MinVolume% = 0
	endif
	    
    audio1MaxVolume$ = BrightAuthor.meta.audio1MaxVolume.GetText()
    if audio1MaxVolume$ <> "" then
		Sign.audio1MaxVolume% = int(val(audio1MaxVolume$))
	else
		Sign.audio1MaxVolume% = 100
	endif
	    
    audio2MinVolume$ = BrightAuthor.meta.audio2MinVolume.GetText()
    if audio2MinVolume$ <> "" then
		Sign.audio2MinVolume% = int(val(audio2MinVolume$))
	else
		Sign.audio2MinVolume% = 0
	endif
	    
    audio2MaxVolume$ = BrightAuthor.meta.audio2MaxVolume.GetText()
    if audio2MaxVolume$ <> "" then
		Sign.audio2MaxVolume% = int(val(audio2MaxVolume$))
	else
		Sign.audio2MaxVolume% = 100
	endif
	    
    audio3MinVolume$ = BrightAuthor.meta.audio3MinVolume.GetText()
    if audio3MinVolume$ <> "" then
		Sign.audio3MinVolume% = int(val(audio3MinVolume$))
	else
		Sign.audio3MinVolume% = 0
	endif
	    
    audio3MaxVolume$ = BrightAuthor.meta.audio3MaxVolume.GetText()
    if audio3MaxVolume$ <> "" then
		Sign.audio3MaxVolume% = int(val(audio3MaxVolume$))
	else
		Sign.audio3MaxVolume% = 100
	endif
	    
    usbAMinVolume$ = BrightAuthor.meta.usbAMinVolume.GetText()
    if usbAMinVolume$ <> "" then
		Sign.usbAMinVolume% = int(val(usbAMinVolume$))
	else
		Sign.usbAMinVolume% = 0
	endif
	    
    usbAMaxVolume$ = BrightAuthor.meta.usbAMaxVolume.GetText()
    if usbAMaxVolume$ <> "" then
		Sign.usbAMaxVolume% = int(val(usbAMaxVolume$))
	else
		Sign.usbAMaxVolume% = 100
	endif
	    
    usbBMinVolume$ = BrightAuthor.meta.usbBMinVolume.GetText()
    if usbBMinVolume$ <> "" then
		Sign.usbBMinVolume% = int(val(usbBMinVolume$))
	else
		Sign.usbBMinVolume% = 0
	endif
	    
    usbBMaxVolume$ = BrightAuthor.meta.usbBMaxVolume.GetText()
    if usbBMaxVolume$ <> "" then
		Sign.usbBMaxVolume% = int(val(usbBMaxVolume$))
	else
		Sign.usbBMaxVolume% = 100
	endif
	    
    usbCMinVolume$ = BrightAuthor.meta.usbCMinVolume.GetText()
    if usbCMinVolume$ <> "" then
		Sign.usbCMinVolume% = int(val(usbCMinVolume$))
	else
		Sign.usbCMinVolume% = 0
	endif
	    
    usbCMaxVolume$ = BrightAuthor.meta.usbCMaxVolume.GetText()
    if usbCMaxVolume$ <> "" then
		Sign.usbCMaxVolume% = int(val(usbCMaxVolume$))
	else
		Sign.usbCMaxVolume% = 100
	endif
	    
    usbDMinVolume$ = BrightAuthor.meta.usbDMinVolume.GetText()
    if usbDMinVolume$ <> "" then
		Sign.usbDMinVolume% = int(val(usbDMinVolume$))
	else
		Sign.usbDMinVolume% = 0
	endif
	    
    usbDMaxVolume$ = BrightAuthor.meta.usbDMaxVolume.GetText()
    if usbDMaxVolume$ <> "" then
		Sign.usbDMaxVolume% = int(val(usbDMaxVolume$))
	else
		Sign.usbDMaxVolume% = 100
	endif
	    
    usbA1MinVolume$ = BrightAuthor.meta.usb700_1MinVolume.GetText()
    if usbA1MinVolume$ <> "" then
		Sign.usbA1MinVolume% = int(val(usbA1MinVolume$))
	else
		Sign.usbA1MinVolume% = 0
	endif
	    
    usbA1MaxVolume$ = BrightAuthor.meta.usb700_1MaxVolume.GetText()
    if usbA1MaxVolume$ <> "" then
		Sign.usbA1MaxVolume% = int(val(usbA1MaxVolume$))
	else
		Sign.usbA1MaxVolume% = 100
	endif
	    
    usbA2MinVolume$ = BrightAuthor.meta.usb700_2MinVolume.GetText()
    if usbA2MinVolume$ <> "" then
		Sign.usbA2MinVolume% = int(val(usbA2MinVolume$))
	else
		Sign.usbA2MinVolume% = 0
	endif
	    
    usbA2MaxVolume$ = BrightAuthor.meta.usb700_2MaxVolume.GetText()
    if usbA2MaxVolume$ <> "" then
		Sign.usbA2MaxVolume% = int(val(usbA2MaxVolume$))
	else
		Sign.usbA2MaxVolume% = 100
	endif

    usbA3MinVolume$ = BrightAuthor.meta.usb700_3MinVolume.GetText()
    if usbA3MinVolume$ <> "" then
		Sign.usbA3MinVolume% = int(val(usbA3MinVolume$))
	else
		Sign.usbA3MinVolume% = 0
	endif
	    
    usbA3MaxVolume$ = BrightAuthor.meta.usb700_3MaxVolume.GetText()
    if usbA3MaxVolume$ <> "" then
		Sign.usbA3MaxVolume% = int(val(usbA3MaxVolume$))
	else
		Sign.usbA3MaxVolume% = 100
	endif

    usbA4MinVolume$ = BrightAuthor.meta.usb700_4MinVolume.GetText()
    if usbA4MinVolume$ <> "" then
		Sign.usbA4MinVolume% = int(val(usbA4MinVolume$))
	else
		Sign.usbA4MinVolume% = 0
	endif
	    
    usbA4MaxVolume$ = BrightAuthor.meta.usb700_4MaxVolume.GetText()
    if usbA4MaxVolume$ <> "" then
		Sign.usbA4MaxVolume% = int(val(usbA4MaxVolume$))
	else
		Sign.usbA4MaxVolume% = 100
	endif

    usbA5MinVolume$ = BrightAuthor.meta.usb700_5MinVolume.GetText()
    if usbA5MinVolume$ <> "" then
		Sign.usbA5MinVolume% = int(val(usbA5MinVolume$))
	else
		Sign.usbA5MinVolume% = 0
	endif
	    
    usbA5MaxVolume$ = BrightAuthor.meta.usb700_5MaxVolume.GetText()
    if usbA5MaxVolume$ <> "" then
		Sign.usbA5MaxVolume% = int(val(usbA5MaxVolume$))
	else
		Sign.usbA5MaxVolume% = 100
	endif

    usbA6MinVolume$ = BrightAuthor.meta.usb700_6MinVolume.GetText()
    if usbA6MinVolume$ <> "" then
		Sign.usbA6MinVolume% = int(val(usbA6MinVolume$))
	else
		Sign.usbA6MinVolume% = 0
	endif
	    
    usbA6MaxVolume$ = BrightAuthor.meta.usb700_6MaxVolume.GetText()
    if usbA6MaxVolume$ <> "" then
		Sign.usbA6MaxVolume% = int(val(usbA6MaxVolume$))
	else
		Sign.usbA6MaxVolume% = 100
	endif

    usbA7MinVolume$ = BrightAuthor.meta.usb700_7MinVolume.GetText()
    if usbA7MinVolume$ <> "" then
		Sign.usbA7MinVolume% = int(val(usbA7MinVolume$))
	else
		Sign.usbA7MinVolume% = 0
	endif
	    
    usbA7MaxVolume$ = BrightAuthor.meta.usb700_7MaxVolume.GetText()
    if usbA7MaxVolume$ <> "" then
		Sign.usbA7MaxVolume% = int(val(usbA7MaxVolume$))
	else
		Sign.usbA7MaxVolume% = 100
	endif

    hdmiMinVolume$ = BrightAuthor.meta.hdmiMinVolume.GetText()
    if hdmiMinVolume$ <> "" then
		Sign.hdmiMinVolume% = int(val(hdmiMinVolume$))
	else
		Sign.hdmiMinVolume% = 0
	endif
	    
    hdmiMaxVolume$ = BrightAuthor.meta.hdmiMaxVolume.GetText()
    if hdmiMaxVolume$ <> "" then
		Sign.hdmiMaxVolume% = int(val(hdmiMaxVolume$))
	else
		Sign.hdmiMaxVolume% = 100
	endif
	
    spdifMinVolume$ = BrightAuthor.meta.spdifMinVolume.GetText()
    if spdifMinVolume$ <> "" then
		Sign.spdifMinVolume% = int(val(spdifMinVolume$))
	else
		Sign.spdifMinVolume% = 0
	endif
	    
    spdifMaxVolume$ = BrightAuthor.meta.spdifMaxVolume.GetText()
    if spdifMaxVolume$ <> "" then
		Sign.spdifMaxVolume% = int(val(spdifMaxVolume$))
	else
		Sign.spdifMaxVolume% = 100
	endif
	
	mediaListInactivityTimeoutIsGlobal$ = lcase(BrightAuthor.meta.mediaListInactivityTimeoutIsGlobal.GetText())
	if mediaListInactivityTimeoutIsGlobal$ = "false" then
		bsp.mediaListInactivityTimeoutIsGlobal = false
	else
		bsp.mediaListInactivityTimeoutIsGlobal = true
	endif

	inactivityTimeout$ = lcase(BrightAuthor.meta.inactivityTimeout.GetText())
	if inactivityTimeout$ = "true" then
		bsp.inactivityTimeout = true
	else
		bsp.inactivityTimeout = false
	endif

	inactivityTime$ = BrightAuthor.meta.inactivityTime.GetText()
	if len(inactivityTime$) > 0 then
		bsp.inactivityTime% = int(val(inactivityTime$))
	else
		bsp.inactivityTime% = 0
	endif

' RF Channel Data
	rfChannelDataFile = BrightAuthor.meta.rfChannelDataFile.GetText()
	if IsString(rfChannelDataFile) and rfChannelDataFile <> "" then
		rfChannelDataFilePath$ = GetPoolFilePath(bsp.assetPoolFiles, rfChannelDataFile)
		if rfChannelDataFilePath$ <> "" then
			tunerData$ = ReadAsciiFile(rfChannelDataFilePath$)
			if tunerData$ <> "" then
				cdataStartTag$ = "<![CDATA["
				cdataEndTag$ = " ]]>"
				docEndTag$ = "</BrightSignRFChannels>"
				cdataIndex% = instr(1, tunerData$, cdataStartTag$)
				if cdataIndex% > 0 then
					fwXML$ = mid(tunerData$, cdataIndex% + len(cdataStartTag$) + 1, len(tunerData$) - (cdataIndex% + len(cdataStartTag$) + 1) - (len(cdataEndTag$) + len(docEndTag$) + 1))
					channelManager = CreateObject("roChannelManager")
					if type(channelManager) = "roChannelManager" then
						channelManager.ClearChannelData()
						ok = channelManager.ImportFromXml(fwXML$)
						if ok then
							bsp.scannedChannels = GetScannedChannels()
						endif
					endif
				endif
			endif
		endif
	endif

	' enable zone support here to ensure that SetGraphicsZOrder works
    EnableZoneSupport(true)

	graphicsZOrder = BrightAuthor.meta.graphicsZOrder.GetText()
	if graphicsZOrder <> "" then
		vm=CreateObject("roVideoMode")
		vm.SetGraphicsZOrder(lcase(graphicsZOrder))
		vm = invalid
	endif
	    
	' mosaic mode / decoders
	videoMode = CreateObject("roVideoMode")
	Sign.isMosaic = false
	isMosaic = BrightAuthor.meta.isMosaic.GetText()
	if lcase(isMosaic) = "true" then
		Sign.isMosaic = true
		mosaicDecoders = BrightAuthor.meta.mosaicDecoders.mosaicDecoder
		for each mosaicDecoder in mosaicDecoders
			decoderName = mosaicDecoder.decoderName.GetText()
			timeSliceMode = mosaicDecoder.timeSliceMode.GetText()
			zOrder = mosaicDecoder.zOrder.GetText()
			friendlyName = mosaicDecoder.friendlyName.GetText()
			enableMosaicDeinterlacer$ = mosaicDecoder.enableMosaicDeinterlacer.GetText()
			enableMosaicDeinterlacer = false
			if lcase(enableMosaicDeinterlacer$) = "true" then
				enableMosaicDeinterlacer = true
			endif

			ok = videoMode.SetDecoderMode(decoderName, timeSliceMode, int(val(zOrder)), friendlyName, enableMosaicDeinterlacer)
		next
	else if bsp.mosaicModeSupported then
		decoders=videoMode.GetDecoderModes()
		for each decoder in decoders
			videoMode.SetDecoderMode(decoders[0].decoder_name, decoders[0].max_decode_size, 0, decoders[0].decoder_name, false)
		next
	endif

	videoMode = invalid

	' get zones

    zoneList = BrightAuthor.zones.zone
    if type(zoneList) <> "roXMLList" then print "Invalid XML file - zone list not found" : stop
    numZones% = zoneList.Count()

    Sign.zonesHSM = CreateObject("roArray", numZones%, true)
    Sign.videoZoneHSM = invalid
    
    for each zone in zoneList

        bsZoneHSM = newZoneHSM(bsp, msgPort, Sign, zone, globalVariables)
        Sign.zonesHSM.push(bsZoneHSM)

        if (bsZoneHSM.type$ = "VideoOrImages" or bsZoneHSM.type$ = "VideoOnly") or Sign.videoZoneHSM = invalid then 
            Sign.videoZoneHSM = bsZoneHSM
        endif
                    
    next
         
    ' audioOutput, audioMode and audioMapping were set incorrectly on earlier versions of presentations
'    if version% < 4 then
'        for each bsZone in Sign.zones
'            if bsZone.type$ = "VideoOrImages" or bsZone.type$ = "VideoOnly" then
'                bsZone.audioOutput% = 4
'                bsZone.audioMode% = 0
'                bsZone.audioMapping% = 0
'            endif
'        next
'    endif
    
    return Sign
    
End Function

'endregion

' Support Video Mode Plugin to set videoMode for single and multi screens
Function parseVideoModePlugin(videoModeInputs As Object, bsp As Object, singleScreenMode As String, multiScreenModes As Object) As Object

	ERR_NORMAL_END = &hFC
	ERR_NO_VALUE_RETURN = 224    ' Value returned when plugin does not contain specified function

	for each videoModePlugin in bsp.videoModePlugins

	  	setVideoModeFunction$ = "result = " + videoModePlugin.functionName$ + "(videoModeInputs, bsp)"

	  	retVal = Eval(setVideoModeFunction$)

	  	if type(retVal) = "roList" then		' compilation error
    		bsp.diagnostics.PrintDebug("Compilation error invoking Eval to parse VideoMode script plugin: return value = " + stri(retVal))
    		bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_SCRIPT_PLUGIN_FAILURE, stri(retVal))
	  	else if retVal <> ERR_NORMAL_END then	' runtime error (function may not exist)
			' log the failure
    		bsp.diagnostics.PrintDebug("Failure executing Eval to execute videoMode plugin: return value = " + stri(retVal) + ", call was " + setVideoModeFunction$)
    		bsp.logging.WriteDiagnosticLogEntry(bsp.diagnosticCodes.EVENT_SCRIPT_PLUGIN_FAILURE, stri(retVal) + chr(9) + videoModePlugin.fileName$)
	  	else
			videoModeFromPlugin = result
			' if videoMode is a string and is not empty, overwrite previously calculated screen mode
			if isString(videoModeFromPlugin) then
				if len(videoModeFromPlugin) > 0 then
					singleScreenMode = videoModeFromPlugin
				endif
			' else if videoMode is a array and is not empty, overwrite previously calculated multi screen modes
			else if type(videoModeFromPlugin) = "roArray" then
				if videoModeFromPlugin.count() > 0 then
					multiScreenModes = videoModeFromPlugin
				endif
			endif
	 	endif
	next

	if singleScreenMode <> "" then
		return singleScreenMode
	endif
	return multiScreenModes

End Function


'region User Variable DB
Sub ResetUserVariable(postMsg As Boolean)

	m.currentValue$ = m.defaultValue$

	m.bsp.UpdateDBVariable(m.bsp.GetCategoryIdFromAccess(m.access$), m.name$, m.currentValue$)

	if postMsg then
		userVariableChanged = CreateObject("roAssociativeArray")
		userVariableChanged["EventType"] = "USER_VARIABLE_CHANGE"
		userVariableChanged["UserVariable"] = m
		m.bsp.msgPort.PostMessage(userVariableChanged)
	endif

	m.bsp.SendUDPNotification("refresh")

End Sub


Sub SetCurrentUserVariableValue(value As Object, postMsg As Boolean)

	if IsString(value) then
		value$ = value
	else
		' only convert integers currently
		value$ = stri(value)
	endif

	m.currentValue$ = value$

	m.bsp.UpdateDBVariable(m.bsp.GetCategoryIdFromAccess(m.access$), m.name$, m.currentValue$)

	if postMsg then
		userVariableChanged = CreateObject("roAssociativeArray")
		userVariableChanged["EventType"] = "USER_VARIABLE_CHANGE"
		userVariableChanged["UserVariable"] = m
		m.bsp.msgPort.PostMessage(userVariableChanged)

		m.bsp.SendUDPNotification("refresh")

	endif

End Sub


Function GetCurrentUserVariableValue() As Object

	return m.currentValue$

End Function


Sub IncrementUserVariable()

	currentValue% = int(val(m.currentValue$))
	currentValue% = currentValue% + 1
	m.currentValue$ = StripLeadingSpaces(stri(currentValue%))

	m.bsp.UpdateDBVariable(m.bsp.GetCategoryIdFromAccess(m.access$), m.name$, m.currentValue$)

	m.bsp.SendUDPNotification("refresh")

End Sub


Function newUserVariable(bsp As Object, name$ As String, currentValue$ As String, defaultValue$ As String, mediaUrl$ As String, access$ As String) As Object

	userVariable = CreateObject("roAssociativeArray")
	userVariable.GetCurrentValue = GetCurrentUserVariableValue
	userVariable.SetCurrentValue = SetCurrentUserVariableValue
	userVariable.Increment = IncrementUserVariable
	userVariable.Reset = ResetUserVariable

	userVariable.bsp = bsp
	userVariable.name$ = name$
	userVariable.currentValue$ = currentValue$
	userVariable.defaultValue$ = defaultValue$
	userVariable.mediaUrl$ = mediaUrl$
	userVariable.access$ = access$
	userVariable.liveDataFeed = invalid

	return userVariable

End Function


Function GetCategoryIdFromAccess(access$ As String) As Integer

	if lcase(access$) = "shared" then
		categoryId% = m.sharedBrightAuthorCategoryId%
	else
		categoryId% = m.privateBrightAuthorCategoryId%
	endif

	return categoryId%

End Function


Sub UpdateDBVariable(categoryId% As Integer, name$ As String, currentValue$ As String)

    params = { cv_param: currentValue$, vn_param: name$, cri_param:  categoryId% }

    m.userVariablesDB.RunBackground("UPDATE Variables2 SET CurrentValue=:cv_param WHERE VariableName=:vn_param AND CategoryReferenceId=:cri_param;", params)

End Sub


Sub UpdateDBVariableDefaultValue(categoryId% As Integer, name$ As String, defaultValue$ As String)

    params = { dv_param: defaultValue$, vn_param: name$, cri_param:  categoryId% }

    m.userVariablesDB.RunBackground("UPDATE Variables2 SET DefaultValue=:dv_param WHERE VariableName=:vn_param AND CategoryReferenceId=:cri_param;", params)

End Sub


Sub UpdateDBVariableMediaUrl(categoryId% As Integer, name$ As String, mediaUrl$ As String)

    params = { mu_param: mediaUrl$, vn_param: name$, cri_param:  categoryId% }

    m.userVariablesDB.RunBackground("UPDATE Variables2 SET MediaUrl=:mu_param WHERE VariableName=:vn_param AND CategoryReferenceId=:cri_param;", params)

End Sub


Sub UpdateDBVariablePosition(categoryId% As Integer, name$ As String, position% As Integer)

    params = { p_param: position%, vn_param: name$, cri_param:  categoryId% }

    m.userVariablesDB.RunBackground("UPDATE Variables2 SET Position=:p_param WHERE VariableName=:vn_param AND CategoryReferenceId=:cri_param;", params)

End Sub


Sub AddDBVariable(categoryId% As Integer, name$ As String, defaultValue$ As String, mediaUrl$ As String, position% As Integer)
	
	insertSQL$ = "INSERT INTO Variables2 (CategoryReferenceId, VariableName, CurrentValue, DefaultValue, MediaUrl, Position) VALUES(?,?,?,?,?,?);"

	params = CreateObject("roArray", 6, false)
	params[ 0 ] = categoryId%
	params[ 1 ] = name$
	params[ 2 ] = defaultValue$
	params[ 3 ] = defaultValue$
	params[ 4 ] = mediaUrl$
	params[ 5 ] = position%

	m.ExecuteDBInsert(insertSQL$, params)

End Sub


Sub AddDBSection(sectionName$ As String)

	insertSQL$ = "INSERT INTO Sections (SectionName) VALUES(:name_param);"

	params = { name_param: sectionName$ }

	m.ExecuteDBInsert(insertSQL$, params)

End Sub


Sub DeleteDBVariable(categoryId% As Integer, variableName$ As String)

	SQLITE_COMPLETE = 100

    params = { :uv_param: variableName$ }

	delete$ = "DELETE FROM Variables2 WHERE VariableName =:uv_param AND CategoryReferenceId = " + StripLeadingSpaces(stri(categoryId%)) + ";"
	
	deleteStatement = m.userVariablesDB.CreateStatement(delete$)

	if type(deleteStatement) <> "roSqliteStatement" then
        m.diagnostics.PrintDebug("DeleteStatement failure - " + delete$)
		stop
	endif

	bindResult = deleteStatement.BindByName(params)

	if not bindResult then
        m.diagnostics.PrintDebug("Bind failure")
		stop
	endif

	sqlResult = deleteStatement.Run()

	if sqlResult <> SQLITE_COMPLETE
        m.diagnostics.PrintDebug("sqlResult <> SQLITE_COMPLETE")
	endif

	deleteStatement.Finalise()

End Sub


Sub AddDBCategory(sectionId% As Integer, categoryName$ As String)

	insertSQL$ = "INSERT INTO Categories (SectionReferenceId, CategoryName) VALUES(?,?);"

	params = CreateObject("roArray", 2, false)
	params[ 0 ] = sectionId%
	params[ 1 ] = categoryName$

	m.ExecuteDBInsert(insertSQL$, params)

End Sub


Sub ExecuteDBInsert(insert$ As String, params As Object)
	
	SQLITE_COMPLETE = 100

	insertStatement = m.userVariablesDB.CreateStatement(insert$)

	if type(insertStatement) <> "roSqliteStatement" then
        m.diagnostics.PrintDebug("CreateStatement failure - " + insert$)
		stop
	endif

	if type(params) = "roArray" then
		bindResult = insertStatement.BindByOffset(params)
	else
		bindResult = insertStatement.BindByName(params)
	endif

	if not bindResult then
        m.diagnostics.PrintDebug("Bind failure")
		stop
	endif

	sqlResult = insertStatement.Run()

	if sqlResult <> SQLITE_COMPLETE
        m.diagnostics.PrintDebug("sqlResult <> SQLITE_COMPLETE")
	endif

	insertStatement.Finalise()

End Sub


Sub ExecuteDBSelect(select$ As String, resultsCallback As Object, selectData As Object, params As Object)

	SQLITE_ROWS = 102

	selectStmt = m.userVariablesDB.CreateStatement(select$)

	if type(selectStmt) <> "roSqliteStatement" then
        m.diagnostics.PrintDebug("CreateStatement failure - " + select$)
		stop
	endif

	bindResult = true
	if type(params) = "roArray" then
		bindResult = selectStmt.BindByOffset(params)
	else if type(params) = "roAssociativeArray" then
		bindResult = selectStmt.BindByName(params)
	endif

	if not bindResult then
        m.diagnostics.PrintDebug("Bind failure")
		stop
	endif

	sqlResult = selectStmt.Run()

	while sqlResult = SQLITE_ROWS

		resultsData = selectStmt.GetData()
	
		resultsCallback(resultsData, selectData)

		sqlResult = selectStmt.Run() 
		   
	end while

	selectStmt.Finalise()

End Sub


Sub GetDBVersionCallback(resultsData As Object, selectData As Object)

	selectData.version$ = resultsData["Version"]

End Sub


Function GetDBVersion() As String

	selectData = {}
	selectData.version$ = ""

	select$ = "SELECT SchemaVersion.Version FROM SchemaVersion;"
	m.ExecuteDBSelect(select$, GetDBVersionCallback, selectData, invalid)

	return selectData.version$

End Function


Sub GetDBTableNamesCallback(resultsData As Object, selectData As Object)

	selectData.tableNames.AddReplace(resultsData["name"], true)

End Sub


Function GetDBTableNames() As Object

	selectData = {}
	selectData.tableNames = {}

	select$ = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
	m.ExecuteDBSelect(select$, GetDBTableNamesCallback, selectData, invalid)

	return selectData.tableNames

End Function


Sub GetDBCategoryIdCallback(resultsData As Object, selectData As Object)

	selectData.categoryId% = resultsData["CategoryId"]

End Sub


Function GetDBCategoryId(sectionId% As Integer, categoryName$ As String) As Object

	selectData = {}
	selectData.categoryId% = -1

	select$ = "SELECT CategoryId FROM Categories WHERE SectionReferenceId = " + StripLeadingSpaces(stri(sectionId%)) + " AND CategoryName='" + categoryName$ + "';"
	m.ExecuteDBSelect(select$, GetDBCategoryIdCallback, selectData, invalid)

	return selectData.categoryId%

End Function


Sub GetDBSectionNamesCallback(resultsData As Object, selectData As Object)

	selectData.sections.push(resultsData["SectionName"])

End Sub


Function GetDBSectionNames() As Object

	selectData = {}
	selectData.sections = []

	select$ = "SELECT Sections.SectionName FROM Sections;"
	m.ExecuteDBSelect(select$, GetDBSectionNamesCallback, selectData, invalid)

	return selectData.sections

End Function


Sub GetDBSectionIdCallback(resultsData As Object, selectData As Object)

	selectData.sectionId% = resultsData["SectionId"]

End Sub


Function GetDBSectionId(sectionName$ As String) As Object

	selectData = {}
	selectData.sectionId% = -1

    params = { sn_param: sectionName$ }
	select$ = "SELECT SectionId FROM Sections WHERE SectionName =:sn_param;"

	m.ExecuteDBSelect(select$, GetDBSectionIdCallback, selectData, params)

	return selectData.sectionId%

End Function


Sub ReadSchema1TablesCallback(resultsData As Object, selectData As Object)

	sectionName$ = resultsData["SectionName"]
	if selectData.userVariableSets.DoesExist(sectionName$) then
		userVariables = selectData.userVariableSets.Lookup(sectionName$)
	else
		userVariables = []
		selectData.userVariableSets.AddReplace(sectionName$, userVariables)
	endif

	variableName$ = resultsData["VariableName"]
	currentValue$ = resultsData["CurrentValue"]
	defaultValue$ = resultsData["DefaultValue"]

	userVariable = newUserVariable(m, variableName$, currentValue$, defaultValue$, "", "")
	userVariables.push(userVariable)

End Sub


Function ReadSchema1Tables() As Object

	selectData = {}
	selectData.userVariableSets = {}

	select$ = "SELECT Sections.SectionName, Variables.VariableName, Variables.CurrentValue, Variables.DefaultValue FROM Variables INNER JOIN Sections ON Sections.SectionId = Variables.SectionReferenceId ORDER BY Sections.SectionName, Variables.VariableId;"
	m.ExecuteDBSelect(select$, ReadSchema1TablesCallback, selectData, invalid)

	return selectData.userVariableSets

End Function


Sub GetUserVariablesGivenCategoryCallback(resultsData As Object, selectData As Object)

	variableName$ = resultsData["VariableName"]
	currentValue$ = resultsData["CurrentValue"]
	defaultValue$ = resultsData["DefaultValue"]
	mediaUrl$ = resultsData["MediaUrl"]

	' access level isn't needed for this list
	userVariable = newUserVariable(m, variableName$, currentValue$, defaultValue$, mediaUrl$, "") 
	selectData.userVariablesList.push(userVariable)

End Sub


Function GetUserVariablesGivenCategory(sectionName$ As String, includeSharedSection As Boolean, categoryName$ As String, sortByPosition As Boolean) As Object

	selectData = {}
	selectData.userVariablesList = []

	select$ = "SELECT Variables2.* FROM Variables2 INNER JOIN Sections ON Sections.SectionId=Categories.SectionReferenceId INNER JOIN Categories ON Categories.CategoryId=Variables2.CategoryReferenceId WHERE Categories.CategoryName='" + categoryName$ + "' AND ("
	
	if includeSharedSection then
		select$ = select$ + "Sections.SectionName='Shared' OR "
	endif

	select$ = select$ + "Sections.SectionName='" + sectionName$ + "')"
		
	if sortByPosition then
		select$ = select$ + " ORDER BY Variables2.Position"
	endif
	select$ = select$ + ";"
	
	m.ExecuteDBSelect(select$, GetUserVariablesGivenCategoryCallback, selectData, invalid)

	return selectData.userVariablesList

End Function


Sub DoGetCategoriesCallback(resultsData As Object, selectData As Object)

	categoryName$ = resultsData["CategoryName"]
	if (categoryName$ <> "BrightAuthor") or (selectData.includeBrightAuthorCategory) then
		selectData.userVariableCategories.push(categoryName$)
	endif

End Sub


Function DoGetCategories(sectionName$ As String, includeShared As Boolean, includeBrightAuthorCategory) As Object

	selectData = {}
	selectData.userVariableCategories = []
	selectData.includeBrightAuthorCategory = includeBrightAuthorCategory

    params = { :sn_param: sectionName$ }

	select$ = "SELECT CategoryName FROM Categories INNER JOIN Sections ON Sections.SectionId = Categories.SectionReferenceId WHERE "
	if includeShared then
		select$ = select$ + "Sections.SectionName = 'Shared' OR "
	endif

	select$ = select$ + "Sections.SectionName =:sn_param;"

	selectStmt = m.userVariablesDB.CreateStatement(select$)

	if type(selectStmt) <> "roSqliteStatement" then
		m.diagnostics.PrintDebug("CreateStatement failure - " + select$)
		stop
	endif
	
	bindResult = selectStmt.BindByName(params)

	if not bindResult then
        m.diagnostics.PrintDebug("Bind failure")
		stop
	endif

	m.ExecuteDBSelect(select$, DoGetCategoriesCallback, selectData, params)

	return selectData.userVariableCategories

End Function


Sub ReadVariablesCallback(resultsData As Object, selectData As Object)

	sectionName$ = resultsData["SectionName"]

	if sectionName$ = selectData.presentationName$ or sectionName$ = "Shared" then
			
		variableName$ = resultsData["VariableName"]
		currentValue$ = resultsData["CurrentValue"]
		defaultValue$ = resultsData["DefaultValue"]
		mediaUrl$ = resultsData["MediaUrl"]

		if sectionName$ = "Shared" then
			access$ = "Shared"
		else
			access$ = "Private"
		endif

		userVariable = newUserVariable(selectData.bsp, variableName$, currentValue$, defaultValue$, mediaUrl$, access$)
		userVariable.position% = -1
		selectData.currentUserVariables.AddReplace(variableName$, userVariable)

	endif

End Sub


Function ReadVariables(presentationName$ As String) As Object

	selectData = {}
	selectData.bsp = m
	selectData.presentationName$ = presentationName$
	selectData.currentUserVariables = {}

	select$ = "SELECT Sections.SectionName, Variables2.VariableName, Variables2.CurrentValue, Variables2.DefaultValue, Variables2.MediaUrl FROM Variables2 INNER JOIN Sections ON Sections.SectionId=Categories.SectionReferenceId INNER JOIN Categories ON Categories.CategoryId=Variables2.CategoryReferenceId WHERE Categories.CategoryName='BrightAuthor' ORDER BY Sections.SectionName;"

	m.ExecuteDBSelect(select$, ReadVariablesCallback, selectData, invalid)

	return selectData.currentUserVariables

End Function


Sub SetDBVersion(version$ As String)

	insertSQL$ = "INSERT INTO SchemaVersion (Version) VALUES(:version_param);"

	params = { version_param: version$ }

	m.ExecuteDBInsert(insertSQL$, params)

End Sub


Sub UpdateDBVersion(version$ As String)

    params = { v_param: version$ }

    m.userVariablesDB.RunBackground("UPDATE SchemaVersion SET Version=:v_param;", params)

End Sub


Sub CreateDBTable(statement$ As String)

	SQLITE_COMPLETE = 100

	createStmt = m.userVariablesDB.CreateStatement(statement$)

	if type(createStmt) <> "roSqliteStatement" then
        m.diagnostics.PrintDebug("CreateStatement failure - " + statement$)
		stop
	endif

	sqlResult = createStmt.Run()

	if sqlResult <> SQLITE_COMPLETE
        m.diagnostics.PrintDebug("sqlResult <> SQLITE_COMPLETE")
	endif

	createStmt.Finalise()

End Sub


Sub DeleteDBTable(tableName$ As String)

	SQLITE_COMPLETE = 100

	deleteStmt = m.userVariablesDB.CreateStatement("DROP TABLE " + tableName$)

	if type(deleteStmt) <> "roSqliteStatement" then
        m.diagnostics.PrintDebug("CreateStatement failure - DeleteDBTable")
		stop
	endif

	sqlResult = deleteStmt.Run()

	if sqlResult <> SQLITE_COMPLETE
        m.diagnostics.PrintDebug("sqlResult <> SQLITE_COMPLETE")
	endif

	deleteStmt.Finalise()

End Sub


Sub CreateSchema2Tables()

	m.CreateDBTable("CREATE TABLE Categories (CategoryId INTEGER PRIMARY KEY AUTOINCREMENT, SectionReferenceId INT, CategoryName TEXT);")

	m.CreateDBTable("CREATE TABLE Variables2 (VariableId INTEGER PRIMARY KEY AUTOINCREMENT, CategoryReferenceId INT, VariableName text, CurrentValue TEXT, DefaultValue TEXT, MediaUrl TEXT, Position INT);")

End Sub


Sub DropSchema1Tables()

	m.DeleteDBTable("Variables")

End Sub


Function DBTablesExist(existingTables, expectedTables) As Boolean

  for each tableName in expectedTables
    if not existingTables.DoesExist(tableName) then
      m.diagnostics.PrintDebug("Table " + tableName + " does not exist in userVariables.db - reset db")
      return false
    endif
  end for

  return true

End Function


Function DbIsValid() As Boolean

' Check for validity of userVariables DB - not an exhaustive check, it merely checks to ensure that the appropriate
' tables are present
'
' Check for existence of schema table. If it doesn't exist, db is invalid
' If it exists, check its value and base subsequent checks on the version
' Check to ensure that the expected tables exist

  tables = m.GetDBTableNames()

  ' common tables
  commonTables = ["SchemaVersion", "Sections"]
  schema1Tables = ["Variables"]
  schema2Tables = ["Variables2", "Categories"]

  if not m.DBTablesExist(tables, commonTables) then
    return false
  endif

  currentSchemaVersion$ = "2.0"
  existingSchemaVersion$ = m.GetDBVersion()

  ' check version of existing db schema to determine what tables to check for
  if existingSchemaVersion$ <> currentSchemaVersion$ then
    tablesExist = m.DBTablesExist(tables, schema1Tables)
  else
    tablesExist = m.DBTablesExist(tables, schema2Tables)
  endif

  return tablesExist

End Function


Sub ReadVariablesDB(presentationName$ As String)

	SQLITE_ROWS = 102

	m.variablesDBExists = true

	m.dbSchemaVersion$ = "2.0"

	if type(m.userVariablesDB) <> "roSqliteDatabase" then

		m.userVariablesDB = CreateObject("roSqliteDatabase")
		m.userVariablesDB.SetPort(m.msgPort)

        m.diagnostics.PrintDebug("Open userVariables.db")

		ok = m.userVariablesDB.Open("userVariables.db")

    if ok then
      ok = m.DBIsValid()
    endif

		if ok then

			version$ = m.GetDBVersion()

			if version$ <> m.dbSchemaVersion$ then

				userVariableSets = m.ReadSchema1Tables()

				m.CreateSchema2Tables()

				' store old data in new tables
				for each sectionName in userVariableSets
					sectionId% = m.GetDBSectionId(sectionName)
					m.AddDBCategory(sectionId%, "BrightAuthor")
					categoryId% = m.GetDBCategoryId(sectionId%, "BrightAuthor")
					userVariables = userVariableSets.Lookup(sectionName)
					position% = 0
					for each userVariable in userVariables
						m.AddDBVariable(categoryId%, userVariable.name$, userVariable.defaultValue$, "", position%)
						if userVariable.currentValue$ <> userVariable.defaultValue$ then
							m.UpdateDBVariable(categoryId%, userVariable.name$, userVariable.currentValue$)
						endif
						position% = position% + 1
					next
				next

				' drop old tables
				m.DropSchema1Tables()

				' update version
				m.UpdateDBVersion(m.dbSchemaVersion$)

			endif

		else

			' in case there is a corrupt file
	        m.diagnostics.PrintDebug("Unable to open userVariables.db, attempt to delete file then create db.")
		    m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_DELETE_USER_VARIABLES_DB, "")
			ok = DeleteFile("userVariables.db")

			ok = m.userVariablesDB.Create("userVariables.db")
			if not ok then
		        m.diagnostics.PrintDebug("Unable to create userVariables.db")
				return
			endif

			m.CreateDBTable("CREATE TABLE SchemaVersion (Version TEXT);")
			m.SetDBVersion(m.dbSchemaVersion$)

			m.CreateDBTable("CREATE TABLE Sections (SectionId INTEGER PRIMARY KEY AUTOINCREMENT, SectionName TEXT);")

			m.CreateSchema2Tables()
	
		endif

	endif

	' get sections, variables for BrightAuthor category
	m.currentUserVariables = m.ReadVariables(presentationName$)

End Sub


Function GetUserVariablesByCategoryList(categoryName$ As String) As Object
	return m.GetUserVariablesGivenCategory(m.activePresentation$, true, categoryName$, true)
End Function


Function GetDBCategoryNames(sectionName$ As String) As Object
	return m.DoGetCategories(sectionName$, false, true)
End Function


Function GetUserVariableCategoryList(sectionName$ As String) As Object
	return m.DoGetCategories(sectionName$, true, false)
End Function


Function GetOrderedVariables(sectionName$ As String) As Object
	return m.GetUserVariablesGivenCategory(m.activePresentation$, true, "BrightAuthor", false)
End Function


Sub ExportVariablesDBToAsciiFile(file As Object)

	file.SendLine("Version" + chr(9) + m.dbSchemaVersion$)
	sectionNames = m.GetDBSectionNames()
	for each sectionName in sectionNames
		file.SendLine("Section" + chr(9) + sectionName)
		sectionId% = m.GetDBSectionId(sectionName)
		if sectionId% > 0 then
			categoryNames = m.GetDBCategoryNames(sectionName)
			for each categoryName in categoryNames
				file.SendLine("Category" + chr(9) + categoryName)
				userVariablesList = m.GetUserVariablesGivenCategory(sectionName, false, categoryName, false)
				for each userVariable in userVariablesList
					file.SendLine(userVariable.name$ + chr(9) + userVariable.currentValue$ + chr(9) + userVariable.defaultValue$)
				next
			next
		endif
	next

End Sub


Function GetUserVariable(variableName$ As String) As Object

	userVariable = invalid

	if m.currentUserVariables.DoesExist(variableName$) then
		userVariable = m.currentUserVariables.Lookup(variableName$)
	endif

	return userVariable

End Function


Sub DeleteVariable(variableName$ As String)

	userVariables = m.currentUserVariables

	if userVariables.DoesExist(variableName$) then

		userVariable = userVariables.Lookup(variableName$)
		if lcase(userVariable.access$) = "shared" then
			categoryId% = m.sharedBrightAuthorCategoryId%
		else
			categoryId% = m.privateBrightAuthorCategoryId%
		endif

		m.DeleteDBVariable(categoryId%, variableName$)
		userVariables.Delete(variableName$)

	endif

	m.SendUDPNotification("refresh")

End Sub


Sub ResetVariables()

	userVariables = m.currentUserVariables

	userVariableList = CreateObject("roList")
	for each variableName in userVariables
		userVariable = userVariables.Lookup(variableName)
		userVariableList.AddTail(userVariable)
	next

	for each userVariable in userVariableList
		userVariable.Reset(false)
	next

	userVariablesReset = CreateObject("roAssociativeArray")
	userVariablesReset["EventType"] = "USER_VARIABLES_RESET"
	m.msgPort.PostMessage(userVariablesReset)

	m.SendUDPNotification("refresh")

End Sub


Sub AssignSystemVariableToUserVariables(userVariable As Object)

	if userVariable.systemVariable$ = "serialNumber" then
		userVariable.SetCurrentValue(m.sysInfo.deviceUniqueID$, false)
	else if userVariable.systemVariable$ = "ipAddressWired" then
		userVariable.SetCurrentValue(m.sysInfo.ipAddressWired$, false)
	else if userVariable.systemVariable$ = "ipAddressWireless" then
		userVariable.SetCurrentValue(m.sysInfo.ipAddressWireless$, false)
	else if userVariable.systemVariable$ = "firmwareVersion" then
		userVariable.SetCurrentValue(m.sysInfo.deviceFWVersion$, false)
	else if userVariable.systemVariable$ = "scriptVersion" then
		userVariable.SetCurrentValue(m.sysInfo.autorunVersion$, false)
	else if userVariable.systemVariable$ = "rfChannelCount" then
		userVariable.SetCurrentValue(StripLeadingSpaces(stri(m.scannedChannels.Count())), false)
	else if userVariable.systemVariable$ = "rfChannelName" or userVariable.systemVariable$ = "rfVirtualChannel" then
		userVariable.SetCurrentValue("", false)
	else if userVariable.systemVariable$ = "tunerScanPercentageComplete" then
		userVariable.SetCurrentValue("0", false)
	else if userVariable.systemVariable$ = "edidMonitorSerialNumber" then
		userVariable.SetCurrentValue(m.sysInfo.edidMonitorSerialNumber$, false)
	else if userVariable.systemVariable$ = "edidYearOfManufacture" then
		userVariable.SetCurrentValue(m.sysInfo.edidYearOfManufacture$, false)
	else if userVariable.systemVariable$ = "edidMonitorName" then
		userVariable.SetCurrentValue(m.sysInfo.edidMonitorName$, false)
	else if userVariable.systemVariable$ = "edidManufacturer" then
		userVariable.SetCurrentValue(m.sysInfo.edidManufacturer$, false)
	else if userVariable.systemVariable$ = "edidUnspecifiedText" then
		userVariable.SetCurrentValue(m.sysInfo.edidUnspecifiedText$, false)
	else if userVariable.systemVariable$ = "edidSerialNumber" then
		userVariable.SetCurrentValue(m.sysInfo.edidSerialNumber$, false)
	else if userVariable.systemVariable$ = "edidManufacturerProductCode" then
		userVariable.SetCurrentValue(m.sysInfo.edidManufacturerProductCode$, false)
	else if userVariable.systemVariable$ = "edidWeekOfManufacture" then
		userVariable.SetCurrentValue(m.sysInfo.edidWeekOfManufacture$, false)
	else if userVariable.systemVariable$ = "activePresentation" then
		if IsString(m.activePresentation$) then
			userVariable.SetCurrentValue(m.activePresentation$, false)
		else
			userVariable.SetCurrentValue("", false)
		endif
	else if userVariable.systemVariable$ = "brightAuthorVersion" then
		userVariable.SetCurrentValue(m.sysInfo.baVersion$, false)
	endif

End Sub


Sub AssignSystemVariablesToUserVariables()

	for each variableName in m.currentUserVariables
		userVariable = m.currentUserVariables.Lookup(variableName)
		if type(userVariable) = "roAssociativeArray" then
			m.AssignSystemVariableToUserVariables(userVariable)
		endif
	next

End Sub

'endregion

'region newSign helpers
Function GetBPConfiguration(bpHardware$ As String, bp900ConfigureAutomatically As Boolean, bp900Configuration% As Integer, bp200ConfigureAutomatically As Boolean, bp200Configuration% As Integer) As Integer
	
	if bpHardware$ = "BP900" then
		if bp900ConfigureAutomatically then
			return 0
		else
			return bp900Configuration%
		endif
	else
		if bp200ConfigureAutomatically then
			return 0
		else
			return bp200Configuration%
		endif
	endif

End Function


Function GetYesNoFromBool(val As Boolean) As String

	if val then return "yes"
	return "no"

End Function


Function GetBoolFromString(str$ As String, defaultValue As Boolean) As Boolean

	if str$ = "" then
		return defaultValue
	else if lcase(str$) = "true" or lcase(str$) = "yes" then
		return true
	else
		return false
	endif

End Function


Function GetIntFromString(str$ As String) As Integer

	if str$ <> "" then
		return int(val(str$))
	else
		return 0
	endif

End Function

'endregion

'region ZoneHSM
Function newZoneHSM(bsp As Object, msgPort As Object, sign As Object, zoneXML As Object, globalVariables As Object) As Object

    zoneType$ = zoneXML.type.GetText()

    ' create objects and read zone specific parameters
    
    if zoneType$ = "VideoOrImages" then
    
        zoneHSM = newVideoOrImagesZoneHSM(bsp, zoneXML)
        
    else if zoneType$ = "VideoOnly" then
    
        zoneHSM = newVideoZoneHSM(bsp, zoneXML)
        
    else if zoneType$ = "Images" then
    
        zoneHSM = newImagesZoneHSM(bsp, zoneXML)
        
    else if zoneType$ = "AudioOnly" then
    
		zoneHSM = newAudioZoneHSM(bsp, zoneXML)
		
    else if zoneType$ = "EnhancedAudio" then

		zoneHSM = newEnhancedAudioZoneHSM(bsp, zoneXML)

	else if zoneType$ = "Ticker" then
    
        zoneHSM = newTickerZoneHSM(bsp, sign, zoneXML)
        
    else if zoneType$ = "Clock" then
    
        zoneHSM = newClockZoneHSM(bsp, zoneXML)
    
    else if zoneType$ = "BackgroundImage" then
    
        zoneHSM = newBackgroundImageZoneHSM(bsp, zoneXML)
            
    endif

	zoneHSM.CreateObjects = CreateObjects
	zoneHSM.CreateCommunicationObjects = CreateCommunicationObjects
	zoneHSM.CreateObjectsNeededForTransitionCommands = CreateObjectsNeededForTransitionCommands
	zoneHSM.CreateObjectForTransitionCommand = CreateObjectForTransitionCommand
    zoneHSM.CreateSerial = CreateSerial
	zoneHSM.CreateUDPSender = CreateUDPSender

    zoneHSM.InitializeZoneCommon = InitializeZoneCommon

	zoneHSM.LoadImageBuffers = LoadImageBuffers
	zoneHSM.AddImageBufferItem = AddImageBufferItem
		        
    zoneHSM.language$ = globalVariables.language$
    
    ' create and read playlist
    playlistXML = zoneXML.playlist
    numPlaylists% = playlistXML.Count()
        
	zoneHSM.useVideoPlayerForImages = false

    if numPlaylists% = 1 then
        zoneHSM.playlist = newPlaylist(bsp, zoneHSM, sign, playlistXML)
    endif

    zoneHSM.playbackActive = false

    return zoneHSM
    
End Function


Function newPlaylist(bsp As Object, zoneHSM As Object, sign As Object, playlistXML As Object) As Object

    playlistBS = CreateObject("roAssociativeArray")
    
    playlistBS.name$ = playlistXML.name.GetText()

' get states
    
    stateList = playlistXML.states.state
    if type(stateList) <> "roXMLList" then print "Invalid XML file - state list not found" : stop
    
    initialStateXML = playlistXML.states.initialState
    if type(initialStateXML) <> "roXMLList" then print "Invalid XML file - initial state not found" : stop

    initialStateName$ = initialStateXML.GetText()

	if zoneHSM.type$ = "Ticker" then
	
		zoneHSM.rssDataFeedItems = CreateObject("roArray", 2, true)

		for each state in stateList
		
			tickerItem = newTickerItem(bsp, zoneHSM, state)
			if tickerItem <> invalid then
				zoneHSM.rssDataFeedItems.push(tickerItem)
			endif

		next
		
	else
	
		zoneHSM.stateTable = CreateObject("roAssociativeArray")
		for each state in stateList
			bsState = newState(bsp, zoneHSM, sign, state, invalid)
		next

	' find the initial state for the playlist
		for each stateName in zoneHSM.stateTable
			bsState = zoneHSM.stateTable[stateName]
			if bsState.id$ = initialStateName$ then
				playlistBS.firstState = GetInitialState(zoneHSM, bsState)
				exit for
			endif
		next

	' find the initial states for each superstate
		allStates = CreateObject("roArray", 1, true)
		for each stateName in zoneHSM.stateTable
			allStates.push(stateName)
		next

		for each stateName in allStates
			bsState = zoneHSM.stateTable[stateName]
			if bsState.type$ = "superState" then
				initialStateName$ = bsState.initialStateName$
				for each innerStateName in zoneHSM.stateTable
					innerState = zoneHSM.stateTable[innerStateName]
					if innerState.id$ = initialStateName$ then
						bsState.firstState = GetInitialState(zoneHSM, innerState)
						exit for
					endif
				next
			endif
		next
			
	' get transitions

		transitionList = playlistXML.states.transition

		for each transition in transitionList
			newTransition(bsp, zoneHSM, sign, transition)
		next

    endif
                  
    return playlistBS
        
End Function


Function GetInitialState(zoneHSM As Object, state As Object) As Object

	if state.type$ <> "superState" return state

	initialStateName$ = state.initialStateName$

	for each stateName in zoneHSM.stateTable
		state = zoneHSM.stateTable[stateName]
		if state.id$ = initialStateName$ then
			return GetInitialState(zoneHSM, state)
		endif
	next

End Function


Function ConvertToByteArray(input$ As String) As Object

	inputSpec = CreateObject("roByteArray")
	
	' convert serial$ into byte array
    byteString$ = StripLeadingSpaces(input$)
    commaPosition = -1
    while commaPosition <> 0	
        commaPosition = instr(1, byteString$, ",")
        if commaPosition = 0 then
			byteValue = val(byteString$)
        else 
            byteValue = val(left(byteString$, commaPosition - 1))
        endif
        inputSpec.push(byteValue)
	    byteString$ = mid(byteString$, commaPosition+1)
    end while
            
	return inputSpec
	            
End Function


Sub newTransition(bsp As Object, zoneHSM As Object, sign As Object, transitionXML As Object)

    stateTable = zoneHSM.stateTable
    
    sourceMediaState$ = transitionXML.sourceMediaState.GetText()

' given the sourceMediaState, find the associated bsState
	bsState = stateTable.Lookup(sourceMediaState$)
    if type(bsState) <> "roAssociativeArray" then print "Media state specified in transition not found" : stop

    userEvent = transitionXML.userEvent
    if userEvent.Count() <> 1 then print "Invalid XML file - userEvent not found" : stop
    userEventName$ = userEvent.name.GetText()

    nextMediaState$ = transitionXML.targetMediaState.GetText()
    
    transition = CreateObject("roAssociativeArray")
	transition.AssignEventInputToUserVariable = AssignEventInputToUserVariable
	transition.AssignWildcardInputToUserVariable = AssignWildcardInputToUserVariable

    transition.targetMediaState$ = nextMediaState$
    
	' if the transition points to a superstate, point it to the first state for the superstate instead
	if transition.targetMediaState$ <> "" then
		targetState = zoneHSM.stateTable[transition.targetMediaState$]
		if targetState.type$ = "superState" and targetState.firstState <> invalid then
			transition.targetMediaState$ = targetState.firstState.id$
		endif
	endif

    nextIsPrevious$ = transitionXML.targetIsPreviousState.GetText()
    transition.targetMediaStateIsPreviousState = false
    if nextIsPrevious$ <> "" and lcase(nextIsPrevious$) = "yes" then
        transition.targetMediaStateIsPreviousState = true
    endif

	transition.remainOnCurrentStateActions = "none"
	remainOnCurrentStateActions$ = transitionXML.remainOnCurrentStateActions.GetText()
	if remainOnCurrentStateActions$ <> "" then
		transition.remainOnCurrentStateActions = lcase(remainOnCurrentStateActions$)
	endif

	transition.assignInputToUserVariable = false
	if lcase(transitionXML.assignInputToUserVariable.GetText()) = "true" then
		transition.assignInputToUserVariable = true
		transition.variableToAssign = invalid
		variableToAssign$ = transitionXML.variableToAssign.GetText()
		if variableToAssign$ <> "" then
			transition.variableToAssign = bsp.GetUserVariable(variableToAssign$)
			if transition.variableToAssign = invalid then
				bsp.diagnostics.PrintDebug("User variable " + variableToAssign$ + " not found.")
			endif
		endif
	endif

	transition.assignWildcardToUserVariable = false
	if lcase(transitionXML.assignWildcardToUserVariable.GetText()) = "true" then
		transition.assignWildcardToUserVariable = true
		transition.variableToAssignFromWildcard = invalid
		variableToAssign$ = transitionXML.variableToAssignFromWildcard.GetText()
		if variableToAssign$ <> "" then
			transition.variableToAssignFromWildcard = bsp.GetUserVariable(variableToAssign$)
			if transition.variableToAssignFromWildcard = invalid then
				bsp.diagnostics.PrintDebug("User variable " + variableToAssign$ + " not found.")
			endif
		endif
	endif

    if userEventName$ = "gpioUserEvent" then

		transition.configuration$ = "press"

		if userEvent.parameters.buttonNumber.GetText() <> "" then

			buttonNumber$ = userEvent.parameters.buttonNumber.GetText()
		
			continuousConfigs = userEvent.parameters.GetNamedElements("pressContinuous")
			if continuousConfigs.Count() = 1 then
				continuousConfig = continuousConfigs[0]
				transition.configuration$ = "pressContinuous"
				transition.initialHoldoff$ = continuousConfig.initialHoldoff.GetText()
				transition.repeatInterval$ = continuousConfig.repeatInterval.GetText()
			endif

		else

	        buttonNumber$ = userEvent.parameters.parameter.GetText()

		endif
		
		buttonDirection$ = "down"
		if userEvent.parameters.buttonDirection.GetText() <> "" then
			buttonDirection$ = userEvent.parameters.buttonDirection.GetText()
		endif

		bsp.ConfigureGPIOInput(buttonNumber$)
		
		if buttonDirection$ = "down" then
	        gpioEvents = bsState.gpioEvents
		    gpioEvents[buttonNumber$] = transition
		else
	        gpioUpEvents = bsState.gpioUpEvents
		    gpioUpEvents[buttonNumber$] = transition
		endif

	else if userEventName$ = "bp900AUserEvent" or userEventName$ = "bp900BUserEvent" or userEventName$ = "bp900CUserEvent" or userEventName$ = "bp900DUserEvent"or userEventName$ = "bp200AUserEvent" or userEventName$ = "bp200BUserEvent" or userEventName$ = "bp200CUserEvent" or userEventName$ = "bp200DUserEvent" then

		transition.configuration$ = "press"

		buttonPanelIndex% = int(val(userEvent.parameters.buttonPanelIndex.GetText()))
		buttonNumber$ = userEvent.parameters.buttonNumber.GetText()
		
		continuousConfigs = userEvent.parameters.GetNamedElements("pressContinuous")
		if continuousConfigs.Count() = 1 then
			continuousConfig = continuousConfigs[0]
			transition.configuration$ = "pressContinuous"
			transition.initialHoldoff$ = continuousConfig.initialHoldoff.GetText()
			transition.repeatInterval$ = continuousConfig.repeatInterval.GetText()
		endif
		
		bsp.ConfigureBPInput(buttonPanelIndex%, buttonNumber$)
		
        bpEvents = bsState.bpEvents
        currentBPEvent = bpEvents[buttonPanelIndex%]
        currentBPEvent.AddReplace(buttonNumber$, transition)
	        
    else if userEventName$ = "gpsEvent" then

		enterRegion$ = userEvent.parameters.enterRegion.GetText()

		transition.radiusInFeet = val(userEvent.parameters.gpsRegion.radiusInFeet.GetText())
		transition.latitude = val(userEvent.parameters.gpsRegion.latitude.GetText())
		transition.latitudeInRadians = ConvertDecimalDegtoRad(transition.latitude)
		transition.longitude = val(userEvent.parameters.gpsRegion.longitude.GetText())
		transition.longitudeInRadians = ConvertDecimalDegtoRad(transition.longitude)

		if lcase(enterRegion$) = "true" then
			bsState.gpsEnterRegionEvents.push(transition)
		else
			bsState.gpsExitRegionEvents.push(transition)
		endif

	else if userEventName$ = "serial" then

        ' support both old style and new style serial events
        if userEvent.parameters.parameter2.Count() = 1 then
            port$ = userEvent.parameters.parameter.GetText()
	        serial$ = userEvent.parameters.parameter2.GetText()
        else
            port$ = "0"
            serial$ = userEvent.parameters.parameter.GetText()
        endif

		' convert USB port if necessary
		if bsp.usbDevicesByConnector.DoesExist(port$) then
			port$ = bsp.usbDevicesByConnector.Lookup(port$)
		endif   

		if IsUsbCommunicationPort(port$) then
			if GetGlobalAA().usbCDCPortConfigurations.DoesExist(port$) then
				usbCDCPortConfiguration = GetGlobalAA().usbCDCPortConfigurations[port$]
				protocol$ = usbCDCPortConfiguration.protocol$
			else if GetGlobalAA().usbHIDPortConfigurations.DoesExist(port$) then
				usbHIDPortConfiguration = GetGlobalAA().usbHIDPortConfigurations[port$]
				protocol$ = usbHIDPortConfiguration.protocol$
			else
				protocol$ = ""
			endif
		else
			port% = int(val(port$))
			serialPortConfiguration = sign.serialPortConfigurations[port%]
			protocol$ = serialPortConfiguration.protocol$
		endif

		serialEvents = bsState.serialEvents
		if type(serialEvents[port$]) <> "roAssociativeArray" then
			serialEvents[port$] = CreateObject("roAssociativeArray")
		endif

	    if protocol$ = "Binary" then
			if type(serialEvents[port$].streamInputTransitionSpecs) <> "roArray" then
				serialEvents[port$].streamInputTransitionSpecs = CreateObject("roArray", 1, true)
			endif
			
			streamInputTransitionSpec = CreateObject("roAssociativeArray")
			streamInputTransitionSpec.transition = transition
			streamInputTransitionSpec.inputSpec = ConvertToByteArray(serial$)
			streamInputTransitionSpec.asciiSpec = serial$
			serialEvents[port$].streamInputTransitionSpecs.push(streamInputTransitionSpec)

		else if protocol$ <> "" then
			serialPortEvents = serialEvents[port$]
			serialPortEvents[serial$] = transition
        endif
		                            
    else if userEventName$ = "timeout" then
    
        bsState.mstimeoutValue% = int(val(userEvent.parameters.parameter.GetText()) * 1000)        
        bsState.mstimeoutEvent = transition

    else if userEventName$ = "timeClockEvent" then

		timeClockEventTransitionSpec = { }
		timeClockEventTransitionSpec.transition = transition

		if type(userEvent.timeClockEvent.timeClockDateTime.GetChildElements()) = "roXMLList" then
			dateTime$ = userEvent.timeClockEvent.timeClockDateTime.dateTime.GetText()
			timeClockEventTransitionSpec.timeClockEventDateTime = FixDateTime(dateTime$)
		else if type(userEvent.timeClockEvent.timeClockDateTimeByUserVariable.GetChildElements()) = "roXMLList" then
			userVariableName$ = userEvent.timeClockEvent.timeClockDateTimeByUserVariable.userVariableName.GetText()
			timeClockEventTransitionSpec.userVariableName$ = userVariableName$
			timeClockEventTransitionSpec.userVariable = bsp.GetUserVariable(userVariableName$)
		else
			timeClockEventTransitionSpec.daysOfWeek% = int(val(userEvent.timeClockEvent.timeClockDailyOnce.daysOfWeek.GetText()))
			if type(userEvent.timeClockEvent.timeClockDailyOnce.GetChildElements()) = "roXMLList" then
				timeClockEventTransitionSpec.timeClockDaily% = int(val(userEvent.timeClockEvent.timeClockDailyOnce.eventTime.GetText()))
			else
				timeClockEventTransitionSpec.daysOfWeek% = int(val(userEvent.timeClockEvent.timeClockDailyPeriodic.daysOfWeek.GetText()))
				timeClockEventTransitionSpec.timeClockPeriodicInterval% = int(val(userEvent.timeClockEvent.timeClockDailyPeriodic.intervalTime.GetText()))
				timeClockEventTransitionSpec.timeClockPeriodicStartTime% = int(val(userEvent.timeClockEvent.timeClockDailyPeriodic.startTime.GetText()))
				timeClockEventTransitionSpec.timeClockPeriodicEndTime% = int(val(userEvent.timeClockEvent.timeClockDailyPeriodic.endTime.GetText()))
			endif
		endif

		if type(bsState.timeClockEvents) <> "roArray" then
			bsState.timeClockEvents = CreateObject("roArray", 1, true) 
		endif

		bsState.timeClockEvents.push(timeClockEventTransitionSpec)

	else if userEventName$ = "mediaEnd" then

		if bsState.type$ = "video" then
        
            bsState.videoEndEvent = transition

		else if bsState.type$ = "liveVideo" then
        
            bsState.videoEndEvent = transition

		else if bsState.type$ = "audio" then
        
            bsState.audioEndEvent = transition

		else if bsState.type$ = "signChannel" or bsState.type$ = "mediaRSS" then
        
            bsState.signChannelEndEvent = transition
        
        else if bsState.type$ = "mediaList" and bsState.mediaType$ = "video" then
        
			bsState.videoEndEvent = transition
			
        else if bsState.type$ = "mediaList" and bsState.mediaType$ = "audio" then
        
			bsState.audioEndEvent = transition
			
        else if bsState.type$ = "mediaList" and bsState.mediaType$ = "allMedia" then

			bsState.videoEndEvent = transition
			bsState.audioEndEvent = transition

		else if bsState.type$ = "playFile" then

            bsState.videoEndEvent = transition
            bsState.audioEndEvent = transition

        else if bsState.type$ = "stream" then

			bsState.videoEndEvent = transition
			bsState.audioEndEvent = transition

		else if bsState.type$ = "mjpeg" then

			bsState.videoEndEvent = transition
		
		else if bsState.type$ = "rfInputChannel" then

			bsState.videoEndEvent = transition
		
		else if bsState.type$ = "rfScan" then

			bsState.videoEndEvent = transition
		
        else if bsState.type$ = "superState" then

			bsState.mediaEndEvent = transition

		endif

	else if userEventName$ = "mediaListEnd" then
	
		bsState.mediaListEndEvent = transition
		        
    else if userEventName$ = "keyboard" then

        keyboardChar$ = userEvent.parameters.parameter.GetText()
		if len(keyboardChar$) > 1 then
			keyboardChar$ = Lcase(keyboardChar$)
		endif

        if type(bsState.keyboardEvents) <> "roAssociativeArray" then
            bsState.keyboardEvents = CreateObject("roAssociativeArray")
        endif
        
        bsState.keyboardEvents[keyboardChar$] = transition
                
    else if userEventName$ = "remote" then

        remote$ = ucase(userEvent.parameters.parameter.GetText())

        if type(bsState.remoteEvents) <> "roAssociativeArray" then
            bsState.remoteEvents = CreateObject("roAssociativeArray")
        endif
        
        bsState.remoteEvents[remote$] = transition
        
    else if userEventName$ = "usb" then

        usbString$ = userEvent.parameters.parameter.GetText()
        
        if type(bsState.usbStringEvents) <> "roAssociativeArray" then
            bsState.usbStringEvents = CreateObject("roAssociativeArray")
        endif
        
        bsState.usbStringEvents[usbString$] = transition
        
    else if userEventName$ = "udp" then

        udp$ = userEvent.parameters.parameter.GetText()
        
        if type(bsState.udpEvents) <> "roAssociativeArray" then
            bsState.udpEvents = CreateObject("roAssociativeArray")
        endif
        
		label$ = userEvent.parameters.label.GetText()
		export$ = LCase(userEvent.parameters.export.GetText())

		transition.udpLabel$ = label$
		if export$ = "true" then
			transition.udpExport = true
		else
			transition.udpExport = false
		endif

        bsState.udpEvents[udp$] = transition
    
    else if userEventName$ = "synchronize" then

		bsp.IsSyncSlave = true

        synchronize$ = userEvent.parameters.parameter.GetText()
        
        if type(bsState.synchronizeEvents) <> "roAssociativeArray" then
            bsState.synchronizeEvents = CreateObject("roAssociativeArray")
        endif
        
        bsState.synchronizeEvents[synchronize$] = transition
    
    else if userEventName$ = "zoneMessage" then
    
        zoneMessage$ = userEvent.parameters.parameter.GetText()
        
        if type(bsState.zoneMessageEvents) <> "roAssociativeArray" then
            bsState.zoneMessageEvents = CreateObject("roAssociativeArray")
        endif
        
        bsState.zoneMessageEvents[zoneMessage$] = transition
        
    else if userEventName$ = "pluginMessageEvent" then
    
		pluginName$ = userEvent.parameters.name.GetText()
		pluginMessage$ = userEvent.parameters.message.GetText()
        ' unique key is concatenation of plugin name and plugin message
		key$ = pluginName$ + pluginMessage$

        if type(bsState.pluginMessageEvents) <> "roAssociativeArray" then
            bsState.pluginMessageEvents = CreateObject("roAssociativeArray")
        endif
        
        bsState.pluginMessageEvents[key$] = transition
        
    else if userEventName$ = "internalSynchronize" then
    
        internalSynchronize$ = userEvent.parameters.parameter.GetText()
        
        if type(bsState.internalSynchronizeEvents) <> "roAssociativeArray" then
            bsState.internalSynchronizeEvents = CreateObject("roAssociativeArray")
        endif
        
        bsState.internalSynchronizeEvents[internalSynchronize$] = transition
        
    else if userEventName$ = "rectangularTouchEvent" then 
        
        if type(bsState.touchEvents) <> "roAssociativeArray" then
            bsState.touchEvents = CreateObject("roAssociativeArray")
        endif
        
        transition.x% = int(val(userEvent.parameters.x.GetText()))
        transition.y% = int(val(userEvent.parameters.y.GetText()))
        transition.width% = int(val(userEvent.parameters.width.GetText()))
        transition.height% = int(val(userEvent.parameters.height.GetText()))
        
        if sign.flipCoordinates then
            videoMode = CreateObject("roVideoMode")
            resX = videoMode.GetResX()
            resY = videoMode.GetResY()
            videoMode = invalid
            
            transition.x% = resX - (transition.x% + transition.width%)
            transition.y% = resY - (transition.y% + transition.height%)
        endif
    
        bsState.touchEvents[stri(sign.numTouchEvents%)] = transition
        sign.numTouchEvents% = sign.numTouchEvents% + 1

    else if userEventName$ = "audioTimeCodeEvent" then
    
        if type(bsState.audioTimeCodeEvents) <> "roAssociativeArray" then
            bsState.audioTimeCodeEvents = CreateObject("roAssociativeArray")
        endif
    
        transition.timeInMS% = int(val(userEvent.parameters.parameter.GetText()))
        bsState.audioTimeCodeEvents[stri(sign.numAudioTimeCodeEvents%)] = transition

        sign.numAudioTimeCodeEvents% = sign.numAudioTimeCodeEvents% + 1

    else if userEventName$ = "videoTimeCodeEvent" then
    
        if type(bsState.videoTimeCodeEvents) <> "roAssociativeArray" then
            bsState.videoTimeCodeEvents = CreateObject("roAssociativeArray")
        endif
    
        transition.timeInMS% = int(val(userEvent.parameters.parameter.GetText()))
        bsState.videoTimeCodeEvents[stri(sign.numVideoTimeCodeEvents%)] = transition

        sign.numVideoTimeCodeEvents% = sign.numVideoTimeCodeEvents% + 1

    else if userEventName$ = "quietUserEvent" then
    
        bsState.quietUserEvent = transition
    
    else if userEventName$ = "loudUserEvent" then
    
		bsState.loudUserEvent = transition
		
    else if userEventName$ = "auxConnectUserEvent" then

        if type(bsState.auxConnectEvents) <> "roAssociativeArray" then
            bsState.auxConnectEvents = CreateObject("roAssociativeArray")
        endif

		audioConnector$ = userEvent.parameters.audioConnector.GetText()
		bsState.auxConnectEvents[audioConnector$] = transition

	else if userEventName$ = "auxDisconnectUserEvent" then

        if type(bsState.auxDisconnectEvents) <> "roAssociativeArray" then
            bsState.auxDisconnectEvents = CreateObject("roAssociativeArray")
        endif

		audioConnector$ = userEvent.parameters.audioConnector.GetText()
		bsState.auxDisconnectEvents[audioConnector$] = transition

	else if userEventName$ = "success" then

		bsState.successEvent = transition

	else if userEventName$ = "fail" then

		bsState.failEvent = transition
					
    endif
    
    ' get commands and conditional targets
    for each transitionItemXML in transitionXML.GetChildElements()

        if transitionItemXML.GetName() = "brightSignCmd" then

			if type(transition.transitionCmds) <> "roArray" then
				transition.transitionCmds = CreateObject("roArray", 1, true)
			endif

            newCmd(bsp, transitionItemXML, transition.transitionCmds)

        endif

		if transitionItemXML.GetName() = "conditionalTarget" then

			if type(transition.conditionalTargets) <> "roArray" then
				transition.conditionalTargets = CreateObject("roArray", 1, true)
			endif

            newConditionalTarget(bsp, zoneHSM, transitionItemXML, transition.conditionalTargets)

		endif

    next

    for each transitionCmd in transition.transitionCmds
    
        ' if the transition command is for an internal synchronize, add an event that the master will receive after it sends the preload command
        if transitionCmd.name$ = "internalSynchronize" then
        
            if type(transition.internalSynchronizeEventsMaster) <> "roAssociativeArray" then
                transition.internalSynchronizeEventsMaster = CreateObject("roAssociativeArray")
            endif
            
            internalSynchronizeMasterTransition = CreateObject("roAssociativeArray")
            internalSynchronizeMasterTransition.targetMediaState$ = nextMediaState$
            internalSynchronizeMasterTransition.targetMediaStateIsPreviousState = false

			transition.internalSynchronizeEventsMaster[transitionCmd.parameters["synchronizeKeyword"].GetCurrentParameterValue()] = internalSynchronizeMasterTransition

            ' modify this state's transition to not go to the next media state
            transition.targetMediaState$ = ""
            
        endif
    
    next
        
End Sub


Function newConditionalTarget(bsp As Object, zoneHSM As Object, conditionalTargetXML As Object, conditionalTargets As Object)

	userVariableName$ = conditionalTargetXML.variableName.GetText()
	operator$ = conditionalTargetXML.operator.GetText()
	if operator$ = "" then operator$ = "EQ"
	targetMediaState$ = conditionalTargetXML.targetMediaState.GetText()
    nextIsPrevious$ = conditionalTargetXML.targetIsPreviousState.GetText()

	if conditionalTargetXML.variableValue.GetText() <> "" then
		userVariableValue$ = conditionalTargetXML.variableValue.GetText()
		userVariableValue = newTextParameterValue(userVariableValue$)
	else
		userVariableValue = newParameterValue(bsp, conditionalTargetXML.variableValueSpec.parameterValue)
	endif

	if conditionalTargetXML.variableValue2.GetText() <> "" then
		userVariableValue2$ = conditionalTargetXML.variableValue2.GetText()
		userVariableValue2 = newTextParameterValue(userVariableValue2$)
	else
		userVariableValue2 = newParameterValue(bsp, conditionalTargetXML.variableValue2Spec.parameterValue)
	endif

	userVariable = bsp.GetUserVariable(userVariableName$)
	if type(userVariable) = "roAssociativeArray" then

		conditionalTarget = { }
		conditionalTarget.userVariable = userVariable
		conditionalTarget.operator$ = operator$
		conditionalTarget.userVariableValue = userVariableValue
		conditionalTarget.userVariableValue2 = userVariableValue2

		conditionalTarget.targetMediaState$ = targetMediaState$
		if conditionalTarget.targetMediaState$ <> "" then
			targetState = zoneHSM.stateTable[conditionalTarget.targetMediaState$]
			if targetState.type$ = "superState" and targetState.firstState <> invalid then
				conditionalTarget.targetMediaState$ = targetState.firstState.id$
			endif
		endif
		
		conditionalTarget.targetMediaStateIsPreviousState = false
		if nextIsPrevious$ <> "" and lcase(nextIsPrevious$) = "yes" then
			conditionalTarget.targetMediaStateIsPreviousState = true
		endif

		brightSignCmdsXML = conditionalTargetXML.brightSignCmd
		if type(brightSignCmdsXML) = "roXMLList" and brightSignCmdsXML.Count() > 0 then
			conditionalTarget.transitionCmds = CreateObject("roArray", 1, true)
			for each brightSignCmdXML in brightSignCmdsXML
		        newCmd(bsp, brightSignCmdXML, conditionalTarget.transitionCmds)
			next
		endif

		conditionalTargets.push(conditionalTarget)

	else

        bsp.diagnostics.PrintDebug("User variable " + userVariableName$ + " not found.")
	
	endif

End Function


Function newTickerItem(bsp As Object, zoneHSM As Object, stateXML As Object) As Object

	item = invalid
	
    if stateXML.rssItem.Count() = 1 then
        item = newRSSPlaylistItem(bsp, zoneHSM, stateXML.rssItem)
	else if stateXML.twitterItem.Count() = 1 then
		item = newTwitterPlaylistItem(bsp, zoneHSM, stateXML.twitterItem)
	else if stateXML.userVariableInTickerItem.Count() = 1 then
		item = newUserVariablePlaylistItem(bsp, zoneHSM, stateXML.userVariableInTickerItem)
    else if stateXML.rssDataFeedPlaylistItem.Count() = 1 then
		item = newRSSDataFeedPlaylistItem(bsp, stateXML.rssDataFeedPlaylistItem)
	else if stateXML.textItem.Count() = 1 then
        item = newTextPlaylistItem(stateXML.textItem)
    endif           

    return item

End Function


Function newState(bsp As Object, zoneHSM As Object, sign As Object, stateXML As Object, superState As Object) As Object

' get the name
    stateName$ = stateXML.name.GetText()
    
    state = zoneHSM.newHState(bsp, stateName$)
	state.name$ = stateName$

	if type(superState) = "roAssociativeArray" then
		state.superState = superState
	else
		state.superState = zoneHSM.stTop
	endif

' create data structures for arrays of specific events    
    state.gpioEvents = CreateObject("roAssociativeArray")
    state.gpioUpEvents = CreateObject("roAssociativeArray")
    
    state.bpEvents = CreateObject("roArray", 4, true)
    state.bpEvents[0] = CreateObject("roAssociativeArray")
    state.bpEvents[1] = CreateObject("roAssociativeArray")
    state.bpEvents[2] = CreateObject("roAssociativeArray")
    state.bpEvents[3] = CreateObject("roAssociativeArray")

    state.serialEvents = CreateObject("roAssociativeArray")
	state.gpsEnterRegionEvents = CreateObject("roArray", 1, true)
	state.gpsExitRegionEvents = CreateObject("roArray", 1, true)

' get the item

    item = CreateObject("roAssociativeArray")

    if stateXML.imageItem.Count() = 1 then
    
        newImagePlaylistItem(bsp, stateXML.imageItem, zoneHSM, state, item)
        state.imageItem = item
        state.type$ = "image"
        zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
    
    else if stateXML.videoItem.Count() = 1 then
    
        newVideoPlaylistItem(bsp, stateXML.videoItem, state, item)
        state.videoItem = item
        state.type$ = "video"
    
    else if stateXML.liveVideoItem.Count() = 1 then
    
        newLiveVideoPlaylistItem(stateXML.liveVideoItem, state)
        state.type$ = "liveVideo"

	else if stateXML.rfInputItem.Count() = 1 then
	
		newRFInputPlaylistItem(bsp, stateXML.rfInputItem, state)
		state.type$ = "rfInputChannel"

	else if stateXML.rfScanItem.Count() = 1 then

		newRFScanPlaylistItem(stateXML.rfScanItem, state)
		state.type$ = "rfScan"

    else if stateXML.eventHandlerItem.Count() = 1 then
    
		newEventHandlerPlaylistItem(stateXML.eventHandlerItem, state)
		state.type$ = "eventHandler"
		
    else if stateXML.eventHandler2Item.Count() = 1 then
    
		newEventHandlerPlaylistItem(stateXML.eventHandler2Item, state)
		state.type$ = "eventHandler"
		
	else if stateXML.liveTextItem.Count() = 1 then
    
		newTemplatePlaylistItemFromLiveTextPlaylistItem(bsp, stateXML.liveTextItem, state)
		state.type$ = "template"

	else if stateXML.templatePlaylistItem.Count() = 1 then
	
		newTemplatePlaylistItem(bsp, stateXML.templatePlaylistItem, state)
		state.type$ = "template"
				
    else if stateXML.audioInItem.Count() = 1 then
    
		newAudioInPlaylistItem(bsp, stateXML.audioInItem, state)
		state.type$ = "audioIn"

		if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
	        zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
		endif
		
    else if stateXML.signChannelItem.Count() = 1 then
    
		' require that the storage is writable
		if bsp.sysInfo.storageIsWriteProtected then DisplayStorageDeviceLockedMessage()

        newSignChannelPlaylistItem(bsp, stateXML.signChannelItem, state)
        state.type$ = "signChannel"

		if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
	        zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
		endif

    else if stateXML.mrssDataFeedPlaylistItem.Count() = 1 or stateXML.rssImageItem.Count() = 1 then

		' require that the storage is writable
		if bsp.sysInfo.storageIsWriteProtected then DisplayStorageDeviceLockedMessage()

		if stateXML.mrssDataFeedPlaylistItem.Count() = 1 then
			newMRSSPlaylistItem(bsp, zoneHSM, stateXML.mrssDataFeedPlaylistItem, state)
		else
	        newRSSImagePlaylistItem(bsp, stateXML.rssImageItem, state)
		endif

		state.type$ = "mediaRSS"

		if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
	        zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
		endif
                    
	else if stateXML.localPlaylistItem.Count() = 1 then
	
		newLocalPlaylistItem(bsp, stateXML.localPlaylistItem, state)
		state.type$ = "mediaRSS"
		
		if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
	        zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
		endif
	                
    else if stateXML.audioItem.Count() = 1 then
    
        newAudioPlaylistItem(bsp, stateXML.audioItem, state, item)
        state.audioItem = item
        state.type$ = "audio"
    
	else if stateXML.mediaSuperState.Count() = 1 then
    
        state.HStateEventHandler = MediaItemEventHandler
		state.ExecuteTransition = ExecuteTransition
		state.GetNextStateName = GetNextStateName
        state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
        
    else if stateXML.backgroundImageItem.Count() = 1 then
    
        newBackgroundImagePlaylistItem(bsp, stateXML.backgroundImageItem, state, item)
        state.backgroundImageItem = item
            
    else if stateXML.mediaListItem.Count() = 1 then
    
		newMediaListPlaylistItem(bsp, zoneHSM, stateXML.mediaListItem, state)
		state.type$ = "mediaList"
		
    else if stateXML.tripleUSBItem.Count() = 1 then

        newTripleUSBPlaylistItem(stateXML.tripleUSBItem, sign, state)
		state.type$ = "tripleUSB"
		
	else if stateXML.interactiveMenuItem.Count() = 1 then
	
		newInteractiveMenuPlaylistItem(bsp, sign, stateXML.interactiveMenuItem, state)
		state.type$ = "interactiveMenuItem"

		if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
	        zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
		endif
	
	else if stateXML.playFileItem.Count() = 1 then
		
		newPlayFilePlaylistItem(bsp, stateXML.playFileItem, state)
		state.type$ = "playFile"

		if stateXML.playFileItem.mediaType.GetText() = "image" then
			zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
		endif

    else if stateXML.streamItem.Count() = 1 then

        newStreamPlaylistItem(bsp, stateXML.streamItem, state)
		state.mediaType$ = "video"
        state.type$ = "stream"

    else if stateXML.videoStreamItem.Count() = 1 then

        newStreamPlaylistItem(bsp, stateXML.videoStreamItem, state)
		state.mediaType$ = "video"
        state.type$ = "stream"

    else if stateXML.audioStreamItem.Count() = 1 then

        newStreamPlaylistItem(bsp, stateXML.audioStreamItem, state)
		state.mediaType$ = "audio"
        state.type$ = "stream"

	else if stateXML.mjpegItem.Count() = 1 then

        newMjpegStreamPlaylistItem(bsp, stateXML.mjpegItem, state)
        state.type$ = "mjpeg"

	else if stateXML.html5Item.Count() = 1 then

		newHtml5PlaylistItem(bsp, stateXML.html5Item, state)
		state.type$ = "html5"

	else if stateXML.xModemItem.Count() = 1 then

		newXModemPlaylistItem(stateXML.xModemItem, state)
		state.type$ = "xModem"

	else if stateXML.superStateItem.Count() = 1 then

		newSuperStateItem(bsp, zoneHSM, sign, stateXML.superStateItem, state)
		state.type$ = "superState"

	endif           

' get any media state commands (commands that are executed when a state is entered)
    state.cmds = CreateObject("roArray", 1, true)

    ' new style commands    
    cmds = stateXML.brightSignCmd
    if stateXML.brightSignCmd.Count() > 0 then
        for each cmd in cmds
            newCmd(bsp, cmd, state.cmds)
        next
    endif

' get media state exit commands
	state.exitCmds = CreateObject("roArray", 1, true)
	exitCmds = stateXML.brightSignExitCommands.brightSignCmd
    if stateXML.brightSignExitCommands.brightSignCmd.Count() > 0 then
        for each cmd in exitCmds
            newCmd(bsp, cmd, state.exitCmds)
        next
    endif

	zoneHSM.stateTable[state.id$] = state

    return state
    
End Function


Function newTextParameterValue(value$ As String) As Object

	parameterValue = CreateObject("roAssociativeArray")
	parameterValue.GetCurrentParameterValue = GetCurrentParameterValue
	parameterValue.GetParameterValueSpec = GetParameterValueSpec

	parameterValue.parameterValueItems = CreateObject("roArray", 1, true)
	parameterValue.parameterValueItems.push(newParameterValueItemFromTextConstant(value$))

	return parameterValue

End Function


Function GetCurrentTextParameterValue() As String

	return m.textValue$

End Function


Function GetParameterValueSpecItemText() As String

	return m.textValue$

End Function


Function newParameterValueItemText(parameterValueItemTextXML As Object) As Object

	parameterValueItem = CreateObject("roAssociativeArray")
	parameterValueItem.GetCurrentValue = GetCurrentTextParameterValue
	parameterValueItem.GetParameterValueSpec = GetParameterValueSpecItemText

	parameterValueItem.type$ = "text"
	parameterValueItem.textValue$ = parameterValueItemTextXML.value.GetText()

	return parameterValueItem

End Function


Function newParameterValueItemFromTextConstant(textValue$ As String) As Object

	parameterValueItem = CreateObject("roAssociativeArray")
	parameterValueItem.GetCurrentValue = GetCurrentTextParameterValue
	parameterValueItem.GetParameterValueSpec = GetParameterValueSpecItemText

	parameterValueItem.type$ = "text"
	parameterValueItem.textValue$ = textValue$

	return parameterValueItem

End Function


Function GetCurrentUserVariableParameterValue() As String

	if type(m.userVariable) = "roAssociativeArray" then
		return m.userVariable.GetCurrentValue()
	else
		return ""
	endif

End Function


Function GetParameterValueSpecItemUserVariable() As String

	if type(m.userVariable) = "roAssociativeArray" then
		return "$$" + m.userVariable.name$ + "$$"
	else
		return ""
	endif

End Function


Function newParameterValueItemUserVariable(bsp As Object, parameterValueItemUserVariableXML As Object) As Object

	parameterValueItem = CreateObject("roAssociativeArray")
	parameterValueItem.GetCurrentValue = GetCurrentUserVariableParameterValue
	parameterValueItem.GetParameterValueSpec = GetParameterValueSpecItemUserVariable

	parameterValueItem.type$ = "userVariable"

	userVariableName$ = parameterValueItemUserVariableXML.userVariable.name.GetText()
	parameterValueItem.userVariable = bsp.GetUserVariable(userVariableName$)
	if type(parameterValueItem.userVariable) <> "roAssociativeArray" then
        bsp.diagnostics.PrintDebug("User variable " + userVariableName$ + " not found.")
	    bsp.logging.WriteDiagnosticLogEntry(bsp.diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND, userVariableName$)
	endif

	return parameterValueItem

End Function


Function GetParameterValueSpecItemMediaCounterVariable() As String

	if type(m.userVariable) = "roAssociativeArray" then
		return "_" + m.userVariable.name$
	else
		return ""
	endif

End Function


Function newParameterValueItemMediaCounterVariable(bsp As Object, parameterValueItemMediaCounterVariable As Object) As Object

	parameterValueItem = CreateObject("roAssociativeArray")
	parameterValueItem.GetCurrentValue = GetCurrentUserVariableParameterValue
	parameterValueItem.GetParameterValueSpec = GetParameterValueSpecItemMediaCounterVariable

	parameterValueItem.type$ = "userVariable"

	variableName$ = parameterValueItemMediaCounterVariable.fileName.GetText()
	userVariableName$ = mid(variableName$, 2)
	parameterValueItem.userVariable = bsp.GetUserVariable(userVariableName$)
	if type(parameterValueItem.userVariable) <> "roAssociativeArray" then
        bsp.diagnostics.PrintDebug("Media counter variable " + userVariableName$ + " not found.")
	    bsp.logging.WriteDiagnosticLogEntry(bsp.diagnosticCodes.EVENT_MEDIA_COUNTER_VARIABLE_NOT_FOUND, userVariableName$)
	endif

	return parameterValueItem

End Function


Function GetCurrentParameterValue() As String

	value$ = ""

	for each parameterValueItem in m.parameterValueItems
		if type(parameterValueItem) = "roAssociativeArray" then
			value$ = value$ + parameterValueItem.GetCurrentValue()
		endif
	next

	return value$

End Function


Function GetVariableName() As String

	variableName$ = ""

	if m.parameterValueItems.Count() = 1 then
		parameterValueItem = m.parameterValueItems[0]
		if type(parameterValueItem) = "roAssociativeArray" then
			if parameterValueItem.type$ = "userVariable" then
				userVariable = parameterValueItem.userVariable
				variableName$ = userVariable.name$
			endif			
		endif
	endif

	return variableName$

End Function


Function GetParameterValueSpec() As String

	parameterValueSpec$ = ""

	for each parameterValueItem in m.parameterValueItems
		parameterValueSpec$ = parameterValueSpec$ + parameterValueItem.GetParameterValueSpec()
	next

	return parameterValueSpec$

End Function


Function newParameterValue(bsp As Object, parameterValueXML As Object) As Object

	parameterValue = CreateObject("roAssociativeArray")
	parameterValue.GetCurrentParameterValue = GetCurrentParameterValue
	parameterValue.GetVariableName = GetVariableName
	parameterValue.GetParameterValueSpec = GetParameterValueSpec

	parameterValue.parameterValueItems = CreateObject("roArray", 1, true)

	if type(parameterValueXML) = "roXMLList" and parameterValueXML.Count() = 1 then
		parameterValueItemsXML = parameterValueXML.GetChildElements()
		for each parameterValueItemXML in parameterValueItemsXML
			if parameterValueItemXML.GetName() = "parameterValueItemText" then
				parameterValue.parameterValueItems.push(newParameterValueItemText(parameterValueItemXML))
			else if parameterValueItemXML.GetName() = "parameterValueItemUserVariable" then
				parameterValue.parameterValueItems.push(newParameterValueItemUserVariable(bsp, parameterValueItemXML))
			else if parameterValueItemXML.GetName() = "parameterValueItemMediaCounterVariable" then
				parameterValue.parameterValueItems.push(newParameterValueItemMediaCounterVariable(bsp, parameterValueItemXML))
			endif
		next
	endif

	return parameterValue

End Function


Sub newCmd(bsp As Object, cmdXML As Object, cmds As Object)

    numCmds% = cmdXML.command.Count()
    if numCmds% > 0 then
        cmdsXML = cmdXML.command
        for each cmd in cmdsXML
            bsCmd = CreateObject("roAssociativeArray")
            bsCmd.name$ = cmd.name.GetText()
            bsCmd.parameters = CreateObject("roAssociativeArray")
            numParameters% = cmd.parameter.Count()

			' IMPLEMENT A MORE GENERAL PURPOSE APPROACH, PROBABLY IN BA
			checkForUSBParameterNames = false
			if bsCmd.name$ = "muteAudioOutputs" or bsCmd.name$ = "unmuteAudioOutputs" or bsCmd.name$ = "setAllAudioOutputs" then
				checkForUSBParameterNames = true
			endif

            if numParameters% > 0 then
                parameters = cmd.parameter
                for each parameter in parameters
					if type(parameter.value) = "roXMLList" and parameter.value.Count() = 1 then
						value$ = parameter.value.GetText()
						parameterValue = newTextParameterValue(value$)
					else
						parameterValue = newParameterValue(bsp, parameter.parameterValue)
					endif

					parameterName = parameter.name.GetText()

					if checkForUSBParameterNames then

						if GetGlobalAA().baUSBParamNameToDeviceParamName.DoesExist(parameterName) then
							parameterName = GetGlobalAA().baUSBParamNameToDeviceParamName[parameterName]
						endif

						if m.bsp.usbBACommandToDeviceCommandMapping.DoesExist(parameterName) then
							parameterName = m.bsp.usbBACommandToDeviceCommandMapping[parameterName]
						endif
					endif

					' if a USB port is specified, perform mapping as needed
					if parameter.name.GetText() = "port" or parameterName = "connector" then
						portValue = parameterValue.getCurrentParameterValue()
						if m.bsp.usbDevicesByConnector.DoesExist(portValue) then
							portValue$ = m.bsp.usbDevicesByConnector[portValue]
							parameterValue = newTextParameterValue(portValue$)							
						endif
					endif

					bsCmd.parameters.AddReplace(parameterName, parameterValue)
                next
            endif

            cmds.push(bsCmd)
            
            if bsCmd.name$ = "sendBPOutput" then
				buttonNumber$ = bsCmd.parameters.buttonNumber.GetCurrentParameterValue()
				buttonPanelIndex% = int(val(bsCmd.parameters.buttonPanelIndex.GetCurrentParameterValue()))
				action$ = bsCmd.parameters.action.GetCurrentParameterValue()
				if buttonNumber$ = "-1" then
					if action$ <> "off" then
						for i% = 0 to 10
							bsp.bpOutputUsed[buttonPanelIndex%, i%] = true
						next
					endif
				else
					buttonNumber% = int(val(buttonNumber$))
					bsp.bpOutputUsed[buttonPanelIndex%, buttonNumber%] = true
				endif
            else if bsCmd.name$ = "switchPresentation" then ' required for compatibility with old published presentations
				presentationName$ = bsCmd.parameters.presentationName.GetCurrentParameterValue()
				' new format has > 1 parameters: presentationName and useUserVariable
				if numParameters% = 1 then
					presentation = {}
					presentation.name$ = presentationName$
					presentation.presentationName$ = presentationName$
					presentation.path$ = presentationName$
					bsp.presentations.AddReplace(presentation.name$, presentation)
				endif
			endif
        next
    endif
        
End Sub


Sub UpdateWidgetVisibility(showImage As Boolean, hideImage As Boolean, clearImage As Boolean, showCanvas As Boolean, hideCanvas As Boolean, showHtml As Boolean, hideHtml As Boolean)

	if hideImage then
		if type(m.imagePlayer) = "roImageWidget" then
			m.imagePlayer.Hide()
		endif
	endif

	if clearImage then
		if type(m.imagePlayer) = "roImageWidget" then
			m.imagePlayer.StopDisplay()
		endif
	endif

	if hideCanvas then
		if type(m.canvasWidget) = "roCanvasWidget" then
			m.canvasWidget.Hide()
		endif
	endif

	if hideHtml then
		if type(m.displayedHtmlWidget) = "roHtmlWidget" then
'			m.displayedHtmlWidget.Hide()
			m.loadingHtmlWidget = invalid
			m.displayedHtmlWidget = invalid
		endif
	endif

	if showImage then
		if type(m.imagePlayer) = "roImageWidget" and m.isVisible then
			m.imagePlayer.Show()
		endif
	endif

	if showCanvas then
		if type(m.canvasWidget) = "roCanvasWidget" and m.isVisible then
			m.canvasWidget.Show()
		endif
	endif

	if showHtml then
		if type(m.displayedHtmlWidget) = "roHtmlWidget" and m.isVisible then
			m.displayedHtmlWidget.Show()
		endif
	endif

End Sub


Sub ShowImageWidget()

	m.UpdateWidgetVisibility(true, false, false, false, true, false, true)

	m.imageHidden = false
	m.canvasHidden = true
	m.htmlHidden = true

End Sub


Sub ClearImagePlane()

	m.UpdateWidgetVisibility(false, false, true, false, true, false, true)

End Sub


Sub ShowCanvasWidget()

	m.UpdateWidgetVisibility(false, true, false, true, false, false, true)

	m.imageHidden = true
	m.canvasHidden = false
	m.htmlHidden = true

End Sub


Sub ShowHtmlWidget()

	m.UpdateWidgetVisibility(false, true, true, false, true, true, false)

	m.imageHidden = true
	m.canvasHidden = true
	m.htmlHidden = false

End Sub


Sub LogPlayStart(itemType$ As String, fileName$ As String)

    if m.playbackActive then
        m.playbackEndTime$ = m.bsp.systemTime.GetLocalDateTime().GetString()
	    m.bsp.logging.WritePlaybackLogEntry(m.name$, m.playbackStartTime$, m.playbackEndTime$, m.playbackItemType$, m.playbackFileName$)
    endif
    
    m.playbackActive = true
    m.playbackStartTime$ = m.bsp.systemTime.GetLocalDateTime().GetString()
    m.playbackItemType$ = itemType$
    m.playbackFileName$ = fileName$
    
End Sub


Sub newMediaPlaylistItem(bsp As Object, playlistItemXML As Object, state As Object, playlistItemBS As Object)

    file = playlistItemXML.file
    fileAttrs = file.GetAttributes()
    playlistItemBS.fileName$ = fileAttrs["name"]
	playlistItemBS.userVariable = bsp.GetUserVariable(playlistItemBS.fileName$)
	if type(bsp.encryptionByFile) = "roAssociativeArray" then
		playlistItemBS.isEncrypted = bsp.encryptionByFile.DoesExist(playlistItemBS.fileName$)
	else
		playlistItemBS.isEncrypted = false
	endif

    state.MediaItemEventHandler = MediaItemEventHandler
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
    state.PreloadItem = PreloadItem

End Sub


Sub newImagePlaylistItem(bsp As Object, playlistItemXML As Object, zoneHSM As Object, state As Object, playlistItemBS As Object)

    newMediaPlaylistItem(bsp, playlistItemXML, state, playlistItemBS)
    playlistItemBS.slideDelayInterval% = int(val(playlistItemXML.slideDelayInterval.GetText()))
    playlistItemBS.slideTransition% = GetSlideTransitionValue(playlistItemXML.slideTransition.GetText())

	playlistItemBS.transitionDuration% = 1000
	if playlistItemXML.transitionDuration.GetText() <> "" then
		playlistItemBS.transitionDuration% = int(val(playlistItemXML.transitionDuration.GetText()))
	endif

	playlistItemBS.useImageBuffer = false
	useImageBuffer$ = playlistItemXML.useImageBuffer.GetText()
	if len(useImageBuffer$) > 0 then
		useImageBuffer$ = lcase(useImageBuffer$)
		if useImageBuffer$ = "true" then
			playlistItemBS.useImageBuffer = true
		endif
	endif

	videoPlayerRequired$ = playlistItemXML.videoPlayerRequired.GetText()
	if len(videoPlayerRequired$) > 0 then
		videoPlayerRequired$ = lcase(videoPlayerRequired$)
		if videoPlayerRequired$ = "true" then
			zoneHSM.useVideoPlayerForImages = true
		endif
	endif

    state.HStateEventHandler = STDisplayingImageEventHandler
	state.DisplayImage = DisplayImage
	state.PreDrawImage = PreDrawImage
	state.DrawImage = DrawImage
	state.PostDrawImage = PostDrawImage
	state.ClearVideo = ClearVideo
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons

End Sub


Function GetProbeData(assetPoolFiles As Object, fileName$ As String) As Object

	probe = invalid

	poolFileInfo = assetPoolFiles.GetPoolFileInfo(fileName$)
	if type(poolFileInfo) = "roAssociativeArray" then
		probe = poolFileInfo.Lookup("probe")
	endif

	return probe

End Function


Sub newVideoPlaylistItem(bsp As Object, playlistItemXML As Object, state As Object, playlistItemBS As Object)

    newMediaPlaylistItem(bsp, playlistItemXML, state, playlistItemBS)

	playlistItemBS.probeData = GetProbeData(bsp.assetPoolFiles, playlistItemBS.fileName$)
    itemVolume$ = playlistItemXML.volume.GetText()
    if itemVolume$ <> "" then
        playlistItemBS.volume% = int(val(itemVolume$))
    endif
    
    playlistItemBS.videoDisplayMode% = 0
    videoDisplayMode$ = playlistItemXML.videoDisplayMode.GetText()
    if videoDisplayMode$ = "3DSBS" then
	    playlistItemBS.videoDisplayMode% = 1
	else if videoDisplayMode$ = "3DTOB" then
	    playlistItemBS.videoDisplayMode% = 2
    endif
    
	playlistItemBS.automaticallyLoop = true
	if lcase(playlistItemXML.automaticallyLoop.GetText()) = "false" then
		playlistItemBS.automaticallyLoop = false
	endif

    state.HStateEventHandler = STVideoPlayingEventHandler
    state.AddVideoTimeCodeEvent = AddVideoTimeCodeEvent
    state.SetVideoTimeCodeEvents = SetVideoTimeCodeEvents
    state.LaunchVideo = LaunchVideo
	state.PrePlayVideo = PrePlayVideo
	state.PlayVideo = PlayVideo
	state.PostPlayVideo = PostPlayVideo
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons

End Sub


Sub	newSuperStateItem(bsp As Object, zoneHSM As Object, sign As Object, playlistItemXML As Object, state As Object)

	state.initialStateName$ = playlistItemXML.initialState.getText()

	subStatesXML = playlistItemXML.state

	for each subStateXML in subStatesXML
		subState = newState(bsp, zoneHSM, sign, subStateXML, state)
	next

    state.HStateEventHandler = STSuperStateEventHandler
    state.MediaItemEventHandler = MediaItemEventHandler
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
    state.ConfigureBPButtons = ConfigureBPButtons

End Sub


Sub newEventHandlerPlaylistItem(playlistItemXML As Object, state As Object)

	state.stopPlayback = false

	stopPlayback$ = playlistItemXML.stopPlayback.getText()
	if lcase(stopPlayback$) = "true" then
		state.stopPlayback = true
	endif

    state.HStateEventHandler = STEventHandlerEventHandler
    state.MediaItemEventHandler = MediaItemEventHandler
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons

End Sub


Sub newXModemPlaylistItem(playlistItemXML As Object, state As Object)

	state.name$ = playlistItemXML.name.GetText()

    file = playlistItemXML.file
    fileAttrs = file.GetAttributes()
    state.fileName$ = fileAttrs["name"]
	state.crc16$ = fileAttrs["crc16"]

	state.port$ = playlistItemXML.serialPort.GetText()
	state.numberOfRetries% = int(val(playlistItemXML.numberOfRetries.GetText()))
    state.HStateEventHandler = STXModemEventHandler
	state.SendXModemBlock = SendXModemBlock
	state.RestartXModemTimeoutTimer = RestartXModemTimeoutTimer
	state.RetryXModemTransfer = RetryXModemTransfer
	state.MediaItemEventHandler = MediaItemEventHandler
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    state.LaunchTimer = LaunchTimer
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames

End Sub


Sub newHtml5PlaylistItem(bsp As Object, playlistItemXML As Object, state As Object)

	state.name$ = playlistItemXML.name.GetText()
	state.htmlSiteName$ = playlistItemXML.htmlSiteName.GetText()

	' get the associated html site
	if bsp.htmlSites.DoesExist(state.htmlSiteName$) then
		htmlSite = bsp.htmlSites.Lookup(state.htmlSiteName$)
		state.contentIsLocal = htmlSite.contentIsLocal
		if state.contentIsLocal then
			state.prefix$ = htmlSite.prefix$
			state.filePath$ = htmlSite.filePath$
			state.url = invalid
		else
			state.url = htmlSite.url
		endif

		state.queryString = htmlSite.queryString
	else
		' what to do here?
		stop
	endif

	state.enableExternalData = false
	if lcase(playlistItemXML.enableExternalData.GetText()) = "true" then
		state.enableExternalData = true
	endif

	state.enableCamera = false
	if lcase(playlistItemXML.enableCamera.GetText()) = "true" then
		state.enableCamera = true
	endif

	state.enableMouseEvents = false
	if lcase(playlistItemXML.enableMouseEvents.GetText()) = "true" then
		state.enableMouseEvents = true
	endif

	state.displayCursor = false
	if lcase(playlistItemXML.displayCursor.GetText()) = "true" then
		state.displayCursor = true
	endif

	state.hwzOn = false
	if lcase(playlistItemXML.hwzOn.GetText()) = "true" then
		state.hwzOn = true
	endif

	state.enableScrollBars = false
	if lcase(playlistItemXML.enableScrollBars.GetText()) = "true" then
		state.enableScrollBars = true
	endif

	state.useUserStylesheet = false
	if lcase(playlistItemXML.useUserStylesheet.GetText()) = "true" then
		state.useUserStylesheet = true
		state.userStylesheet = playlistItemXML.userStylesheet.GetText()
	endif

	state.customFonts = []
	customFontsXML = playlistItemXML.customFont
	if type(customFontsXML) = "roXMLList" and customFontsXML.Count() > 0 then
		for each customFontXML in customFontsXML
			state.customFonts.push(customFontXML.GetText())
		next
	endif

    state.HStateEventHandler = STHTML5PlayingEventHandler
	state.MediaItemEventHandler = MediaItemEventHandler
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    state.LaunchTimer = LaunchTimer
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames

End Sub


Sub newRFScanPlaylistItem(playlistItemXML As Object, state As Object)

	state.scanSpec = { }

	channelMap$ = playlistItemXML.channelMap.GetText()
	if channelMap$ <> "ATSC_QAM" then
		state.scanSpec["ChannelMap"] = channelMap$
	endif

	modulationType$ = playlistItemXML.modulationType.GetText()

	if channelMap$ <> "ATSC" and modulationType$ <> "QAM64_QAM256" then
		state.scanSpec["ModulationType"] = modulationType$
	endif

	firstRFChannel$ = playlistItemXML.firstRFChannel.GetText()
	if firstRFChannel$ <> "" then
		state.scanSpec["FirstRfChannel"] = int(val(firstRFChannel$))
	endif

	lastRFChannel$ = playlistItemXML.lastRFChannel.GetText()
	if lastRFChannel$ <> "" then
		state.scanSpec["LastRfChannel"] = int(val(lastRFChannel$))
	endif

    state.HStateEventHandler = STRFScanHandler
	state.ProcessScannedChannels = ProcessScannedChannels
    state.MediaItemEventHandler = MediaItemEventHandler
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
	state.UpdateTunerScanPercentageComplete = UpdateTunerScanPercentageComplete

End Sub


Sub newRFInputPlaylistItem(bsp As Object, playlistItemXML As Object, state As Object)

	state.channelDescriptor = { }

	state.firstScannedChannel = false
	virtualChannel$ = playlistItemXML.rfInChannelDescriptor.rfInputChannel.virtualChannel.getText()		' user specified channel name
	if virtualChannel$ <> "" then
		state.channelDescriptor.VirtualChannel = virtualChannel$
	else
		virtualChannel$ = playlistItemXML.rfInVirtualChannel.virtualChannel.getText()					' user specified virtual channel
		if virtualChannel$ <> "" then
			state.channelDescriptor.VirtualChannel = virtualChannel$
		else
			userVariable$ = playlistItemXML.rfInUserVariable.userVariable.getText()						' user specified user variable
			if userVariable$ <> "" then
				state.userVariable = bsp.GetUserVariable(userVariable$)
				state.channelDescriptor = invalid
			else
				state.channelDescriptor = bsp.scannedChannels[0]										' user specified first scanned channel
				state.firstScannedChannel = true
			endif
		endif
	endif

	state.reentryAction$ = playlistItemXML.reentryAction.GetText()

	if type(playlistItemXML.channelUp) = "roXMLList" and playlistItemXML.channelUp.Count() = 1 then
		state.channelUpEvent = CreateObject("roAssociativeArray")
		SetStateEvent(bsp, state.channelUpEvent, playlistItemXML.channelUp)
	endif

	if type(playlistItemXML.channelDown) = "roXMLList" and playlistItemXML.channelDown.Count() = 1 then
		state.channelDownEvent = CreateObject("roAssociativeArray")
		SetStateEvent(bsp, state.channelDownEvent, playlistItemXML.channelDown)
	endif

    itemVolume$ = playlistItemXML.volume.GetText()
    if itemVolume$ <> "" then
        state.volume% = int(val(itemVolume$))
    endif

	state.overscan = false
	if lcase(playlistItemXML.overscan.GetText()) = "true" then
		state.overscan = true
	endif

    state.HStateEventHandler = STRFInputPlayingHandler
	state.HandleIntraStateEvent = HandleIntraStateEvent
	state.ConfigureIntraStateEventHandlerButton = ConfigureIntraStateEventHandlerButton
    state.MediaItemEventHandler = MediaItemEventHandler
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer

End Sub


Sub newLiveVideoPlaylistItem(playlistItemXML As Object, state As Object)

    itemVolume$ = playlistItemXML.volume.GetText()
    if itemVolume$ <> "" then
        state.volume% = int(val(itemVolume$))
    endif

	state.overscan = false
	if lcase(playlistItemXML.overscan.GetText()) = "true" then
		state.overscan = true
	endif

    state.HStateEventHandler = STLiveVideoPlayingEventHandler
    state.MediaItemEventHandler = MediaItemEventHandler
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer

End Sub


Sub newInteractiveMenuPlaylistItem(bsp As Object, sign As Object, playlistItemXML As Object, state As Object)

	state.backgroundImage$ = ParseFileNameXML(playlistItemXML.backgroundImage)
	if state.backgroundImage$ <> "" then
		state.backgroundImageUseImageBuffer = ParseBoolAttribute(playlistItemXML.backgroundImage[0], "useImageBuffer")
	endif
	state.backgroundImageUserVariable = bsp.GetUserVariable(state.backgroundImage$)

	state.navigateToLastSelectedOnEntry = false
	if lcase(playlistItemXML.navigateToLastSelectedOnEntry.GetText()) = "true" then
		state.navigateToLastSelectedOnEntry = true
	endif

	state.upNavigation = ParseNavigation(bsp, sign, playlistItemXML.upNavigationEvent)
	state.downNavigation = ParseNavigation(bsp, sign, playlistItemXML.downNavigationEvent)
	state.leftNavigation = ParseNavigation(bsp, sign, playlistItemXML.leftNavigationEvent)
	state.rightNavigation = ParseNavigation(bsp, sign, playlistItemXML.rightNavigationEvent)
	state.enterNavigation = ParseNavigation(bsp, sign, playlistItemXML.enterNavigationEvent)
	state.backNavigation = ParseNavigation(bsp, sign, playlistItemXML.backNavigationEvent)
	state.nextClipNavigation = ParseNavigation(bsp, sign, playlistItemXML.nextClipNavigationEvent)
	state.previousClipNavigation = ParseNavigation(bsp, sign, playlistItemXML.previousClipNavigationEvent)
    
    interactiveMenuItemsXML = playlistItemXML.interactiveMenuItems.interactiveMenuItem

	state.interactiveMenuItems = CreateObject("roArray", 2, true)

	for each interactiveMenuItemXML in interactiveMenuItemsXML
		interactiveMenuItem = newInteractiveMenuItem(bsp, interactiveMenuItemXML)
		state.interactiveMenuItems.push(interactiveMenuItem)	
	next
	
	state.lastInteractiveMenuNavigationIndex% = 0

    state.HStateEventHandler = STInteractiveMenuEventHandler
	state.HandleInteractiveMenuEventInput = HandleInteractiveMenuEventInput
    state.DrawInteractiveMenu = DrawInteractiveMenu
    state.DisplayNavigationOverlay = DisplayNavigationOverlay
    state.NavigateToMenuItem = NavigateToMenuItem
    state.RestartInteractiveMenuInactivityTimer = RestartInteractiveMenuInactivityTimer
    state.ExecuteInteractiveMenuEnter = ExecuteInteractiveMenuEnter
    state.LaunchInteractiveMenuVideoClip = LaunchInteractiveMenuVideoClip
    state.LaunchInteractiveMenuAudioClip = LaunchInteractiveMenuAudioClip
    state.DisplayInteractiveMenuImage = DisplayInteractiveMenuImage
    state.NextPrevInteractiveMenuLaunchMedia = NextPrevInteractiveMenuLaunchMedia
	state.ConsumeSerialByteInput = ConsumeSerialByteInput
    state.MediaItemEventHandler = MediaItemEventHandler
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    state.IsPlayingClip = IsPlayingClip
    state.ClearPlayingClip = ClearPlayingClip
    state.PreloadItem = PreloadItem
	state.ConfigureNavigationButton = ConfigureNavigationButton
	
End Sub


Function newInteractiveMenuItem(bsp As Object, interactiveMenuItemXML As Object) As Object

	interactiveMenuItem = CreateObject("roAssociativeArray")

	interactiveMenuItem.index$ = interactiveMenuItemXML.index.GetText()
	scaleScreenElement(bsp, false, interactiveMenuItem, interactiveMenuItemXML)

	interactiveMenuItem.selectedImage$ = ParseFileNameXML(interactiveMenuItemXML.selectedImage)
	interactiveMenuItem.selectedImageUseImageBuffer = ParseBoolAttribute(interactiveMenuItemXML.selectedImage[0], "useImageBuffer")	
		
	interactiveMenuItem.unselectedImage$ = ParseFileNameXML(interactiveMenuItemXML.unselectedImage)
	interactiveMenuItem.unselectedImageUseImageBuffer = ParseBoolAttribute(interactiveMenuItemXML.unselectedImage[0], "useImageBuffer")	

    interactiveMenuItem.targetType$ = interactiveMenuItemXML.targetType.GetText()
	if interactiveMenuItem.targetType$ = "mediaFile" then

		mediaFileName = ""

		interactiveMenuItem.targetVideoFile$ = ParseFileNameXML(interactiveMenuItemXML.targetVideoFile)
		interactiveMenuitem.targetVideoFileUserVariable = bsp.GetUserVariable(interactiveMenuItem.targetVideoFile$)
		if interactiveMenuItem.targetVideoFile$ <> "" then
			mediaFileName = interactiveMenuItem.targetVideoFile$
		endif

		interactiveMenuItem.targetAudioFile$ = ParseFileNameXML(interactiveMenuItemXML.targetAudioFile)
		interactiveMenuitem.targetAudioFileUserVariable = bsp.GetUserVariable(interactiveMenuItem.targetAudioFile$)
		if interactiveMenuItem.targetAudioFile$ <> "" then
			mediaFileName = interactiveMenuItem.targetAudioFile$
		endif

		interactiveMenuItem.targetImageFile$ = ParseFileNameXML(interactiveMenuItemXML.targetImageFile)
		if interactiveMenuItem.targetImageFile$ <> "" then
			mediaFileName = interactiveMenuItem.targetImageFile$
		endif

		interactiveMenuitem.targetImageFileUserVariable = bsp.GetUserVariable(interactiveMenuItem.targetImageFile$)
		if interactiveMenuItem.targetImageFile$ <> "" then
			element = interactiveMenuItemXML.targetImageFile[0]
			imageFileAttributes = element.GetAttributes()
			timeout$ = imageFileAttributes.Lookup("timeout")
			interactiveMenuItem.targetImageFileTimeout% = int(val(timeout$))	
			interactiveMenuItem.targetImageFileUseImageBuffer = ParseBoolAttribute(interactiveMenuItemXML.targetImageFile[0], "useImageBuffer")	
		else if interactiveMenuItem.targetVideoFile$ <> "" then
			interactiveMenuItem.probeData = GetProbeData(bsp.assetPoolFiles, interactiveMenuItem.targetVideoFile$)
		else if interactiveMenuItem.targetAudioFile$ <> "" then
			interactiveMenuItem.probeData = GetProbeData(bsp.assetPoolFiles, interactiveMenuItem.targetAudioFile$)
		endif

		if type(bsp.encryptionByFile) = "roAssociativeArray" and mediaFileName <> "" then
			interactiveMenuItem.isEncrypted = bsp.encryptionByFile.DoesExist(mediaFileName)
		else
			interactiveMenuItem.isEncrypted = false
		endif

	else if interactiveMenuItem.targetType$ = "mediaState" then
	    interactiveMenuItem.targetMediaState$ = interactiveMenuItemXML.targetMediaState.GetText()
	endif
		
    nextIsPrevious$ = interactiveMenuItemXML.targetIsPreviousState.GetText()
    interactiveMenuItem.targetIsPreviousState = false
    if nextIsPrevious$ <> "" and lcase(nextIsPrevious$) = "yes" then
        interactiveMenuItem.targetIsPreviousState = true
    endif

    interactiveMenuItem.enterCmds = CreateObject("roArray", 1, true)
    cmdsXML = interactiveMenuItemXML.enterBrightSignCmds.brightSignCmd
    if cmdsXML.Count() > 0 then
        for each cmdXML in cmdsXML
            newCmd(bsp, cmdXML, interactiveMenuItem.enterCmds)
        next
    endif

	for each cmd in interactiveMenuItem.enterCmds
		commandName$ = cmd.name$
        if commandName$ = "sendUDPCommand" or commandName$ = "sendUDPBytesCommand" or commandName$ = "synchronize" then
            bsp.CreateUDPSender(bsp)
        else if commandName$ = "sendSerialStringCommand" or commandName$ = "sendSerialBlockCommand" or commandName$ = "sendSerialByteCommand" or commandName$ = "sendSerialBytesCommand" then
			' TODO - Bose port$?
			port$ = cmd.parameters["port"].GetCurrentParameterValue()
			bsp.CreateSerial(bsp, port$, true)
        endif
	next

	interactiveMenuItem.upNavigationIndex$ = interactiveMenuItemXML.upNavigationMenuItem.GetText()
	if IsString(interactiveMenuItem.upNavigationIndex$) and interactiveMenuItem.upNavigationIndex$ <> "" then
		interactiveMenuItem.upNavigationIndex% = int(val(interactiveMenuItem.upNavigationIndex$))	
	else
		interactiveMenuItem.upNavigationIndex% = -1	
	endif
	
	interactiveMenuItem.downNavigationIndex$ = interactiveMenuItemXML.downNavigationMenuItem.GetText()
	if IsString(interactiveMenuItem.downNavigationIndex$) and interactiveMenuItem.downNavigationIndex$ <> "" then
		interactiveMenuItem.downNavigationIndex% = int(val(interactiveMenuItem.downNavigationIndex$))	
	else
		interactiveMenuItem.downNavigationIndex% = -1	
	endif

	interactiveMenuItem.leftNavigationIndex$ = interactiveMenuItemXML.leftNavigationMenuItem.GetText()
	if IsString(interactiveMenuItem.leftNavigationIndex$) and interactiveMenuItem.leftNavigationIndex$ <> "" then
		interactiveMenuItem.leftNavigationIndex% = int(val(interactiveMenuItem.leftNavigationIndex$))	
	else
		interactiveMenuItem.leftNavigationIndex% = -1	
	endif

	interactiveMenuItem.rightNavigationIndex$ = interactiveMenuItemXML.rightNavigationMenuItem.GetText()
	if IsString(interactiveMenuItem.rightNavigationIndex$) and interactiveMenuItem.rightNavigationIndex$ <> "" then
		interactiveMenuItem.rightNavigationIndex% = int(val(interactiveMenuItem.rightNavigationIndex$))	
	else
		interactiveMenuItem.rightNavigationIndex% = -1	
	endif
	
	return interactiveMenuItem
	
End Function


Function ParseFileNameXML(fileNameXML As Object) As String

	if fileNameXML.Count() = 1 then
		fileElement = fileNameXML[0]
		fileAttributes = fileElement.GetAttributes()
		fileName$ = fileAttributes.Lookup("name")
	else
		fileName$ = ""
	endif

	return fileName$
	
End Function


Function ParseBoolAttribute(elementXML As Object, attr$ As String) As Boolean

	element = elementXML
	attributes = element.GetAttributes()
	val = attributes.Lookup(attr$)
	if IsString(val) and lcase(val) = "true" then
		return true
	endif
	
	return false
	
End Function


Function ParseNavigation(bsp As Object, sign As Object, navigationXML As Object) As Object

	navigation = invalid
	
	if navigationXML.Count() = 1 then
		navigationElement = navigationXML[0]
		userEventsList = navigationElement.userEvent
		if userEventsList.Count() > 0 then
			navigation = CreateObject("roAssociativeArray")
			for each userEvent in userEventsList
				ParseUserEvent(bsp, sign, navigation, userEvent)
			next
		endif
	endif

	return navigation
	
End Function


Sub ParseUserEvent(bsp As Object, sign As Object, aa As Object, userEvent As Object)

    userEventName$ = userEvent.name.GetText()

	if userEventName$ = "keyboard" then

		aa.keyboardChar$ = userEvent.parameters.parameter.GetText()
		if len(aa.keyboardChar$) > 1 then
			aa.keyboardChar$ = Lcase(aa.keyboardChar$)
		endif
        
    else if userEventName$ = "zoneMessage" then
    
		aa.zoneMessage$ = userEvent.parameters.parameter.GetText()
        
    else if userEventName$ = "remote" then

        aa.remoteEvent$ = ucase(userEvent.parameters.parameter.GetText())

        if type(bsp.remote) <> "roIRRemote" then
            bsp.remote = CreateObject("roIRRemote")
            bsp.remote.SetPort(bsp.msgPort)
        endif
		        
	else if userEventName$ = "serial" then
	
        port$ = userEvent.parameters.parameter.GetText()

		' convert USB port if necessary
		if bsp.usbDevicesByConnector.DoesExist(port$) then
			port$ = bsp.usbDevicesByConnector.Lookup(port$)
		endif   

        serial$ = userEvent.parameters.parameter2.GetText()

		if IsUsbCommunicationPort(port$) then
' check if it exists - could be CDC?			
			usbHIDPortConfiguration = GetGlobalAA().usbHIDPortConfigurations[port$]
			protocol$ = usbHIDPortConfiguration.protocol$
		else
			port% = int(val(port$))
			serialPortConfiguration = sign.serialPortConfigurations[port%]
			protocol$ = serialPortConfiguration.protocol$
		endif

		aa.serialEvent = CreateObject("roAssociativeArray")
		aa.serialEvent.port$ = port$
		aa.serialEvent.protocol$ = protocol$
		
	    if protocol$ = "Binary" then
			aa.serialEvent.inputSpec = ConvertToByteArray(serial$)
			aa.serialEvent.asciiSpec = serial$
		else
			aa.serialEvent.serial$ = serial$
		endif
		
	    bsp.CreateSerial(bsp, aa.serialEvent.port$, false)
				
	else if userEventName$ = "bp900AUserEvent" or userEventName$ = "bp900BUserEvent" or userEventName$ = "bp900CUserEvent" or userEventName$ = "bp900DUserEvent" or userEventName$ = "bp200AUserEvent" or userEventName$ = "bp200BUserEvent" or userEventName$ = "bp200CUserEvent" or userEventName$ = "bp200DUserEvent" then
	
		aa.bpEvent = {}
		
		aa.bpEvent.buttonPanelIndex% = int(val(userEvent.parameters.buttonPanelIndex.GetText()))
		aa.bpEvent.buttonNumber$ = userEvent.parameters.buttonNumber.GetText()
		
		bsp.ConfigureBPInput(aa.bpEvent.buttonPanelIndex%, aa.bpEvent.buttonNumber$)
		
	else if userEventName$ = "gpioUserEvent" then
	
		if userEvent.parameters.buttonNumber.GetText() <> "" then
			buttonNumber$ = userEvent.parameters.buttonNumber.GetText()
		else
			buttonNumber$ = userEvent.parameters.parameter.GetText()
		endif

		aa.gpioEvent = {}
		aa.gpioEvent.buttonNumber$ = buttonNumber$

		bsp.ConfigureGPIOInput(buttonNumber$)
	
	endif
	
End Sub


Sub scaleScreenElement(bsp As Object, setDimensions As Boolean, item As Object, itemXML As Object)

	if bsp.configuredResX <> bsp.actualResX or bsp.configuredResY <> bsp.actualResY then
		xOffset = val(itemXML.x.GetText()) / bsp.configuredResX
		x% = xOffset * bsp.actualResX
		item.x% = x%

		yOffset = val(itemXML.Y.GetText()) / bsp.configuredResY
		y% = yOffset * bsp.actualResY
		item.y% = y%

		if setDimensions then
			width% = bsp.actualResX / bsp.configuredResX * val(itemXML.width.GetText())
			item.width% = width%
	
			height% = bsp.actualResY / bsp.configuredResY * val(itemXML.height.GetText())
			item.height% = height%
		endif

	else
		item.x% = int(val(itemXML.x.GetText()))
		item.y% = int(val(itemXML.y.GetText()))
		if setDimensions then
			item.width% = int(val(itemXML.width.GetText()))
			item.height% = int(val(itemXML.height.GetText()))
		endif
	endif

End Sub


Sub newBaseTemplateItem(templateItem As Object, templateItemXML As Object)

	' scale text item if necessary
	bsp = GetGlobalAA().bsp
	scaleScreenElement(bsp, true, templateItem, templateItemXML)

	templateItem.type$ = templateItemXML.GetName()
	templateItem.layer% = int(val(templateItemXML.layer.GetText()))

End Sub


Sub ParseTemplateWidgets(item As Object, itemXML As Object)
	
	' text widget items
	item.numberOfLines% = int(val(itemXML.textWidget.numberOfLines.GetText()))
	
	item.rotation$ = "0"
	ele = itemXML.textWidget.GetNamedElements("rotation")
	if ele.Count() = 1 then
		rotation$ = ele[0].GetText()
		if rotation$ = "90" then
			item.rotation$ = "270"
		else if rotation$ = "180" then
			item.rotation$ = "180"
		else if rotation$ = "270" then
			item.rotation$ = "90"
		endif
	endif

	item.alignment$ = "left"
	ele = itemXML.textWidget.GetNamedElements("alignment")
	if ele.Count() = 1 then
		alignment$ = ele[0].GetText()
		if alignment$ = "center" then
			item.alignment$ = "center"
		else if alignment$ = "right" then
			item.alignment$ = "right"
		endif
	endif

	' widget items
	item.foregroundTextColor$ = GetHexColor(itemXML.widget.foregroundTextColor.GetAttributes())
	item.backgroundTextColor$ = GetHexColor(itemXML.widget.backgroundTextColor.GetAttributes())
	item.font$ = itemXML.widget.font.GetText()
	if item.font$ = "" then item.font$ = "System"

	item.fontSize% = 0
	if type(itemXML.widget.fontSize) = "roXMLList" and itemXML.widget.fontSize.Count() = 1 then
		if itemXML.widget.fontSize.GetText() <> "" then
			item.fontSize% = int(val(itemXML.widget.fontSize.GetText()))
		endif
	endif

	item.backgroundColorSpecified = false
	if lcase(itemXML.backgroundColorSpecified.GetText()) = "true" then
		item.backgroundColorSpecified = true
	endif

End Sub


Sub newTextTemplateItem(templateItem As Object, templateItemXML As Object)

	' fill in base class members
	newBaseTemplateItem(templateItem, templateItemXML)

	ParseTemplateWidgets(templateItem, templateItemXML)

End Sub


Sub newConstantTextTemplateItem(bsp As Object, templateItems As Object, templateItemXML As Object)

	templateItem = { }
	
	newTextTemplateItem(templateItem, templateItemXML)
	
	templateItem.textString$ = templateItemXML.text.GetText()

	templateItems.push(templateItem)

End Sub


Sub newSystemVariableTextTemplateItem(bsp As Object, templateItems As Object, templateItemXML As Object)

	templateItem = { }
	
	newTextTemplateItem(templateItem, templateItemXML)
	
	templateItem.systemVariableType$ = templateItemXML.systemVariable.GetText()

	templateItems.push(templateItem)

End Sub


Sub newMediaCounterTextTemplateItem(bsp As Object, templateItems As Object, templateItemXML As Object)

	templateItem = { }
	
	newTextTemplateItem(templateItem, templateItemXML)
	
	fileName$ = templateItemXML.fileName.GetText()
	templateItem.userVariable = bsp.GetUserVariable(fileName$)
	if type(templateItem.userVariable) <> "roAssociativeArray" then
	    bsp.diagnostics.PrintDebug("Media counter variable " + fileName$ + " not found.")
	    bsp.logging.WriteDiagnosticLogEntry(bsp.diagnosticCodes.EVENT_MEDIA_COUNTER_VARIABLE_NOT_FOUND, fileName$)
	endif

	templateItems.push(templateItem)

End Sub


Sub newUserVariableTextTemplateItem(bsp As Object, templateItems As Object, templateItemXML As Object)

	templateItem = { }
	
	newTextTemplateItem(templateItem, templateItemXML)
	
	name$ = templateItemXML.name.GetText()
	templateItem.userVariable = bsp.GetUserVariable(name$)
	if type(templateItem.userVariable) <> "roAssociativeArray" then
	    bsp.diagnostics.PrintDebug("User variable " + name$ + " not found.")
	    bsp.logging.WriteDiagnosticLogEntry(bsp.diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND, name$)
	endif

	templateItems.push(templateItem)

End Sub


Function newLiveTextDataEntryTemplateItem(bsp As Object, templateItems As Object, templateItemXML As Object)

	templateItem = { }
	
	newTextTemplateItem(templateItem, templateItemXML)
	
	liveDataFeedName$ = CleanName(templateItemXML.liveDataFeedName.GetText())
	templateItem.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedName$)

	return templateItem

End Function


Sub newIndexedLiveTextDataEntryTemplateItem(bsp As Object, templateItems As Object, templateItemXML As Object)

	templateItem = newLiveTextDataEntryTemplateItem(bsp, templateItems, templateItemXML)

	index$ = templateItemXML.index.GetText()
	if index$ <> "" then
		' old style index was zero based; new style is 1 based.
		index% = int(val(index$))
		index% = index% + 1
		index$ = StripLeadingSpaces(str(index%))
		templateItem.index = newTextParameterValue(index$)
	else
		templateItem.index = newParameterValue(bsp, templateItemXML.indexSpec.parameterValue)
	endif

	templateItems.push(templateItem)

End Sub


Sub newTitledLiveTextDataEntryTemplateItem(bsp As Object, templateItems As Object, templateItemXML As Object)

	templateItem = newLiveTextDataEntryTemplateItem(bsp, templateItems, templateItemXML)

	title$ = templateItemXML.title.GetText()
	if title$ <> "" then
		templateItem.title = newTextParameterValue(title$)
	else
		templateItem.title = newParameterValue(bsp, templateItemXML.titleSpec.parameterValue)
	endif

	templateItems.push(templateItem)

End Sub


Function newSimpleRSSTextTemplateItem(bsp As Object, templateItems As Object, templateItemXML As Object)

	templateItem = { }
	
	newTextTemplateItem(templateItem, templateItemXML)

	templateItem.id$ = templateItemXML.id.GetText()
	templateItem.elementName$ = templateItemXML.elementName.GetText()
	
	templateItems.push(templateItem)

	return templateItem

End Function


Sub newImageTemplateItem(bsp As Object, templateItems As Object, templateItemXML As Object)

	templateItem = { }
	
	newBaseTemplateItem(templateItem, templateItemXML)

	templateItem.fileName$ = templateItemXML.fileName.GetText()

	templateItems.push(templateItem)

End Sub


Function newMRSSTextTemplateItem(bsp As Object, templateItems As Object, templateItemXML As Object) As Object

	templateItem = { }
	
	newTextTemplateItem(templateItem, templateItemXML)
	
	templateItems.push(templateItem)

	return templateItem

End Function


Function newMRSSMediaTemplateItem(bsp As Object, templateItems As Object, templateItemXML As Object) As Object

	templateItem = { }
	
	newBaseTemplateItem(templateItem, templateItemXML)
		
	templateItems.push(templateItem)

	return templateItem

End Function


Sub newTemplateItem(bsp As Object, state As Object, templateItems As Object, templateItemXML As Object)

	name$ = templateItemXML.GetName()

	if name$ = "constantTextTemplateItem" then
		
		newConstantTextTemplateItem(bsp, templateItems, templateItemXML)

	else if name$ = "systemVariableTextTemplateItem" then
	
		newSystemVariableTextTemplateItem(bsp, templateItems, templateItemXML)

	else if name$ = "mediaCounterTemplateItem" then

		newMediaCounterTextTemplateItem(bsp, templateItems, templateItemXML)
	
	else if name$ = "userVariableTemplateItem" then

		newUserVariableTextTemplateItem(bsp, templateItems, templateItemXML)

	else if name$ = "indexedLiveTextDataEntryTemplateItem" then

		newIndexedLiveTextDataEntryTemplateItem(bsp, templateItems, templateItemXML)

	else if name$ = "titledLiveTextDataEntryTemplateItem" then

		newTitledLiveTextDataEntryTemplateItem(bsp, templateItems, templateItemXML)

	else if name$ = "imageTemplateItem" then

		newImageTemplateItem(bsp, templateItems, templateItemXML)

	else if name$ = "simpleRSSTextTemplateItem" then

		templateItem = newSimpleRSSTextTemplateItem(bsp, templateItems, templateItemXML)

		if type(state.simpleRSSTextTemplateItems) <> "roAssociativeArray" then
			state.simpleRSSTextTemplateItems = { }
		endif

		simpleRSS = state.simpleRSSTextTemplateItems.Lookup(templateItem.id$)
		if type(simpleRSS) <> "roAssociativeArray" then
			simpleRSS = { }
			simpleRSS.currentIndex% = 0
			simpleRSS.liveDataFeed = templateItem.liveDataFeed
			simpleRSS.displayTime% = int(val(templateItemXML.displayTime.GetText()))

			rssLiveDataFeedNamesXMLList = templateItemXML.GetNamedElements("rssLiveDataFeedName")
			if type(rssLiveDataFeedNamesXMLList) = "roXMLList" and rssLiveDataFeedNamesXMLList.Count() > 0 then
				simpleRSS.rssLiveDataFeeds = CreateObject("roArray", rssLiveDataFeedNamesXMLList.Count(), true)
				for each rssLiveDataFeedNameXML in rssLiveDataFeedNamesXMLList
					liveDataFeedName$ = CleanName(rssLiveDataFeedNameXML.GetText())
					liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedName$)
					if type(liveDataFeed) = "roAssociativeArray" then
						simpleRSS.rssLiveDataFeeds.push(liveDataFeed)
					endif
				next
			endif
			simpleRSS.currentLiveDataFeedIndex% = 0

			simpleRSS.items = CreateObject("roArray", 1, true)

			state.simpleRSSTextTemplateItems.AddReplace(templateItem.id$, simpleRSS)
		endif

		simpleRSS.items.push(templateItem)

	else if name$ = "mrssTextTemplateItem" then

		elementName$ = templateItemXML.elementName.GetText()

		templateItem = newMRSSTextTemplateItem(bsp, templateItems, templateItemXML)

		if elementName$ = "title" then
			state.mrssTitleTemplateItem = templateItem
		else if elementName$ = "description" then
			state.mrssDescriptionTemplateItem = templateItem
		else		' custom field
			if type(state.mrssCustomFieldTemplateItems) <> "roAssociativeArray" then
				state.mrssCustomFieldTemplateItems = {}
			endif
			state.mrssCustomFieldTemplateItems.AddReplace(elementName$, templateItem)

		endif

	else if name$ = "mrssImageTemplateItem" or name$ = "mrssMediaTemplateItem" then

		state.mrssMediaTemplateItem = newMRSSMediaTemplateItem(bsp, templateItems, templateItemXML)

	else if name$ = "customFields" then

		childElements = templateItemXML.GetChildElements()
		for each childElement in childElements
			newTemplateItem(bsp, state, state.templateItems, childElement)
		next

	endif

End Sub


Sub ParseTemplateBackgroundImageXML(bsp As Object, state As Object, playlistItemXML As Object)

    backgroundImageXML = playlistItemXML.backgroundImage
	if backgroundImageXML.Count() = 1 then
		backgroundImageFileElement = backgroundImageXML[0]
		backgroundImageAttributes = backgroundImageFileElement.GetAttributes()
		state.backgroundImage$ = backgroundImageAttributes.Lookup("name")

		if backgroundImageFileElement.HasAttribute("width") then
			state.backgroundImageWidth% = int(val(backgroundImageAttributes.Lookup("width")))
		else
			state.backgroundImageWidth% = -1
		endif

		if backgroundImageFileElement.HasAttribute("height") then
			state.backgroundImageHeight% = int(val(backgroundImageAttributes.Lookup("height")))
		else
			state.backgroundImageHeight% = -1
		endif

	else
		state.backgroundImage$ = ""
	endif
	state.backgroundImageUserVariable = bsp.GetUserVariable(state.backgroundImage$)

End Sub


Sub SetTemplateHandlers(state As Object)

    state.HStateEventHandler = STTemplatePlayingEventHandler
    state.MediaItemEventHandler = MediaItemEventHandler
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    state.PreloadItem = PreloadItem
	state.SetBackgroundImageSizeLocation = SetBackgroundImageSizeLocation
	state.ScaleBackgroundImageToFit = ScaleBackgroundImageToFit

	state.SetupTemplateMRSS = SetupTemplateMRSS
	state.FindMRSSContent = FindMRSSContent
	state.GetNextMRSSTemplateItem = GetNextMRSSTemplateItem
	state.GetMRSSTemplateItem = GetMRSSTemplateItem
	state.ClearTemplateItems = ClearTemplateItems
	state.RedisplayTemplateItems = RedisplayTemplateItems
	state.BuildTemplateItems = BuildTemplateItems
	state.BuildTemplateItem = BuildTemplateItem
	state.BuildTextTemplateItem = BuildTextTemplateItem
	state.TemplateUsesAnyUserVariable = TemplateUsesAnyUserVariable
	state.TemplateUsesUserVariable = TemplateUsesUserVariable
	state.TemplateUsesSystemVariable = TemplateUsesSystemVariable

	state.PlayVideo = PlayVideo
	state.SetVideoTimeCodeEvents = SetVideoTimeCodeEvents

	state.LaunchWaitForContentTimer = LaunchWaitForContentTimer

End Sub


Sub newTemplatePlaylistItem(bsp As Object, playlistItemXML As Object, state As Object)

	ParseTemplateBackgroundImageXML(bsp, state, playlistItemXML)

	' retrieve mrss live data feed names
	mrssLiveDataFeedNamesXMLList = playlistItemXML.GetNamedElements("mrssLiveDataFeedName")
	if type(mrssLiveDataFeedNamesXMLList) = "roXMLList" and mrssLiveDataFeedNamesXMLList.Count() > 0 then
		state.mrssActive = true
		state.mrssLiveDataFeeds = CreateObject("roArray", mrssLiveDataFeedNamesXMLList.Count(), true)
		for each mrssLiveDataFeedNameXML in mrssLiveDataFeedNamesXMLList
			liveDataFeedName$ = CleanName(mrssLiveDataFeedNameXML.GetText())
			liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedName$)
			if type(liveDataFeed) = "roAssociativeArray" then
				state.mrssLiveDataFeeds.push(liveDataFeed)
			endif
		next
	else
		state.mrssActive = false
	endif

	' retrieve template items
	state.templateItems = CreateObject("roArray", 1, true)	
	childElements = playlistItemXML.GetChildElements()
	for each childElement in childElements
		newTemplateItem(bsp, state, state.templateItems, childElement)
	next

	SetTemplateHandlers(state)

End Sub


Sub newTemplatePlaylistItemFromLiveTextPlaylistItem(bsp As Object, playlistItemXML As Object, state As Object)

	ParseTemplateBackgroundImageXML(bsp, state, playlistItemXML)

	state.mrssActive = false

	' compatibility with old data format
	liveDataFeedUpdateInterval% = 600
	if playlistItemXML.liveTextRSSUpdateInterval.GetText() <> "" then
		liveDataFeedUpdateInterval% = int(val(playlistItemXML.liveTextRSSUpdateInterval.GetText()))
	endif

    textItemsXML = playlistItemXML.textItem

	textItems = CreateObject("roArray", 1, true)
    for each textItemXML in textItemsXML
		newTextItem(bsp, textItems, textItemXML, liveDataFeedUpdateInterval%)
    next

	state.templateItems = CreateObject("roArray", 1, true)	

	for each textItem in textItems

		templateItem = { }	
		newTextTemplateItemFromTextItem(templateItem, textItem)
		
		if textItem.textType$ = "constant" then
			templateItem.type$ = "constantTextTemplateItem"
			templateItem.textString$ = textItem.textString$
		else if textItem.textType$ = "system" then
			templateItem.type$ = "systemVariableTextTemplateItem"
			templateItem.systemVariableType$ = textItem.systemVariableType$
		else if textItem.textType$ = "indexedLiveTextData" then
			templateItem.type$ = "indexedLiveTextDataEntryTemplateItem"
			templateItem.liveDataFeed = textItem.liveDataFeed

			' old style index was zero based; new style is 1 based.
			index% = textItem.index%
			index% = index% + 1
			index$ = StripLeadingSpaces(str(index%))
			templateItem.index = newTextParameterValue(index$)
		else if textItem.textType$ = "titledLiveTextData" then
			templateItem.type$ = "titledLiveTextDataEntryTemplateItem"
			templateItem.liveDataFeed = textItem.liveDataFeed
			templateItem.title = newTextParameterValue(textItem.title$)
		else if textItem.textType$ = "mediaCounter" then
			templateItem.type$ = "mediaCounterTemplateItem"
			templateItem.userVariable = textItem.userVariable
		else if textItem.textType$ = "userVariable" then
			templateItem.type$ = "userVariableTemplateItem"
			templateItem.userVariable = textItem.userVariable
		endif

		state.templateItems.push(templateItem)

	next

	SetTemplateHandlers(state)

End Sub


Sub newBaseTemplateItemFromTextItem(templateItem As Object, textItem As Object)

	templateItem.x% = textItem.x%
	templateItem.y% = textItem.y%
	templateItem.width% = textItem.width%
	templateItem.height% = textItem.height%
	templateItem.layer% = 1

End Sub


Sub newTextTemplateItemFromTextItem(templateItem As Object, textItem As Object)

	' fill in base class members
	newBaseTemplateItemFromTextItem(templateItem, textItem)

	' text widget items
	templateItem.numberOfLines% = textItem.numberOfLines%
	templateItem.rotation$ = textItem.rotation$
	templateItem.alignment$ = textItem.alignment$

	' widget items
	templateItem.foregroundTextColor$ = textItem.foregroundTextColor$
	templateItem.backgroundTextColor$ = textItem.backgroundTextColor$
	templateItem.font$ = textItem.font$
	templateItem.fontSize% = textItem.fontSize%

	templateItem.backgroundColorSpecified = textItem.backgroundColorSpecified

End Sub


Sub newTextItem(bsp As Object, textItems As Object, textItemXML As Object, liveDataFeedUpdateInterval% As Integer)

	textItem = CreateObject("roAssociativeArray")
	
	textItem.x% = int(val(textItemXML.x.GetText()))
	textItem.y% = int(val(textItemXML.y.GetText()))
	textItem.width% = int(val(textItemXML.width.GetText()))
	textItem.height% = int(val(textItemXML.height.GetText()))

	if textItemXML.textItemConstant.Count() = 1 then
		textItem.textType$ = "constant"
		textItemConstantElement = textItemXML.textItemConstant[0]
		textItem.textString$ = textItemConstantElement.text.GetText()
	else if textItemXML.textItemSystemVariable.Count() = 1 then
		textItem.textType$ = "system"
		textItemSystemVariableElement = textItemXML.textItemSystemVariable[0]
		textItem.systemVariableType$ = textItemSystemVariableElement.systemVariable.GetText()
	else if textItemXML.textItemIndexedLiveTextDataEntry.Count() = 1 then
		textItem.textType$ = "indexedLiveTextData"
		textItemIndexedLiveTextDataEntryElement = textItemXML.textItemIndexedLiveTextDataEntry[0]
		index$ = textItemIndexedLiveTextDataEntryElement.Index.GetText()
		textItem.index% = int(val(index$))
		if textItemIndexedLiveTextDataEntryElement.liveDataFeedName.GetText() <> "" then
			liveDataFeedName$ = CleanName(textItemIndexedLiveTextDataEntryElement.liveDataFeedName.GetText())
			textItem.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedName$)
		else
			if type(textItemIndexedLiveTextDataEntryElement.LiveTextDataUrlSpec) = "roXMLList" and textItemIndexedLiveTextDataEntryElement.LiveTextDataUrlSpec.Count() = 1 then
				url = newParameterValue(bsp, textItemIndexedLiveTextDataEntryElement.LiveTextDataUrlSpec.parameterValue)
			else
				liveTextDataUrl$ = textItemIndexedLiveTextDataEntryElement.LiveTextDataUrl.GetText()
				url = newTextParameterValue(liveTextDataUrl$)
			endif
			liveDataFeed = newLiveDataFeedFromOldDataFormat(bsp, CleanName(url), liveDataFeedUpdateInterval%)
			bsp.liveDataFeeds.AddReplace(liveDataFeed.name$, liveDataFeed)
		endif
	else if textItemXML.textItemTitledLiveTextDataEntry.Count() = 1 then
		textItem.textType$ = "titledLiveTextData"
		textItemTitledLiveTextDataEntryElement = textItemXML.textItemTitledLiveTextDataEntry[0]
		textItem.title$ = textItemTitledLiveTextDataEntryElement.Title.GetText()
		if textItemTitledLiveTextDataEntryElement.liveDataFeedName.GetText() <> "" then
			liveDataFeedName$ = CleanName(textItemTitledLiveTextDataEntryElement.liveDataFeedName.GetText())
			textItem.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedName$)
		else
			if type(textItemTitledLiveTextDataEntryElement.LiveTextDataUrlSpec) = "roXMLList" and textItemTitledLiveTextDataEntryElement.LiveTextDataUrlSpec.Count() = 1 then
				url = newParameterValue(bsp, textItemTitledLiveTextDataEntryElement.LiveTextDataUrlSpec.parameterValue)
			else
				liveTextDataUrl$ = textItemTitledLiveTextDataEntryElement.LiveTextDataUrl.GetText()
				url = newTextParameterValue(liveTextDataUrl$)
			endif
			liveDataFeed = newLiveDataFeedFromOldDataFormat(bsp, CleanName(url), liveDataFeedUpdateInterval%)
			bsp.liveDataFeeds.AddReplace(liveDataFeed.name$, liveDataFeed)
		endif
	else if textItemXML.textItemMediaCounter.Count() = 1 then
		textItem.textType$ = "mediaCounter"
		fileName$ = textItemXML.textItemMediaCounter.fileName.GetText()
		textItem.userVariable = bsp.GetUserVariable(fileName$)
		if type(textItem.userVariable) <> "roAssociativeArray" then
		    bsp.diagnostics.PrintDebug("Media counter variable " + fileName$ + " not found.")
		    bsp.logging.WriteDiagnosticLogEntry(bsp.diagnosticCodes.EVENT_MEDIA_COUNTER_VARIABLE_NOT_FOUND, fileName$)
		endif
	else if textItemXML.textItemUserVariable.Count() = 1 then
		textItem.textType$ = "userVariable"
		userVariableName$ = textItemXML.textItemUserVariable.name.GetText()
		textItem.userVariable = bsp.GetUserVariable(userVariableName$)
		if type(textItem.userVariable) <> "roAssociativeArray" then
		    bsp.diagnostics.PrintDebug("User variable " + userVariableName$ + " not found.")
		    bsp.logging.WriteDiagnosticLogEntry(bsp.diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND, fileName$)
		endif
	else
		stop
	endif

	ParseTemplateWidgets(textItem, textItemXML)

	textItems.push(textItem)

End Sub

	
Sub newAudioInPlaylistItem(bsp As Object, playlistItemXML As Object, state As Object)
	
    file = playlistItemXML.file
    fileAttrs = file.GetAttributes()
    state.imageFileName$ = fileAttrs["name"]
	state.imageUserVariable = bsp.GetUserVariable(state.imageFileName$)

	state.useImageBuffer = false
	useImageBuffer$ = playlistItemXML.useImageBuffer.GetText()
	if len(useImageBuffer$) > 0 then
		useImageBuffer$ = lcase(useImageBuffer$)
		if useImageBuffer$ = "true" then
			state.useImageBuffer = true
		endif
	endif

    state.HStateEventHandler = STAudioInPlayingEventHandler
    state.MediaItemEventHandler = MediaItemEventHandler
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons

End Sub


Sub newSignChannelPlaylistItem(bsp As Object, playlistItemXML As Object, state As Object)

	state.slideTransition% = 15

	signChannelRevision$ = "315"
	url$ = "http://rss.signchannel.com/productId=RK" + bsp.sysInfo.deviceModel$ + "/frameId=" + bsp.sysInfo.deviceUniqueID$ + "/version=" + signChannelRevision$

	liveDataFeedName$ = "signChannel"
	liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedName$)
	if type(liveDataFeed) <> "roAssociativeArray" then
		liveDataFeed = newLiveDataFeedFromMRSSFormat( bsp, liveDataFeedName$, url$, true )
		bsp.liveDataFeeds.AddReplace(liveDataFeed.name$, liveDataFeed)
	endif

	state.liveDataFeed = liveDataFeed

	SetMRSSHandlers( state )

End Sub


Sub newMRSSPlaylistItem(bsp As Object, zoneHSM As Object, playlistItemXML As Object, state As Object)

	state.slideTransition% = 0

	liveDataFeedName$ = CleanName( playlistItemXML.liveDataFeedName.GetText() )
	if liveDataFeedName$ <> "" then
		state.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedName$)
		if state.liveDataFeed <> invalid and lcase(state.liveDataFeed.usage$) = "mrsswith4k" then
			zoneHSM.useVideoPlayerForImages = true
		endif
	endif

	videoPlayerRequired$ = playlistItemXML.videoPlayerRequired.GetText()
	if len(videoPlayerRequired$) > 0 then
		videoPlayerRequired$ = lcase(videoPlayerRequired$)
		if videoPlayerRequired$ = "true" then
			zoneHSM.useVideoPlayerForImages = true
		endif
	endif

	SetMRSSHandlers( state )

End Sub


Sub newRSSImagePlaylistItem(bsp As Object, playlistItemXML As Object, state As Object)

	state.slideTransition% = 0

    rssSpec = playlistItemXML.rssSpec
    rssSpecAttrs = rssSpec.GetAttributes()

	isDynamicPlaylist = false
	if rssSpecAttrs.DoesExist("usesBSNDynamicPlaylist") then
		usesDynamicPlaylist = rssSpecAttrs.Lookup("usesBSNDynamicPlaylist")
		if lcase(usesDynamicPlaylist) = "true" then
			isDynamicPlaylist = true
		endif
	endif
    
	if rssSpecAttrs.DoesExist("url") then
		url$ = rssSpecAttrs["url"]
	else
		url = newParameterValue(bsp, playlistItemXML.urlSpec.parameterValue)
		url$ = url.GetCurrentParameterValue()
	endif

	liveDataFeedName$ = CleanName( url$ )
	liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedName$)
	if type(liveDataFeed) <> "roAssociativeArray" then
		liveDataFeed = newLiveDataFeedFromMRSSFormat( bsp, liveDataFeedName$, url$, isDynamicPlaylist )
		bsp.liveDataFeeds.AddReplace(liveDataFeed.name$, liveDataFeed)
	endif

	state.liveDataFeed = liveDataFeed

	SetMRSSHandlers( state )

End Sub


Sub	newLocalPlaylistItem(bsp As Object, playlistItemXML As Object, state As Object)

	state.slideTransition% = 0

    attrs = playlistItemXML.defaultPlaylist.mediaRSSSpec.GetAttributes()
	if attrs = invalid then
		' new format
		liveDataFeedName$ = playlistItemXML.defaultPlaylistDataFeedName.GetText()

		devicePlaylists = playlistItemXML.devicePlaylist
		for each devicePlaylist in devicePlaylists
			deviceName$ = devicePlaylist.deviceName.GetText()
			deviceLiveDataFeedName$ = devicePlaylist.dynamicPlaylistDataFeedName.GetText()
			if deviceName$ = bsp.sysInfo.deviceUniqueID$ then
				liveDataFeedName$ = CleanName(deviceLiveDataFeedName$)
				exit for
			endif
		next

		if liveDataFeedName$ <> "" then
			state.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedName$)
		endif
	else
		url$ = attrs["url"]
		name$ = attrs["name"]

		devicePlaylists = playlistItemXML.devicePlaylist
		for each devicePlaylist in devicePlaylists
			deviceName$ = devicePlaylist.deviceName.GetText()
			attrs = devicePlaylist.dynamicPlaylist.mediaRSSSpec.GetAttributes()
			dynamicPlaylistUrl$ = attrs["url"]
			if deviceName$ = bsp.sysInfo.deviceUniqueID$ then
				url$ = dynamicPlaylistUrl$
				name$ = attrs["name"]
				exit for
			endif
		next

		liveDataFeedName$ = CleanName(name$)
		liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedName$)
		if type(liveDataFeed) <> "roAssociativeArray" then
			liveDataFeed = newLiveDataFeedFromMRSSFormat( bsp, liveDataFeedName$, url$, true )
			bsp.liveDataFeeds.AddReplace(liveDataFeed.name$, liveDataFeed)
		endif
		state.liveDataFeed = liveDataFeed
	endif

	SetMRSSHandlers( state )

End Sub


Sub SetMRSSHandlers( state As Object )

    state.HStateEventHandler = STPlayingMediaRSSEventHandler
    state.MediaItemEventHandler = MediaItemEventHandler
    state.ExecuteTransition = ExecuteTransition
    state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
    state.PreloadItem = PreloadItem
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    
	state.LaunchWaitForContentTimer = LaunchWaitForContentTimer
	state.ProtectMRSSFeed = ProtectMRSSFeed
	state.ProtectMRSSItem = ProtectMRSSItem
	state.DisplayMRSSItem = DisplayMRSSItem
	state.AdvanceToNextMRSSItem = AdvanceToNextMRSSItem
	state.AtEndOfFeed = AtEndOfFeed
	state.GetHtmlWidgetFilePath = GetHtmlWidgetFilePath

	state.DisplayImage = DisplayImage
	state.PreDrawImage = PreDrawImage
	state.DrawImage = DrawImage
	state.PostDrawImage = PostDrawImage
	state.ClearVideo = ClearVideo
	state.LaunchVideo = LaunchVideo
	state.PrePlayVideo = PrePlayVideo
	state.PlayVideo = PlayVideo
	state.PostPlayVideo = PostPlayVideo
	state.SetVideoTimeCodeEvents = SetVideoTimeCodeEvents

	state.GetAnyMediaRSSTransition = GetAnyMediaRSSTransition

End Sub


Sub newTripleUSBPlaylistItem(playlistItemXML As Object, sign As Object, state As Object)

	state.boseProductName$ = playlistItemXML.boseProductName.GetText()
	state.noiseThreshold% = int(val(playlistItemXML.noiseThreshold.GetText()))
    state.tripleUSBPort$ = sign.tripleUSBPort$
    
	boseProductSpec = state.bsp.GetBoseProductSpec(state.boseProductName$)
	if type(boseProductSpec) = "roAssociativeArray" then
	    boseProductInSign = sign.boseProducts.Lookup(state.boseProductName$)
	    state.boseProductPort$ = boseProductInSign.port$
	    state.volumeTable = boseProductSpec.volumeTable
	else
		state.boseProductPort$ = ""
		state.volumeTable = invalid
    endif
    
	state.HStateEventHandler = STTripleUSBEventHandler
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
	state.SendVolumeCommand = SendVolumeCommand

End Sub


Function GetBoolFromXML(xmlValue$ As String) As Boolean

	value$ = lcase(xmlValue$)
	if value$ = "true" then
		return true
	else
		return false
	endif

End Function


Sub SetStateEvent(bsp As Object, stateEvent as Object, eventXML As Object)

	userEventsList = eventXML.userEvent
	if userEventsList.Count() = 0 then print "Invalid XML file - userEvent not found" : stop
	for each userEvent in userEventsList

		userEventName$ = userEvent.name.GetText()

		if userEventName$ = "gpioUserEvent" then
			if userEvent.parameters.buttonNumber.GetText() <> "" then
				stateEvent.gpioUserEventButtonNumber$ = userEvent.parameters.buttonNumber.GetText()
			else
				stateEvent.gpioUserEventButtonNumber$ = userEvent.parameters.parameter.GetText()        
			endif
			bsp.ConfigureGPIOInput(stateEvent.gpioUserEventButtonNumber$)
		else if userEventName$ = "bp900AUserEvent" or userEventName$ = "bp900BUserEvent" or userEventName$ = "bp900CUserEvent" or userEventName$ = "bp900DUserEvent" or userEventName$ = "bp200AUserEvent" or userEventName$ = "bp200BUserEvent" or userEventName$ = "bp200CUserEvent" or userEventName$ = "bp200DUserEvent" then
			stateEvent.bpUserEventButtonNumber$ = userEvent.parameters.buttonNumber.GetText()
			stateEvent.bpUserEventButtonPanelIndex$ = userEvent.parameters.buttonPanelIndex.GetText()
			bpIndex% = int(val(stateEvent.bpUserEventButtonPanelIndex$))
			bsp.ConfigureBPInput(bpIndex%, stateEvent.bpUserEventButtonNumber$)
		else if userEventName$ = "serial" then
			port$ = userEvent.parameters.parameter.GetText()
			if bsp.usbDevicesByConnector.DoesExist(port$) then	' convert USB port if necessary
				port$ = bsp.usbDevicesByConnector.Lookup(port$)
			endif   
			stateEvent.serialUserEventPort$ = port$
			stateEvent.serialUserEventSerialEvent$ = userEvent.parameters.parameter2.GetText()
			bsp.CreateSerial(bsp, stateEvent.serialUserEventPort$, false)
		else if userEventName$ = "udp" then
			stateEvent.udpUserEvent$ = userEvent.parameters.parameter.GetText()
			stateEvent.udpLabel$ = userEvent.parameters.label.GetText()
			if stateEvent.udpLabel$ = "" then
				stateEvent.udpLabel$ = stateEvent.udpUserEvent$
			endif

			export$ = LCase(userEvent.parameters.export.GetText())
			if export$ = "true" then
				stateEvent.udpExport = true
			else
				stateEvent.udpExport = false
			endif
		else if userEventName$ = "keyboard" then
			stateEvent.keyboardUserEvent$ = userEvent.parameters.parameter.GetText()
		else if userEventName$ = "zoneMessage" then
			stateEvent.zoneMessageUserEvent$ = userEvent.parameters.parameter.GetText()
		else if userEventName$ = "remote" then
			stateEvent.remoteUserEvent$ = ucase(userEvent.parameters.parameter.GetText())
	        if type(bsp.remote) <> "roIRRemote" then
		        bsp.remote = CreateObject("roIRRemote")
			    bsp.remote.SetPort(bsp.msgPort)
			endif
		else if userEventName$ = "synchronize" then
			stateEvent.synchronizeUserEvent$ = userEvent.parameters.parameter.GetText()
			bsp.IsSyncSlave = true
		endif

	next

End Sub


Function SetMediaListSynchronize(bsp As Object, navigation As Object, playlistItemXML As Object, brightSignCmds As String)

	navigation.sendSynchronizeCommand = false
	brightSignCmdsXML = playlistItemXML.GetNamedElements(brightSignCmds).brightSignCmd
	if type(brightSignCmdsXML) = "roXMLList" and brightSignCmdsXML.Count() > 0 then
		cmds = CreateObject("roArray", 1, true)
		for each brightSignCmdXML in brightSignCmdsXML
		    newCmd(bsp, brightSignCmdXML, cmds)
		next

		bsp.IsSyncMaster = true

		navigation.sendSynchronizeCommand = true
		navigation.synchronizeKeyword = cmds[0].parameters["synchronizeKeyword"].getCurrentParameterValue()
	endif

End Function


Sub newMediaListPlaylistItem(bsp As Object, zoneHSM As Object, playlistItemXML As Object, state As Object)

	if zoneHSM.type$ <> "EnhancedAudio" then

		if type(bsp.mediaListInactivity) <> "roAssociativeArray" then
			bsp.mediaListInactivity = CreateObject("roAssociativeArray")
			bsp.mediaListInactivity.mediaListStates = CreateObject("roList")
		endif

		bsp.mediaListInactivity.mediaListStates.AddTail(state)
	
	endif
		
	state.mediaType$ = playlistItemXML.mediaType.GetText()
	
	state.advanceOnMediaEnd = GetBoolFromXML(playlistItemXML.advanceOnMediaEnd.GetText())
	state.advanceOnImageTimeout = GetBoolFromXML(playlistItemXML.advanceOnImageTimeout.GetText())
	state.playFromBeginning = GetBoolFromXML(playlistItemXML.playFromBeginning.GetText())
	state.shuffle = GetBoolFromXML(playlistItemXML.shuffle.GetText())

	if lcase(playlistItemXML.support4KImages.getText()) = "true" then
		zoneHSM.useVideoPlayerForImages = true
	endif

	if playlistItemXML.slideTransition.GetText() = "" then
		slideTransition$ = "No effect"
	else
		slideTransition$ = playlistItemXML.slideTransition.GetText()
	endif
    state.slideTransition% = GetSlideTransitionValue(slideTransition$)

    state.transitionDuration% = 1000
	if playlistItemXML.transitionDuration.GetText() <> "" then
		state.transitionDuration% = int(val(playlistItemXML.transitionDuration.GetText()))
	endif

	state.sendZoneMessage = GetBoolFromXML(playlistItemXML.sendZoneMessage.GetText())

	if playlistItemXML.startIndex.GetText() = "" then
		state.specifiedStartIndex% = 0
	else
		state.specifiedStartIndex% = int(val(playlistItemXML.startIndex.GetText())) - 1
	endif
	state.startIndex% = state.specifiedStartIndex%

	state.populateFromMediaLibrary = true
	populateFromMediaLibrary = playlistItemXML.populateFromMediaLibrary.GetText()
	if lcase(populateFromMediaLibrary) = "false" then
		state.populateFromMediaLibrary = false
	endif

	liveDataFeedName$ = playlistItemXML.liveDataFeedName.GetText()
	if liveDataFeedName$ <> "" then
		state.liveDataFeed = bsp.liveDataFeeds.Lookup(CleanName(liveDataFeedName$))
	else
		state.liveDataFeed = invalid
	endif

	imageTimeout$ = lcase(playlistItemXML.imageTimeout.GetText())
	if imageTimeout$ = "" then
		state.imageTimeout = 5000
	else
		state.imageTimeout = val(imageTimeout$) * 1000
	endif

	mediaListStateInactivityTimeout = lcase(playlistItemXML.mediaListStateInactivityTimeout.GetText())
	if mediaListStateInactivityTimeout = "true" then
		state.mediaListStateInactivityTimeout = true
	else
		state.mediaListStateInactivityTimeout = false
	endif

	mediaListStateInactivityTime = playlistItemXML.mediaListStateInactivityTime.GetText()
	if len(mediaListStateInactivityTime) > 0 then
		state.mediaListStateInactivityTime% = int(val(mediaListStateInactivityTime))
	else
		state.mediaListStateInactivityTime% = 0
	endif
	
	if type(playlistItemXML.next) = "roXMLList" and playlistItemXML.next.Count() = 1 then
		state.nextNavigation = CreateObject("roAssociativeArray")
		SetStateEvent(bsp, state.nextNavigation, playlistItemXML.next)
	endif
		
	if type(playlistItemXML.previous) = "roXMLList" and playlistItemXML.previous.Count() = 1 then
		state.previousNavigation = CreateObject("roAssociativeArray")
		SetStateEvent(bsp, state.previousNavigation, playlistItemXML.previous)
	endif

' parse BrightSignCmdsTransitionNextItem
	state.transitionNextItemCmds = CreateObject("roArray", 1, true)
	transitionNextItemCmds = playlistItemXML.brightSignCmdsTransitionNextItem.brightSignCmd
    if transitionNextItemCmds.Count() > 0 then
        for each cmd in transitionNextItemCmds
            newCmd(bsp, cmd, state.transitionNextItemCmds)
        next
    endif

' parse BrightSignCmdsTransitionPrevItem
	state.transitionPrevItemCmds = CreateObject("roArray", 1, true)
	transitionPrevItemCmds = playlistItemXML.brightSignCmdsTransitionPrevItem.brightSignCmd
    if transitionPrevItemCmds.Count() > 0 then
        for each cmd in transitionPrevItemCmds
            newCmd(bsp, cmd, state.transitionPrevItemCmds)
        next
    endif
		
	if state.mediaType$ = "image" or state.mediaType$ = "allMedia" then
		zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
	endif

	if state.populateFromMediaLibrary then

		childElements = playlistitemXML.files.getChildElements()
		state.numItems% = childElements.Count()
		state.items = CreateObject("roArray", state.numItems%, true)

		for each childElement in childElements
			item = {}
			if childElement.getName() = "videoItem" then
				newVideoPlaylistItem(bsp, childElement, state, item)
				item.type = "video"
			else if childElement.getName() = "imageItem" then
				newImagePlaylistItem(bsp, childElement, zoneHSM, state, item)
				item.type = "image"
			else if childElement.getName() = "audioItem" then
				newAudioPlaylistItem(bsp, childElement, state, item)
				item.type = "audio"
			endif

			state.items.push(item)
		next

		state.playbackIndices = CreateObject("roArray", state.numItems%, true)
		for i% = 0 to state.numItems%-1
			state.playbackIndices[i%] = i%
		next

	else

		state.numItems% = 0

	endif

	state.playbackActive = false
	state.playbackIndex% = state.startIndex%

    state.HStateEventHandler = STDisplayingMediaListItemEventHandler
	state.PopulateMediaListFromLiveDataFeed = PopulateMediaListFromLiveDataFeed
	state.ShuffleMediaListContent = ShuffleMediaListContent
	state.ConfigureIntraStateEventHandlerButton = ConfigureIntraStateEventHandlerButton
	state.PrePlayAudio = PrePlayAudio
	state.PlayAudio = PlayAudio
	state.PostPlayAudio = PostPlayAudio
	state.PlayMixerAudio = PlayMixerAudio
	state.PrePlayVideo = PrePlayVideo
	state.PlayVideo = PlayVideo
	state.PostPlayVideo = PostPlayVideo
	state.PreDrawImage = PreDrawImage
	state.DrawImage = DrawImage
	state.PostDrawImage = PostDrawImage
	state.ClearVideo = ClearVideo
	state.StartInactivityTimer = StartInactivityTimer
	state.HandleIntraStateEvent = HandleIntraStateEvent
	state.LaunchMediaListPlaybackItem = LaunchMediaListPlaybackItem
	state.AdvanceMediaListPlayback = AdvanceMediaListPlayback
	state.RetreatMediaListPlayback = RetreatMediaListPlayback
	
    state.AddAudioTimeCodeEvent = AddAudioTimeCodeEvent
    state.SetAudioTimeCodeEvents = SetAudioTimeCodeEvents

    state.AddVideoTimeCodeEvent = AddVideoTimeCodeEvent
    state.SetVideoTimeCodeEvents = SetVideoTimeCodeEvents

    state.MediaItemEventHandler = MediaItemEventHandler
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
    state.PreloadItem = PreloadItem

	state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
	
	state.AtEndOfMediaList = AtEndOfMediaList

End Sub


Sub newPlayFilePlaylistItem(bsp As Object, playlistItemXML As Object, state As Object)

	state.filesTable = CreateObject("roAssociativeArray")

	state.mediaType$ = playlistItemXML.mediaType.GetText()

    state.slideTransition% = GetSlideTransitionValue(playlistItemXML.slideTransition.GetText())

	state.payload$ = ""

	state.specifyLocalFiles = GetBoolFromXML(playlistItemXML.specifyLocalFiles.GetText())

	state.useDefaultMedia = false
	if lcase(playlistItemXML.useDefaultMedia.GetText()) = "true" then
		state.useDefaultMedia = true
	    state.defaultMediaFileName$ = playlistItemXML.defaultMediaFileName.GetText()
	endif

	state.useUserVariable = false
	if lcase(playlistItemXML.useUserVariable.GetText()) = "true" then
		state.useUserVariable = true
		state.userVariable = bsp.GetUserVariable(playlistItemXML.userVariable.name.GetText())
		if type(state.userVariable) <> "roAssociativeArray" then
			state.useUserVariable = false
		endif
	endif

	liveDataFeedName$ = playlistItemXML.liveDataFeedName.GetText()
	if liveDataFeedName$ <> "" then
		state.liveDataFeed = bsp.liveDataFeeds.Lookup(CleanName(liveDataFeedName$))
	else
		state.liveDataFeed = invalid
	endif

	if state.specifyLocalFiles then

		files = playlistItemXML.filesTable.file
		if playlistItemXML.filesTable.file.Count() > 0 then
			for each file in files
				fileAttrs = file.GetAttributes()
				key$ = fileAttrs["key"]
				if fileAttrs.DoesExist("label") then
					label$ = fileAttrs["label"]
				else
					label$ = key$
				endif
				if fileAttrs.DoesExist("export") and LCase(fileAttrs["export"]) <> "true" then
					export = false
				else
					export = true
				endif

				fileTableEntry = CreateObject("roAssociativeArray")
				fileTableEntry.label$ = label$
				fileTableEntry.export = export
				fileTableEntry.fileName$ = fileAttrs["name"]
				fileTableEntry.fileType$ = fileAttrs["type"]
				if fileTableEntry.fileType$ = "video" or fileTableEntry.fileType$ = "audio" then
					fileTableEntry.probeData = GetProbeData(bsp.assetPoolFiles, fileTableEntry.fileName$)
				endif
				fileTableEntry.userVariable = bsp.GetUserVariable(fileTableEntry.fileName$)
				fileTableEntry.automaticallyLoop = true
				fileTableEntry.isEncrypted = false
				fileTableEntry.videoDisplayMode% = 0
				videoDisplayMode$ = fileAttrs["videoDisplayMode"]
				if videoDisplayMode$ = "3DSBS" then
					fileTableEntry.videoDisplayMode% = 1
				else if videoDisplayMode$ = "3DTOB" then
					fileTableEntry.videoDisplayMode% = 2
				endif
				state.filesTable.AddReplace(key$, fileTableEntry)
			next
		endif

	endif

    state.HStateEventHandler = STPlayFileEventHandler
	state.PopulatePlayFileFromLiveDataFeed = PopulatePlayFileFromLiveDataFeed
	state.MediaItemEventHandler = MediaItemEventHandler
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
	state.DisplayImage = DisplayImage
	state.PreDrawImage = PreDrawImage
	state.DrawImage = DrawImage
	state.PostDrawImage = PostDrawImage
	state.ClearVideo = ClearVideo
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    state.LaunchVideo = LaunchVideo
	state.PrePlayVideo = PrePlayVideo
	state.PlayVideo = PlayVideo
	state.PostPlayVideo = PostPlayVideo
    state.SetVideoTimeCodeEvents = SetVideoTimeCodeEvents
	state.LaunchAudio = LaunchAudio
	state.PrePlayAudio = PrePlayAudio
	state.PlayAudio = PlayAudio
	state.PostPlayAudio = PostPlayAudio
    state.SetAudioTimeCodeEvents = SetAudioTimeCodeEvents
    state.PreloadItem = PreloadItem

End Sub


Sub newStreamPlaylistItem(bsp As Object, playlistItemXML As Object, state As Object)

	streamSpecAttrs = playlistItemXML.streamSpec.GetAttributes()

	if type(streamSpecAttrs["url"]) = "roString" then
		state.url = newTextParameterValue(streamSpecAttrs["url"])
	else	
		state.url = newParameterValue(bsp, playlistItemXML.url.parameterValue)
	endif

    state.HStateEventHandler = STStreamPlayingEventHandler
	state.MediaItemEventHandler = MediaItemEventHandler
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    state.LaunchTimer = LaunchTimer
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames

End Sub


Sub newMjpegStreamPlaylistItem(bsp As Object, playlistItemXML As Object, state As Object)

	mjpegSpecAttrs = playlistItemXML.mjpegSpec.GetAttributes()

	if type(mjpegSpecAttrs["url"]) = "roString" then
		state.url = newTextParameterValue(mjpegSpecAttrs["url"])
	else	
		state.url = newParameterValue(bsp, playlistItemXML.url.parameterValue)
	endif

	state.rotation% = int(val(mjpegSpecAttrs["rotation"]))

    state.HStateEventHandler = STMjpegPlayingEventHandler
	state.MediaItemEventHandler = MediaItemEventHandler
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    state.LaunchTimer = LaunchTimer
	state.ExecuteTransition = ExecuteTransition
	state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames

End Sub


Sub newAudioPlaylistItem(bsp As Object, playlistItemXML As Object, state As Object, playlistItemBS As Object)

    newMediaPlaylistItem(bsp, playlistItemXML, state, playlistItemBS)

	playlistItemBS.probeData = GetProbeData(bsp.assetPoolFiles, playlistItemBS.fileName$)

    itemVolume$ = playlistItemXML.volume.GetText()
    if itemVolume$ <> "" then
        playlistItemBS.volume% = int(val(itemVolume$))
    endif
    
    state.HStateEventHandler = STAudioPlayingEventHandler
    state.AddAudioTimeCodeEvent = AddAudioTimeCodeEvent
    state.SetAudioTimeCodeEvents = SetAudioTimeCodeEvents
	state.LaunchAudio = LaunchAudio
	state.PrePlayAudio = PrePlayAudio
	state.PlayAudio = PlayAudio
	state.PostPlayAudio = PostPlayAudio
	state.LaunchMixerAudio = LaunchMixerAudio
	state.PlayMixerAudio = PlayMixerAudio
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    
End Sub


Function newUserVariablePlaylistItem(bsp As Object, zoneHSM As Object, playlistItemXML As Object) As Object

	item = {}

    item.textStrings = []

	item.userVariableName = playlistItemXML.userVariableName.GetText()

	userVariables = bsp.currentUserVariables

	for each userVariableKey in userVariables
		userVariable = userVariables.Lookup(userVariableKey)
		if userVariable.name$ = item.userVariableName then
			tickerItem = userVariable.GetCurrentValue()
			item.textStrings.push(tickerItem)
		endif
	next

    item.isRSSFeed = false
   	item.isUserVariable = true

	return item

End Function


Function newTextPlaylistItem(playlistItemXML As Object) As Object

	item = CreateObject("roAssociativeArray")
	
    strings = playlistItemXML.strings

    numTextStrings% = 0
    if strings <> invalid then
        children = strings.GetChildElements()
        if children <> invalid then
            numTextStrings% = children.Count()
        end if
    end if

    item.textStrings = CreateObject("roArray", numTextStrings%, true)

    for each textStringXML in strings.GetChildElements()
        textString = textStringXML.GetText()
        item.textStrings.push(textString)
    next

	item.isRSSFeed = false
    item.isUserVariable = false

    return item

End Function


Function newTwitterPlaylistItem(bsp As Object, zoneHSM As Object, playlistItemXML As Object) As Object

	item = CreateObject("roAssociativeArray")
	
    ' read twitter user name
    twitterSpec = playlistItemXML.twitterSpec
    twitterSpecAttrs = twitterSpec.GetAttributes()
	if twitterSpecAttrs["AuthToken"] = invalid return invalid

	twitterUserName$ = twitterSpecAttrs["userName"]
	if Left(twitterUserName$, 1) = "@" then
		twitterUserName$ = Mid(twitterUserName$, 2)
	endif

	jsonUrl$ = "https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=" + twitterUserName$ + "&tweet_mode=extended"
	url = newTextParameterValue(jsonUrl$)

	authData = CreateObject("roAssociativeArray")
	authData.AuthType = "OAuth 1.0a"
	authData.AuthToken = twitterSpecAttrs["AuthToken"]
	authData.ConsumerKey = twitterSpecAttrs["BSConsumerKey"]
	authData.EncryptedTwitterSecrets = twitterSpecAttrs["EncryptedTwitterSecrets"]

	if type(twitterSpecAttrs["updateInterval"]) = "roString" then
		updateInterval$ = twitterSpecAttrs["updateInterval"]
		if updateInterval$ = "" then
			updateInterval$ = "300"
		endif
		updateInterval% = int(val(updateInterval$))
	else
		updateInterval% = 300
	endif

	liveDataFeed = newLiveDataFeedWithAuthData(bsp, url, authData, updateInterval%)

	if not bsp.liveDataFeeds.DoesExist(liveDataFeed.name$) then
		liveDataFeed.isJSON = true
		liveDataFeed.isTwitterFeed = true

		if type(twitterSpecAttrs["restrictNumberOfTweets"]) = "roString" then
			restrictNumberOfTweets$ = twitterSpecAttrs["restrictNumberOfTweets"]
			if lcase(restrictNumberOfTweets$) = "true" then
				if type(twitterSpecAttrs["numberOfTweetsToShow"]) = "roString" then
					numberOfTweetsToShow$ = twitterSpecAttrs["numberOfTweetsToShow"]
					if numberOfTweetsToShow$ <> "" then
						liveDataFeed.restrictNumberOfItems = true
						liveDataFeed.numberOfItemsToDisplay% = int(val(numberOfTweetsToShow$))
					endif
				endif
			endif
		endif

		bsp.liveDataFeeds.AddReplace(liveDataFeed.name$, liveDataFeed)
	else
		liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeed.name$)
	endif

	item.liveDataFeed = liveDataFeed
    item.rssTitle$ = item.liveDataFeed.name$

	item.twitterUserName$ = twitterSpecAttrs["userName"] + ": "
	item.isRSSFeed = true
	item.isUserVariable = false


    return item
    
End Function


Function newRSSDataFeedPlaylistItem(bsp As Object, playlistItemXML As Object) As Object

	item = CreateObject("roAssociativeArray")
	
	liveDataFeedName$ = CleanName( playlistItemXML.liveDataFeedName.GetText() )
	item.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedName$)
    item.rssTitle$ = item.liveDataFeed.name$
    
	item.isRSSFeed = true
   	item.isUserVariable = false

    return item

End Function


Function newRSSPlaylistItem(bsp As Object, zoneHSM As Object, playlistItemXML As Object) As Object

	item = CreateObject("roAssociativeArray")
	
    rssSpec = playlistItemXML.rssSpec
    rssSpecAttrs = rssSpec.GetAttributes()
    
	url = newTextParameterValue(rssSpecAttrs["url"])

	url$ = url.GetCurrentParameterValue()

    ' determine if this is a twitter feed
	isTwitterFeed = false
    index% = Instr(1, url$, "api.twitter.com")
    if index% > 0 then
		userNameIndex% = Instr(1, url$, "screen_name=")
		if userNameIndex% > 0 then
			isTwitterFeed = true
			item.twitterUserName$ = Mid(url$, userNameIndex% + 12)
			' Be careful if you change the Twitter URL. It must be a normalized form for OAuth
			' authentication to work. (Refer to OAuth docs.)
			jsonUrl$ = "https://api.twitter.com/1/statuses/user_timeline.json?screen_name=" + item.twitterUserName$
			url = newTextParameterValue(jsonUrl$)
		endif
	endif
    
	liveDataFeed = newLiveDataFeedFromOldDataFormat(bsp, url, zoneHSM.rssDownloadPeriodicValue%)
	bsp.liveDataFeeds.AddReplace(liveDataFeed.name$, liveDataFeed)

	item.liveDataFeed = liveDataFeed
    item.rssTitle$ = item.liveDataFeed.name$
	item.isRSSFeed = true
	item.isUserVariable = false
	
	if isTwitterFeed then
		liveDataFeed.isJSON = true
		liveDataFeed.isTwitterFeed = true
	else
		liveDataFeed.isJSON = false
		liveDataFeed.isTwitterFeed = false
	endif
    
	return item
    
End Function


Sub newBackgroundImagePlaylistItem(bsp As Object, playlistItemXML As Object, state As Object, playlistItemBS As Object)

    newMediaPlaylistItem(bsp, playlistItemXML, state, playlistItemBS)

    state.HStateEventHandler = STDisplayingBackgroundImageEventHandler

End Sub


Function GetViewModeValue(viewModeSpec$ As String) As Integer

    viewMode% = 2
    
    if viewModeSpec$ = "Scale to Fill" then
        viewMode% = 0
    else if viewModeSpec$ = "Letterboxed and Centered" then
        viewMode% = 1
    endif
    
    return viewMode%
    
End Function


Function GetAudioOutputValue(audioOutputSpec$ As String) As Integer

    audioOutput% = 0
    
    if audioOutputSpec$ = "USB Audio" then
        audioOutput% = 1
    else if audioOutputSpec$ = "SPDIF Audio with Stereo PCM (HDMI Audio)" then
        audioOutput% = 2
    else if audioOutputSpec$ = "SPDIF Audio, Raw Multichannel" then
        audioOutput% = 3
    else if audioOutputSpec$ = "Analog Audio with Raw Multichannel on SPDIF" then
        audioOutput% = 4
    endif
    
    return audioOutput%
    
End Function


Function GetAudioModeValue(audioModeSpec$ As String) As Integer

    audioMode% = 0
    
    if audioModeSpec$ = "Multichannel Mixed Down to Stereo" then
        audioMode% = 1
    else if audioModeSpec$ = "No Audio" then
        audioMode% = 2
    else if audioModeSpec$ = "Mono Left Mixdown" then
		audioMode% = 3
	else if audioModeSpec$ = "Mono Right Mixdown" then
		audioMode% = 4
	endif
    
    return audioMode%
    
End Function


Function GetAudioMappingValue(audioMappingSpec$ As String) As Integer

    audioMapping% = 0
    
    if audioMappingSpec$ = "Audio-2" then
        audioMapping% = 1
    else if audioMappingSpec$ = "Audio-3" then
        audioMapping% = 2
    endif
    
    return audioMapping%
    
End Function


Function GetAudioMappingSpan(audioOutput% As Integer, audioMappingSpec$ As String) As Integer

    audioMappingSpan% = 1

    if audioOutput% = 0 and audioMappingSpec$ = "Audio-all" then
        audioMappingSpan% = 3
    endif
    
    return audioMappingSpan%

End Function


Function GetImageModeValue(imageModeSpec$ As String) As Integer

    imageMode% = 1
    
    if imageModeSpec$ = "Center Image" then
        imageMode% = 0
    else if imageModeSpec$ = "Scale to Fill and Crop" then
        imageMode% = 2
    else if imageModeSpec$ = "Scale to Fill" then
        imageMode% = 3
    endif
    
    return imageMode%
    
End Function


Function GetSlideTransitionValue(slideTransitionSpec$ As String) As Integer

    slideTransition% = 0
    
    if slideTransitionSpec$ = "Image wipe from top" then
        slideTransition% = 1
    else if slideTransitionSpec$ = "Image wipe from bottom" then
        slideTransition% = 2
    else if slideTransitionSpec$ = "Image wipe from left" then
        slideTransition% = 3
    else if slideTransitionSpec$ = "Image wipe from right" then
        slideTransition% = 4
    else if slideTransitionSpec$ = "Explode from center" then
        slideTransition% = 5
    else if slideTransitionSpec$ = "Explode from top left" then
        slideTransition% = 6
    else if slideTransitionSpec$ = "Explode from top right" then
        slideTransition% = 7
    else if slideTransitionSpec$ = "Explode from bottom left" then
        slideTransition% = 8
    else if slideTransitionSpec$ = "Explode from bottom right" then
        slideTransition% = 9
    else if slideTransitionSpec$ = "Venetian blinds - vertical" then
        slideTransition% = 10
    else if slideTransitionSpec$ = "Venetian blinds - horizontal" then
        slideTransition% = 11
    else if slideTransitionSpec$ = "Comb effect - vertical" then
        slideTransition% = 12
    else if slideTransitionSpec$ = "Comb effect - horizontal" then
        slideTransition% = 13
    else if slideTransitionSpec$ = "Fade to background color" then
        slideTransition% = 14
    else if slideTransitionSpec$ = "Fade to new image" then
        slideTransition% = 15
    else if slideTransitionSpec$ = "Slide from top" then
        slideTransition% = 16
    else if slideTransitionSpec$ = "Slide from bottom" then
        slideTransition% = 17
    else if slideTransitionSpec$ = "Slide from left" then
        slideTransition% = 18
    else if slideTransitionSpec$ = "Slide from right" then
        slideTransition% = 19
    endif
    
    return slideTransition%
    
End Function

'endregion

'region BSP Methods
' *************************************************
'
' BSP Methods
'
' *************************************************
Sub InitializeTouchScreen(zone As Object)

    if type(m.touchScreen) <> "roTouchScreen" then
        m.touchScreen = CreateObject("roTouchScreen")
        m.touchScreen.SetPort(m.msgPort)
        REM Puts up a cursor if a mouse is attached
        REM The cursor must be a 32 x 32 BMP
        REM The x,y position is the hot spot point
        m.touchScreen.SetCursorBitmap("cursor.bmp", 16, 16)

        videoMode = CreateObject("roVideoMode")
        resX = videoMode.GetResX()
        resY = videoMode.GetResY()
        videoMode = invalid
        
        m.touchScreen.SetResolution(resX, resY)
        m.touchScreen.SetCursorPosition(resX / 2, resY / 2)
    endif

	if type(zone.enabledRegions) <> "roList" then
		zone.enabledRegions = CreateObject("roList")
	endif
	    
End Sub


Sub AddRectangularTouchRegion(zone As Object, touchEvent As Object, eventNum% As Integer)

    x% = touchEvent.x% + zone.x%
    y% = touchEvent.y% + zone.y%
    m.touchScreen.AddRectangleRegion(x%, y%, touchEvent.width%, touchEvent.height%, eventNum%)
    m.touchScreen.EnableRegion(eventNum%, false)

End Sub


Sub SetTouchRegions(state As Object)

	zone = state.stateMachine
	
REM Display the cursor if there is a touch event active in this state
REM If there is only one touch event we assume that it is to exit and don't display the cursor

    if type(m.touchScreen) <> "roTouchScreen" return

    ' clear out all regions in the active zone
    
    if type(zone.enabledRegions) = "roList" then    
		for each eventNum in zone.enabledRegions
			m.touchScreen.EnableRegion(eventNum, false)
		next
		zone.enabledRegions.Clear()
	endif

    numTouchRegions% = 0
    if type(state.touchEvents) = "roAssociativeArray" then
        for each eventNum in state.touchEvents
            m.touchScreen.EnableRegion(val(eventNum), true)
            zone.enabledRegions.AddTail(val(eventNum))
            numTouchRegions% = numTouchRegions% + 1
        next
    endif
    
	if state.type$ = "html5" and state.displayCursor then

        m.touchScreen.EnableCursor(true)
        m.diagnostics.PrintDebug("Html5 state - Cursor enabled")

    else if m.sign.touchCursorDisplayMode$ = "auto" then
    
        if numTouchRegions% > 1 then

            m.touchScreen.EnableCursor(true)
            m.diagnostics.PrintDebug("Cursor enabled")
            
        else

            m.touchScreen.EnableCursor(false)
            m.diagnostics.PrintDebug("Cursor disabled")
            
        endif

    else if m.sign.touchCursorDisplayMode$ = "display" and m.sign.numTouchEvents% > 0 then
    
        m.touchScreen.EnableCursor(true)
        m.diagnostics.PrintDebug("Cursor enabled")

    else
    
        m.touchScreen.EnableCursor(false)
        m.diagnostics.PrintDebug("Cursor disabled")

    endif
    
    return
    
End Sub


' call this function to determine whether or not it is suitable to send the current command to a video player
' the heuristics are as follows: if the current zone type is AudioOnly or EnhancedAudio and there are audio items
' in the zone, then don't send the current command to a video player
Function SendCommandToVideo() As Boolean

	if m.type$ = "AudioOnly" or m.type$ = "EnhancedAudio" then
		for each stateName in m.stateTable
			state = m.stateTable[stateName]
			if (state.type$ = "audio") or (state.type$ = "audioIn") or (state.type$ = "playFile" and state.mediaType$ = "audio") or (state.type$ = "mediaList" and state.mediaType$ = "audio") then
				return false
			endif
		next
	endif

	return true

End Function


Sub MapDigitalOutput(player As Object, parameters As Object)

	parameter = parameters["mapping"]
	digitalOutput$ = parameter.GetCurrentParameterValue()

    m.diagnostics.PrintDebug("Map digital output " + digitalOutput$)
    if type(player) <> "roInvalid" then
        player.MapDigitalOutput(int(val(digitalOutput$)))
    endif

End Sub


Sub SetAudioVolumeLimits(zone As Object, audioSettings As Object)

	if zone.presentationUsesRoAudioOutputParameters and m.currentPresentationUsesRoAudioOutputParameters then

		audioSettings.minVolume% = zone.minimumVolume%
		audioSettings.maxVolume% = zone.maximumVolume%

	else

		audioOutput% = audioSettings.audioOutput%
		stereoMapping% = audioSettings.stereoMapping%

		ANALOG_AUDIO = 0
		USB_AUDIO = 1
		DIGITAL_AUDIO_STEREO_PCM = 2
		DIGITAL_AUDIO_RAW_AC3 = 3
		ANALOG_HDMI_RAW_AC3 = 4
	
		if audioOutput% = ANALOG_AUDIO or audioOutput% = ANALOG_HDMI_RAW_AC3 then
			if stereoMapping% = 0 then
				audioSettings.minVolume% = m.sign.audio1MinVolume%
				audioSettings.maxVolume% = m.sign.audio1MaxVolume%
			else if stereoMapping% = 1 then
				audioSettings.minVolume% = m.sign.audio2MinVolume%
				audioSettings.maxVolume% = m.sign.audio2MaxVolume%
			else
				audioSettings.minVolume% = m.sign.audio3MinVolume%
				audioSettings.maxVolume% = m.sign.audio3MaxVolume%
			endif
		else if audioOutput% = USB_AUDIO then
			audioSettings.minVolume% = m.sign.usbAMinVolume%
			audioSettings.maxVolume% = m.sign.usbAMaxVolume%
		else if audioOutput% = DIGITAL_AUDIO_STEREO_PCM then
			audioSettings.minVolume% = m.sign.hdmiMinVolume%
			audioSettings.maxVolume% = m.sign.hdmiMaxVolume%
		else
			audioSettings.minVolume% = 0
			audioSettings.maxVolume% = 100
		endif
	
	endif
		
End Sub


Sub SetAudioMode1(parameters As Object)

	parameter = parameters["zoneId"]
	zoneId$ = parameter.GetCurrentParameterValue()

	parameter = parameters["mode"]
	mode$ = parameter.GetCurrentParameterValue()

	zone = m.GetZone(zoneId$)
	if type(zone) = "roAssociativeArray" then
		if lcase(mode$) = "passthrough" then
			mode% = 0
		else if lcase(mode$) = "left" then
			mode% = 3
		else if lcase(mode$) = "right" then
			mode% = 4
		else
			mode% = 1
		endif

		if type(zone.videoPlayer) = "roVideoPlayer" then
			zone.videoPlayer.SetAudioMode(mode%)
		endif

		if IsAudioPlayer(zone.audioPlayer) then
			zone.audioPlayer.SetAudioMode(mode%)
		endif
	endif

End Sub


Sub SetAllAudioOutputs(parameters As Object)

	parameter = parameters["zoneId"]
	zoneId$ = parameter.GetCurrentParameterValue()

	parameter = parameters["analog"]
	analog$ = parameter.GetCurrentParameterValue()

	if parameters.DoesExist("analog2") then
		parameter = parameters["analog2"]
		analog2$ = parameter.GetCurrentParameterValue()
	else
		analog2$ = "none"
	endif

	if parameters.DoesExist("analog3") then
		parameter = parameters["analog3"]
		analog3$ = parameter.GetCurrentParameterValue()
	else
		analog3$ = "none"
	endif

	parameter = parameters["hdmi"]
	hdmi$ = parameter.GetCurrentParameterValue()

	parameter = parameters["spdif"]
	spdif$ = parameter.GetCurrentParameterValue()

	if parameters.DoesExist("usbA") then
		parameter = parameters["usbA"]
		usbA$ = parameter.GetCurrentParameterValue()
	else
		usbA$ = "none"
	endif

	if parameters.DoesExist("usbB") then
		parameter = parameters["usbB"]
		usbB$ = parameter.GetCurrentParameterValue()
	else
		usbB$ = "none"
	endif

	if parameters.DoesExist("usbC") then
		parameter = parameters["usbC"]
		usbC$ = parameter.GetCurrentParameterValue()
	else
		usbC$ = "none"
	endif

	if parameters.DoesExist("usbD") then
		parameter = parameters["usbD"]
		usbD$ = parameter.GetCurrentParameterValue()
	else
		usbD$ = "none"
	endif

	if parameters.DoesExist("usbA1") then
		parameter = parameters["usbA1"]
		usbA1$ = parameter.GetCurrentParameterValue()
	else
		usbA1$ = "none"
	endif

	if parameters.DoesExist("usbA2") then
		parameter = parameters["usbA2"]
		usbA2$ = parameter.GetCurrentParameterValue()
	else
		usbA2$ = "none"
	endif

	if parameters.DoesExist("usbA3") then
		parameter = parameters["usbA3"]
		usbA3$ = parameter.GetCurrentParameterValue()
	else
		usbA3$ = "none"
	endif

	if parameters.DoesExist("usbA4") then
		parameter = parameters["usbA4"]
		usbA4$ = parameter.GetCurrentParameterValue()
	else
		usbA4$ = "none"
	endif

	if parameters.DoesExist("usbA5") then
		parameter = parameters["usbA5"]
		usbA5$ = parameter.GetCurrentParameterValue()
	else
		usbA5$ = "none"
	endif

	if parameters.DoesExist("usbA6") then
		parameter = parameters["usbA6"]
		usbA6$ = parameter.GetCurrentParameterValue()
	else
		usbA6$ = "none"
	endif

	if parameters.DoesExist("usbA7") then
		parameter = parameters["usbA7"]
		usbA7$ = parameter.GetCurrentParameterValue()
	else
		usbA7$ = "none"
	endif

	pcm = CreateObject("roArray", 1, true)
	compressed = CreateObject("roArray", 1, true)
	multichannel = CreateObject("roArray", 1, true)

	analogAudioOutput = CreateObject("roAudioOutput", "Analog:1")
	analog2AudioOutput = CreateObject("roAudioOutput", "Analog:2")
	analog3AudioOutput = CreateObject("roAudioOutput", "Analog:3")
	hdmiAudioOutput = CreateObject("roAudioOutput", "HDMI")
	spdifAudioOutput = CreateObject("roAudioOutput", "SPDIF")

	if lcase(analog$) <> "none" and lcase(analog$) <> "multichannel" then
		pcm.push(analogAudioOutput)
	endif

	' FIXME - if needed
	if lcase(analog2$) = "pcm" and type(analog2AudioOutput) = "roAudioOutput" then
		pcm.push(analog2AudioOutput)
	endif

	if lcase(analog3$) = "pcm" then
		pcm.push(analog3AudioOutput)
	endif

	if lcase(analog$)="multichannel" then
		multichannel.push(analogAudioOutput)
	else if lcase(analog2$)="multichannel" then
		multichannel.push(analog2AudioOutput)
	else if lcase(analog3$)="multichannel" then
		multichannel.push(analog3AudioOutput)
	endif

	if lcase(hdmi$) = "passthrough" then
		compressed.push(hdmiAudioOutput)
	else if lcase(hdmi$) <> "none" then
		pcm.push(hdmiAudioOutput)
	endif

	if lcase(spdif$) = "passthrough" then
		compressed.push(spdifAudioOutput)
	else if lcase(spdif$) <> "none" then
		pcm.push(spdifAudioOutput)
	endif

	m.SetUSBAudioOutput("A", usbA$, pcm, multichannel)
	m.SetUSBAudioOutput("B", usbB$, pcm, multichannel)
	m.SetUSBAudioOutput("C", usbC$, pcm, multichannel)
	m.SetUSBAudioOutput("D", usbD$, pcm, multichannel)
	m.SetUSBAudioOutput("A/1", usbA1$, pcm, multichannel)
	m.SetUSBAudioOutput("A/2", usbA2$, pcm, multichannel)
	m.SetUSBAudioOutput("A/3", usbA3$, pcm, multichannel)
	m.SetUSBAudioOutput("A/4", usbA4$, pcm, multichannel)
	m.SetUSBAudioOutput("A/5", usbA5$, pcm, multichannel)
	m.SetUSBAudioOutput("A/6", usbA6$, pcm, multichannel)
	m.SetUSBAudioOutput("A/7", usbA7$, pcm, multichannel)

	if pcm.Count() = 0 then
		noPCMAudioOutput = CreateObject("roAudioOutput", "none")
		pcm.push(noPCMAudioOutput)
	endif

	if compressed.Count() = 0 then
		noCompressedAudioOutput = CreateObject("roAudioOutput", "none")
		compressed.push(noCompressedAudioOutput)
	endif

	if multichannel.Count() = 0 then
		noMultichannelAudioOutput = CreateObject("roAudioOutput", "none")
		multichannel.push(noMultichannelAudioOutput)
	endif

	zone = m.GetZone(zoneId$)

	if type(zone) = "roAssociativeArray" then

		if type(zone.videoPlayer) = "roVideoPlayer" then
			zone.videoPlayer.SetPcmAudioOutputs(pcm)
			zone.videoPlayer.SetCompressedAudioOutputs(compressed)
			zone.videoPlayer.SetMultichannelAudioOutputs(multichannel)
		endif

		if IsAudioPlayer(zone.audioPlayer) then
			zone.audioPlayer.SetPcmAudioOutputs(pcm)
			zone.audioPlayer.SetCompressedAudioOutputs(compressed)
			zone.audioPlayer.SetMultichannelAudioOutputs(multichannel)
		endif

	endif

End Sub


Sub SetUSBAudioOutput(port$ As String, audioType$ As String, pcm As Object, multichannel As Object)

	if m.boseUSBAudioDevicesByConnector.DoesExist(port$) then
		usbPort$ = m.boseUSBAudioDevicesByConnector[port$]
		usbAudioOutput = CreateObject("roAudioOutput", usbPort$)
		if type(usbAudioOutput) = "roAudioOutput" then
			if lcase(audioType$) = "pcm" then
				pcm.push(usbAudioOutput)
			else if lcase(audioType$) = "multichannel" then
				multichannel.push(usbAudioOutput)
			endif
		endif
	endif

End Sub


Sub UnmuteAudioConnector(connector$ As String)

	audioOutput = CreateObject("roAudioOutput", connector$)
	if type(audioOutput) = "roAudioOutput" then
		audioOutput.SetMute(false)
	endif

End Sub


Function DeviceMatchesFid(boseProductPort As String, portName As String, fid As String, fidSubstring As String) As Boolean

	if boseProductPort = portName and instr(1, fid, fidSubstring) = 1 then
		return true
	endif
	
	return false

End Function


Function DeviceMatchesUSB700(boseProductPort As String, fid As String) As String

	if DeviceMatchesFid(boseProductPort, "USB 700_1", fid, "A/1") then
		return "USB A/1"
	else if DeviceMatchesFid(boseProductPort, "USB 700_2", fid, "A/2") then
		return "USB A/2"
	else if DeviceMatchesFid(boseProductPort, "USB 700_3", fid, "A/3") then
		return "USB A/3"
	else if DeviceMatchesFid(boseProductPort, "USB 700_4", fid, "A/4") then
		return "USB A/4"
	else if DeviceMatchesFid(boseProductPort, "USB 700_5", fid, "A/5") then
		return "USB A/5"
	else if DeviceMatchesFid(boseProductPort, "USB 700_6", fid, "A/6") then
		return "USB A/6"
	else if DeviceMatchesFid(boseProductPort, "USB 700_7", fid, "A/7") then
		return "USB A/7"
	endif

	return ""

End Function


Function GetConnectedUSBDeviceName(model As String, connectedUSBDevices As Object, boseProductPort As String) As String

	if lcase(model) = "hd972" then

		for each connectedUSBDevice in connectedUSBDevices
		
			fid = connectedUSBDevice.fid
			
			if DeviceMatchesFid(boseProductPort, "USB A", fid, "A") then
				return "USB A"
			else if DeviceMatchesFid(boseProductPort, "USB B", fid, "B") then
				return "USB B"
			else if DeviceMatchesFid(boseProductPort, "USB C", fid, "C") then
				return "USB C"
			else if DeviceMatchesFid(boseProductPort, "USB D", fid, "D") then
				return "USB D"
			endif

		next

	else if lcase(model) = "xt1143" or lcase(model) = "xd1033" or lcase(model) = "xt1144" or lcase(model) = "xd1034" then

		for each connectedUSBDevice in connectedUSBDevices
		
			fid = connectedUSBDevice.fid

			if DeviceMatchesFid(boseProductPort, "USB A", fid, "A") then
				return "USB A"
			else if DeviceMatchesFid(boseProductPort, "USB B", fid, "B") then
				return "USB B"
			else if DeviceMatchesFid(boseProductPort, "USB Type_A", fid, "B") then
				return "USB B"
			else if DeviceMatchesFid(boseProductPort, "USB Type_C", fid, "A") then
				return "USB A"
			else
				usbDeviceName$ = DeviceMatchesUSB700(boseProductPort, fid)
				if usbDeviceName$ <> "" then
					return usbDeviceName$
				endif
			endif
		next
	
	else if lcase(model) = "ls423" or lcase(model) = "ls424" then

		for each connectedUSBDevice in connectedUSBDevices
		
			fid = connectedUSBDevice.fid

			' does this make sense for the LS423/LS424 (there is no Type A port)?
			if DeviceMatchesFid(boseProductPort, "USB A", fid, "A") then
				return "USB A"
			else if DeviceMatchesFid(boseProductPort, "USB Type_C", fid, "A") then
				return "USB A"
			else
				usbDeviceName$ = DeviceMatchesUSB700(boseProductPort, fid)
				if usbDeviceName$ <> "" then
					return usbDeviceName$
				endif
			endif
		next

	endif

	return ""

End Function


Function BuildUSBDevicesByConnector(sign As Object) As Object

	di = CreateObject("roDeviceInfo")
	connectedUSBDevices = di.GetUSBTopology({ array : true })

	usbDevicesByConnector = {}
	usbBACommandToDeviceCommandMapping = {}
	usbConnectorToConnectorParameterMapping = {}

	for each connector in sign.boseProductsByConnector

		usbDeviceName = GetConnectedUSBDeviceName(di.GetModel(), connectedUSBDevices, connector)

		if usbDeviceName <> "" then
			usbDevicesByConnector.AddReplace(connector, usbDeviceName)

			' Specify the following in a lookup table
			if lcase(connector) = "usb a" then
				connectorParameter = "usbA"
			else if lcase(connector) = "usb b" then
				connectorParameter = "usbB"
			else if lcase(connector) = "usb type_a" then
				connectorParameter = "usbTypeA"
			else if lcase(connector) = "usb type_c" then
				connectorParameter = "usbTypeC"
			else if lcase(connector) = "usb 700_1" then
				connectorParameter = "usb700_1"
			else if lcase(connector) = "usb 700_2" then
				connectorParameter = "usb700_2"
			else if lcase(connector) = "usb 700_3" then
				connectorParameter = "usb700_3"
			else if lcase(connector) = "usb 700_4" then
				connectorParameter = "usb700_4"
			else if lcase(connector) = "usb 700_5" then
				connectorParameter = "usb700_5"
			else if lcase(connector) = "usb 700_6" then
				connectorParameter = "usb700_6"
			else if lcase(connector) = "usb 700_7" then
				connectorParameter = "usb700_7"
			else
				stop
			endif

			usbConnectorToConnectorParameterMapping.AddReplace(lcase(connectorParameter), lcase(connector))

			if lcase(usbDeviceName) = "usb a" then	' true for XT1143
				usbDeviceNameParameter = "usbA"
			else if lcase(usbDeviceName) = "usb b" then	' true for XT1143
				usbDeviceNameParameter = "usbB"
			else if lcase(usbDeviceName) = "usb a/1"
				usbDeviceNameParameter = "usbA/1"
			else if lcase(usbDeviceName) = "usb a/2"
				usbDeviceNameParameter = "usbA/2"
			else if lcase(usbDeviceName) = "usb a/3"
				usbDeviceNameParameter = "usbA/3"
			else if lcase(usbDeviceName) = "usb a/4"
				usbDeviceNameParameter = "usbA/4"
			else if lcase(usbDeviceName) = "usb a/5"
				usbDeviceNameParameter = "usbA/5"
			else if lcase(usbDeviceName) = "usb a/6"
				usbDeviceNameParameter = "usbA/6"
			else if lcase(usbDeviceName) = "usb a/7"
				usbDeviceNameParameter = "usbA/7"
			else
				stop
			endif

			usbBACommandToDeviceCommandMapping.AddReplace(connectorParameter, usbDeviceNameParameter)

		endif
	next

	usbMappings = {}
	usbMappings.usbDevicesByConnector = usbDevicesByConnector
	usbMappings.usbBACommandToDeviceCommandMapping = usbBACommandToDeviceCommandMapping
	usbMappings.usbConnectorToConnectorParameterMapping = usbConnectorToConnectorParameterMapping

	return usbMappings

End Function


Function BuildBoseUSBAudioDevicesByConnector(sign As Object) As Object

	boseUSBAudioDevicesByConnector = {}

	for each boseProductPort in sign.boseProductsByConnector
		if lcase(left(boseProductPort, 3)) = "usb" then
			usbPort$ = "USB:" + right(boseProductPort, len(boseProductPort) - 4)
			if GetGlobalAA().usbAudioPortConfigurations.DoesExist(boseProductPort) then
				usbAudioPortConfiguration = GetGlobalAA().usbAudioPortConfigurations[boseProductPort]
				usbPort$ = usbPort$ + "." + usbAudioPortConfiguration.usbAudioInterfaceIndex$
			endif
			boseUSBAudioDevicesByConnector.AddReplace(right(boseProductPort, len(boseProductPort) - 4), usbPort$)
		endif
	next

	return boseUSBAudioDevicesByConnector

End Function


Sub UnmuteAllAudio()

	m.UnmuteAudioConnector("Analog:1")
	m.UnmuteAudioConnector("Analog:2")
	m.UnmuteAudioConnector("Analog:3")
	m.UnmuteAudioConnector("HDMI")
	m.UnmuteAudioConnector("SPDIF")

	for each boseUSBPort in m.boseUSBAudioDevicesByConnector
		usbPort$ = m.boseUSBAudioDevicesByConnector[boseUSBPort]
		m.UnmuteAudioConnector(usbPort$)
	next

End Sub


Sub MuteAudioOutput(muteOn as Boolean, parameters As Object, parameterName$ As String, objectName$ As String)

	if parameters.DoesExist(parameterName$) then
		parameter = parameters[parameterName$]
		mute$ = parameter.GetCurrentParameterValue()
		if lcase(mute$) = "true" then
			audioOutput = CreateObject("roAudioOutput", objectName$)
			if type(audioOutput) = "roAudioOutput" then
				audioOutput.SetMute(muteOn)
			endif
		endif
	endif

End Sub


Sub MuteAudioOutputs(muteOn as Boolean, parameters As Object)

	m.MuteAudioOutput(muteOn, parameters, "analog", "Analog:1")
	m.MuteAudioOutput(muteOn, parameters, "analog2", "Analog:2")
	m.MuteAudioOutput(muteOn, parameters, "analog3", "Analog:3")
	m.MuteAudioOutput(muteOn, parameters, "hdmi", "HDMI")
	m.MuteAudioOutput(muteOn, parameters, "spdif", "SPDIF")
	
	for each portName in parameters
		if left(lcase(portName), 3) = "usb" and len(portName) >= 4 then
			port$ = right(portName, len(portName) - 3)

			if GetGlobalAA().usbParamNameToDeviceParamName.DoesExist(port$) then
				port$ = GetGlobalAA().usbParamNameToDeviceParamName[port$]
			endif

			if m.boseUSBAudioDevicesByConnector.DoesExist(port$) then
				deviceName$ = m.boseUSBAudioDevicesByConnector[port$]
				m.MuteAudioOutput(muteOn, parameters, portName, deviceName$)
			endif
		endif
	next

End Sub


Sub SetConnectorVolume(parameters As Object)

	parameter = parameters["connector"]
	connector$ = parameter.GetCurrentParameterValue()
	parameter = parameters["volume"]
	volume$ = parameter.GetCurrentParameterValue()
	volume% = int(val(volume$))

	if lcase(connector$) = "analog" then
		audioOutput = CreateObject("roAudioOutput", "Analog:1")
		m.analogVolume% = volume%
	else if lcase(connector$) = "analog2" then
		audioOutput = CreateObject("roAudioOutput", "Analog:2")
		m.analog2Volume% = volume%
	else if lcase(connector$) = "analog3" then
		audioOutput = CreateObject("roAudioOutput", "Analog:3")
		m.analog3Volume% = volume%
	else if lcase(connector$) = "hdmi" then
		audioOutput = CreateObject("roAudioOutput", "HDMI")
		m.hdmiVolume% = volume%
	else if lcase(connector$) = "spdif" then
		audioOutput = CreateObject("roAudioOutput", "SPDIF")
		m.spdifVolume% = volume%
	else if left(lcase(connector$), 3) = "usb" then
		port$ = right(connector$, len(connector$) - 4)
		if m.boseUSBAudioDevicesByConnector.DoesExist(port$) then
			deviceName$ = m.boseUSBAudioDevicesByConnector[port$]
			audioOutput = CreateObject("roAudioOutput", deviceName$)

			if port$ = "A" then
				m.usbVolumeA% = volume%
			else if port$ = "B" then
				m.usbVolumBA% = volume%
			else if port$ = "C" then
				m.usbVolumeC% = volume%
			else if port$ = "D" then
				m.usbVolumeD% = volume%
			else if port$ = "TypeA" then
				m.usbVolumeTypeA% = volume%
			else if port$ = "TypeC" then
				m.usbVolumeTypeC% = volume%
			else if port$ = "A/1" then
				m.usbVolumeA1% = volume%
			else if port$ = "A/2" then
				m.usbVolumeA2% = volume%
			else if port$ = "A/3" then
				m.usbVolumeA3% = volume%
			else if port$ = "A/4" then
				m.usbVolumeA4% = volume%
			else if port$ = "A/5" then
				m.usbVolumeA5% = volume%
			else if port$ = "A/6" then
				m.usbVolumeA6% = volume%
			else if port$ = "A/7" then
				m.usbVolumeA7% = volume%
			endif
		endif
	endif

	if type(audioOutput) = "roAudioOutput" then
		audioOutput.SetVolume(volume%)
	endif

End Sub


Sub ChangeConnectorVolume(multiplier% As Integer, parameters As Object)

	parameter = parameters["connector"]
	connector$ = parameter.GetCurrentParameterValue()

	parameter = parameters["volume"]
	volumeDelta$ = parameter.GetCurrentParameterValue()
	volumeDelta% = int(val(volumeDelta$)) * multiplier%

	if lcase(connector$) = "analog" then
		m.analogVolume% = ExecuteChangeConnectorVolume("Analog:1", m.analogVolume% + volumeDelta%, m.sign.audio1MinVolume%, m.sign.audio1MaxVolume%)
	else if lcase(connector$) = "analog2" then
		m.analog2Volume% = ExecuteChangeConnectorVolume("Analog:2", m.analog2Volume% + volumeDelta%, m.sign.audio2MinVolume%, m.sign.audio2MaxVolume%)
	else if lcase(connector$) = "analog3" then
		m.analog3Volume% = ExecuteChangeConnectorVolume("Analog:3", m.analog3Volume% + volumeDelta%, m.sign.audio3MinVolume%, m.sign.audio3MaxVolume%)
	else if lcase(connector$) = "hdmi" then
		m.hdmiVolume% = ExecuteChangeConnectorVolume("HDMI", m.hdmiVolume% + volumeDelta%, m.sign.hdmiMinVolume%, m.sign.hdmiMaxVolume%)
	else if lcase(connector$) = "spdif" then
		m.spdifVolume% = ExecuteChangeConnectorVolume("SPDIF", m.spdifVolume% + volumeDelta%, m.sign.spdifMinVolume%, m.sign.spdifMaxVolume%)
	else if left(lcase(connector$), 3) = "usb" then
	
		key$ = ucase(right(connector$, len(connector$) - 3))

		if m.usbConnectorToConnectorParameterMapping.DoesExist("usb"+lcase(key$)) then
			fullUsbId = m.usbConnectorToConnectorParameterMapping.Lookup("usb"+lcase(key$))
			if m.usbDevicesByConnector.DoesExist(fullUsbId) then
				if m.usbDevicesByConnector.DoesExist(fullUsbId) then
					usbDeviceId = m.usbDevicesByConnector.Lookup(fullUsbId)
					key$ = ucase(right(usbDeviceId, len(usbDeviceId) - 4))
				endif
			endif
		endif

		if m.boseUSBAudioDevicesByConnector.DoesExist(key$) then

			usbPort$ = m.boseUSBAudioDevicesByConnector[key$]

			if key$ = "A" then
				m.usbVolumeA% = ExecuteChangeConnectorVolume(usbPort$, m.usbVolumeA% + volumeDelta%, m.sign.usbAMinVolume%, m.sign.usbAMaxVolume%)
			else if key$ = "B" then
				m.usbVolumeB% = ExecuteChangeConnectorVolume(usbPort$, m.usbVolumeB% + volumeDelta%, m.sign.usbBMinVolume%, m.sign.usbBMaxVolume%)
			else if key$ = "C" then
				m.usbVolumeC% = ExecuteChangeConnectorVolume(usbPort$, m.usbVolumeC% + volumeDelta%, m.sign.usbCMinVolume%, m.sign.usbCMaxVolume%)
			else if key$ = "D" then
				m.usbVolumeD% = ExecuteChangeConnectorVolume(usbPort$, m.usbVolumeD% + volumeDelta%, m.sign.usbDMinVolume%, m.sign.usbDMaxVolume%)
			else if key$ = "A/1" then
				m.usbVolumeA1% = ExecuteChangeConnectorVolume(usbPort$, m.usbVolumeA1% + volumeDelta%, m.sign.usbA1MinVolume%, m.sign.usbA1MaxVolume%)
			else if key$ = "A/2" then
				m.usbVolumeA2% = ExecuteChangeConnectorVolume(usbPort$, m.usbVolumeA2% + volumeDelta%, m.sign.usbA2MinVolume%, m.sign.usbA2MaxVolume%)
			else if key$ = "A/3" then
				m.usbVolumeA3% = ExecuteChangeConnectorVolume(usbPort$, m.usbVolumeA3% + volumeDelta%, m.sign.usbA3MinVolume%, m.sign.usbA3MaxVolume%)
			else if key$ = "A/4" then
				m.usbVolumeA4% = ExecuteChangeConnectorVolume(usbPort$, m.usbVolumeA4% + volumeDelta%, m.sign.usbA4MinVolume%, m.sign.usbA4MaxVolume%)
			else if key$ = "A/5" then
				m.usbVolumeA5% = ExecuteChangeConnectorVolume(usbPort$, m.usbVolumeA5% + volumeDelta%, m.sign.usbA5MinVolume%, m.sign.usbA5MaxVolume%)
			else if key$ = "A/6" then
				m.usbVolumeA6% = ExecuteChangeConnectorVolume(usbPort$, m.usbVolumeA6% + volumeDelta%, m.sign.usbA6MinVolume%, m.sign.usbA6MaxVolume%)
			else if key$ = "A/7" then
				m.usbVolumeA7% = ExecuteChangeConnectorVolume(usbPort$, m.usbVolumeA7% + volumeDelta%, m.sign.usbA7MinVolume%, m.sign.usbA7MaxVolume%)
			endif
		endif
	endif

End Sub


Function ExecuteChangeConnectorVolume(connector$ As String, newVolume% As Integer, minVolume% As Integer, maxVolume% As Integer) As Integer

	audioOutput = CreateObject("roAudioOutput", connector$)
	if type(audioOutput) = "roAudioOutput" then
		if newVolume% > maxVolume% then
			newVolume% = maxVolume%
		else if newVolume% < minVolume%
			newVolume% = minVolume%
		endif
		audioOutput.SetVolume(newVolume%)
	endif

	return newVolume%

End Function


Sub SetZoneVolume(parameters As Object)

	parameter = parameters["zoneId"]
	zoneId$ = parameter.GetCurrentParameterValue()

	parameter = parameters["volume"]
	volume$ = parameter.GetCurrentParameterValue()
	volume% = int(val(volume$))

	zone = m.GetZone(zoneId$)
	if type(zone) = "roAssociativeArray" then
		if type(zone.videoPlayer) = "roVideoPlayer" then
			zone.videoPlayer.SetVolume(volume%)
	        for i% = 0 to 5
		        zone.videoChannelVolumes[i%] = volume%
			next
		endif
		if IsAudioPlayer(zone.audioPlayer) then
			zone.audioPlayer.SetVolume(volume%)
            for i% = 0 to 5
                zone.audioChannelVolumes[i%] = volume%
            next
		endif
	endif

End Sub


Sub ChangeZoneVolume(multiplier% As Integer, parameters As Object)

	parameter = parameters["zoneId"]
	zoneId$ = parameter.GetCurrentParameterValue()

	parameter = parameters["volume"]
	volumeDelta$ = parameter.GetCurrentParameterValue()
	volumeDelta% = int(val(volumeDelta$)) * multiplier%

	zone = m.GetZone(zoneId$)
	if type(zone) = "roAssociativeArray" then

		if type(zone.videoPlayer) = "roVideoPlayer" then
			if multiplier% > 0 then
				minVolume% = zone.videoPlayerAudioSettings.minVolume%
				maxVolume% = zone.videoPlayerAudioSettings.maxVolume%
			else
				minVolume% = zone.videoPlayerAudioSettings.minVolume%
				maxVolume% = zone.videoPlayerAudioSettings.maxVolume%
			endif
		    m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, 63, volumeDelta%, minVolume%, maxVolume%)
		endif

		if IsAudioPlayer(zone.audioPlayer) then
			if multiplier% > 0 then
				minVolume% = zone.audioPlayerAudioSettings.minVolume%
				maxVolume% = zone.audioPlayerAudioSettings.maxVolume%
			else
				minVolume% = zone.audioPlayerAudioSettings.minVolume%
				maxVolume% = zone.audioPlayerAudioSettings.maxVolume%
			endif
		    m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, 63, volumeDelta%, minVolume%, maxVolume%)
		endif

	endif

End Sub


Sub SetZoneChannelVolume(parameters As Object)

	parameter = parameters["zoneId"]
	zoneId$ = parameter.GetCurrentParameterValue()

	parameter = parameters["channel"]
	channelMask$ = parameter.GetCurrentParameterValue()

	parameter = parameters["volume"]
	volume$ = parameter.GetCurrentParameterValue()
	volume% = int(val(volume$))

	zone = m.GetZone(zoneId$)
	if type(zone) = "roAssociativeArray" then

		if type(zone.videoPlayer) = "roVideoPlayer" then
			player = zone.videoPlayer
			channelVolumes = zone.videoChannelVolumes
		else if IsAudioPlayer(zone.audioPlayer) then
			player = zone.audioPlayer
			channelVolumes = zone.audioChannelVolumes
		endif

		m.SetChannelVolumes(player, channelVolumes, int(val(channelMask$)), int(val(volume$)))
	endif

End Sub


Sub ChangeZoneChannelVolume(multiplier% As Integer, parameters As Object)
	
	parameter = parameters["zoneId"]
	zoneId$ = parameter.GetCurrentParameterValue()

	parameter = parameters["channel"]
	channelMask$ = parameter.GetCurrentParameterValue()

	parameter = parameters["volume"]
	volumeDelta$ = parameter.GetCurrentParameterValue()
	volumeDelta% = int(val(volumeDelta$)) * multiplier%

	zone = m.GetZone(zoneId$)
	if type(zone) = "roAssociativeArray" then

		if type(zone.videoPlayer) = "roVideoPlayer" then
			player = zone.videoPlayer
			channelVolumes = zone.videoChannelVolumes
			minVolume% = zone.videoPlayerAudioSettings.minVolume%
			maxVolume% = zone.videoPlayerAudioSettings.maxVolume%
		else if IsAudioPlayer(zone.audioPlayer) then
			player = zone.audioPlayer
			channelVolumes = zone.audioChannelVolumes
			minVolume% = zone.audioPlayerAudioSettings.minVolume%
			maxVolume% = zone.audioPlayerAudioSettings.maxVolume%
		endif

		m.ChangeChannelVolumes(player, channelVolumes, int(val(channelMask$)), volumeDelta%, minVolume%, maxVolume%)

	endif

End Sub


Sub SetAudioOutput(zone As Object, videoPlayerRequired As Boolean, parameters As Object)

	parameter = parameters["output"]
	audioOutput$ = parameter.GetCurrentParameterValue()

    m.diagnostics.PrintDebug("Set audio output " + audioOutput$)
    audioOutput% = int(val(audioOutput$))
    
	player = invalid

    if videoPlayerRequired then
		zone = m.GetVideoZone(zone)
		if type(zone) = "roAssociativeArray" then
			player = zone.videoPlayer
			zone.videoPlayerAudioSettings.audioOutput% = audioOutput%
			if audioOutput% <> 0 then
				zone.videoPlayerAudioSettings.audioMappingSpan% = 1
			endif
			m.SetAudioVolumeLimits(zone, zone.videoPlayerAudioSettings) 
		endif
	else if IsAudioPlayer(zone.audioPlayer) then
		player = zone.audioPlayer
		zone.audioPlayerAudioSettings.audioOutput% = audioOutput%
		if audioOutput% <> 0 then
			zone.audioPlayerAudioSettings.audioMappingSpan% = 1
		endif
		m.SetAudioVolumeLimits(zone, zone.audioPlayerAudioSettings) 
	endif
	
    if type(player) = "roVideoPlayer" or IsAudioPlayer(player) then
        player.SetAudioOutput(int(val(audioOutput$)))
    endif

End Sub


Sub SetAudioMode(player As Object, parameters As Object)

	parameter = parameters["mode"]
	audioMode$ = parameter.GetCurrentParameterValue()

    if audioMode$ <> "" then
        m.diagnostics.PrintDebug("Set audio mode " + audioMode$)
        if type(player) <> "roInvalid" then
            player.SetAudioMode(int(val(audioMode$)))
        endif
    endif
        
End Sub


Sub MapStereoOutput(zone As Object, useVideoPlayer As Boolean, parameters As Object)  

	parameter = parameters["mapping"]
	mapping$ = parameter.GetCurrentParameterValue()
    
    m.diagnostics.PrintDebug("Map stereo output " + mapping$)
    
    mapping% = 0
    spanning% = 1
    if mapping$ = "onboard-audio2" then
        mapping% = 1
    else if mapping$ = "onboard-audio3" then
        mapping% = 2
    else if mapping$ = "onboard-audio-all" then
		spanning% = 3
    endif
    
'        if m.sysInfo.expanderPresent then
'            mapping% = mapping% + 3
'        endif

	player = invalid

    if useVideoPlayer then
		zone = m.GetVideoZone(zone)
		if type(zone) = "roAssociativeArray" then
			player = zone.videoPlayer
			zone.videoPlayerAudioSettings.stereoMapping% = mapping%
			zone.videoPlayerAudioSettings.audioMappingSpan% = spanning%
			m.SetAudioVolumeLimits(zone, zone.videoPlayerAudioSettings) 
		endif
	else if IsAudioPlayer(zone.audioPlayer) then
		player = zone.audioPlayer
		zone.audioPlayerAudioSettings.stereoMapping% = mapping%
		zone.audioPlayerAudioSettings.audioMappingSpan% = spanning%
		m.SetAudioVolumeLimits(zone, zone.audioPlayerAudioSettings) 
	endif

    if type(player) = "roVideoPlayer" or IsAudioPlayer(player) then
        player.MapStereoOutput(mapping%)
        player.SetStereoMappingSpan(spanning%)
    endif

End Sub


Sub SetSpdifMute(player As Object, parameters As Object)  

	parameter = parameters["mute"]
	muteOn$ = parameter.GetCurrentParameterValue()
    
    m.diagnostics.PrintDebug("Set SPDIF Mute " + muteOn$)
    if type(player) <> "roInvalid" then
        player.SetSpdifMute(int(val(muteOn$)))
    endif

End Sub


Sub SetAnalogMute(channelVolumes As Object, player As Object, parameters As Object)  
    
	parameter = parameters["mute"]
	muteOn$ = parameter.GetCurrentParameterValue()
    
    m.diagnostics.PrintDebug("Set Analog Mute " + muteOn$)
    if type(player) <> "roInvalid" and type(channelVolumes) = "roArray" then
        muteOn% = int(val(muteOn$))
        if muteOn% = 0 then
            for i% = 0 to 5
                mask% = 2 ^ i%
                player.SetChannelVolumes(mask%, channelVolumes[i%])
            next
        else
            player.SetChannelVolumes(63, 0)
        endif
    endif

End Sub


Sub SetHDMIMute(parameters As Object)

	parameter = parameters["mute"]
	muteOn$ = parameter.GetCurrentParameterValue()
    
    m.diagnostics.PrintDebug("Set HDMI Mute " + muteOn$)

    videoMode = CreateObject("roVideoMode")

	if muteOn$ = "1" then
		disableHDMIAudio = true
	else
		disableHDMIAudio = false
	endif
	
    videoMode.HdmiAudioDisable(disableHDMIAudio)
    videoMode = invalid

End Sub


Sub SetVideoVolume(zone As Object, parameter$ As String)

    volume% = int(val(parameter$))
    
	zone = m.GetVideoZone(zone)
    if type(zone) = "roAssociativeArray" then
        zone.videoPlayer.SetVolume(volume%)
        for i% = 0 to 5
            zone.videoChannelVolumes[i%] = volume%
        next
    endif

End Sub


Sub SetVideoVolumeByConnector(zone As Object, output$ As String, volume$ As String)

	volume% = int(val(volume$))

	zone = m.GetVideoZone(zone)
    if type(zone) = "roAssociativeArray" then
		if zone.videoPlayerAudioSettings.audioMappingSpan% = 3 then
			if output$ = "onboard-audio1" then
				channelMask% = 3
				zone.videoChannelVolumes[0] = volume%
				zone.videoChannelVolumes[1] = volume%
			else if output$ = "onboard-audio2" then
				channelMask% = 12
				zone.videoChannelVolumes[2] = volume%
				zone.videoChannelVolumes[3] = volume%
			else if output$ = "onboard-audio3" then
				channelMask% = 48
				zone.videoChannelVolumes[4] = volume%
				zone.videoChannelVolumes[5] = volume%
			else
				channelMask% = 63
				for i% = 0 to 5
					zone.videoChannelVolumes[i%] = volume%
				next
			endif
		else
			channelMask% = 63
			for i% = 0 to 5
				zone.videoChannelVolumes[i%] = volume%
			next
		endif
        
'			if analogOutput$ = "onboard-audio1" then
'				channelMask% = 3
'				zone.videoChannelVolumes[0] = volume%
'				zone.videoChannelVolumes[1] = volume%
'			else if analogOutput$ = "onboard-audio2" then
'				channelMask% = 12
'				zone.videoChannelVolumes[2] = volume%
'				zone.videoChannelVolumes[3] = volume%
'			else if analogOutput$ = "onboard-audio3" then
'				channelMask% = 48
'				zone.videoChannelVolumes[4] = volume%
'				zone.videoChannelVolumes[5] = volume%
'			else
'				channelMask% = 63
'				for i% = 0 to 5
'					zone.videoChannelVolumes[i%] = volume%
'				next
'			endif

        zone.videoPlayer.SetChannelVolumes(channelMask%, volume%)
    endif

End Sub


Sub SetVideoChannnelVolume(zone As Object, channelMask$ As String, volume$ As String)
    
	zone = m.GetVideoZone(zone)
    if type(zone) = "roAssociativeArray" then
	    m.SetChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, int(val(channelMask$)), int(val(volume$)))
    endif
    
End Sub


Sub IncrementVideoChannnelVolumes(zone As Object, channelMask$ As String, volumeDelta$ As String)

	zone = m.GetVideoZone(zone)
	if type(zone) = "roAssociativeArray" then
		channelMask% = int(val(channelMask$))
		m.ChangeVideoVolume(zone, channelMask%, int(val(volumeDelta$)), zone.videoPlayerAudioSettings.minVolume%, zone.videoPlayerAudioSettings.maxVolume%)
	endif
    
End Sub


Sub DecrementVideoChannnelVolumes(zone As Object, channelMask$ As String, volumeDelta$ As String)

	zone = m.GetVideoZone(zone)
	if type(zone) = "roAssociativeArray" then
		channelMask% = int(val(channelMask$))
		delta% = int(val(volumeDelta$))
		m.ChangeVideoVolume(zone, channelMask%, -delta%, zone.videoPlayerAudioSettings.minVolume%, zone.videoPlayerAudioSettings.maxVolume%)
	endif

End Sub


Sub SetAudioVolume(zone As Object, parameter$ As String)

    volume% = int(val(parameter$))
    
    if type(zone) = "roAssociativeArray" then
		if IsAudioPlayer(zone.audioPlayer) then
            zone.audioPlayer.SetVolume(volume%)
            for i% = 0 to 5
                zone.audioChannelVolumes[i%] = volume%
            next
        endif
    endif

End Sub


Sub SetAudioVolumeByConnector(zone As Object, output$ As String, volume$ As String)
    
    if type(zone) = "roAssociativeArray" then
    
		if IsAudioPlayer(zone.audioPlayer) then
        
			volume% = int(val(volume$))

			if zone.audioPlayerAudioSettings.audioMappingSpan% = 3 then
				if output$ = "onboard-audio1" then
					channelMask% = 3
					zone.audioChannelVolumes[0] = volume%
					zone.audioChannelVolumes[1] = volume%
				else if output$ = "onboard-audio2" then
					channelMask% = 12
					zone.audioChannelVolumes[2] = volume%
					zone.audioChannelVolumes[3] = volume%
				else if output$ = "onboard-audio3" then
					channelMask% = 48
					zone.audioChannelVolumes[4] = volume%
					zone.audioChannelVolumes[5] = volume%
				else
					channelMask% = 63
					for i% = 0 to 5
						zone.audioChannelVolumes[i%] = volume%
					next
				endif
			else
				channelMask% = 63
				for i% = 0 to 5
					zone.audioChannelVolumes[i%] = volume%
				next
			endif

'			if analogOutput$ = "onboard-audio1" then
'				channelMask% = 3
'				zone.audioChannelVolumes[0] = volume%
'				zone.audioChannelVolumes[1] = volume%
'			else if analogOutput$ = "onboard-audio2" then
'				channelMask% = 12
'				zone.audioChannelVolumes[2] = volume%
'				zone.audioChannelVolumes[3] = volume%
'			else if analogOutput$ = "onboard-audio3" then
'				channelMask% = 48
'				zone.audioChannelVolumes[4] = volume%
'				zone.audioChannelVolumes[5] = volume%
'			else
'				channelMask% = 63
'				for i% = 0 to 5
'					zone.audioChannelVolumes[i%] = volume%
'				next
'			endif

            zone.audioPlayer.SetChannelVolumes(channelMask%, volume% )
	
        endif
        
    endif

End Sub


Sub SetAudioChannnelVolume(zone As Object, channelMask$ As String, volume$ As String)
    
    if type(zone) = "roAssociativeArray" then
		if IsAudioPlayer(zone.audioPlayer) then
            m.SetChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, int(val(channelMask$)), int(val(volume$)))
        endif
    endif
    
End Sub


Sub IncrementAudioVolume(zone As Object, parameter$ As String, maxVolume% As Integer)

    m.ChangeAudioVolume(zone, 63, int(val(parameter$)), 0, maxVolume%)        

End Sub


Sub DecrementAudioVolume(zone As Object, parameter$ As String, minVolume% As Integer)

    delta% = int(val(parameter$))
    m.ChangeAudioVolume(zone, 63, -delta%, minVolume%, 100)        

End Sub


Sub SetChannelVolumes(player As Object, channelVolumes As Object, channelMask% As Integer, volume% As Integer)

    for i% = 0 to 5
        mask% = 2 ^ i%
        if channelMask% and mask% then
            channelVolumes[i%] = volume%
            player.SetChannelVolumes(mask%, channelVolumes[i%])
' print "SetChannelVolumes - mask = ";mask%;", volume = ";channelVolumes[i%]                  
        endif
    next
            
End Sub


Function GetVideoZone(zone As Object) As Object

	if type(zone) = "roAssociativeArray" then
		if type(zone.videoPlayer) = "roVideoPlayer" then
			return zone
		endif
	endif

    if type(m.sign) = "roAssociativeArray" then
        if type(m.sign.videoZoneHSM) = "roAssociativeArray" and type(m.sign.videoZoneHSM.videoPlayer) = "roVideoPlayer" then
            return m.sign.videoZoneHSM
        endif
    endif

	return invalid

End Function


Function GetZone(zoneId$ As String) As Object

	for each zone in m.sign.zonesHSM
		if zone.id$ = zoneId$ then
			return zone
		endif
	next

	return invalid

End Function


Sub ChangeChannelVolumes(player As Object, channelVolumes As Object, channelMask% As Integer, delta% As Integer, minVolume% As Integer, maxVolume% As Integer)

    for i% = 0 to 5
        mask% = 2 ^ i%
        if channelMask% and mask% then
            channelVolumes[i%] = channelVolumes[i%] + delta%
            if channelVolumes[i%] > maxVolume% then
                channelVolumes[i%] = maxVolume%
            else if channelVolumes[i%] < minVolume% then
                channelVolumes[i%] = minVolume%
            endif
            player.SetChannelVolumes(mask%, channelVolumes[i%])
' print "SetChannelVolumes - mask = ";mask%;", volume = ";channelVolumes[i%]                  
        endif
    next
            
End Sub


Sub ChangeVideoVolumeByConnector(zone As Object, output$ As String, volumeDelta% As Integer)
    
    if type(zone) = "roAssociativeArray" then
    		
		if zone.videoPlayerAudioSettings.audioMappingSpan% = 3 then
			if output$ = "onboard-audio1" then
				m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, 3, volumeDelta%, m.sign.audio1MinVolume%, m.sign.audio1MaxVolume%)
			else if output$ = "onboard-audio2" then
				m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, 12, volumeDelta%, m.sign.audio2MinVolume%, m.sign.audio2MaxVolume%)
			else if output$ = "onboard-audio3" then
				m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, 48, volumeDelta%, m.sign.audio3MinVolume%, m.sign.audio3MaxVolume%)
			else
				m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, 3, volumeDelta%, m.sign.audio1MinVolume%, m.sign.audio1MaxVolume%)
				m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, 12, volumeDelta%, m.sign.audio2MinVolume%, m.sign.audio2MaxVolume%)
				m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, 48, volumeDelta%, m.sign.audio3MinVolume%, m.sign.audio3MaxVolume%)
			endif
'			else if zone.videoPlayerAudioSettings.audioOutput% = 0 then
'	            m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, 3, volumeDelta%, m.sign.audio1MinVolume%, m.sign.audio1MaxVolume%)
'	            m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, 12, volumeDelta%, m.sign.audio2MinVolume%, m.sign.audio2MaxVolume%)
'	            m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, 48, volumeDelta%, m.sign.audio3MinVolume%, m.sign.audio3MaxVolume%)
		else
	        m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, 63, volumeDelta%, zone.videoPlayerAudioSettings.minVolume%, zone.videoPlayerAudioSettings.maxVolume%)
		endif
    
    endif

End Sub


Sub IncrementVideoVolumeByConnector(zone As Object, output$ As String, volumeDelta$ As String)

	zone = m.GetVideoZone(zone)
    delta% = int(val(volumeDelta$))
	m.ChangeVideoVolumeByConnector(zone, output$, delta%)

End Sub


Sub DecrementVideoVolumeByConnector(zone As Object, output$ As String, volumeDelta$ As String)

	zone = m.GetVideoZone(zone)
    delta% = int(val(volumeDelta$))
	m.ChangeVideoVolumeByConnector(zone, output$, -delta%)
	
End Sub


Sub ChangeVideoVolume(zone As Object, channelMask% as Integer, delta% As Integer, minVolume% As Integer, maxVolume% As Integer)

    m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, channelMask%, delta%, minVolume%, maxVolume%)
    
End Sub


Sub IncrementVideoVolume(zone As Object, volumeDelta$ As String)

	zone = m.GetVideoZone(zone)

	if type(zone) = "roAssociativeArray" then
	    m.ChangeVideoVolume(zone, 63, int(val(volumeDelta$)), 0, zone.videoPlayerAudioSettings.maxVolume%)        
	endif

End Sub


Sub DecrementVideoVolume(zone As Object, volumeDelta$ As String)

	zone = m.GetVideoZone(zone)

	if type(zone) = "roAssociativeArray" then
	    delta% = int(val(volumeDelta$))
	    m.ChangeVideoVolume(zone, 63, -delta%, zone.videoPlayerAudioSettings.minVolume%, 100)
	endif

End Sub


Sub ChangeAudioVolumeByConnector(zone As Object, output$ As String, volumeDelta% As Integer)
    
    if type(zone) = "roAssociativeArray" then
    
		if IsAudioPlayer(zone.audioPlayer) then
		
			if zone.audioPlayerAudioSettings.audioMappingSpan% = 3 then
				if output$ = "onboard-audio1" then
					m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, 3, volumeDelta%, m.sign.audio1MinVolume%, m.sign.audio1MaxVolume%)
				else if output$ = "onboard-audio2" then
					m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, 12, volumeDelta%, m.sign.audio2MinVolume%, m.sign.audio2MaxVolume%)
				else if output$ = "onboard-audio3" then
					m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, 48, volumeDelta%, m.sign.audio3MinVolume%, m.sign.audio3MaxVolume%)
				else
					m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, 3, volumeDelta%, m.sign.audio1MinVolume%, m.sign.audio1MaxVolume%)
					m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, 12, volumeDelta%, m.sign.audio2MinVolume%, m.sign.audio2MaxVolume%)
					m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, 48, volumeDelta%, m.sign.audio3MinVolume%, m.sign.audio3MaxVolume%)
				endif
'			else if zone.audioPlayerAudioSettings.audioOutput% = 0 then
'	            m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, 3, volumeDelta%, m.sign.audio1MinVolume%, m.sign.audio1MaxVolume%)
'	            m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, 12, volumeDelta%, m.sign.audio2MinVolume%, m.sign.audio2MaxVolume%)
'	            m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, 48, volumeDelta%, m.sign.audio3MinVolume%, m.sign.audio3MaxVolume%)
			else
	            m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, 63, volumeDelta%, zone.audioPlayerAudioSettings.minVolume%, zone.audioPlayerAudioSettings.maxVolume%)
			endif
        endif
    
    endif

End Sub


Sub IncrementAudioVolumeByConnector(zone As Object, output$ As String, volumeDelta$ As String)

	m.ChangeAudioVolumeByConnector(zone, output$, int(val(volumeDelta$)))
	
End Sub


Sub DecrementAudioVolumeByConnector(zone As Object, output$ As String, volumeDelta$ As String)

    delta% = int(val(volumeDelta$))
	m.ChangeAudioVolumeByConnector(zone, output$, -delta%)
	
End Sub


Sub ChangeAudioVolume(zone As Object, channelMask% as Integer, delta% As Integer, minVolume% As Integer, maxVolume% As Integer)

    if type(zone) = "roAssociativeArray" then
		if IsAudioPlayer(zone.audioPlayer) then
            m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, channelMask%, delta%, minVolume%, maxVolume%)
        endif
    endif
    
End Sub


Sub IncrementAudioChannelVolumes(zone As Object, channelMask$ As String, volumeDelta$ As String)
    
	if IsAudioPlayer(zone.audioPlayer) then
		channelMask% = int(val(channelMask$))
		m.ChangeAudioVolume(zone, channelMask%, int(val(volumeDelta$)), zone.audioPlayerAudioSettings.minVolume%, zone.audioPlayerAudioSettings.maxVolume%)
    endif
    
End Sub


Sub DecrementAudioChannelVolumes(zone As Object, channelMask$ As String, volumeDelta$ As String)

	if IsAudioPlayer(zone.audioPlayer) then
		channelMask% = int(val(channelMask$))
		delta% = int(val(volumeDelta$))
		m.ChangeAudioVolume(zone, channelMask%, -delta%, zone.audioPlayerAudioSettings.minVolume%, zone.audioPlayerAudioSettings.maxVolume%)
    endif

End Sub


Sub ConfigureAudioResources()

	if type(m.videoPlayer) = "roVideoPlayer" then
		m.videoPlayer.ConfigureAudioResources()
	else if IsAudioPlayer(m.audioPlayer) then
		m.audioPlayer.ConfigureAudioResources()
	endif

End Sub


Sub CecDisplayOn()

	m.SendCecCommand("400D", "true")

End Sub


Sub CecDisplayOff()

	m.SendCecCommand("4036", "true")

End Sub


Sub CecSetSourceBrightSign()

	m.SendCecCommand("4F821000", "false")

End Sub


Sub CecPhilipsSetVolume(volume% As Integer)

	b = CreateObject("roByteArray")
	b[0] = volume%
	volumeAsAscii$ = b.ToHexString()
	b = invalid
	setVolume$ = "40A0000C3022" + volumeAsAscii$
	SendCecCommand(setVolume$, "true")
	
End Sub


Sub SendCecCommand(cecCommand$ As String, cecSubstituteSourceAddress$ As String)

	cec = CreateObject("roCecInterface")
	if type(cec) = "roCecInterface" then
		if m.useInitiatorAddressFromPacketSupported and lcase(cecSubstituteSourceAddress$) = "false" then
      cec.UseInitiatorAddressFromPacket(true)
		endif
		b = CreateObject("roByteArray")
		b.fromhexstring(cecCommand$)
		cec.SendRawMessage(b)
		cec = invalid
	endif
	
End Sub


Sub PauseVideo(zone As Object)

	zone = m.GetVideoZone(zone)
	if type(zone) = "roAssociativeArray" then
        zone.videoPlayer.Pause()
    endif

End Sub


Sub ResumeVideo(zone As Object)

	zone = m.GetVideoZone(zone)
	if type(zone) = "roAssociativeArray" then
        zone.videoPlayer.Resume()
    endif

End Sub


Sub SetPowerSaveMode(enablePowerSaveMode As Boolean)

    videoMode = CreateObject("roVideoMode")
    videoMode.SetPowerSaveMode(enablePowerSaveMode)
    videoMode = invalid

End Sub


Function GetAttachedFiles() As Object

	return m.additionalPublishedFiles

End Function


Function PostponeRestart() As Boolean

	if m.dontChangePresentationUntilMediaEndEventReceived then
		m.restartPendingMediaEnd = true
	endif

	return m.dontChangePresentationUntilMediaEndEventReceived

End Function


Function ProcessMediaEndEvent() As Boolean

	executeContentRestart = m.restartPendingMediaEnd

	if executeContentRestart then

        m.diagnostics.PrintDebug("ProcessMediaEndEvent - execute content update")

		m.restartPendingMediaEnd = false

		' send internal message to prepare for restart
		prepareForRestartEvent = CreateObject("roAssociativeArray")
		prepareForRestartEvent["EventType"] = "PREPARE_FOR_RESTART"
		m.msgPort.PostMessage(prepareForRestartEvent)

		' send internal message indicating that new content is available
		contentUpdatedEvent = CreateObject("roAssociativeArray")
		contentUpdatedEvent["EventType"] = "CONTENT_UPDATED"
		m.msgPort.PostMessage(contentUpdatedEvent)

	endif

	return executeContentRestart

End Function


Function DeviceSupportsRotation() As Boolean

	if m.sysInfo.deviceFamily$ = "panther" or m.sysInfo.deviceFamily$ = "puma" then
		return false
	endif

	return true

End Function


'endregion

'region Common Zone State Machine Methods
' *************************************************
'
' Common Zone State Machine Methods
'
' *************************************************

Sub newZoneCommon(bsp As Object, zoneXML As Object, zoneHSM As Object)

	zoneHSM.audioPlayer = invalid
	zoneHSM.videoPlayer = invalid

    zoneHSM.name$ = zoneXML.name.GetText()

	' scale the zones if necessary
	zoneHSM.originalWidth% = int(val(zoneXML.width.GetText()))
	zoneHSM.originalHeight% = int(val(zoneXML.height.GetText()))
	scaleScreenElement(bsp, true, zoneHSM, zoneXML)

    zoneHSM.type$ = zoneXML.type.GetText()
    zoneHSM.id$ = zoneXML.id.GetText()

	zoneHSM.isVisible = true
	zoneHSM.imageHidden = false
	zoneHSM.canvasHidden = false
	zoneHSM.htmlHidden = false

	zoneHSM.mosaicDecoderName = ""

    zoneHSM.bsp = bsp

	zoneHSM.ConfigureAudioResources = ConfigureAudioResources
	zoneHSM.SetAudioOutputAndMode = SetAudioOutputAndMode

	zoneHSM.LogPlayStart = LogPlayStart
	zoneHSM.ClearImagePlane = ClearImagePlane
	zoneHSM.ShowImageWidget = ShowImageWidget
	zoneHSM.ShowCanvasWidget = ShowCanvasWidget
	zoneHSM.ShowHtmlWidget = ShowHtmlWidget
	zoneHSM.UpdateWidgetVisibility = UpdateWidgetVisibility

	zoneHSM.SendCommandToVideo = SendCommandToVideo

    zoneHSM.stTop = zoneHSM.newHState(bsp, "Top")
    zoneHSM.stTop.HStateEventHandler = STTopEventHandler
    
	zoneHSM.topState = zoneHSM.stTop

End Sub


Sub InitializeZoneCommon(msgPort As Object)

    zoneHSM = m

    zoneHSM.msgPort = msgPort
    
    zoneHSM.isVideoZone = false
    zoneHSM.preloadState = invalid
    zoneHSM.preloadedStateName$ = ""
    
    zoneHSM.rectangle = CreateObject("roRectangle", zoneHSM.x%, zoneHSM.y%, zoneHSM.width%, zoneHSM.height%)

    ' byte arrays to store stream byte input
    zoneHSM.serialStreamInputBuffers = CreateObject("roArray", 8, true)
    for i% = 0 to 7
		zoneHSM.serialStreamInputBuffers[i%] = CreateObject("roByteArray")
	next
    
End Sub

'endregion

'region MediaItem Methods
' *************************************************
'
' MediaItem Methods
'
' *************************************************

Function GetNextStateName(transition As Object) As Object

	nextState = { }

	if type(transition.conditionalTargets) = "roArray" then
		for each conditionalTarget in transition.conditionalTargets

			matchFound = false

			currentValue% = val(conditionalTarget.userVariable.GetCurrentValue())

			userVariableValue = conditionalTarget.userVariableValue.GetCurrentParameterValue()
			userVariableValue% = val(userVariableValue)

			if conditionalTarget.operator$ = "EQ" then
				if conditionalTarget.userVariable.GetCurrentValue() = userVariableValue then
					matchFound = true
				endif
			else if conditionalTarget.operator$ = "NEQ" then
				if conditionalTarget.userVariable.GetCurrentValue() <> userVariableValue then
					matchFound = true
				endif
			else if conditionalTarget.operator$ = "LT" then
				if currentValue% < userVariableValue% then
					matchFound = true
				endif
			else if conditionalTarget.operator$ = "LTE" then
				if currentValue% <= userVariableValue% then
					matchFound = true
				endif
			else if conditionalTarget.operator$ = "GT" then
				if currentValue% > userVariableValue% then
					matchFound = true
				endif
			else if conditionalTarget.operator$ = "GTE" then
				if currentValue% >= userVariableValue% then
					matchFound = true
				endif
			else if conditionalTarget.operator$ = "B" then
				userVariableValue2 = conditionalTarget.userVariableValue2.GetCurrentParameterValue()
				userVariableValue2% = val(userVariableValue2)
				if currentValue% >= userVariableValue% and currentValue% <= userVariableValue2% then
					matchFound = true
				endif
			endif

			if matchFound then
				if conditionalTarget.targetMediaStateIsPreviousState then
					nextState$ = m.stateMachine.previousStateName$
				else
					nextState$ = conditionalTarget.targetMediaState$
				endif

				nextState.nextState$ = nextState$
				nextState.actualTarget = conditionalTarget
				return nextState
			endif

		next
	endif

    if transition.targetMediaStateIsPreviousState then
        nextState$ = m.stateMachine.previousStateName$
    else
        nextState$ = transition.targetMediaState$
    endif

	nextState.nextState$ = nextState$
	nextState.actualTarget = transition
    return nextState
    
End Function


Sub UpdatePreviousCurrentStateNames()

	m.stateMachine.previousStateName$ = m.id$

End Sub


Function GetAnyMediaRSSTransition() As Object

	transition = invalid

	' support others?

	if type(m.signChannelEndEvent) = "roAssociativeArray" then
		transition = m.signChannelEndEvent
	else if type(m.mstimeoutEvent) = "roAssociativeArray" then
		transition = m.mstimeoutEvent
	endif

	return transition

End Function


Function ExecuteTransition(transition As Object, stateData As Object, payload$ As String) As String

	nextState$ = "init"

	while nextState$ <> ""

		' before transitioning to next state, ensure that the transition is allowed
		nextState = m.GetNextStateName(transition)
		nextState$ = nextState.nextState$
		actualTarget = nextState.actualTarget

		if nextState$ <> "" then

			nextState = m.stateMachine.stateTable[nextState$]

			if nextState.type$ = "mediaRSS" and nextState.rssURL$ = "" then
				' skip an empty localized playlist
				m.bsp.diagnostics.PrintDebug("Unassigned local playlist " + nextState.name$ + " encountered, attempt to navigate to next state.")
				m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_UNASSIGNED_LOCAL_PLAYLIST, nextState.name$)

				defaultTransition = nextState.GetAnyMediaRSSTransition()
				if defaultTransition <> invalid then
					transition = defaultTransition
				else
					' no transition found - not sure what to do
					m.bsp.diagnostics.PrintDebug("Unable to navigate from unassigned local playlist " + nextState.name$)
					m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_UNASSIGNED_LOCAL_PLAYLIST_NO_NAVIGATION, nextState.name$)
					exit while
				endif
			
			else

				if nextState.type$ = "playFile" then
					if nextState.useUserVariable then
						userVariable = nextState.userVariable
						payload$ = userVariable.GetCurrentValue()
					endif

					if not nextState.useDefaultMedia and not nextState.filesTable.DoesExist(payload$) then
						m.bsp.diagnostics.PrintDebug("transition cancelled - payload " + payload$ + " not found in target state's table")
						return "HANDLED"
					else
						' set payload$ member before ExecuteTransitionCommands is called - needed if there is a synchronize transition command
						nextState.payload$ = payload$
					endif
				endif

				exit while

			endif

		endif

	end while

	switchToNewPresentation = m.bsp.ExecuteTransitionCommands(m.stateMachine, actualTarget)
	
	if switchToNewPresentation then
		return "HANDLED"
	endif

	if nextState$ = "" then

		if transition.remainOnCurrentStateActions = "stop" then
			if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
				m.stateMachine.videoPlayer.Stop()
			endif
		else if transition.remainOnCurrentStateActions = "stopclear" then
			if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
				m.stateMachine.videoPlayer.StopClear()
			endif
			if type(m.stateMachine.imagePlayer) = "roImageWidget" then
				m.stateMachine.imagePlayer.StopDisplay()
			endif
		endif

		return "HANDLED"

	else
	    stateData.nextState = m.stateMachine.stateTable[nextState$]
		stateData.nextState.payload$ = payload$

		m.UpdatePreviousCurrentStateNames()

	    return "TRANSITION"
	endif

End Function


Sub AssignWildcardInputToUserVariable(bsp As Object, input$ As String)

	if type(m.variableToAssignFromWildcard) = "roAssociativeArray" then
		
		m.variableToAssignFromWildcard.SetCurrentValue(input$, true)

	endif

End Sub


Sub AssignEventInputToUserVariable(bsp As Object, input$ As String)

	if type(m.variableToAssign) = "roAssociativeArray" then
		
		m.variableToAssign.SetCurrentValue(input$, true)

	else

		userVariablesUpdated = false

		regex = CreateObject("roRegEx", "!!", "i")
		variableAssignments = regex.Split(input$)
		if variableAssignments.Count() > 0 then
			for each variableAssignment in variableAssignments
				regex = CreateObject("roRegEx",":","i")
				parts = regex.Split(variableAssignment)
				if parts.Count() = 2 then
					variableToAssign$ = parts[0]
					newValue$ = parts[1]
					variableToAssign = bsp.GetUserVariable(variableToAssign$)
					if variableToAssign = invalid then
						bsp.diagnostics.PrintDebug("User variable " + variableToAssign$ + " not found.")
					else
						variableToAssign.SetCurrentValue(newValue$, false)
						userVariablesUpdated = true
					endif
				endif
			next
		endif

		if userVariablesUpdated then
			userVariablesChanged = CreateObject("roAssociativeArray")
			userVariablesChanged["EventType"] = "USER_VARIABLES_UPDATED"
			bsp.msgPort.PostMessage(userVariablesChanged)

			' Notify controlling devices to refresh
			bsp.SendUDPNotification("refresh")
		endif

	endif

End Sub


Function MediaItemEventHandler(event As Object, stateData As Object) As Object

    if type(event) = "roControlDown" and IsControlPort(m.bsp.controlPort) and type(m.auxDisconnectEvents) = "roAssociativeArray" and stri(event.GetSourceIdentity()) = stri(m.bsp.controlPort.GetIdentity()) then

        m.bsp.diagnostics.PrintDebug("Control Down" + str(event.GetInt()))
        gpioNum$ = StripLeadingSpaces(str(event.GetInt()))

		if gpioNum$ = "31" then
			if type(m.auxDisconnectEvents["BrightSignAuxIn"]) = "roAssociativeArray" then
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "auxDisconnect", gpioNum$, "1")
				return m.ExecuteTransition(m.auxDisconnectEvents["BrightSignAuxIn"], stateData, "")
			endif
		endif

		' need to update if aux in HW reintroduced
'		if type(m.auxConnectEvents) = "roAssociativeArray" then
'			if gpioNum$ = "3" then
'				if type(m.auxConnectEvents["Aux300Audio1"]) = "roAssociativeArray" then
'					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "auxConnect", gpioNum$, "1")
'					return m.ExecuteTransition(m.auxConnectEvents["Aux300Audio1"], stateData, "")
'				endif
'			else if gpioNum$ = "4" then
'				if type(m.auxConnectEvents["Aux300Audio2"]) = "roAssociativeArray" then
'					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "auxConnect", gpioNum$, "1")
'					return m.ExecuteTransition(m.auxConnectEvents["Aux300Audio2"], stateData, "")
'				endif
'			else if gpioNum$ = "5" then
'				if type(m.auxConnectEvents["Aux300Audio3"]) = "roAssociativeArray" then
'					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "auxConnect", gpioNum$, "1")
'					return m.ExecuteTransition(m.auxConnectEvents["Aux300Audio3"], stateData, "")
'				endif
'			endif
'		endif

	else if type(event) = "roControlUp" and IsControlPort(m.bsp.controlPort) and type(m.auxConnectEvents) = "roAssociativeArray" and stri(event.GetSourceIdentity()) = stri(m.bsp.controlPort.GetIdentity()) then
        
		m.bsp.diagnostics.PrintDebug("Control Up" + str(event.GetInt()))
        gpioNum$ = StripLeadingSpaces(str(event.GetInt()))

		if gpioNum$ = "31" then
			if type(m.auxConnectEvents["BrightSignAuxIn"]) = "roAssociativeArray" then
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "auxConnect", gpioNum$, "1")
				return m.ExecuteTransition(m.auxConnectEvents["BrightSignAuxIn"], stateData, "")
			endif
		endif

		' need to update if aux in HW reintroduced
'		if type(m.auxDisconnectEvents) = "roAssociativeArray" then
'			if gpioNum$ = "3" then
'				if type(m.auxDisconnectEvents["Aux300Audio1"]) = "roAssociativeArray" then
'					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "auxDisconnect", gpioNum$, "1")
'					return m.ExecuteTransition(m.auxDisconnectEvents["Aux300Audio1"], stateData, "")
'				endif
'			else if gpioNum$ = "4" then
'				if type(m.auxDisconnectEvents["Aux300Audio2"]) = "roAssociativeArray" then
'					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "auxDisconnect", gpioNum$, "1")
'					return m.ExecuteTransition(m.auxDisconnectEvents["Aux300Audio2"], stateData, "")
'				endif
'			else if gpioNum$ = "5" then
'				if type(m.auxDisconnectEvents["Aux300Audio3"]) = "roAssociativeArray" then
'					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "auxDisconnect", gpioNum$, "1")
'					return m.ExecuteTransition(m.auxDisconnectEvents["Aux300Audio3"], stateData, "")
'				endif
'			endif
'		endif

    else if type(event) = "roTimerEvent" then
        
		if type(m.mstimeoutEvent) = "roAssociativeArray" then
            if type(m.mstimeoutTimer) = "roTimer" then
                if stri(event.GetSourceIdentity()) = stri(m.mstimeoutTimer.GetIdentity()) then
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timer", "", "1")
					return m.ExecuteTransition(m.mstimeoutEvent, stateData, "")
                endif
            endif
        endif

		if type(m.timeClockEvents) = "roArray" then
			for each timeClockEvent in m.timeClockEvents
				if type(timeClockEvent.timer) = "roTimer" then
	                if stri(event.GetSourceIdentity()) = stri(timeClockEvent.timer.GetIdentity()) then
						systemTime = CreateObject("roSystemTime")
						currentDateTime = systemTime.GetLocalDateTime()

						' daily timer
						if type(timeClockEvent.timeClockDaily%) = "roInt" then

							triggerEvent = EventActiveToday(currentDateTime, timeClockEvent.daysOfWeek%)

							' restart timer
							LaunchTimeClockEventTimer(m, timeClockEvent)

							if not triggerEvent then
								m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timeClock", "", "0")
								return "HANDLED"
							endif

						' periodic timer
						else if type(timeClockEvent.timeClockPeriodicInterval%) = "roInt" then

							' units in seconds rather than minutes?
							currentTime% = currentDateTime.GetHour() * 60 + currentDateTime.GetMinute()
							startTime% = timeClockEvent.timeClockPeriodicStartTime%
							endTime% = timeClockEvent.timeClockPeriodicEndTime%
							intervalTime% = timeClockEvent.timeClockPeriodicInterval%

							triggerEvent = false
							withinWindow = TimeWithinWindow(currentTime%, startTime%, endTime%)
							if withinWindow then
								triggerEvent = EventActiveToday(currentDateTime, timeClockEvent.daysOfWeek%)
							endif

							' restart timer
							LaunchTimeClockEventTimer(m, timeClockEvent)

							if not triggerEvent then
								m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timeClock", "", "0")
								return "HANDLED"
							endif
						endif

						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timeClock", "", "1")
						return m.ExecuteTransition(timeClockEvent.transition, stateData, "")

					endif
				endif
			next
		endif

		if type(m.bsp.mediaListInactivity) = "roAssociativeArray" then
			if type(m.bsp.mediaListInactivity.timer) = "roTimer" then
				if stri(event.GetSourceIdentity()) = stri(m.bsp.mediaListInactivity.timer.GetIdentity()) then
					' reset indices for all media lists
					if type(m.bsp.mediaListInactivity.mediaListStates) = "roList" then
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaListInactivityTimer", "", "1")
						for each mediaListState in m.bsp.mediaListInactivity.mediaListStates
							mediaListState.playbackIndex% = mediaListState.startIndex%
						next
					else
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaListInactivityTimer", "", "0")
					endif
					return "HANDLED"
				endif
			endif
		endif

		userData = event.GetUserData()
		if type(userData) = "roAssociativeArray" then
			if IsString(userData.id) then
				if userData.id = "mediaList" then
					mediaListState = userData.state
					mediaListState.playbackIndex% = mediaListState.startIndex%
					return "HANDLED"
				endif
			endif
		endif

		m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timer", "", "0")

    else if type(event) = "roTouchEvent" then
		touchIndex$ = str(event.GetInt())
		m.bsp.diagnostics.PrintDebug("Touch event" + touchIndex$)
        if type(m.touchEvents) = "roAssociativeArray" then
			touchEvent = m.touchEvents[touchIndex$]
            if type(touchEvent) = "roAssociativeArray" then
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "touch", touchIndex$, "1")
				return m.ExecuteTransition(touchEvent, stateData, "")
            endif
        endif
		m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "touch", touchIndex$, "0")

	else if type(event) = "roStreamEndEvent" then
	' Bose port?
		port$ = event.GetUserData()

		m.bsp.diagnostics.PrintDebug("roStreamEndEvent received on port " + port$)
		m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "streamEnd", port$, "0")
		m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_STREAM_END, port$)

 		outputOnly = false

' WHAT IS THE PURPOSE OF THIS CODE?
		if type(m.bsp.serial) = "roAssociativeArray" and m.bsp.serial.DoesExist(port$) then
			m.bsp.serial[port$] = invalid
		endif

		if type(m.bsp.serialOutputOnlySpec) = "roAssociativeArray" and m.bsp.serialOutputOnlySpec.DoesExist(port$) then
			outputOnly = m.bsp.serialOutputOnlySpec[port$]
		endif

		m.bsp.ScheduleRetryCreateSerial(port$, outputOnly)

    else if type(event) = "roStreamLineEvent" then
        
        serialEvent$ = event.GetString()
		port$ = event.GetUserData()

		if not IsUsbCommunicationPort(port$) then

			port% = int(val(port$))

			if m.bsp.gpsConfigured and m.bsp.sign.serialPortConfigurations[port%].gps then

				gpsData = ParseGPSdataGPRMCformat(event)

				if gpsData.valid then

					' log GPS events on first event, then no more frequently than every 30 seconds
					logGPSEvent = false
					currentTime = m.bsp.systemTime.GetLocalDateTime()
					if type(m.nextTimeToLogGPSEvent$) = "roString" then
						if currentTime.GetString() > m.nextTimeToLogGPSEvent$ then
							logGPSEvent = true
						endif
					else
						logGPSEvent = true
					endif

					if logGPSEvent then
						currentTime.AddSeconds(30)
						m.nextTimeToLogGPSEvent$ = currentTime.GetString()
					endif

					if gpsData.fixActive then

						if logGPSEvent then
							m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_GPS_LOCATION, str(gpsData.latitude) + ":" + str(gpsData.longitude))
						endif

						m.bsp.diagnostics.PrintDebug("GPS location: " + str(gpsData.latitude) + "," + str(gpsData.longitude))
						m.bsp.gpsLocation.latitude = gpsData.latitude
						m.bsp.gpsLocation.longitude = gpsData.longitude

						latitudeInRadians = ConvertDecimalDegtoRad(m.bsp.gpsLocation.latitude)
						longitudeInRadians = ConvertDecimalDegtoRad(m.bsp.gpsLocation.longitude)

						for each transition in m.gpsEnterRegionEvents

							distance = CalcGPSDistance (latitudeInRadians, longitudeInRadians, transition.latitudeInRadians, transition.longitudeInRadians)
							m.bsp.diagnostics.PrintDebug("GPS distance from longitude " + str(transition.longitude) + ", latitude " + str(transition.latitude) + " = " + str(distance))

							if distance < transition.radiusInFeet then
								m.bsp.diagnostics.PrintDebug("GPS enter region")
								m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpsEnterRegion", str(gpsData.latitude) + ":" + str(gpsData.longitude), "1")
								return m.ExecuteTransition(transition, stateData, "")
							endif

						next

						for each transition in m.gpsExitRegionEvents

							distance = CalcGPSDistance (latitudeInRadians, longitudeInRadians, transition.latitudeInRadians, transition.longitudeInRadians)
							m.bsp.diagnostics.PrintDebug("GPS distance from longitude " + str(transition.longitude) + ", latitude " + str(transition.latitude) + " = " + str(distance))

							if distance > transition.radiusInFeet then
								m.bsp.diagnostics.PrintDebug("GPS exit region")
								m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpsExitRegion", str(gpsData.latitude) + ":" + str(gpsData.longitude), "1")
								return m.ExecuteTransition(transition, stateData, "")
							endif

						next

					else
						if logGPSEvent then
							m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_GPS_NOT_LOCKED, "")
						endif

						m.bsp.gpsLocation.latitude = invalid
						m.bsp.gpsLocation.longitude = invalid
					endif
				else
					' print "GPS not valid"
				endif

				stateData.nextState = m.superState
				return "SUPER"

			endif
		endif

	    m.bsp.diagnostics.PrintDebug("Serial Line Event " + event.GetString())

        serialEvents = m.serialEvents

        if type(serialEvents) = "roAssociativeArray" then
            if type(serialEvents[port$]) = "roAssociativeArray" then
				' look for an exact match first
                if type(serialEvents[port$][serialEvent$]) = "roAssociativeArray" then
					transition = serialEvents[port$][serialEvent$]
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "1")
					return m.ExecuteTransition(transition, stateData, serialEvent$)
				else
					' if there's no exact match, check for old style wildcard
	                if type(serialEvents[port$]["<*>"]) = "roAssociativeArray" then
						serialEvent$ = "<*>"
						transition = serialEvents[port$][serialEvent$]
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + event.GetString(), "1")

						if transition.assignInputToUserVariable then
							transition.AssignEventInputToUserVariable(m.bsp, event.GetString())
						endif

						return m.ExecuteTransition(transition, stateData, event.GetString())
					else
						' look for regular expression match with each of the possible serial events for the current state
						for each serialEventSpec in serialEvents[port$]
							' only look for regular expressions if spec includes wildcard
							if instr(1, serialEventSpec, "(.*)") > 0 then
								r = CreateObject("roRegEx", serialEventSpec, "i")
								if type(r) = "roRegex" then
									matches = r.match(serialEvent$)
									if matches.Count() > 0 then
										transition = serialEvents[port$][serialEventSpec]
										m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "1")

										if transition.assignInputToUserVariable then
											transition.AssignEventInputToUserVariable(m.bsp, event.GetString())
										endif

										if matches.Count() > 1 and transition.assignWildcardToUserVariable then
											transition.AssignWildcardInputToUserVariable(m.bsp, matches[1])
										endif

										return m.ExecuteTransition(transition, stateData, serialEvent$)
									endif
								endif
							endif
						next
					endif
				endif
            endif
        endif
		m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "0")
        
    else if type(event) = "roStreamByteEvent" then
	    m.bsp.diagnostics.PrintDebug("Serial Byte Event " + str(event.GetInt()))
	    
	    serialByte% = event.GetInt()
		port$ = event.GetUserData()

		' compare the serialStreamInput to all expected inputs. execute transition if a match is found.
		if type(m.serialEvents[port$]) = "roAssociativeArray" then

			port% = int(val(port$))
			serialStreamInput = m.stateMachine.serialStreamInputBuffers[port%]
			while serialStreamInput.Count() >= 64
				serialStreamInput.Shift()
			end while
			serialStreamInput.push(serialByte%)

			if type(m.serialEvents[port$].streamInputTransitionSpecs) = "roArray" then
				streamInputTransitionSpecs = m.serialEvents[port$].streamInputTransitionSpecs
				for i% = 0 to streamInputTransitionSpecs.Count() - 1
					streamInputTransitionSpec = streamInputTransitionSpecs[i%]
					streamInputSpec = streamInputTransitionSpec.inputSpec
					
					if ByteArraysMatch(serialStreamInput, streamInputSpec) then
						serialStreamInput.Clear()
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serialBytes", port$ + " " + streamInputTransitionSpec.asciiSpec, "1")
						return m.ExecuteTransition(streamInputTransitionSpec.transition, stateData, "")
					endif
				next
			endif
		endif

    else if type(event) = "roIRRemotePress" then
		m.bsp.diagnostics.PrintDebug("Remote Event" + stri(event.GetInt()))

		remoteEvent% = event.GetInt()
		remoteEvent$ = ConvertToRemoteCommand(remoteEvent%)
    
		remoteEvents = m.remoteEvents
    
		if type(remoteEvents) = "roAssociativeArray" then
			if type(remoteEvents[remoteEvent$]) = "roAssociativeArray" then
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "remote", remoteEvent$, "1")
				return m.ExecuteTransition(m.remoteEvents[remoteEvent$], stateData, remoteEvent$)
			endif
		endif            
		m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "remote", remoteEvent$, "0")

    else if type(event) = "roKeyboardPress" then
    
		' note - this code does not fully cover the case where one state is waiting for keyboard input
		' and another state is waiting for barcode input.
	    
		m.bsp.diagnostics.PrintDebug("Keyboard Press" + str(event.GetInt()))

		keyboardChar$ = chr(event.GetInt())
		keyboardCharForLog$ = StripLeadingSpaces(stri(event.GetInt()))

        usbStringEvents = m.usbStringEvents
        keyboardEvents = m.keyboardEvents

        checkUSBInputString = false
        if type(usbStringEvents) = "roAssociativeArray" then
            if event.GetInt() <> 13 then
                m.usbInputBuffer$ = m.usbInputBuffer$ + keyboardChar$
				if m.usbInputLogBuffer$ = "" then
					m.usbInputLogBuffer$ = keyboardCharForLog$
				else
					m.usbInputLogBuffer$ = m.usbInputLogBuffer$ + "," + keyboardCharForLog$
				endif
                checkUSBInputString = false
            else
                checkUSBInputString = true
            endif
        endif
                    
        ' check for bar code input (usb characters terminated by an Enter key)
        if type(usbStringEvents) = "roAssociativeArray" then
            if checkUSBInputString then
				if type(usbStringEvents[m.usbInputBuffer$]) = "roAssociativeArray" then
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "usb", m.usbInputLogBuffer$, "1")
					action$ = m.ExecuteTransition(m.usbStringEvents[m.usbInputBuffer$], stateData, m.usbInputBuffer$)
					if event.GetInt() = 13 then
						m.usbInputBuffer$ = ""
						m.usbInputLogBuffer$ = ""
					endif
					return action$
				else
					' check for wildcards
					usbInputBuffer$ = "<any>"
					if type(usbStringEvents[usbInputBuffer$]) = "roAssociativeArray" then
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "usb", m.usbInputLogBuffer$, "1")
						action$ = m.ExecuteTransition(m.usbStringEvents[usbInputBuffer$], stateData, m.usbInputBuffer$)
						if event.GetInt() = 13 then
							m.usbInputBuffer$ = ""
							m.usbInputLogBuffer$ = ""
						endif
						return action$
					endif
				endif
            endif
        endif

        ' check for single keyboard characters
		keyboardPayload$ = keyboardChar$
        if type(keyboardEvents) = "roAssociativeArray" then        
                                
			' if keyboard input is non printable character, convert it to the special code
			keyboardCode$ = m.bsp.GetNonPrintableKeyboardCode(event.GetInt())
			if keyboardCode$ <> "" then
				keyboardChar$ = keyboardCode$
				keyboardPayload$ = keyboardChar$
			endif

            if type(keyboardEvents[keyboardChar$]) = "roAssociativeArray" then
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "keyboard", keyboardCharForLog$, "1")
				action$ = m.ExecuteTransition(m.keyboardEvents[keyboardChar$], stateData, keyboardPayload$)
		        if event.GetInt() = 13 then
					m.usbInputBuffer$ = ""
					m.usbInputLogBuffer$ = ""
				endif
				return action$
            else if type(keyboardEvents["<any>"]) = "roAssociativeArray" then
				keyboardChar$ = "<any>"
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "keyboard", keyboardCharForLog$, "1")
				action$ = m.ExecuteTransition(m.keyboardEvents[keyboardChar$], stateData, keyboardPayload$)
				if event.GetInt() = 13 then
					m.usbInputBuffer$ = ""
					m.usbInputLogBuffer$ = ""
				endif
				return action$
			endif
        endif        

		m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "usb", keyboardCharForLog$, "0")

        ' clear the buffer when the user presses enter
        if event.GetInt() = 13 then
			m.usbInputBuffer$ = ""
			m.usbInputLogBuffer$ = ""
		endif
    
    else if type(event) = "roSyncManagerEvent" then

		synchronizeEvent$ = event.GetId()

		synchronizeEvents = m.synchronizeEvents
		if type(synchronizeEvents) = "roAssociativeArray" then

			if type(synchronizeEvents[synchronizeEvent$]) = "roAssociativeArray" or type(synchronizeEvents["<any>"]) = "roAssociativeArray" then
				m.stateMachine.syncInfo = CreateObject("roAssociativeArray")
				m.stateMachine.syncInfo.SyncDomain = event.GetDomain()
				m.stateMachine.syncInfo.SyncId = event.GetId()
				m.stateMachine.syncInfo.SyncIsoTimestamp = event.GetIsoTimestamp()

				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "enhancedSynchronize", synchronizeEvent$, "1")
			
				if type(synchronizeEvents[synchronizeEvent$]) = "roAssociativeArray"
					return m.ExecuteTransition(m.synchronizeEvents[synchronizeEvent$], stateData, synchronizeEvent$)
				else
					' Synchronising with a PlayFile state
					return m.ExecuteTransition(m.synchronizeEvents["<any>"], stateData, synchronizeEvent$)
				endif
			endif

		endif

	else if type(event) = "roDatagramEvent" then

		' could be either a udp event or a synchronize event
    
		m.bsp.diagnostics.PrintDebug("UDP Event " + event.GetString())
    
		udpEvent$ = event.GetString()
    
        synchronizeEvents = m.synchronizeEvents
        udpEvents = m.udpEvents
            
        ' check to see if this is a synchronization preload or play event
        if type(synchronizeEvents) = "roAssociativeArray" then
			index% = instr(1, udpEvent$, "pre-")
			if index% = 1 then
				' preload next file
				synchronizeEvent$ = mid(udpEvent$, 5)
				if type(synchronizeEvents[synchronizeEvent$]) = "roAssociativeArray" then
                
					' get the next file and preload it
					nextState$ = synchronizeEvents[synchronizeEvent$].targetMediaState$
					nextState = m.stateMachine.stateTable[nextState$]

					preloadRequired = true
					if type(m.stateMachine.preloadState) = "roAssociativeArray" then
						if m.stateMachine.preloadedStateName$ = nextState.name$
							preloadRequired = false
						endif
					endif                                    

					' set this variable so that launchVideo knows what has been preloaded
					m.stateMachine.preloadState = nextState
                    
					' currently only support preload / synchronizing with images and videos
					if preloadRequired then
						m.stateMachine.preloadState.PreloadItem()
					endif

					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "synchronize-pre", synchronizeEvent$, "1")

					' ?? return "HANDLED" ??
				endif
			endif
			index% = instr(1, udpEvent$, "ply-")
			if index% = 1 then
				' just transition to the next state where the file will be played
				synchronizeEvent$ = mid(udpEvent$, 5)
				if type(synchronizeEvents[synchronizeEvent$]) = "roAssociativeArray" then
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "synchronize-play", synchronizeEvent$, "1")
					return m.ExecuteTransition(m.synchronizeEvents[synchronizeEvent$], stateData, "")
				endif
			endif

		endif

        if type(udpEvents) = "roAssociativeArray" then
            if type(udpEvents[udpEvent$]) = "roAssociativeArray" then
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "udp", udpEvent$, "1")
				transition = udpEvents[udpEvent$]
				return m.ExecuteTransition(transition, stateData, udpEvent$)
			else
				' if there's no exact match, check for old style wildcard
		        if type(udpEvents["<any>"]) = "roAssociativeArray" then
					udpEvent$ = "<any>"
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "udp", event.GetString(), "1")
					transition = udpEvents[udpEvent$]

					if transition.assignInputToUserVariable then
						transition.AssignEventInputToUserVariable(m.bsp, event.GetString())
					endif

					return m.ExecuteTransition(transition, stateData, event.GetString())
				else
					' look for regular expression match with each of the possible udp events for the current state
					for each udpEventSpec in udpEvents
						' only look for regular expressions if spec includes wildcard
						if instr(1, udpEventSpec, "(.*)") > 0 then
							r = CreateObject("roRegEx", udpEventSpec, "i")
							if type(r) = "roRegex" then
								matches = r.match(udpEvent$)
								if matches.Count() > 0 then
									transition = udpEvents[udpEventSpec]
									m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "udp", udpEvent$, "1")

									if transition.assignInputToUserVariable then
										transition.AssignEventInputToUserVariable(m.bsp, event.GetString())
									endif

									if matches.Count() > 1 and transition.assignWildcardToUserVariable then
										transition.AssignWildcardInputToUserVariable(m.bsp, matches[1])
									endif

									return m.ExecuteTransition(transition, stateData, udpEvent$)
								endif
							endif
						endif
					next
				endif
            endif
        endif          

		m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "udp", event.GetString(), "0")
        
    else if type(event) = "roAssociativeArray" then      ' internal message event
        if IsString(event["EventType"]) then
        
			if event["EventType"] = "BPControlDown" then
				bpIndex$ = event["ButtonPanelIndex"]
				bpIndex% = int(val(bpIndex$))
				bpNum$ = event["ButtonNumber"]
				bpNum% = int(val(bpNum$))
				m.bsp.diagnostics.PrintDebug("BP Press" + bpNum$ + " on button panel" + bpIndex$)
				bpEvents = m.bpEvents

				' bpEvents["-1"] => any bp button
				currentBPEvent = bpEvents[bpIndex%]
				transition = currentBPEvent[bpNum$]
				if type(transition) <> "roAssociativeArray" then
					transition = currentBPEvent["-1"]
				endif
				
				if type(transition) = "roAssociativeArray" then
					payload$ = bpIndex$ + "-" + StripLeadingSpaces(stri(bpNum% + 1))
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "1")
					return m.ExecuteTransition(transition, stateData, payload$)
				endif 
				
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "0")

	        else if event["EventType"] = "GPIOControlDown" then

				gpioNum$ = event["ButtonNumber"]

		        m.bsp.diagnostics.PrintDebug("Control Down " + gpioNum$)
				gpioEvents = m.gpioEvents
				if type(gpioEvents[gpioNum$]) = "roAssociativeArray" then
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioButton", gpioNum$, "1")
					return m.ExecuteTransition(gpioEvents[gpioNum$], stateData, "")
				endif

	        else if event["EventType"] = "GPIOControlUp" then

				gpioNum$ = event["ButtonNumber"]

		        m.bsp.diagnostics.PrintDebug("Control Up " + gpioNum$)

				gpioUpEvents = m.gpioUpEvents
				if type(gpioUpEvents[gpioNum$]) = "roAssociativeArray" then
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioButton", gpioNum$, "1")
					return m.ExecuteTransition(gpioUpEvents[gpioNum$], stateData, "")
				endif

			else if event["EventType"] = "SEND_ZONE_MESSAGE" then
	        
				sendZoneMessageParameter$ = event["EventParameter"]

				m.bsp.diagnostics.PrintDebug("ZoneMessageEvent " + sendZoneMessageParameter$)

                if type(m.zoneMessageEvents) = "roAssociativeArray" then
					if type(m.zoneMessageEvents[sendZoneMessageParameter$]) = "roAssociativeArray" then
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "sendZoneMessage", sendZoneMessageParameter$, "1")
						return m.ExecuteTransition(m.zoneMessageEvents[sendZoneMessageParameter$], stateData, "")
					else
						' look for regular expression match with each of the possible zone message events for the current state
						for each sendZoneMessageSpec in m.zoneMessageEvents
							' only look for regular expressions if spec includes wildcard
							if instr(1, sendZoneMessageSpec, "(.*)") > 0 then
								r = CreateObject("roRegEx", sendZoneMessageSpec, "i")
								if type(r) = "roRegex" then
									matches = r.match(sendZoneMessageParameter$)
									if matches.Count() > 0 then
										transition = m.zoneMessageEvents[sendZoneMessageSpec]
										m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "sendZoneMessage", sendZoneMessageParameter$, "1")

										if transition.assignInputToUserVariable then
											transition.AssignEventInputToUserVariable(m.bsp, sendZoneMessageParameter$)
										endif

										if matches.Count() > 1 and transition.assignWildcardToUserVariable then
											transition.AssignWildcardInputToUserVariable(m.bsp, matches[1])
										endif

										return m.ExecuteTransition(transition, stateData, sendZoneMessageParameter$)
									endif
								endif
							endif
						next
					endif
				endif

				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "sendZoneMessage", sendZoneMessageParameter$, "0")
							
	        else if event["EventType"] = "EVENT_PLUGIN_MESSAGE" then
	        
				pluginName$ = event["PluginName"]
				pluginMessage$ = event["PluginMessage"]

				m.bsp.diagnostics.PrintDebug("PluginMessageEvent " + pluginName$ + " " + pluginMessage$)

				key$ = pluginName$ + pluginMessage$

				if type(m.pluginMessageEvents) = "roAssociativeArray" then
					if type(m.pluginMessageEvents[key$]) = "roAssociativeArray" then
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "sendPluginMessage", key$, "1")
						return m.ExecuteTransition(m.pluginMessageEvents[key$], stateData, "")
					else
						key$ = pluginName$ + "<any>"
						if type(m.pluginMessageEvents[key$]) = "roAssociativeArray" then
							m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "sendPluginMessage", key$, "1")
							return m.ExecuteTransition(m.pluginMessageEvents[key$], stateData, "")
						else
							for each pluginMessageEvent in m.pluginMessageEvents
								if instr(1, pluginMessageEvent, "(.*)") > 0 then
									r = CreateObject("roRegEx", pluginMessageEvent, "i")
									if type(r) = "roRegex" then
										key$ = pluginName$ + pluginMessage$
										matches = r.match(key$)
										if matches.Count() > 0 then
											transition = m.pluginMessageEvents[pluginMessageEvent]
											m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "sendPluginMessage", key$, "1")

											if transition.assignInputToUserVariable then
												transition.AssignEventInputToUserVariable(m.bsp, event.PluginMessage)
											endif

											if matches.Count() > 1 and transition.assignWildcardToUserVariable then
												transition.AssignWildcardInputToUserVariable(m.bsp, matches[1])
											endif

											return m.ExecuteTransition(transition, stateData, event.PluginMessage)
										endif
									endif
								endif
							next
						endif
					endif
				endif

				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "sendPluginMessage", key$, "0")

			else if event["EventType"] = "INTERNAL_SYNC_PRELOAD" then

				internalSyncParameter$ = event["EventParameter"]

				m.bsp.diagnostics.PrintDebug("InternalSyncPreloadEvent " + internalSyncParameter$)

				actedOn$ = "0"

                if type(m.internalSynchronizeEvents) = "roAssociativeArray" then
					if type(m.internalSynchronizeEvents[internalSyncParameter$]) = "roAssociativeArray" then
						nextState$ = m.internalSynchronizeEvents[internalSyncParameter$].targetMediaState$
		                m.stateMachine.preloadState = m.stateMachine.stateTable[nextState$]
						m.stateMachine.preloadState.PreloadItem()
						actedOn$ = "1"
					endif
				endif

				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "internalSyncPreload", internalSyncParameter$, actedOn$)

				return "HANDLED"
							
            else if event["EventType"] = "INTERNAL_SYNC_MASTER_PRELOAD" then

				internalSyncParameter$ = event["EventParameter"]

				m.bsp.diagnostics.PrintDebug("InternalSyncMasterPreload " + internalSyncParameter$)

				actedOn$ = "0"

                if type(m.internalSynchronizeEventsMaster) = "roAssociativeArray" then
					if type(m.internalSynchronizeEventsMaster[internalSyncParameter$]) = "roAssociativeArray" then
	                    m.bsp.diagnostics.PrintDebug("post play message with parameter " + internalSyncParameter$)
						internalSyncPlay = CreateObject("roAssociativeArray")
						internalSyncPlay["EventType"] = "INTERNAL_SYNC_PLAY"
						internalSyncPlay["EventParameter"] = internalSyncParameter$
						m.stateMachine.msgPort.PostMessage(internalSyncPlay)
						actedOn$ = "1"
					endif
                endif
            
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "internalSyncMasterPreload", internalSyncParameter$, actedOn$)

                return "HANDLED"

	        else if event["EventType"] = "INTERNAL_SYNC_PLAY" then

				internalSyncParameter$ = event["EventParameter"]

				m.bsp.diagnostics.PrintDebug("InternalSyncPlayEvent " + internalSyncParameter$)

                if type(m.internalSynchronizeEventsMaster) = "roAssociativeArray" then
					if type(m.internalSynchronizeEventsMaster[internalSyncParameter$]) = "roAssociativeArray" then
                        m.bsp.diagnostics.PrintDebug("master play event received - prepare to return")
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "internalSyncMasterPlay", internalSyncParameter$, "1")
						return m.ExecuteTransition(m.internalSynchronizeEventsMaster[internalSyncParameter$], stateData, "")
					endif
				endif

                if type(m.internalSynchronizeEvents) = "roAssociativeArray" then
					if type(m.internalSynchronizeEvents[internalSyncParameter$]) = "roAssociativeArray" then
                        m.bsp.diagnostics.PrintDebug("slave play event received - prepare to return")
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "internalSyncSlavePlay", internalSyncParameter$, "1")
						return m.ExecuteTransition(m.internalSynchronizeEvents[internalSyncParameter$], stateData, "")
					endif
				endif
	
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "internalSyncPlay", internalSyncParameter$, "0")

            else if event["EventType"] = "PREPARE_FOR_RESTART" then

                m.bsp.diagnostics.PrintDebug(m.id$ + " - PREPARE_FOR_RESTART")

				if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
					m.stateMachine.videoPlayer = invalid
				endif
				
				if IsAudioPlayer(m.stateMachine.audioPlayer) then
					m.stateMachine.audioPlayer = invalid
				endif
				
				if type(m.stateMachine.imagePlayer) = "roImageWidget" then
					m.stateMachine.imagePlayer = invalid
				endif
	            
				return "HANDLED"
				
			endif
		endif

	else if (type(event) = "roVideoEvent" and type(m.stateMachine.videoPlayer) = "roVideoPlayer" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity()) or (type(event) = "roAudioEvent" and IsAudioPlayer(m.stateMachine.audioPlayer) and event.GetSourceIdentity() = m.stateMachine.audioPlayer.GetIdentity()) then

        if event.GetInt() = 8 then
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "0")
        else if event.GetInt() = 12 then
'			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "0")
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timeCode", "", "0")
		endif

    endif
               
    stateData.nextState = m.superState
    return "SUPER"
               
End Function


' m is bsp
Sub WaitForSyncResponse(parameter$ As String)

    udpReceiver = CreateObject("roDatagramReceiver", m.udpReceivePort)
    msgPort = CreateObject("roMessagePort")
    udpReceiver.SetPort(msgPort)

    m.udpSender.Send("ply-" + parameter$)
    
    while true
        msg = wait(50, msgPort)
        if type(msg) = "roDatagramEvent" or msg = invalid then
            udpReceiver = invalid
            return
		endif
    endwhile
    
End Sub


Function EventActiveToday(currentDateTime As Object, daysOfWeek% As Integer) As Boolean

	bitwiseDaysOfWeek% = daysOfWeek%
	currentDayOfWeek = currentDateTime.GetDayOfWeek()
	bitDayOfWeek% = 2 ^ currentDayOfWeek
	if (bitwiseDaysOfWeek% and bitDayOfWeek%) <> 0 then
		return true
	endif

	return false

End Function


Function TimeWithinWindow(currentTime% As Integer, startTime% As Integer, endTime% As Integer) As Boolean

	withinWindow = false

	if startTime% = endTime% then
		withinWindow = true
	else if startTime% < endTime% then
		if currentTime% >= startTime% and currentTime% < endTime% then
			withinWindow = true
		endif
	else if currentTime% < endTime% or currentTime% > startTime% then
		withinWindow = true
	endif

	return withinWindow

End Function


Function IsTimeoutInFuture(timeoutDateTime As Object) As Boolean

	systemTime = CreateObject("roSystemTime")
	currentDateTime = systemTime.GetLocalDateTime()
	systemTime = invalid

	return currentDateTime.GetString() < timeoutDateTime.GetString()

End Function


Sub LaunchTimeClockEventTimer(state As Object, timeClockEvent As Object)

    if type(timeClockEvent.timer) = "roTimer" then
		timeClockEvent.timer.Stop()
		timeClockEvent.timer = invalid
	endif

	timer = CreateObject("roTimer")

	if type(timeClockEvent.timeClockEventDateTime) = "roDateTime" then

		dateTime = timeClockEvent.timeClockEventDateTime

		' only set timer if it is in the future
		if not IsTimeoutInFuture(dateTime) then
			return
		endif

        state.bsp.diagnostics.PrintDebug("Set timeout to " + dateTime.GetString())
		timer.SetDateTime(dateTime)

	else if type(timeClockEvent.userVariableName$) = "roString" then
		if type(timeClockEvent.userVariable) = "roAssociativeArray" then
			dateTime$ = timeClockEvent.userVariable.GetCurrentValue()
			dateTime = FixDateTime(dateTime$)

			if type(dateTime) = "roDateTime" then
				' only set timer if it is in the future
				if not IsTimeoutInFuture(dateTime) then
					print "Specified timer is in the past, don't set it: timer time is ";dateTime.GetString()
					return
				endif

		        state.bsp.diagnostics.PrintDebug("Set timeout to " + dateTime.GetString())
				timer.SetDateTime(dateTime)
			else
		        state.bsp.diagnostics.PrintDebug("Timeout specification " + dateTime$ + " is invalid")
				state.bsp.logging.WriteDiagnosticLogEntry(state.bsp.diagnosticCodes.EVENT_INVALID_DATE_TIME_SPEC, dateTime$)
			endif
		endif
	else if type(timeClockEvent.timeClockDaily%) = "roInt" then
		hours% = timeClockEvent.timeClockDaily% / 60
		minutes% = timeClockEvent.timeClockDaily% - (hours% * 60)
		timer.SetTime(hours%, minutes%, 0, 0)
		timer.SetDate(-1, -1, -1)
	else
	    systemTime = CreateObject("roSystemTime")
		currentDateTime = systemTime.GetLocalDateTime()
			
		' units in seconds rather than minutes?
		currentTime% = currentDateTime.GetHour() * 60 + currentDateTime.GetMinute()
		startTime% = timeClockEvent.timeClockPeriodicStartTime%
		endTime% = timeClockEvent.timeClockPeriodicEndTime%
		intervalTime% = timeClockEvent.timeClockPeriodicInterval%

		withinWindow = TimeWithinWindow(currentTime%, startTime%, endTime%)

'		print "currentDateTime = ";currentDateTime.GetString()
'		print "currentTime% = ";currentTime%
'		print "withinWindow = ";withinWindow

		if not withinWindow then
			' set timer for next start time
			hours% = startTime% / 60
			minutes% = startTime% - (hours% * 60)
			timer.SetTime(hours%, minutes%, 0, 0)
			timer.SetDate(-1, -1, -1)
'			print "set time to ";hours%;" hours, ";minutes%;" minutes"
		else
			' set timer for next appropriate time
			if currentTime% > startTime% then
				minutesSinceStartTime% = currentTime% - startTime%
			else
				minutesSinceStartTime% = currentTime% + (24 * 60 - startTime%)
			endif

			' elapsed intervals since the start time?
			numberOfElapsedIntervals% = minutesSinceStartTime% / intervalTime%
			numberOfIntervalsUntilNextTimeout% = numberOfElapsedIntervals% + 1

			' determine time for next timeout
			nextTimeoutTime% = startTime% + (numberOfIntervalsUntilNextTimeout% * intervalTime%)

			' check for wrap to next day
			if nextTimeoutTime% > (24 * 60) then
				nextTimeoutTime% = nextTimeoutTime% - (24 * 60)
			endif

			' set timer for next start time
			hours% = nextTimeoutTime% / 60
			minutes% = nextTimeoutTime% - (hours% * 60)
			timer.SetTime(hours%, minutes%, 0, 0)
			timer.SetDate(-1, -1, -1)

		    state.bsp.diagnostics.PrintDebug("Set timeout to " + stri(hours%) + " hours, " + stri(minutes%) + " minutes.")

		endif

		systemTime = invalid

	endif

    timer.SetPort(state.stateMachine.msgPort)
    timer.Start()
    timeClockEvent.timer = timer

End Sub


Sub LaunchTimer()

    if type(m.mstimeoutEvent) = "roAssociativeArray" then
    
        timer = CreateObject("roTimer")
        timer.SetPort(m.stateMachine.msgPort)
		timer.SetElapsed(0, m.mstimeoutValue%)
        timer.Start()
        m.mstimeoutTimer = timer

	endif

	if type(m.timeClockEvents) = "roArray" then
	
		for each timeClockEvent in m.timeClockEvents
			LaunchTimeClockEventTimer(m, timeClockEvent)
		next

	endif

End Sub


Sub PreloadItem()

	zone = m.stateMachine
	
	if m.type$ = "mediaList" or m.type$ = "mediaRSS" or m.type$ = "signChannel" or m.type$ = "liveText" or m.type$ = "interactiveMenuItem" then
		zone.preloadedStateName$ = ""
		return
	endif

	if m.type$ = "playFile" then
		fileTableEntry = m.filesTable.Lookup(m.payload$)
		fileName$ = fileTableEntry.fileName$
		fileType$ = fileTableEntry.fileType$
		if fileType$ = "image" then
			imageItem = {}
			imageItem.fileName$ = fileName$
			imageItem.useImageBuffer = false
			imageItem.isEncrypted = false
		else if fileType$ = "video" then
			videoItem = {}
			videoItem.fileName$ = fileName$
			videoItem.probeData = fileTableEntry.probeData
			videoItem.isEncrypted = false
		endif
	else if type(m.imageItem) = "roAssociativeArray" then
		imageItem = {}
		imageItem.fileName$ = m.imageItem.fileName$
		imageItem.useImageBuffer = m.imageItem.useImageBuffer
	else if type(m.videoItem) = "roAssociativeArray" then
		videoItem = {}
		videoItem.fileName$ = m.videoItem.fileName$
		if type(m.videoItem.probeData) = "roString" then
			videoItem.probeData = m.videoItem.probeData
		else
			videoItem.probeData = invalid
		endif
	endif

    if type(imageItem) = "roAssociativeArray" then
        if imageItem.useImageBuffer = true then
			m.bsp.diagnostics.PrintDebug("Did not preload file in PreloadItem as it is using an image buffer: " + imageItem.fileName$)
        else
			imageItemFilePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, imageItem.fileName$)

			isEncrypted = m.bsp.encryptionByFile.DoesExist(imageItem.fileName$)
			'isEncrypted = m.bsp.contentEncrypted
			if isEncrypted then
				aa = { }
				aa.AddReplace("Filename", imageItemFilePath$)
				aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
				aa.AddReplace("EncryptionKey", imageItem.fileName$)
				zone.imagePlayer.PreloadFile(aa)
			else
				zone.imagePlayer.PreloadFile(imageItemFilePath$)
			endif

			zone.preloadedStateName$ = m.name$
			m.bsp.diagnostics.PrintDebug("Preloaded file in PreloadItem: " + imageItem.fileName$ + ", " + imageItemFilePath$)
        endif
    else if type(videoItem) = "roAssociativeArray" then
        videoItemFilePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, videoItem.fileName$)

		aa = { }
		aa.AddReplace("Filename", videoItemFilePath$)
		
		if type(videoItem.probeData) = "roString" then
			m.bsp.diagnostics.PrintDebug("PreloadItem: probeData = " + videoItem.probeData)
			aa.AddReplace("ProbeString", videoItem.probeData)
		endif

		isEncrypted = m.bsp.encryptionByFile.DoesExist(videoItem.fileName$)
		'isEncrypted = m.bsp.contentEncrypted
		if isEncrypted then
			aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
			aa.AddReplace("EncryptionKey", videoItem.fileName$)
		endif

		ok = zone.videoPlayer.PreloadFile(aa)
        zone.preloadedStateName$ = m.name$
        m.bsp.diagnostics.PrintDebug("Preloaded file in PreloadItem: " + videoItem.fileName$)
    endif

End Sub

'endregion

'region Images State Machine
' *************************************************
'
' Images State Machine
'
' *************************************************
Function newImagesZoneHSM(bsp As Object, zoneXML As Object) As Object

    zoneHSM = newHSM()
	zoneHSM.ConstructorHandler = ImageZoneConstructor
	zoneHSM.InitialPseudostateHandler = ImageZoneGetInitialState

    newZoneCommon(bsp, zoneXML, zoneHSM)
    
    zoneHSM.imageMode% = GetImageModeValue(zoneXML.zoneSpecificParameters.imageMode.GetText())

	zoneHSM.numImageItems% = 0
	
    return zoneHSM
    
End Function


Sub ImageZoneConstructor()

	m.InitializeZoneCommon(m.bsp.msgPort)
	
    zoneHSM = m
    
	if zoneHSM.numImageItems% > 0 then

		imagePlayer = CreateObject("roImageWidget", zoneHSM.rectangle)

		if CanUseScreenModes({}) then
		  ' no need to rotate per zone if already rotated by screen
		else if m.bsp.DeviceSupportsRotation() then
			if m.bsp.sign.monitorOrientation = "portrait" then
				imagePlayer.SetTransform("rot90")
			else if m.bsp.sign.monitorOrientation = "portraitbottomonright" then
				imagePlayer.SetTransform("rot270")
			endif
		endif

		zoneHSM.imagePlayer = imagePlayer
	    
		' initialize image player parameters
		imagePlayer.SetDefaultMode(zoneHSM.imageMode%)

		m.LoadImageBuffers()

	else
	
		zoneHSM.imagePlayer = invalid
		
	endif
	
	m.CreateObjects()
	
	m.activeState = m.playlist.firstState
	if type(m.playlist.firstState) = "roAssociativeArray" then
		m.previousStateName$ = m.playlist.firstState.id$
	else
		m.previousStateName$ = ""
	endif
		
End Sub

Function ImageZoneGetInitialState() As Object

	return m.activeState

End Function


Function STDisplayingImageEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.DisplayImage("image")

                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

            else
            
		        return m.MediaItemEventHandler(event, stateData)

            endif
            
        endif
    else
        return m.MediaItemEventHandler(event, stateData)
    endif
            
    stateData.nextState = m.superState
    return "SUPER"
    
End Function

'endregion

'region Enhanced Audio State Machine
' *************************************************
'
' Enhanced Audio State Machine
'
' *************************************************
Function newEnhancedAudioZoneHSM(bsp As Object, zoneXML As Object) As Object
    
	zoneHSM = newHSM()
	zoneHSM.ConstructorHandler = EnhancedAudioZoneConstructor
	zoneHSM.InitializeAudioZoneCommon = InitializeAudioZoneCommon
	zoneHSM.InitialPseudostateHandler = EnhancedAudioZoneGetInitialState

    newZoneCommon(bsp, zoneXML, zoneHSM)
    newAudioZoneCommon(zoneXML, zoneHSM)
    
	zoneHSM.fadeLength% = 4
	fadeLength$ = zoneXML.zoneSpecificParameters.fadeLength.GetText()
	if fadeLength$ <> "" then
		zoneHSM.fadeLength% = int(val(fadeLength$))
	endif

    return zoneHSM

End Function


Sub EnhancedAudioZoneConstructor()

    audioPlayer = CreateObject("roAudioPlayerMx")
	m.InitializeAudioZoneCommon(audioPlayer)

End Sub


Function EnhancedAudioZoneGetInitialState() As Object

	return m.activeState

End Function

'endregion

'region Audio State Machine
' *************************************************
'
' Audio State Machine
'
' *************************************************
Function newAudioZoneHSM(bsp As Object, zoneXML As Object) As Object

    zoneHSM = newHSM()
	zoneHSM.ConstructorHandler = AudioZoneConstructor
	zoneHSM.InitializeAudioZoneCommon = InitializeAudioZoneCommon
	zoneHSM.InitialPseudostateHandler = AudioZoneGetInitialState

    newZoneCommon(bsp, zoneXML, zoneHSM)
    newAudioZoneCommon(zoneXML, zoneHSM)

    return zoneHSM

End Function


Sub newAudioZoneCommon(zoneXML As Object, zoneHSM As Object)

    zoneHSM.audioOutput% = GetAudioOutputValue(zoneXML.zoneSpecificParameters.audioOutput.GetText())
    zoneHSM.audioMode% = GetAudioModeValue(zoneXML.zoneSpecificParameters.audioMode.GetText())
    zoneHSM.audioMapping% = GetAudioMappingValue(zoneXML.zoneSpecificParameters.audioMapping.GetText())
	zoneHSM.audioMappingSpan% = GetAudioMappingSpan(zoneHSM.audioOutput%, zoneXML.zoneSpecificParameters.audioMapping.GetText())

	zoneHSM.analogOutput$ = zoneXML.zoneSpecificParameters.analogOutput.GetText()
	zoneHSM.analog2Output$ = zoneXML.zoneSpecificParameters.analog2Output.GetText()
	zoneHSM.analog3Output$ = zoneXML.zoneSpecificParameters.analog3Output.GetText()
	zoneHSM.hdmiOutput$ = zoneXML.zoneSpecificParameters.hdmiOutput.GetText()
	zoneHSM.spdifOutput$ = zoneXML.zoneSpecificParameters.spdifOutput.GetText()
	zoneHSM.usbOutputA$ = zoneXML.zoneSpecificParameters.usbOutputA.GetText()
	zoneHSM.usbOutputB$ = zoneXML.zoneSpecificParameters.usbOutputB.GetText()
	zoneHSM.usbOutputC$ = zoneXML.zoneSpecificParameters.usbOutputC.GetText()
	zoneHSM.usbOutputD$ = zoneXML.zoneSpecificParameters.usbOutputD.GetText()
	zoneHSM.usbOutputTypeA$ = zoneXML.zoneSpecificParameters.usbOutputTypeA.GetText()
	zoneHSM.usbOutputTypeC$ = zoneXML.zoneSpecificParameters.usbOutputTypeC.GetText()
	zoneHSM.usbOutputA1$ = zoneXML.zoneSpecificParameters.usbOutput700_1.GetText()
	zoneHSM.usbOutputA2$ = zoneXML.zoneSpecificParameters.usbOutput700_2.GetText()
	zoneHSM.usbOutputA3$ = zoneXML.zoneSpecificParameters.usbOutput700_3.GetText()
	zoneHSM.usbOutputA4$ = zoneXML.zoneSpecificParameters.usbOutput700_4.GetText()
	zoneHSM.usbOutputA5$ = zoneXML.zoneSpecificParameters.usbOutput700_5.GetText()
	zoneHSM.usbOutputA6$ = zoneXML.zoneSpecificParameters.usbOutput700_6.GetText()
	zoneHSM.usbOutputA7$ = zoneXML.zoneSpecificParameters.usbOutput700_7.GetText()
	zoneHSM.audioMixMode$ = zoneXML.zoneSpecificParameters.audioMixMode.GetText()

	if zoneHSM.analogOutput$ <> "" and zoneHSM.hdmiOutput$ <> "" and zoneHSM.spdifOutput$ <> "" and zoneHSM.audioMixMode$ <> "" then
		zoneHSM.presentationUsesRoAudioOutputParameters = true
	else
		zoneHSM.presentationUsesRoAudioOutputParameters = false
	endif

    zoneHSM.initialAudioVolume% = 100
    audioVolume$ = zoneXML.zoneSpecificParameters.audioVolume.GetText()
    if audioVolume$ <> "" then
        zoneHSM.initialAudioVolume% = int(val(audioVolume$))
    endif

	zoneHSM.minimumVolume% = 0
    minimumVolume$ = zoneXML.zoneSpecificParameters.minimumVolume.GetText()
    if minimumVolume$ <> "" then
        zoneHSM.minimumVolume% = int(val(minimumVolume$))
    endif

	zoneHSM.maximumVolume% = 0
    maximumVolume$ = zoneXML.zoneSpecificParameters.maximumVolume.GetText()
    if maximumVolume$ <> "" then
        zoneHSM.maximumVolume% = int(val(maximumVolume$))
    endif

End Sub


Sub InitializeAudioZoneCommon(audioPlayer As Object)

	m.InitializeZoneCommon(m.bsp.msgPort)
	
    zoneHSM = m
    
    ' create players
    
    zoneHSM.audioVolume% = zoneHSM.initialAudioVolume%
    
    zoneHSM.audioChannelVolumes = CreateObject("roArray", 6, true)
    for i% = 0 to 5
        zoneHSM.audioChannelVolumes[i%] = zoneHSM.audioVolume%
    next
    
    ' initialize audio player parameters
    audioPlayer.SetPort(zoneHSM.msgPort)
    
    zoneHSM.audioPlayer = audioPlayer
    
	m.SetAudioOutputAndMode(audioPlayer)

    ' audioPlayer.SetAudioOutput(zoneHSM.audioOutput%)
    ' audioPlayer.SetAudioMode(zoneHSM.audioMode%)
    audioPlayer.MapStereoOutput(zoneHSM.audioMapping%)
	audioPlayer.SetStereoMappingSpan(zoneHSM.audioMappingSpan%)
    audioPlayer.SetVolume(zoneHSM.audioVolume%)
    audioPlayer.SetLoopMode(false)

	' Panther only
	zoneHSM.ConfigureAudioResources()

	zoneHSM.audioPlayerAudioSettings = CreateObject("roAssociativeArray")
	zoneHSM.audioPlayerAudioSettings.audioOutput% = zoneHSM.audioOutput%
	zoneHSM.audioPlayerAudioSettings.stereoMapping% = zoneHSM.audioMapping%
	zoneHSM.audioPlayerAudioSettings.audioMappingSpan% = zoneHSM.audioMappingSpan%
	m.bsp.SetAudioVolumeLimits(zoneHSM, zoneHSM.audioPlayerAudioSettings) 

	m.activeState = m.playlist.firstState
	if type(m.playlist.firstState) = "roAssociativeArray" then
		m.previousStateName$ = m.playlist.firstState.id$
	else
		m.previousStateName$ = ""
	endif
	
	audioInput = CreateObject("roAudioInput", m.bsp.sign.audioInSampleRate%)
	if type(audioInput) = "roAudioInput" then
		zoneHSM.audioInput = audioInput
	endif
	
	m.CreateObjects()

End Sub


Sub AudioZoneConstructor()

    audioPlayer = CreateObject("roAudioPlayer")
	m.InitializeAudioZoneCommon(audioPlayer)

End Sub


Function AudioZoneGetInitialState() As Object

	return m.activeState

End Function

'endregion

'region Video State Machine
' *************************************************
'
' Video State Machine
'
' *************************************************
Function newVideoZoneHSM(bsp As Object, zoneXML As Object) As Object

    zoneHSM = newHSM()

	zoneHSM.InitializeVideoZoneObjects = InitializeVideoZoneObjects
	zoneHSM.ConstructorHandler = VideoZoneConstructor
	zoneHSM.InitialPseudostateHandler = VideoZoneGetInitialState

    newZoneCommon(bsp, zoneXML, zoneHSM)
    
    zoneHSM.viewMode% = GetViewModeValue(zoneXML.zoneSpecificParameters.viewMode.GetText())
    zoneHSM.audioOutput% = GetAudioOutputValue(zoneXML.zoneSpecificParameters.audioOutput.GetText())
    zoneHSM.audioMode% = GetAudioModeValue(zoneXML.zoneSpecificParameters.audioMode.GetText())
    zoneHSM.audioMapping% = GetAudioMappingValue(zoneXML.zoneSpecificParameters.audioMapping.GetText())
	zoneHSM.audioMappingSpan% = GetAudioMappingSpan(zoneHSM.audioOutput%, zoneXML.zoneSpecificParameters.audioMapping.GetText())
	
	zoneHSM.analogOutput$ = zoneXML.zoneSpecificParameters.analogOutput.GetText()
	zoneHSM.analog2Output$ = zoneXML.zoneSpecificParameters.analog2Output.GetText()
	zoneHSM.analog3Output$ = zoneXML.zoneSpecificParameters.analog3Output.GetText()
	zoneHSM.hdmiOutput$ = zoneXML.zoneSpecificParameters.hdmiOutput.GetText()
	zoneHSM.spdifOutput$ = zoneXML.zoneSpecificParameters.spdifOutput.GetText()
	zoneHSM.usbOutputA$ = zoneXML.zoneSpecificParameters.usbOutputA.GetText()
	zoneHSM.usbOutputB$ = zoneXML.zoneSpecificParameters.usbOutputB.GetText()
	zoneHSM.usbOutputC$ = zoneXML.zoneSpecificParameters.usbOutputC.GetText()
	zoneHSM.usbOutputD$ = zoneXML.zoneSpecificParameters.usbOutputD.GetText()
	zoneHSM.usbOutputTypeA$ = zoneXML.zoneSpecificParameters.usbOutputTypeA.GetText()
	zoneHSM.usbOutputTypeC$ = zoneXML.zoneSpecificParameters.usbOutputTypeC.GetText()
	zoneHSM.usbOutputA1$ = zoneXML.zoneSpecificParameters.usbOutput700_1.GetText()
	zoneHSM.usbOutputA2$ = zoneXML.zoneSpecificParameters.usbOutput700_2.GetText()
	zoneHSM.usbOutputA3$ = zoneXML.zoneSpecificParameters.usbOutput700_3.GetText()
	zoneHSM.usbOutputA4$ = zoneXML.zoneSpecificParameters.usbOutput700_4.GetText()
	zoneHSM.usbOutputA5$ = zoneXML.zoneSpecificParameters.usbOutput700_5.GetText()
	zoneHSM.usbOutputA6$ = zoneXML.zoneSpecificParameters.usbOutput700_6.GetText()
	zoneHSM.usbOutputA7$ = zoneXML.zoneSpecificParameters.usbOutput700_7.GetText()

	zoneHSM.audioMixMode$ = zoneXML.zoneSpecificParameters.audioMixMode.GetText()

	if zoneHSM.analogOutput$ <> "" and zoneHSM.hdmiOutput$ <> "" and zoneHSM.spdifOutput$ <> "" and zoneHSM.audioMixMode$ <> "" then
		zoneHSM.presentationUsesRoAudioOutputParameters = true
	else
		zoneHSM.presentationUsesRoAudioOutputParameters = false
	endif

    zoneHSM.initialVideoVolume% = 100
    videoVolume$ = zoneXML.zoneSpecificParameters.videoVolume.GetText()
    if videoVolume$ <> "" then
        zoneHSM.initialVideoVolume% = int(val(videoVolume$))
    endif
    
    zoneHSM.initialAudioVolume% = 100
    audioVolume$ = zoneXML.zoneSpecificParameters.audioVolume.GetText()
    if audioVolume$ <> "" then
        zoneHSM.initialAudioVolume% = int(val(audioVolume$))
    endif
    
	zoneHSM.minimumVolume% = 0
    minimumVolume$ = zoneXML.zoneSpecificParameters.minimumVolume.GetText()
    if minimumVolume$ <> "" then
        zoneHSM.minimumVolume% = int(val(minimumVolume$))
    endif

	zoneHSM.maximumVolume% = 0
    maximumVolume$ = zoneXML.zoneSpecificParameters.maximumVolume.GetText()
    if maximumVolume$ <> "" then
        zoneHSM.maximumVolume% = int(val(maximumVolume$))
    endif

    zoneHSM.videoInput$ = zoneXML.zoneSpecificParameters.liveVideoInput.GetText()
    zoneHSM.videoStandard$ = zoneXML.zoneSpecificParameters.liveVideoStandard.GetText()
    zoneHSM.brightness% = int(val(zoneXML.zoneSpecificParameters.brightness.GetText()))
    zoneHSM.contrast% = int(val(zoneXML.zoneSpecificParameters.contrast.GetText()))
    zoneHSM.saturation% = int(val(zoneXML.zoneSpecificParameters.saturation.GetText()))
    zoneHSM.hue% = int(val(zoneXML.zoneSpecificParameters.hue.GetText()))
    
	zoneHSM.zOrderFront = true
	zOrderFront$ = zoneXML.zoneSpecificParameters.zOrderFront.GetText()
	if lcase(zOrderFront$) = "false" then
		zoneHSM.zOrderFront = false
	endif

	zoneHSM.mosaicDecoderName = zoneXML.zoneSpecificParameters.mosaicDecoderName.GetText()

    return zoneHSM

End Function


' use roAudioOutput if all of the following are true
'		current presentation was published for a device that supports roAudioOutput (m.bsp.currentPresentationUsesRoAudioOutputParameters)
'		current presentation includes parameters for roAudioOutput (m.presentationUsesRoAudioOutputParameters)
'			this only applies to Panther. Old Panther presentations did not use roAudioOutput parameters
Sub SetAudioOutputAndMode(player As Object)
	
	if m.presentationUsesRoAudioOutputParameters and m.bsp.currentPresentationUsesRoAudioOutputParameters then

		pcm = CreateObject("roArray", 1, true)
		compressed = CreateObject("roArray", 1, true)
		multichannel = CreateObject("roArray", 1, true)

		analogAudioOutput = CreateObject("roAudioOutput", "Analog:1")
		analog2AudioOutput = CreateObject("roAudioOutput", "Analog:2")
		analog3AudioOutput = CreateObject("roAudioOutput", "Analog:3")
		hdmiAudioOutput = CreateObject("roAudioOutput", "HDMI")
		spdifAudioOutput = CreateObject("roAudioOutput", "SPDIF")

		if lcase(m.analogOutput$) <> "none" and lcase(m.analogOutput$) <> "multichannel" then
			pcm.push(analogAudioOutput)
		endif

		if lcase(m.analog2Output$) = "pcm" then
			pcm.push(analog2AudioOutput)
		endif

		if lcase(m.analog3Output$) = "pcm" then
			pcm.push(analog3AudioOutput)
		endif

		if lcase(m.analogOutput$)="multichannel" then
			multichannel.push(analogAudioOutput)
		else if lcase(m.analog2Output$)="multichannel" then
			multichannel.push(analog2AudioOutput)
		else if lcase(m.analog3Output$)="multichannel" then
			multichannel.push(analog3AudioOutput)
		endif

		if lcase(m.hdmiOutput$) = "passthrough" then
			compressed.push(hdmiAudioOutput)
		else if lcase(m.hdmiOutput$) <> "none" then
			pcm.push(hdmiAudioOutput)
		endif

		if lcase(m.spdifOutput$) = "passthrough" then
			compressed.push(spdifAudioOutput)
		else if lcase(m.spdifOutput$) <> "none" then
			pcm.push(spdifAudioOutput)
		endif

		gaa = GetGlobalAA()

		for each boseUSBPort in m.bsp.boseUSBAudioDevicesByConnector
			usbPort$ = m.bsp.boseUSBAudioDevicesByConnector[boseUSBPort]
			usbAudioOutput = CreateObject("roAudioOutput", usbPort$)

' convert boseUSBPort to usb friendly name

			usbDeviceName = "USB " + boseUSBPort
			for each key in gaa.usbDevicesByConnector
				if usbDeviceName = gaa.usbDevicesByConnector[key] then
					usbFriendlyName = key
				endif
			next

			if usbFriendlyName = "USB A" then
				spec$ = m.usbOutputA$
			else if usbFriendlyName = "USB B" then
				spec$ = m.usbOutputB$
			else if usbFriendlyName = "USB C" then
				spec$ = m.usbOutputC$
			else if usbFriendlyName = "USB D" then
				spec$ = m.usbOutputD$
			else if usbFriendlyName = "USB Type_A" then
				spec$ = m.usbOutputTypeA$
			else if usbFriendlyName = "USB Type_C" then
				spec$ = m.usbOutputTypeC$
			else if usbFriendlyName = "USB 700_1" then
				spec$ = m.usbOutputA1$
			else if usbFriendlyName = "USB 700_2" then
				spec$ = m.usbOutputA2$
			else if usbFriendlyName = "USB 700_3" then
				spec$ = m.usbOutputA3$
			else if usbFriendlyName = "USB 700_4" then
				spec$ = m.usbOutputA4$
			else if usbFriendlyName = "USB 700_5" then
				spec$ = m.usbOutputA5$
			else if usbFriendlyName = "USB 700_6" then
				spec$ = m.usbOutputA6$
			else if usbFriendlyName = "USB 700_7" then
				spec$ = m.usbOutputA7$
			endif

			if type(usbAudioOutput) = "roAudioOutput" then
				if lcase(spec$) = "pcm" then
					pcm.push(usbAudioOutput)
				else if lcase(spec$) = "multichannel" then
					multichannel.push(usbAudioOutput)
				endif
			endif
		next

		if pcm.Count() = 0 then
			noPCMAudioOutput = CreateObject("roAudioOutput", "none")
			pcm.push(noPCMAudioOutput)
		endif

		if compressed.Count() = 0 then
			noCompressedAudioOutput = CreateObject("roAudioOutput", "none")
			compressed.push(noCompressedAudioOutput)
		endif

		if multichannel.Count() = 0 then
			noMultichannelAudioOutput = CreateObject("roAudioOutput", "none")
			multichannel.push(noMultichannelAudioOutput)
		endif

		player.SetPcmAudioOutputs(pcm)
		player.SetCompressedAudioOutputs(compressed)
		player.SetMultichannelAudioOutputs(multichannel)

		if lcase(m.audioMixMode$) = "passthrough" then
			player.SetAudioMode(0)
		else if lcase(m.audioMixMode$) = "left" then
			player.SetAudioMode(3)
		else if lcase(m.audioMixMode$) = "right" then
			player.SetAudioMode(4)
		else
			player.SetAudioMode(1)
		endif

	else
	
		player.SetAudioOutput(m.audioOutput%)
		player.SetAudioMode(m.audioMode%)

	endif

End Sub


Sub InitializeVideoZoneObjects()
	
	m.InitializeZoneCommon(m.bsp.msgPort)
	
    zoneHSM = m
    
    ' create players
    
	' reclaim memory (destroy any leaked video players)
	RunGarbageCollector()

    videoPlayer = CreateObject("roVideoPlayer")
    if type(videoPlayer) <> "roVideoPlayer" then print "videoPlayer creation failed" : stop

	if CanUseScreenModes({}) then
		' no need to rotate per zone if already rotated by screen
	else if m.bsp.DeviceSupportsRotation() then
		if m.bsp.sign.monitorOrientation = "portrait" then
			videoPlayer.SetTransform("rot90")
		else if m.bsp.sign.monitorOrientation = "portraitbottomonright" then
			videoPlayer.SetTransform("rot270")
		endif
	endif

    videoPlayer.SetRectangle(zoneHSM.rectangle)
	
    videoInput = CreateObject("roVideoInput")
	
    zoneHSM.videoPlayer = videoPlayer
    zoneHSM.videoInput = videoInput
    zoneHSM.isVideoZone = true
    zoneHSM.videoVolume% = zoneHSM.initialVideoVolume%
    zoneHSM.audioVolume% = zoneHSM.initialAudioVolume%
    
    zoneHSM.videoChannelVolumes = CreateObject("roArray", 6, true)
    zoneHSM.audioChannelVolumes = CreateObject("roArray", 6, true)
    for i% = 0 to 5
        zoneHSM.videoChannelVolumes[i%] = zoneHSM.videoVolume%
        zoneHSM.audioChannelVolumes[i%] = zoneHSM.audioVolume%
    next
    
    ' initialize video player parameters
    videoPlayer.SetPort(zoneHSM.msgPort)
    videoPlayer.SetViewMode(zoneHSM.viewMode%)
    videoPlayer.SetLoopMode(false)

	m.SetAudioOutputAndMode(videoPlayer)

    ' videoPlayer.SetAudioOutput(zoneHSM.audioOutput%)
    ' videoPlayer.SetAudioMode(zoneHSM.audioMode%)
    videoPlayer.MapStereoOutput(zoneHSM.audioMapping%)
    videoPlayer.SetStereoMappingSpan(zoneHSM.audioMappingSpan%)
    videoPlayer.SetVolume(zoneHSM.videoVolume%)

	' Panther only
 	zoneHSM.ConfigureAudioResources()

	' refactor to not puma, not panther
	if m.bsp.sysInfo.deviceFamily$ = "cheetah" or m.bsp.sysInfo.deviceFamily$ = "tiger" or m.bsp.sysInfo.deviceFamily$ = "lynx" or m.bsp.sysInfo.deviceFamily$ = "impala" or m.bsp.sysInfo.deviceFamily$ = "malibu" or m.bsp.sysInfo.deviceFamily$ = "pantera" then
		if zoneHSM.zOrderFront then
			videoPlayer.ToFront()
		else
			videoPlayer.ToBack()
		endif
	endif

	zoneHSM.videoPlayerAudioSettings = CreateObject("roAssociativeArray")
	zoneHSM.videoPlayerAudioSettings.audioOutput% = zoneHSM.audioOutput%
	zoneHSM.videoPlayerAudioSettings.stereoMapping% = zoneHSM.audioMapping%
	zoneHSM.videoPlayerAudioSettings.audioMappingSpan% = zoneHSM.audioMappingSpan%
	m.bsp.SetAudioVolumeLimits(zoneHSM, zoneHSM.videoPlayerAudioSettings) 
	
    ' initialize live video parameters
    videoInput.SetInput(zoneHSM.videoInput$)
    videoInput.SetStandard(zoneHSM.videoStandard$)
    videoInput.SetControlValue("brightness", zoneHSM.brightness%)
    videoInput.SetControlValue("contrast", zoneHSM.contrast%)
    videoInput.SetControlValue("saturation", zoneHSM.saturation%)
    videoInput.SetControlValue("hue", zoneHSM.hue%)

	' initialize tuner parameter
	m.currentChannelIndex% = 0
	m.firstTuneToChannel = true

	m.activeState = m.playlist.firstState
	if type(m.playlist.firstState) = "roAssociativeArray" then
		m.previousStateName$ = m.playlist.firstState.id$
	else
		m.previousStateName$ = ""
	endif
		
End Sub


Sub VideoZoneConstructor()

	activeState = m.InitializeVideoZoneObjects()
	
	m.CreateObjects()

End Sub


Function VideoZoneGetInitialState() As Object

	return m.activeState

End Function


Function STStreamPlayingEventHandler(event As Object, stateData As Object) As Object

    MEDIA_END = 8

    stateData.nextState = invalid
    
	if type(m.stateMachine.audioPlayer) = "roAudioPlayer" or type(m.stateMachine.audioPlayer) = "roAudioPlayerMx" then
		audioPlayer = m.stateMachine.audioPlayer
	else
		audioPlayer = m.stateMachine.videoPlayer
	endif

    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.ConfigureBPButtons()
				m.ConfigureGPIOButtons()
				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)

				url$ = m.url.GetCurrentParameterValue()
				m.rtspStream = CreateObject("roRtspStream", url$)

				if m.mediaType$ = "video" then
					aa = {}
					aa["Rtsp"] = m.rtspStream
					if m.bsp.sign.isVideoWall and m.bsp.sign.videoWallType$ = "stretched" then
						aa["MultiscreenWidth"] = m.bsp.sign.videoWallNumColumns%
						aa["MultiscreenHeight"] = m.bsp.sign.videoWallNumRows%
						aa["MultiscreenX"] = m.bsp.sign.videoWallColumnPosition%
						aa["MultiscreenY"] = m.bsp.sign.videoWallRowPosition%
						aa["Mode"] = m.stateMachine.viewMode%
					endif

					loopMode% = 1
					if type(m.videoEndEvent) = "roAssociativeArray" then loopMode% = 0
					m.stateMachine.videoPlayer.SetLoopMode(loopMode%)

					if m.stateMachine.mosaicDecoderName <> "" then
						aa.Decoder = m.stateMachine.mosaicDecoderName
					endif

					ok = m.stateMachine.videoPlayer.PlayFile(aa)
					
					if ok = 0 then
						m.bsp.diagnostics.PrintDebug("Error playing rtsp file in STStreamPlayingEventHandler: url = " + url$)
						videoStreamPlaybackFailure = CreateObject("roAssociativeArray")
						videoStreamPlaybackFailure["EventType"] = "VideoStreamPlaybackFailure"
						m.stateMachine.msgPort.PostMessage(videoStreamPlaybackFailure)
					endif
				else
					ok = audioPlayer.PlayFile({Rtsp: m.rtspStream})
					if ok = 0 then
						m.bsp.diagnostics.PrintDebug("Error playing rtsp file in STStreamPlayingEventHandler: url = " + url$)
						audioStreamPlaybackFailure = CreateObject("roAssociativeArray")
						audioStreamPlaybackFailure["EventType"] = "AudioStreamPlaybackFailure"
						m.stateMachine.msgPort.PostMessage(audioStreamPlaybackFailure)
					endif
				endif

				m.bsp.SetTouchRegions(m)

				m.stateMachine.ClearImagePlane()

				m.LaunchTimer()    

				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "stream")

				' playback logging
				m.stateMachine.LogPlayStart("stream", url$)

                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
				          
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

			else if event["EventType"] = "VideoStreamPlaybackFailure" then
        
				if m.bsp.ProcessMediaEndEvent() then
					return "HANDLED"
				endif
				if type(m.videoEndEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.videoEndEvent, stateData, "")
				endif
				PostMediaEndEvent(m.bsp.msgPort)

			else if event["EventType"] = "AudioStreamPlaybackFailure" then
        
				if m.bsp.ProcessMediaEndEvent() then
					return "HANDLED"
				endif
				if type(m.audioEndEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.audioEndEvent, stateData, "")
				endif

            else
            
		        return m.MediaItemEventHandler(event, stateData)

            endif
            
        endif
            
	else if type(event) = "roVideoEvent" and type(m.stateMachine.videoPlayer) = "roVideoPlayer" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() then            
        if event.GetInt() = MEDIA_END then
            m.bsp.diagnostics.PrintDebug("Video Event" + stri(event.GetInt()))
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif
            if type(m.videoEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.videoEndEvent, stateData, "")
			endif
        endif
	else if type(event) = "roAudioEvent" and type(m.stateMachine.audioPlayer) = "roAudioPlayer" and event.GetSourceIdentity() = audioPlayer.GetIdentity() then            
        if event.GetInt() = MEDIA_END then
            m.bsp.diagnostics.PrintDebug("Audio Event" + stri(event.GetInt()))
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif
            if type(m.audioEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.audioEndEvent, stateData, "")
			endif
			PostMediaEndEvent(m.bsp.msgPort)
        endif
    else
        return m.MediaItemEventHandler(event, stateData)
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Function STMjpegPlayingEventHandler(event As Object, stateData As Object) As Object
	
    MEDIA_END = 8

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.ConfigureBPButtons()
				m.ConfigureGPIOButtons()
				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)

				if type(m.stateMachine.mjpegUrl) <> "roUrlTransfer" then
					m.stateMachine.mjpegUrl = CreateObject("roUrlTransfer")
					m.stateMachine.mjpegUrl.SetUserAgent(m.bsp.userAgent$)
				endif

				url$ = m.url.GetCurrentParameterValue()
				m.stateMachine.mjpegUrl.SetURL(url$)
				m.stateMachine.mjpegUrl.SetProxy("")
			
				if type(m.stateMachine.mjpegMimeStream) <> "roMimeStream" then
					binding% = GetBinding(m.bsp.contentXfersEnabledWired, m.bsp.contentXfersEnabledWireless)
		            m.bsp.diagnostics.PrintDebug("### Binding for mjpegMimeStream is " + stri(binding%))
					ok = m.stateMachine.mjpegUrl.BindToInterface(binding%)
					if not ok then stop
					m.stateMachine.mjpegMimeStream = CreateObject("roMimeStream", m.stateMachine.mjpegUrl)
				endif

				if type(m.stateMachine.mjpegVideoPlayer) <> "roVideoPlayer" then 
					m.stateMachine.mjpegVideoPlayer = CreateObject("roVideoPlayer")

					if CanUseScreenModes({}) then
					  ' no need to rotate per zone if already rotated by screen
					else if m.bsp.DeviceSupportsRotation() then
						if m.bsp.sign.monitorOrientation = "portrait" then
							m.stateMachine.mjpegVideoPlayer.SetTransform("rot90")
						else if m.bsp.sign.monitorOrientation = "portraitbottomonright" then
							m.stateMachine.mjpegVideoPlayer.SetTransform("rot270")
						endif
					endif

					m.stateMachine.mjpegVideoPlayer.SetRectangle(m.stateMachine.rectangle)
					m.stateMachine.mjpegVideoPlayer.SetPort(m.bsp.msgPort)
				endif

				aa = {}
				aa["PictureStream"] = m.stateMachine.mjpegMimeStream
				aa["Rotate"] = m.rotation%
				if m.bsp.sign.isVideoWall and m.bsp.sign.videoWallType$ = "stretched" then
					aa["MultiscreenWidth"] = m.bsp.sign.videoWallNumColumns%
					aa["MultiscreenHeight"] = m.bsp.sign.videoWallNumRows%
					aa["MultiscreenX"] = m.bsp.sign.videoWallColumnPosition%
					aa["MultiscreenY"] = m.bsp.sign.videoWallRowPosition%
					aa["Mode"] = m.stateMachine.viewMode%
				endif

				if m.stateMachine.mosaicDecoderName <> "" then
					aa.Decoder = m.stateMachine.mosaicDecoderName
				endif

				ok = m.stateMachine.mjpegVideoPlayer.PlayFile(aa)
				m.bsp.SetTouchRegions(m)

				m.stateMachine.ClearImagePlane()

				m.LaunchTimer()    

				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "mjpeg")

				' playback logging
				m.stateMachine.LogPlayStart("mjpeg", url$)

                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")

				m.stateMachine.mjpegUrl = invalid
				m.stateMachine.mjpegMimeStream = invalid
				m.stateMachine.mjpegVideoPlayer = invalid
				          
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

'			else if event["EventType"] = "VideoPlaybackFailureEvent" then
        
'				if type(m.videoEndEvent) = "roAssociativeArray" then
'					return m.ExecuteTransition(m.videoEndEvent, stateData, "")
'				endif

            else
            
		        return m.MediaItemEventHandler(event, stateData)

            endif
            
        endif
            
	else if type(event) = "roVideoEvent" and event.GetSourceIdentity() = m.stateMachine.mjpegVideoPlayer.GetIdentity() then            
        if event.GetInt() = MEDIA_END then
            m.bsp.diagnostics.PrintDebug("Video Event" + stri(event.GetInt()))
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif
            if type(m.videoEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.videoEndEvent, stateData, "")
			endif
			PostMediaEndEvent(m.bsp.msgPort)
        endif
    else
        return m.MediaItemEventHandler(event, stateData)
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Function STVideoPlayingEventHandler(event As Object, stateData As Object) As Object

    MEDIA_END = 8
	VIDEO_TIME_CODE = 12
	
    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.LaunchVideo("video")

                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

			else if event["EventType"] = "VideoPlaybackFailureEvent" then
        
				if m.bsp.ProcessMediaEndEvent() then
					return "HANDLED"
				endif
				if type(m.videoEndEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.videoEndEvent, stateData, "")
				endif
				PostMediaEndEvent(m.bsp.msgPort)

            else
            
		        return m.MediaItemEventHandler(event, stateData)

            endif
            
        endif

	else if type(event) = "roVideoEvent" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() then
        if event.GetInt() = MEDIA_END then
            m.bsp.diagnostics.PrintDebug("Video Event" + stri(event.GetInt()))
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif
            if type(m.videoEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.videoEndEvent, stateData, "")
            else if not(type(m.synchronizeEvents) = "roAssociativeArray" or type(m.internalSynchronizeEvents) = "roAssociativeArray") then
				' looping video - since LaunchVideo is not called, perform logging here.
			    file$ = m.videoItem.fileName$
				m.stateMachine.LogPlayStart("video", file$)
			endif

			PostMediaEndEvent(m.bsp.msgPort)

        else if event.GetInt() = VIDEO_TIME_CODE then
			videoTimeCodeIndex$ = str(event.GetData())
			m.bsp.diagnostics.PrintDebug("Video TimeCode Event " + videoTimeCodeIndex$)
            if type(m.videoTimeCodeEvents) = "roAssociativeArray" then
                videoTimeCodeEvent = m.videoTimeCodeEvents[videoTimeCodeIndex$]
                if type(videoTimeCodeEvent) = "roAssociativeArray" then
					m.bsp.ExecuteTransitionCommands(m.stateMachine, videoTimeCodeEvent)
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "1")
					return "HANDLED"      
                endif
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "0")
            endif
        endif
    else
        return m.MediaItemEventHandler(event, stateData)
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Sub PostMediaEndEvent(msgPort As Object)

	mediaEndEvent = { }
	mediaEndEvent["EventType"] = "MEDIA_END"
    msgPort.PostMessage(mediaEndEvent)

End Sub


Sub SetAudioTimeCodeEvents()

	if type(m.stateMachine.audioPlayer) = "roAudioPlayer" then
		player = m.stateMachine.audioPlayer
	else
		player = m.stateMachine.videoPlayer
	endif

	player.ClearEvents()

    if type(m.audioTimeCodeEvents) = "roAssociativeArray" then
        for each eventNum in m.audioTimeCodeEvents
            m.AddAudioTimeCodeEvent(m.audioTimeCodeEvents[eventNum].timeInMS%, int(val(eventNum)))                    
        next
    endif
    
End Sub


Sub AddAudioTimeCodeEvent(timeInMS% As Integer, eventNum% As Integer)

    if type(m.stateMachine.audioPlayer) = "roAudioPlayer" then
        m.stateMachine.audioPlayer.AddEvent(eventNum%, timeInMS%)
	else
        m.stateMachine.videoPlayer.AddEvent(eventNum%, timeInMS%)
    endif

End Sub


Sub SetVideoTimeCodeEvents()

    m.stateMachine.videoPlayer.ClearEvents()

    if type(m.videoTimeCodeEvents) = "roAssociativeArray" then
        for each eventNum in m.videoTimeCodeEvents
            m.AddVideoTimeCodeEvent(m.videoTimeCodeEvents[eventNum].timeInMS%, int(val(eventNum)))                    
        next
    endif
    
End Sub


Sub AddVideoTimeCodeEvent(timeInMS% As Integer, eventNum% As Integer)

    if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
        m.stateMachine.videoPlayer.AddEvent(eventNum%, timeInMS%)
    endif
        
End Sub

'endregion

'region Video or Images State Machine
' *************************************************
'
' VideoOrImages State Machine
'
' *************************************************
Function newVideoOrImagesZoneHSM(bsp As Object, zoneXML As Object) As Object

    zoneHSM = newVideoZoneHSM(bsp, zoneXML)
	zoneHSM.ConstructorHandler = VideoOrImagesZoneConstructor
	zoneHSM.InitialPseudostateHandler = VideoOrImagesZoneGetInitialState
    
    zoneHSM.imageMode% = GetImageModeValue(zoneXML.zoneSpecificParameters.imageMode.GetText())

	zoneHSM.numImageItems% = 0

    return zoneHSM
    
End Function


Sub LoadImageBuffers()

	stateTable = m.stateTable
    for each stateName in stateTable
        state = stateTable[stateName]
        if type(state.imageItem) = "roAssociativeArray" then
			imageItem = state.imageItem
			if imageItem.useImageBuffer then
				m.AddImageBufferItem(imageItem.fileName$)
			endif
		else if state.type$ = "audioIn" then
			if state.useImageBuffer then
				m.AddImageBufferItem(state.imageFileName$)
			endif
		else if state.type$ = "interactiveMenuItem" then
			
			if state.backgroundImage$ <> "" and state.backgroundImageUseImageBuffer then
				m.AddImageBufferItem(state.backgroundImage$)
			endif
					
			if type(state.interactiveMenuItems) = "roArray" then
				for each interactiveMenuItem in state.interactiveMenuItems
					if interactiveMenuItem.selectedImageUseImageBuffer then
						m.AddImageBufferItem(interactiveMenuItem.selectedImage$)
					endif
					if interactiveMenuItem.selectedImageUseImageBuffer then
						m.AddImageBufferItem(interactiveMenuItem.unselectedImage$)
					endif
					if interactiveMenuItem.targetType$ = "mediaFile" and IsString(interactiveMenuItem.targetImageFile$) and interactiveMenuItem.targetImageFile$ <> "" and interactiveMenuItem.targetImageFileUseImageBuffer then
						m.AddImageBufferItem(interactiveMenuItem.targetImageFile$)
					endif
				next
			endif
		endif
	next

End Sub


Sub AddImageBufferItem(fileName$ As String)

    filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, fileName$)
    if type(m.bsp.imageBuffers) <> "roAssociativeArray" then
		m.bsp.imageBuffers = CreateObject("roAssociativeArray")
	endif
	if not m.bsp.imageBuffers.DoesExist(filePath$) then

		if type(m.bsp.encryptionByFile) = "roAssociativeArray" and m.bsp.encryptionByFile.DoesExist(fileName$) and m.bsp.contentEncryptionSupported then
		'if m.bsp.contentEncrypted then
			aa = {}
			aa.filename = filePath$
			aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
			aa.AddReplace("EncryptionKey", fileName$)
			imageBuffer = CreateObject("roImageBuffer", 0, aa)
		else
			imageBuffer = CreateObject("roImageBuffer", 0, filePath$)
		endif
		
		m.bsp.imageBuffers.AddReplace(filePath$, imageBuffer)
	
	endif
	
End Sub


Sub ClearImageBuffers()

    if type(m.imageBuffers) = "roAssociativeArray" then
		for each imageBuffer in m.imageBuffers
			imageBuffer = invalid
		next
		m.imageBuffers = invalid
    endif

End Sub


Sub VideoOrImagesZoneConstructor()

	m.InitializeVideoZoneObjects()
    
    zoneHSM = m
    
    ' create players
	if zoneHSM.numImageItems% > 0 then

		imagePlayer = CreateObject("roImageWidget", zoneHSM.rectangle)

		if CanUseScreenModes({}) then
		  ' no need to rotate per zone if already rotated by screen
		else if m.bsp.DeviceSupportsRotation() then
			if m.bsp.sign.monitorOrientation = "portrait" then
				imagePlayer.SetTransform("rot90")
			else if m.bsp.sign.monitorOrientation = "portraitbottomonright" then
				imagePlayer.SetTransform("rot270")
			endif
		endif
	    
		zoneHSM.imagePlayer = imagePlayer
	    
		' initialize image player parameters
		imagePlayer.SetDefaultMode(zoneHSM.imageMode%)

		m.LoadImageBuffers()

	else
	
		zoneHSM.imagePlayer = invalid
		
	endif
    
    zoneHSM.audioVolume% = zoneHSM.initialAudioVolume%
    
	audioInput = CreateObject("roAudioInput", m.bsp.sign.audioInSampleRate%)
	if type(audioInput) = "roAudioInput" then
		zoneHSM.audioInput = audioInput
	endif
	
	zoneHSM.audioPlayerAudioSettings = CreateObject("roAssociativeArray")
	zoneHSM.audioPlayerAudioSettings.audioOutput% = zoneHSM.audioOutput%
	zoneHSM.audioPlayerAudioSettings.stereoMapping% = zoneHSM.audioMapping%
	m.bsp.SetAudioVolumeLimits(zoneHSM, zoneHSM.audioPlayerAudioSettings) 

	m.CreateObjects()

End Sub


Function VideoOrImagesZoneGetInitialState() As Object
		
	return m.activeState

End Function


Sub PopulatePlayFileFromLiveDataFeed()

	if type(m.liveDataFeed.assetPoolFiles) = "roAssetPoolFiles" then

		m.filesTable = {}

		numFiles% = m.liveDataFeed.fileUrls.Count()

		for i% = 0 to numFiles% - 1
			
			key$ = m.liveDataFeed.fileKeys[i%]

			url$ = m.liveDataFeed.fileUrls[i%]
			filePath$ = m.liveDataFeed.assetPoolFiles.GetPoolFilePath(url$)

			fileTableEntry = CreateObject("roAssociativeArray")
			fileTableEntry.fileName$ = url$
			fileTableEntry.filePath$ = filePath$
			fileTableEntry.fileType$ = m.mediaType$
			fileTableEntry.automaticallyLoop = true
			fileTableEntry.isEncrypted = false
			fileTableEntry.videoDisplayMode% = 0
			m.filesTable.AddReplace(key$, fileTableEntry)

		next

	endif

End Sub


Function STPlayFileEventHandler(event As Object, stateData As Object) As Object

    MEDIA_END = 8

    stateData.nextState = invalid

    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.bsp.diagnostics.PrintDebug(m.id$ + ": payload is " + m.payload$)

				if m.useUserVariable then
					userVariable = m.userVariable
					m.payload$ = userVariable.GetCurrentValue()
				endif

				if m.filesTable.IsEmpty() or (not m.useDefaultMedia and not m.filesTable.DoesExist(m.payload$)) then
					if m.filesTable.IsEmpty() then
						m.bsp.diagnostics.PrintDebug(m.id$ + ": files not loaded yet")
					else
						m.bsp.diagnostics.PrintDebug(m.id$ + ": no file associated with payload")
					endif
					m.ConfigureBPButtons()
					m.ConfigureGPIOButtons()
					m.usbInputBuffer$ = ""
					m.usbInputLogBuffer$ = ""
					m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
					m.LaunchTimer()    
					m.bsp.SetTouchRegions(m)
				else
					fileTableEntry = m.filesTable.Lookup(m.payload$)
					if type(fileTableEntry) = "roAssociativeArray" then
						fileName$ = fileTableEntry.fileName$
						fileType$ = fileTableEntry.fileType$
					else
						fileName$ = m.defaultMediaFileName$
						fileType$ = m.mediaType$
					endif
					m.imageItem = invalid
					m.videoItem = invalid
					m.audioItem = invalid
					if fileType$ = "image" then
						m.imageItem = CreateObject("roAssociativeArray")
						m.imageItem.fileName$ = fileName$
						m.imageItem.isEncrypted = m.bsp.encryptionByFile.DoesExist(fileName$)
						if type(fileTableEntry) = "roAssociativeArray" then
							if type(fileTableEntry.filePath$) = "roString" then
								m.imageItem.filePath$ = fileTableEntry.filePath$
							endif
							m.imageItem.userVariable = fileTableEntry.userVariable
						endif
						m.imageItem.slideTransition% = m.slideTransition%
						m.imageItem.useImageBuffer = false
						m.DisplayImage("playFile")
					else if fileType$ = "video" then
						m.videoItem = CreateObject("roAssociativeArray")
						m.videoItem.fileName$ = fileName$
						m.videoItem.isEncrypted = m.bsp.encryptionByFile.DoesExist(fileName$)
						if type(fileTableEntry) = "roAssociativeArray" then
							if type(fileTableEntry.filePath$) = "roString" then
								m.videoItem.filePath$ = fileTableEntry.filePath$
							endif
							m.videoItem.probeData = fileTableEntry.probeData
							m.videoItem.videoDisplayMode% = fileTableEntry.videoDisplayMode%
							m.videoItem.userVariable = fileTableEntry.userVariable
							m.videoItem.automaticallyLoop = fileTableEntry.automaticallyLoop
						else
							m.videoItem.videoDisplayMode% = 0
						endif
						m.LaunchVideo("playFile")
					else if fileType$ = "audio" then
						m.audioItem = CreateObject("roAssociativeArray")
						m.audioItem.fileName$ = fileName$
						m.audioItem.isEncrypted = m.bsp.encryptionByFile.DoesExist(fileName$)
						if type(fileTableEntry) = "roAssociativeArray" then
							if type(fileTableEntry.filePath$) = "roString" then
								m.audioItem.filePath$ = fileTableEntry.filePath$
							endif
							m.audioItem.probeData = fileTableEntry.probeData
							m.audioItem.userVariable = fileTableEntry.userVariable
						endif
						m.LaunchAudio("playFile")
					endif
				endif

				return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
					
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

				return "HANDLED"
            
            endif
            
        endif

    else if type(m.videoItem) = "roAssociativeArray" and type(event) = "roVideoEvent" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() then
        if event.GetInt() = MEDIA_END then
            m.bsp.diagnostics.PrintDebug("Video Event" + stri(event.GetInt()))
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif
            if type(m.videoEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.videoEndEvent, stateData, "")
            endif
			PostMediaEndEvent(m.bsp.msgPort)
        endif

    else if type(m.audioItem)="roAssociativeArray" and IsAudioEvent(m.stateMachine, event) then
        if event.GetInt() = MEDIA_END then
            m.bsp.diagnostics.PrintDebug("Audio Event" + stri(event.GetInt()))
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif
            if type(m.audioEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.audioEndEvent, stateData, "")
            endif
			PostMediaEndEvent(m.bsp.msgPort)
        endif

	endif

	return m.MediaItemEventHandler(event, stateData)	

End Function


' this code explicitly does not catch roAudioEventMx events - those are handled elsewhere.
Function IsAudioEvent(stateMachine As Object, event As Object) As Boolean

	return (type(stateMachine.audioPlayer)="roAudioPlayer" and type(event) = "roAudioEvent" and event.GetSourceIdentity() = stateMachine.audioPlayer.GetIdentity()) or (type(stateMachine.videoPlayer)="roVideoPlayer" and type(event) = "roVideoEvent" and event.GetSourceIdentity() = stateMachine.videoPlayer.GetIdentity())

End Function


Function IsAudioPlayer(audioPlayer As Object) As Boolean

	return type(audioPlayer) = "roAudioPlayer" or type(audioPlayer) = "roAudioPlayerMx"

End Function


Sub ConfigureIntraStateEventHandlerButton(navigation As Object)

	if type(navigation) = "roAssociativeArray" then
		if type(navigation.bpUserEventButtonPanelIndex$) = "roString" and type(navigation.bpUserEventButtonNumber$) = "roString" then
			bpEvent = { }
			bpEvent.buttonPanelIndex% = int(val(navigation.bpUserEventButtonPanelIndex$))
			bpEvent.buttonNumber$ = navigation.bpUserEventButtonNumber$
			bpEvent.configuration$ = "press"
			m.bsp.ConfigureBPButton(bpEvent.buttonPanelIndex%, bpEvent.buttonNumber$, bpEvent)
		endif
		if type(navigation.gpioUserEventButtonNumber$) = "roString" then
			gpioEvent = { }
			gpioEvent.buttonNumber$ = navigation.gpioUserEventButtonNumber$
			gpioEvent.configuration$ = "press"
			m.bsp.ConfigureGPIOButton(gpioEvent.buttonNumber$, gpioEvent)
		endif
	endif

End Sub


Sub PopulateMediaListFromLiveDataFeed()

	if type(m.liveDataFeed.assetPoolFiles) = "roAssetPoolFiles" then

		m.numItems% = m.liveDataFeed.itemUrls.Count()
		m.items = CreateObject("roArray", m.numItems%, true)
		for i% = 0 to m.liveDataFeed.itemUrls.Count() - 1
			url = m.liveDataFeed.itemUrls[i%]
			fileName$ = m.liveDataFeed.fileKeys[i%]
			filePath$ = m.liveDataFeed.assetPoolFiles.GetPoolFilePath(url)

			item = {}
			item.fileName$ = fileName$
			item.filePath$ = filePath$
			item.isEncrypted = false

			if m.mediaType$ = "image" then
				item.slideTransition% = 0
				item.useImageBuffer = false
				item.type = "image"
			else if m.mediaType$ = "video" then
			    item.videoDisplayMode% = 0
				item.type = "video"
			else if m.mediaType$ = "audio" then
				item.type = "audio"
			else if m.mediaType$ = "allMedia" then
				if type(m.liveDataFeed.fileTypes) = "roArray" and m.liveDataFeed.fileTypes.Count() > i% then
					item.type = m.liveDataFeed.fileTypes[i%]
				endif
				if item.type = "image" then
					item.slideTransition% = 0
					item.useImageBuffer = false
				else if item.type = "video" then
					item.videoDisplayMode% = 0
				endif
			endif

			m.items.push(item)

		next

		m.playbackIndices = CreateObject("roArray", m.numItems%, true)
		for i% = 0 to m.numItems%-1
			m.playbackIndices[i%] = i%
		next

	endif

End Sub


Sub ShuffleMediaListContent()

	randomNumbers = CreateObject("roArray", m.numItems%, true)
	for each item in m.items
		randomNumbers.push(rnd(10000))
	next
		
	numItemsToSort% = m.numItems%
					
	for i% = numItemsToSort% - 1 to 1 step -1
		for j% = 0 to i%-1
			index0 = m.playbackIndices[j%]
			value0 = randomNumbers[index0]
			index1 = m.playbackIndices[j%+1]
			value1 = randomNumbers[index1]
			if value0 > value1 then
				k% = m.playbackIndices[j%]
				m.playbackIndices[j%] = m.playbackIndices[j%+1]
				m.playbackIndices[j%+1] = k%
			endif
		next
	next

End Sub


Function STDisplayingMediaListItemEventHandler(event As Object, stateData As Object) As Object

    MEDIA_START = 3
    MEDIA_END = 8
    MEDIA_ERROR = 16

	VIDEO_TIME_CODE = 12

    stateData.nextState = invalid

    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				' if using a live data feed, populate items here
				if type(m.liveDataFeed) = "roAssociativeArray" then
					' ensure that live data feed content has been loaded
					if type(m.liveDataFeed.assetPoolFiles) = "roAssetPoolFiles" then
						m.PopulateMediaListFromLiveDataFeed()
					endif
				endif

				if m.bsp.mediaListInactivityTimeoutIsGlobal and type(m.bsp.mediaListInactivity) = "roAssociativeArray" then

					if type(m.bsp.mediaListInactivity.timer) = "roTimer" then
						m.bsp.mediaListInactivity.timer.Stop()
					else if m.bsp.inactivityTimeout then
						m.bsp.mediaListInactivity.timer = CreateObject("roTimer")
						m.bsp.mediaListInactivity.timer.SetPort(m.bsp.msgPort)
					endif

				endif

				if m.mediaListStateInactivityTimeout then

					m.mediaListInactivityTimer = CreateObject("roTimer")
					m.mediaListInactivityTimer.SetPort(m.bsp.msgPort)

				endif

				m.ConfigureIntraStateEventHandlerButton(m.nextNavigation)
				m.ConfigureIntraStateEventHandlerButton(m.previousNavigation)
				
				m.firstItemDisplayed = false

				' prevent start index from pointing beyond the number of items in the case where m.playFromBeginning is false
				if m.numItems% > 0 and m.startIndex% >= m.numItems% then
					m.startIndex% = 0
				endif

				' reset playback index if appropriate
				if m.playFromBeginning then
					m.playbackIndex% = m.startIndex%
				endif

				if m.numItems% > 0 then

					m.playbackActive = true

					' prevent start index from pointing beyond the number of items
					if m.playFromBeginning then
						if m.specifiedStartIndex% >= m.numItems% then
							m.startIndex% = 0
						else
							m.startIndex% = m.specifiedStartIndex%
						endif
						m.playbackIndex% = m.startIndex%
					endif

					' reshuffle media list if appropriate
					if m.playbackIndex% = m.startIndex% and m.shuffle then
						m.ShuffleMediaListContent()
					endif

					m.AdvanceMediaListPlayback(true, false)

				else

					m.playbackActive = false

          emptyListPlaybackEvent = CreateObject("roAssociativeArray")
          emptyListPlaybackEvent["EventType"] = "EmptyListPlaybackEvent"
          m.stateMachine.msgPort.PostMessage(emptyListPlaybackEvent)

				endif
				
				return "HANDLED"

			else if event["EventType"] = "EmptyListPlaybackEvent" then
        
				if type(m.mediaListEndEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.mediaListEndEvent, stateData, "")
				endif

			else if event["EventType"] = "VideoPlaybackFailureEvent" then
        
				if m.bsp.ProcessMediaEndEvent() then
					return "HANDLED"
				endif
				if type(m.videoEndEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.videoEndEvent, stateData, "")
				endif
				PostMediaEndEvent(m.bsp.msgPort)

			else if event["EventType"] = "AudioPlaybackFailureEvent" then
        
				if m.bsp.ProcessMediaEndEvent() then
					return "HANDLED"
				endif
				if type(m.audioEndEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.audioEndEvent, stateData, "")
				endif

            else if event["EventType"] = "CONTENT_DATA_FEED_LOADED" then

				if type(m.liveDataFeed) = "roAssociativeArray" and event["Name"] = m.liveDataFeed.name$ then

					m.PopulateMediaListFromLiveDataFeed()

					'reset the playback index to the start point
					if m.specifiedStartIndex% >= m.numItems% then
						m.startIndex% = 0
					else
						m.startIndex% = m.specifiedStartIndex%
					endif					
					m.playbackIndex% = m.startIndex%

					if m.numItems% > 0 then		
						if m.shuffle then
							m.ShuffleMediaListContent()
						endif

						if not m.playbackActive then
							m.playbackActive = true
							m.AdvanceMediaListPlayback(true, false)
						endif
					endif

					return "HANDLED"

				endif
				
            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
	
				m.StartInactivityTimer()
				
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

				return "HANDLED"
            
            endif
            
        endif
    
	' detect whether or not this is an event that indicates that the media list has completed a loop - if yes, act on it if there is a mediaListEnd event
	' test with media end event on media list first
	else if m.AtEndOfMediaList(event) and type(m.mediaListEndEvent) = "roAssociativeArray" then
		return m.ExecuteTransition(m.mediaListEndEvent, stateData, "")
    
	else if (m.mediaType$ = "video" or m.mediaType$ = "allMedia") and type(event) = "roVideoEvent" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() then
		if event.GetInt() = MEDIA_END then
            m.bsp.diagnostics.PrintDebug("Video Event" + stri(event.GetInt()))
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif

			if m.advanceOnMediaEnd then
				if not(m.playbackIndex% = m.startIndex% and type(m.videoEndEvent) = "roAssociativeArray") then
					m.AdvanceMediaListPlayback(true, true)
					return "HANDLED"
				endif
			endif
            if type(m.videoEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.videoEndEvent, stateData, "")
            endif
			PostMediaEndEvent(m.bsp.msgPort)
        else if event.GetInt() = VIDEO_TIME_CODE then
			videoTimeCodeIndex$ = str(event.GetData())
			m.bsp.diagnostics.PrintDebug("Video TimeCode Event " + videoTimeCodeIndex$)
            if type(m.videoTimeCodeEvents) = "roAssociativeArray" then
                videoTimeCodeEvent = m.videoTimeCodeEvents[videoTimeCodeIndex$]
                if type(videoTimeCodeEvent) = "roAssociativeArray" then
					m.bsp.ExecuteTransitionCommands(m.stateMachine, videoTimeCodeEvent)
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "1")
					return "HANDLED"      
                endif
            endif
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "0")
        endif
	else if (m.mediaType$ = "audio" or m.mediaType$ = "allMedia") and m.stateMachine.type$ = "EnhancedAudio" and type(event) = "roAudioEventMx" then
        m.bsp.diagnostics.PrintDebug("AudioMx Event" + stri(event.GetInt()))
        if event.GetInt() = MEDIA_START then
	        if event.GetSourceIdentity() = m.stateMachine.audioPlayer.GetIdentity() then

				' index of track that just started playing
				currentTrackIndex% = int(val(event.GetUserData()))

				
				 'get index of track to queue
				
					m.playbackIndex% = currentTrackIndex% + 1
					if m.playbackIndex% >= m.numItems% then
						m.playbackIndex% = 0
					endif

					m.audioItem = m.items[m.playbackIndices[m.playbackIndex%]]

					'send zone message for the current track
					if m.sendZoneMessage  then 
						item = m.items[m.playbackIndices[currentTrackIndex%]]
						fileNameWithoutExtension$ = item.filename$

					'if the file name has an extension, remove it before sending
						ext=GetFileExtension(item.filename$)
						if type(ext) = "roString" then
							index=instr(1, item.filename$, ext)
							if index > 2 then
								fileNameWithoutExtension$ = mid(item.filename$, 1, index-2)
							endif
						endif

						' send ZoneMessage using the file name as the message
						zoneMessageCmd = CreateObject("roAssociativeArray")
						zoneMessageCmd["EventType"] = "SEND_ZONE_MESSAGE"
						zoneMessageCmd["EventParameter"] = fileNameWithoutExtension$
						m.bsp.msgPort.PostMessage(zoneMessageCmd)

					endif
				

				if not(m.playbackIndex% = m.startIndex% and type(m.audioEndEvent) = "roAssociativeArray") then
					m.PlayMixerAudio(false, m.playbackIndex%, false)
				endif

				' at this point, m.playbackIndex% points to both the item that is queued as well as the next item to play - the concept of
				' "next item to play" is needed for NextNavigation, BackNavigation, and re-entering the state
				

'				m.AdvanceMediaListPlayback(false)
				return "HANDLED"
			endif
		else if event.GetInt() = MEDIA_END then
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif
            if type(m.audioEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.audioEndEvent, stateData, "")
            endif
			PostMediaEndEvent(m.bsp.msgPort)
		endif
    else if (m.mediaType$ = "audio" or m.mediaType$ = "allMedia") and m.stateMachine.type$ <> "EnhancedAudio" and IsAudioEvent(m.stateMachine, event) then
        if event.GetInt() = MEDIA_END then
            m.bsp.diagnostics.PrintDebug("Audio Event" + stri(event.GetInt()))
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif
			if m.advanceOnMediaEnd then
				if not(m.playbackIndex% = m.startIndex% and type(m.audioEndEvent) = "roAssociativeArray") then
					m.AdvanceMediaListPlayback(true, true)
					return "HANDLED"
				endif
			endif
            if type(m.audioEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.audioEndEvent, stateData, "")
            endif
			PostMediaEndEvent(m.bsp.msgPort)
        endif
	else if type(event) = "roTimerEvent" then
		if m.advanceOnImageTimeout then

			if type(m.advanceOnImageTimeoutTimer) = "roTimer" and event.GetSourceIdentity() = m.advanceOnImageTimeoutTimer.GetIdentity() then
				if m.playbackIndex% <> m.startIndex% or type(m.mstimeoutEvent) <> "roAssociativeArray" then
					m.AdvanceMediaListPlayback(true, true)
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timeout", "", "1")
					return "HANDLED"
				endif
			endif

			if m.playbackIndex% = m.startIndex% and type(m.mstimeoutEvent) = "roAssociativeArray" and type(m.advanceOnImageTimeoutTimer) = "roTimer" and event.GetSourceIdentity() = m.advanceOnImageTimeoutTimer.GetIdentity() then
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timeout", "", "1")
				return m.ExecuteTransition(m.mstimeoutEvent, stateData, "")
			endif

			return m.MediaItemEventHandler(event, stateData)	

		endif
		m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timeout", "", "0")
	endif

	if type(m.nextNavigation) = "roAssociativeArray" and m.playbackActive then
		advance = m.HandleIntraStateEvent(event, m.nextNavigation)
		if advance then

			if type(m.advanceOnImageTimeoutTimer) = "roTimer" then
				m.advanceOnImageTimeoutTimer.Stop()
				m.advanceOnImageTimeoutTimer = invalid
			endif

			m.AdvanceMediaListPlayback(true, true)
			return "HANDLED"
		endif
	endif
	
	if type(m.previousNavigation) = "roAssociativeArray" and m.playbackActive then
		retreat = m.HandleIntraStateEvent(event, m.previousNavigation)
		if retreat then

			if type(m.advanceOnImageTimeoutTimer) = "roTimer" then
				m.advanceOnImageTimeoutTimer.Stop()
				m.advanceOnImageTimeoutTimer = invalid
			endif

			m.RetreatMediaListPlayback(true, true)
			return "HANDLED"
		endif
	endif    
    
	return m.MediaItemEventHandler(event, stateData)	

End Function


Function AtEndOfMediaList(event As Object) As Boolean

    MEDIA_END = 8
	endOfMediaEvent = false

	if (m.mediaType$ = "video" or m.mediaType$ = "allMedia") and type(event) = "roVideoEvent" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() and event.GetInt() = MEDIA_END then
		endOfMediaEvent = true
	else if (m.mediaType$ = "audio" or m.mediaType$ = "allMedia") and m.stateMachine.type$ = "EnhancedAudio" and type(event) = "roAudioEventMx" and event.GetInt() = MEDIA_END then
		endOfMediaEvent = true
    else if (m.mediaType$ = "audio" or m.mediaType$ = "allMedia") and m.stateMachine.type$ <> "EnhancedAudio" and IsAudioEvent(m.stateMachine, event) and event.GetInt() = MEDIA_END then
		endOfMediaEvent = true
	else if type(event) = "roTimerEvent" and m.advanceOnImageTimeout and type(m.advanceOnImageTimeoutTimer) = "roTimer" and event.GetSourceIdentity() = m.advanceOnImageTimeoutTimer.GetIdentity() and (m.playbackIndex% <> m.startIndex% or type(m.mstimeoutEvent) <> "roAssociativeArray") then
		endOfMediaEvent = true
	endif

	if endOfMediaEvent and m.playbackIndex% = 0 then
		return true
	else
		return false
	endif

End Function


Function HandleIntraStateEvent(event As Object, navigation As Object) As Boolean

    if type(event) = "roAssociativeArray" and IsString(event["EventType"]) then
		if event["EventType"] = "BPControlDown" and IsString(navigation.bpUserEventButtonNumber$) then
			bpIndex$ = event["ButtonPanelIndex"]
			bpNum$ = event["ButtonNumber"]
			m.bsp.diagnostics.PrintDebug("BP Press, button number " + bpNum$ + ", button index " + bpIndex$)
			if navigation.bpUserEventButtonNumber$ = bpNum$ and navigation.bpUserEventButtonPanelIndex$ = bpIndex$ then
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "1")
				return true
			endif
		else if event["EventType"] = "GPIOControlDown" and IsString(navigation.gpioUserEventButtonNumber$) then
			gpioNum$ = event["ButtonNumber"]
			m.bsp.diagnostics.PrintDebug("GPIO Press, button number " + gpioNum$)
			if navigation.gpioUserEventButtonNumber$ = gpioNum$ then
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioDown", gpioNum$, "1")
				return true
			endif
		else if event["EventType"] = "SEND_ZONE_MESSAGE" and IsString(navigation.zoneMessageUserEvent$) then
			zoneMessage$ = event["EventParameter"]
			m.bsp.diagnostics.PrintDebug("ZoneMessageEvent " + zoneMessage$)
			if navigation.zoneMessageUserEvent$ = zoneMessage$ then
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "zoneMessage", zoneMessage$, "1")
				return true
			endif
		endif
	endif
    
    if type(event) = "roControlDown" and IsControlPort(m.bsp.controlPort) and stri(event.GetSourceIdentity()) = stri(m.bsp.controlPort.GetIdentity()) and IsString(navigation.gpioUserEvent$) then
        gpioNum$ = StripLeadingSpaces(str(event.GetInt()))
        m.bsp.diagnostics.PrintDebug("Button Press" + gpioNum$)
        if navigation.gpioUserEvent$ = gpioNum$ then
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioButton", gpioNum$, "1")
			return true
		endif
    endif
    
    if type(event) = "roSyncManagerEvent" and IsString(navigation.synchronizeUserEvent$) then

		synchronizeEvent$ = event.GetId()
		m.bsp.diagnostics.PrintDebug("Synchronize Event" + synchronizeEvent$)

		if navigation.synchronizeUserEvent$ = synchronizeEvent$ then

			m.stateMachine.syncInfo = CreateObject("roAssociativeArray")
			m.stateMachine.syncInfo.SyncDomain = event.GetDomain()
			m.stateMachine.syncInfo.SyncId = event.GetId()
			m.stateMachine.syncInfo.SyncIsoTimestamp = event.GetIsoTimestamp()

			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "enhancedSynchronize", synchronizeEvent$, "1")
			return true
		endif

	endif

    if type(event) = "roDatagramEvent" and IsString(navigation.udpUserEvent$) then
		udpEvent$ = event.GetString()
		m.bsp.diagnostics.PrintDebug("UDP Event" + udpEvent$)

        if navigation.udpUserEvent$ = udpEvent$ or navigation.udpEvent$ = "<any>" then
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "udp", udpEvent$, "1")
			return true
		else
			if instr(1, navigation.udpUserEvent$, "(.*)") > 0 then
				r = CreateObject("roRegEx", navigation.udpUserEvent$, "i")
				if type(r) = "roRegex" then
					matches = r.match(udpEvent$)
					if matches.Count() > 0 then
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "udp", udpEvent$, "1")
						return true
					endif
				endif
			endif
		endif
	endif

	if type(event) = "roKeyboardPress" and IsString(navigation.keyboardUserEvent$) then
		keyboardChar$ = chr(event.GetInt())
		m.bsp.diagnostics.PrintDebug("Keyboard Press" + keyboardChar$)
		
		' if keyboard input is non printable character, convert it to the special code
		keyboardCode$ = m.bsp.GetNonPrintableKeyboardCode(event.GetInt())
		if keyboardCode$ <> "" then
			keyboardChar$ = keyboardCode$
		endif

        if navigation.keyboardUserEvent$ = keyboardChar$ or navigation.keyboardUserEvent$ = "<any>" then
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "keyboard", keyboardChar$, "1")
			return true
		endif
    endif   

    if type(event) = "roIRRemotePress" and IsString(navigation.remoteUserEvent$) then
		m.bsp.diagnostics.PrintDebug("Remote Event" + stri(event.GetInt()))

		remoteEvent% = event.GetInt()
		remoteEvent$ = ConvertToRemoteCommand(remoteEvent%)
    
        if navigation.remoteUserEvent$ = remoteEvent$ then
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "remote", remoteEvent$, "1")
			return true
		endif
	endif

    if type(event) = "roStreamLineEvent" and IsString(navigation.serialUserEventPort$) and IsString(navigation.serialUserEventSerialEvent$) then

		port$ = event.GetUserData()
        serialEvent$ = event.GetString()

	    m.bsp.diagnostics.PrintDebug("Serial Line Event " + event.GetString())

		if port$ = navigation.serialUserEventPort$ then
			if (serialEvent$ = navigation.serialUserEventSerialEvent$ or navigation.serialUserEventSerialEvent$ = "<*>") then
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", serialEvent$, "1")
				return true
			else
				if instr(1, navigation.serialUserEventSerialEvent$, "(.*)") > 0 then
					r = CreateObject("roRegEx", navigation.serialUserEventSerialEvent$, "i")
					if type(r) = "roRegex" then
						matches = r.match(serialEvent$)
						if matches.Count() > 0 then
							m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", serialEvent$, "1")
							return true
						endif
					endif
				endif
			endif
		endif

	endif
	         
    return false
    
End Function


Sub LaunchMediaListPlaybackItem(playImmediate As Boolean, executeNextCommands As Boolean, executePrevCommands As Boolean)

	' Make sure we have a valid list
	itemIndex = m.playbackIndices[m.playbackIndex%]
	if itemIndex = invalid then
		' Attempting to play an empty list - log the failure
		m.bsp.diagnostics.PrintDebug("LaunchMediaListPlaybackItem failed - attempted to play an empty " + m.mediaType$ + " list")
		m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_EMPTY_MEDIA_PLAYLIST, m.mediaType$)

		emptyListPlaybackEvent = CreateObject("roAssociativeArray")
		emptyListPlaybackEvent["EventType"] = "EmptyListPlaybackEvent"
		m.stateMachine.msgPort.PostMessage(emptyListPlaybackEvent)

    return

	endif

	' get current media item and launch playback
	item = m.items[itemIndex]

	if m.sendZoneMessage and not((m.mediaType$ = "audio" or (m.mediaType$ = "allMedia" and item.type = "audio")) and m.statemachine.type$ = "EnhancedAudio")  then

	m.bsp.diagnostics.PrintDebug("DEBUG: LaunchMediaListPlaybackItem sending Zone message")
	fileNameWithoutExtension$ = item.filename$

		' if the file name has an extension, remove it before sending
		ext=GetFileExtension(item.filename$)
		if type(ext) = "roString" then
			index=instr(1, item.filename$, ext)
			if index > 2 then
				fileNameWithoutExtension$ = mid(item.filename$, 1, index-2)
			endif
		endif

		' send ZoneMessage using the file name as the message
		zoneMessageCmd = CreateObject("roAssociativeArray")
		zoneMessageCmd["EventType"] = "SEND_ZONE_MESSAGE"
		zoneMessageCmd["EventParameter"] = fileNameWithoutExtension$
		m.bsp.msgPort.PostMessage(zoneMessageCmd)

	endif

	if executeNextCommands then
		if type(m.transitionNextItemCmds) = "roArray" then
			for each cmd in m.transitionNextItemCmds
				m.bsp.ExecuteCmd(m.stateMachine, cmd.name$, cmd.parameters)
			next
		endif
	endif

	if executePrevCommands then
		if type(m.transitionPrevItemCmds) = "roArray" then
			for each cmd in m.transitionPrevItemCmds
				m.bsp.ExecuteCmd(m.stateMachine, cmd.name$, cmd.parameters)
			next
		endif
	endif

	if m.mediaType$ = "image" or (m.mediaType$ = "allMedia" and item.type = "image") then
	
		m.imageItem = item
		m.imageItem.slideTransition% = m.slideTransition%
		m.imageItem.transitionDuration% = m.transitionDuration%

		if not m.firstItemDisplayed then
			m.PreDrawImage()
		endif

		m.DrawImage(true)

		if not m.firstItemDisplayed then
			m.PostDrawImage("imageList")
		else
			m.ClearVideo()
		endif

		' if advancing on image timeout, set the timer
		if type(m.advanceOnImageTimeoutTimer) = "roTimer" then
			m.advanceOnImageTimeoutTimer.Stop()
		endif

		if m.advanceOnImageTimeout then
			if type(m.advanceOnImageTimeoutTimer) <> "roTimer" then
				m.advanceOnImageTimeoutTimer = CreateObject("roTimer")
				m.advanceOnImageTimeoutTimer.SetPort(m.stateMachine.msgPort)
			endif
			m.advanceOnImageTimeoutTimer.SetElapsed(0, m.imageTimeout)
			m.advanceOnImageTimeoutTimer.Start()
		endif
	
	else if m.mediaType$ = "video" or (m.mediaType$ = "allMedia" and item.type = "video") then
	
		m.videoItem = item

		if not m.firstItemDisplayed then
			m.PrePlayVideo()
		endif

		m.PlayVideo(not m.firstItemDisplayed, true)

		if not m.firstItemDisplayed then
			m.PostPlayVideo("videoList")
		else
			m.stateMachine.ClearImagePlane()
		endif
	
	else if m.mediaType$ = "audio" or (m.mediaType$ = "allMedia" and item.type = "audio")  then
	
		m.audioItem = item

		if not m.firstItemDisplayed then
			m.PrePlayAudio()
		endif

		if m.stateMachine.type$ = "EnhancedAudio" then
			m.PlayMixerAudio(not m.firstItemDisplayed, m.playbackIndex%, playImmediate)
		else
			m.PlayAudio(not m.firstItemDisplayed, true)
		endif

		if not m.firstItemDisplayed then
			m.PostPlayAudio("audioList")
		endif

	endif

	m.firstItemDisplayed = true

End Sub


Sub AdvanceMediaListPlayback(playImmediate As Boolean, executeNextCommands As Boolean)

	m.LaunchMediaListPlaybackItem(playImmediate, executeNextCommands, false)
	
	m.playbackIndex% = m.playbackIndex% + 1
	if m.playbackIndex% >= m.numItems% then
		m.playbackIndex% = 0
	endif
				
End Sub


Sub RetreatMediaListPlayback(playImmediate As Boolean, executePrevCommands As Boolean)
	
	' index currently points to 'next' track - need to retreat by 2 to get to previous track
    for i% = 0 to 1
		m.playbackIndex% = m.playbackIndex% - 1
		if m.playbackIndex% < 0 then
			m.playbackIndex% = m.numItems% - 1
		endif
	next

	m.LaunchMediaListPlaybackItem(playImmediate, false, executePrevCommands)
	
	m.playbackIndex% = m.playbackIndex% + 1
	if m.playbackIndex% >= m.numItems% then
		m.playbackIndex% = 0
	endif
				
End Sub


Sub StartInactivityTimer()

	if m.bsp.mediaListInactivityTimeoutIsGlobal and m.bsp.inactivityTimeout then
		if type(m.bsp.mediaListInactivity) = "roAssociativeArray" then
			if type(m.bsp.mediaListInactivity.timer) = "roTimer" then
				m.bsp.mediaListInactivity.timer.SetElapsed(m.bsp.inactivityTime%, 0)
				m.bsp.mediaListInactivity.timer.SetPort(m.bsp.msgPort)
				m.bsp.mediaListInactivity.timer.Start()
			endif
		endif
	else if m.mediaListStateInactivityTimeout then
		if type(m.mediaListInactivityTimer) = "roTimer" then
			userData = {}
			userData.id = "mediaList"
			userData.state = m
			m.mediaListInactivityTimer.SetUserData(userData)
			m.mediaListInactivityTimer.SetElapsed(m.mediaListStateInactivityTime%, 0)
			m.mediaListInactivityTimer.Start()
		endif
	endif
	
End Sub


Sub ConfigureBPButtons()

	for buttonPanelIndex% = 0 to 3
		bpEvents = m.bpEvents[buttonPanelIndex%]
		for each buttonNumber in bpEvents
			bpEvent = bpEvents[buttonNumber]
			m.bsp.ConfigureBPButton(buttonPanelIndex%, buttonNumber, bpEvent)
		next
	next
	
End Sub


Sub ConfigureGPIOButtons()

	gpioEvents = m.gpioEvents
	for each buttonNumber in gpioEvents
		gpioEvent = gpioEvents[buttonNumber]
		m.bsp.ConfigureGPIOButton(buttonNumber, gpioEvent)
	next
	
End Sub


Function ItemIsEncrypted(item As Object) As Boolean

	if type(item.IsEncrypted) = "roBoolean" and item.isEncrypted then
		return true
	else
		return false
	endif

End Function


Sub PrePlayVideo()

	m.ConfigureBPButtons()
	m.ConfigureGPIOButtons()
	
	m.usbInputBuffer$ = ""
	m.usbInputLogBuffer$ = ""

End Sub


Sub PlayVideo(executeEntryCmds As Boolean, disableLoopMode As Boolean)

	' set video mode before executing commands - required order for working around LG (maybe others) bugs getting back to 2-D mode
	videoMode = CreateObject("roVideoMode")
	videoMode.Set3dMode(m.videoItem.videoDisplayMode%)
	videoMode = invalid

    loopMode% = 1
	if disableLoopMode or (type(m.videoItem.automaticallyLoop) = "roBoolean" and (not m.videoItem.automaticallyLoop)) or type(m.videoEndEvent) = "roAssociativeArray" or type(m.synchronizeEvents) = "roAssociativeArray" or type(m.internalSynchronizeEvents) = "roAssociativeArray" then loopMode% = 0
    
    m.stateMachine.videoPlayer.SetLoopMode(loopMode%)

	file$ = m.videoItem.fileName$
	if type(m.videoItem.filePath$) = "roString" then
		filePath$ = m.videoItem.filePath$
	else
		filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, file$)
	endif

	' determine whether or not a preload has been performed
	preloaded = false
	if type(m.stateMachine.preloadState) = "roAssociativeArray" then
		if m.stateMachine.preloadedStateName$ = m.name$ then
			preloaded = true
		endif
	endif

	syncInProgress = false
	if type(m.stateMachine.syncInfo) = "roAssociativeArray" then
		syncInProgress = true
	endif

	m.stateMachine.videoPlayer.EnableSafeRegionTrimming(false)

	if not preloaded and not syncInProgress then
		m.stateMachine.videoPlayer.Stop()
	endif
	
    m.SetVideoTimeCodeEvents()

	if executeEntryCmds then
		m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
	endif

	if preloaded then
		ok = m.stateMachine.videoPlayer.Play()

		if ok = 0 then
			m.bsp.diagnostics.PrintDebug("Error playing preloaded file in PlayVideo: " + file$ + ", " + filePath$)
			m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
            videoPlaybackFailure = CreateObject("roAssociativeArray")
            videoPlaybackFailure["EventType"] = "VideoPlaybackFailureEvent"
            m.stateMachine.msgPort.PostMessage(videoPlaybackFailure)
		endif

		m.stateMachine.preloadState = invalid
		m.stateMachine.preloadedStateName$ = ""

		m.bsp.diagnostics.PrintDebug("LaunchVideo: play preloaded file " + file$ + ", loopMode = " + str(loopMode%))
	else
		aa = { }
		aa.AddReplace("Filename", filePath$)
		
		if type(m.videoItem.probeData) = "roString" then
			m.bsp.diagnostics.PrintDebug("LaunchVideo: probeData = " + m.videoItem.probeData)
			aa.AddReplace("ProbeString", m.videoItem.probeData)
		endif

		if syncInProgress then

			aa.AddReplace("SyncDomain", m.stateMachine.syncInfo.SyncDomain)
			aa.AddReplace("SyncId", m.stateMachine.syncInfo.SyncId)
			aa.AddReplace("SyncIsoTimestamp", m.stateMachine.syncInfo.SyncIsoTimestamp)

			if m.bsp.sign.isVideoWall and m.bsp.sign.videoWallType$ = "stretched" then
				aa["MultiscreenWidth"] = m.bsp.sign.videoWallNumColumns%
				aa["MultiscreenHeight"] = m.bsp.sign.videoWallNumRows%
				aa["MultiscreenX"] = m.bsp.sign.videoWallColumnPosition%
				aa["MultiscreenY"] = m.bsp.sign.videoWallRowPosition%
				aa["Mode"] = m.stateMachine.viewMode%
			endif

		endif

		if ItemIsEncrypted(m.videoItem) then
			aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
			aa.AddReplace("EncryptionKey", file$)
		endif

		if m.stateMachine.mosaicDecoderName <> "" then
			aa.Decoder = m.stateMachine.mosaicDecoderName
		endif

		ok = m.stateMachine.videoPlayer.PlayFile(aa)
		if ok = 0 then
			m.bsp.diagnostics.PrintDebug("Error playing file in LaunchVideo: " + file$ + ", " + filePath$)
			m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
            videoPlaybackFailure = CreateObject("roAssociativeArray")
            videoPlaybackFailure["EventType"] = "VideoPlaybackFailureEvent"
            m.stateMachine.msgPort.PostMessage(videoPlaybackFailure)
		endif

		if syncInProgress then
			m.bsp.diagnostics.PrintDebug("LaunchVideo: play synchronized file " + file$)
			m.stateMachine.syncInfo = invalid
		endif
	endif
    
	if type(m.videoItem.userVariable) = "roAssociativeArray" then
		m.videoItem.userVariable.Increment()
	endif

	' playback logging
	m.stateMachine.LogPlayStart("video", file$)

End Sub


Sub PostPlayVideo(stateType$ As String)

    m.bsp.SetTouchRegions(m)

	m.stateMachine.ClearImagePlane()

    m.LaunchTimer()    

	' state logging
	m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, stateType$)

End Sub


Sub LaunchVideo(stateType$ As String)

	m.PrePlayVideo()

	m.PlayVideo(true, false)

	m.PostPlayVideo(stateType$)

End Sub


Sub PreDrawImage()

	m.ConfigureBPButtons()
	m.ConfigureGPIOButtons()
	
	m.usbInputBuffer$ = ""
	m.usbInputLogBuffer$ = ""

	m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)

End Sub


Sub DrawImage( setTransition As Boolean )

	file$ = m.imageItem.fileName$
	if type(m.imageItem.filePath$) = "roString" then
		filePath$ = m.imageItem.filePath$
	else
		filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, file$)
	endif

	if m.stateMachine.useVideoPlayerForImages then

		m.bsp.diagnostics.PrintDebug("PlayStaticImage in DisplayImage: " + file$)

		if type(m.stateMachine.videoPlayer) <> "roVideoPlayer" then
			m.bsp.diagnostics.PrintDebug("DisplayImage attempted to call PlayStaticImage without a videoPlayer: " + file$)
			m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, "PlayStaticImage with no videoPlayer: " + file$)
		else

			aa = { }
			aa.AddReplace("Filename", filePath$)

			if ItemIsEncrypted(m.imageItem) then
				aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
				aa.AddReplace("EncryptionKey", file$)
			endif

			if m.bsp.contentEncryptionSupported then
				ok = m.stateMachine.videoPlayer.PlayStaticImage(aa)
			else
				ok = m.stateMachine.videoPlayer.PlayStaticImage(filePath$)
			endif
			if not ok then
				m.bsp.diagnostics.PrintDebug("Error returned from PlayStaticImage in DisplayImage: " + file$)
				m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
			endif

			if type(m.stateMachine.imagePlayer) = "roImageWidget" then
				ok = m.stateMachine.imagePlayer.StopDisplay()
			endif
		endif

	else

		if setTransition then
			m.stateMachine.imagePlayer.SetDefaultTransition(m.imageItem.slideTransition%)
		endif

		if type(m.imageItem.transitionDuration%) = "roInt" then
			m.stateMachine.imagePlayer.SetTransitionDuration(m.imageItem.transitionDuration%)
		endif

		if m.imageItem.useImageBuffer and type(m.bsp.imageBuffers) = "roAssociativeArray" and m.bsp.imageBuffers.DoesExist(filePath$) then
			m.bsp.diagnostics.PrintDebug("Use imageBuffer for " + file$ + " in DisplayImage: ")
			imageBuffer = m.bsp.imageBuffers.Lookup(filePath$)
			m.stateMachine.ClearImagePlane()
			m.stateMachine.imagePlayer.DisplayBuffer(imageBuffer, 0, 0)
		else

			if type(m.stateMachine.syncInfo) = "roAssociativeArray" then

				aa = { }
				aa.AddReplace("Filename", filePath$)
				aa.AddReplace("SyncDomain", m.stateMachine.syncInfo.SyncDomain)
				aa.AddReplace("SyncId", m.stateMachine.syncInfo.SyncId)
				aa.AddReplace("SyncIsoTimestamp", m.stateMachine.syncInfo.SyncIsoTimestamp)
				aa.AddReplace("Transition", m.imageItem.slideTransition%)

				ok = m.stateMachine.imagePlayer.DisplayFile(aa)
				if ok = 0 then
					m.bsp.diagnostics.PrintDebug("Error displaying synchronized file: " + file$)
					m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
				else
					m.bsp.diagnostics.PrintDebug("DisplayImage: display synchronized file " + file$)
				endif

				m.stateMachine.syncInfo = invalid

			else

				' determine whether or not a preload has been performed
				preloaded = false
				if type(m.stateMachine.preloadState) = "roAssociativeArray" then
					if m.stateMachine.preloadedStateName$ = m.name$
						preloaded = true
						m.bsp.diagnostics.PrintDebug("Use preloaded file " + file$ + " in DisplayImage: ")
						m.bsp.diagnostics.PrintDebug("DisplayPreload in DisplayImage: " + file$)
						ok = m.stateMachine.imagePlayer.DisplayPreload()
						if ok = 0 then
							m.bsp.diagnostics.PrintDebug("Error in DisplayPreload in DisplayImage: " + file$ + ", " + filePath$)
							m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
						endif

					endif
				endif

				if not preloaded then

					aa = {}
					aa.filename = filePath$

					if ItemIsEncrypted(m.imageItem) then
						aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
						aa.AddReplace("EncryptionKey", file$)
					endif

					ok = m.stateMachine.imagePlayer.DisplayFile(aa)
					if not ok then
						m.bsp.diagnostics.PrintDebug("Error displaying file in DisplayImage: " + file$ + ", " + filePath$)
						m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
					else
						m.bsp.diagnostics.PrintDebug("Displayed file in DisplayImage: " + file$)
					endif

				endif

			endif

		endif

	endif

	m.stateMachine.ShowImageWidget()

	m.stateMachine.preloadState = 0
	m.stateMachine.preloadedStateName$ = ""

	if type(m.imageItem.userVariable) = "roAssociativeArray" then
		m.imageItem.userVariable.Increment()
	endif
    
	' playback logging
	m.stateMachine.LogPlayStart("image", file$)
                    
End Sub


Sub PostDrawImage(stateType$ As String)

	m.ClearVideo()

    m.LaunchTimer()    

    m.bsp.SetTouchRegions(m)

	' state logging
	m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, stateType$)

End Sub


Sub ClearVideo()

	' this shouldn't be done for 4K image playback but must be done for other images
    if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
		if not m.stateMachine.useVideoPlayerForImages then
			m.stateMachine.videoPlayer.StopClear()
		endif
    endif

End Sub


Sub DisplayImage(stateType$ As String)

	m.PreDrawImage()

	m.DrawImage(true)

	m.PostDrawImage(stateType$)

End Sub


Sub PrePlayAudio()

	m.ConfigureBPButtons()
	m.ConfigureGPIOButtons()
	
	m.usbInputBuffer$ = ""
	m.usbInputLogBuffer$ = ""

    if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
        m.stateMachine.videoPlayer.StopClear()
    endif
    
End Sub


Sub PlayAudio(executeEntryCmds As Boolean, disableLoopMode As Boolean)

    loopMode% = 1
    if disableLoopMode or type(m.audioEndEvent) = "roAssociativeArray" then loopMode% = 0
    
	if type(m.stateMachine.audioPlayer) = "roAudioPlayer" then
		player = m.stateMachine.audioPlayer
	else
		player = m.stateMachine.videoPlayer
	endif

    player.SetLoopMode(loopMode%)

	player.Stop()

    m.SetAudioTimeCodeEvents()

	if executeEntryCmds then
		m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
	endif

  if type(m.audioItem.fileName$) = "roString" and len(m.audioItem.fileName$) > 0  then
    file$ = m.audioItem.fileName$
  else
    file$ = m.audioItem.filePath$
  endif

	if type(m.audioItem.filePath$) = "roString" then
		filePath$ = m.audioItem.filePath$
	else
		filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, m.audioItem.fileName$)
	endif

	aa = { }
	aa.AddReplace("Filename", filePath$)
		
	if type(m.audioItem.probeData) = "roString" then
		m.bsp.diagnostics.PrintDebug("LaunchAudio: probeData = " + m.audioItem.probeData)
		aa.AddReplace("ProbeString", m.audioItem.probeData)
	endif

	ok = player.PlayFile(aa)

	if ok = 0 then
		m.bsp.diagnostics.PrintDebug("Error playing audio file: " + file$ + ", " + filePath$)
		m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)

        audioPlaybackFailure = CreateObject("roAssociativeArray")
        audioPlaybackFailure["EventType"] = "AudioPlaybackFailureEvent"
        m.stateMachine.msgPort.PostMessage(audioPlaybackFailure)
	endif

	m.stateMachine.ClearImagePlane()

	if type(m.audioItem.userVariable) = "roAssociativeArray" then
		m.audioItem.userVariable.Increment()
	endif
    
	' playback logging
	m.stateMachine.LogPlayStart("audio", file$)

End Sub


Sub PostPlayAudio(stateType$ As String)

    m.bsp.SetTouchRegions(m)

    m.LaunchTimer()    

	' state logging
	m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, stateType$)

End Sub


Sub LaunchAudio(stateType$ As String)

	m.PrePlayAudio()

	m.PlayAudio(true, false)

	m.PostPlayAudio(stateType$)

End Sub


Function STAudioInPlayingEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.ConfigureBPButtons()
				m.ConfigureGPIOButtons()

				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""

				if type(m.stateMachine.audioPlayer) = "roVideoPlayer" then
					videoZone = m.bsp.GetVideoZone(m.stateMachine)
					if type(videoZone) = "roAssociativeArray" then
						videoZone.videoPlayer.StopClear()
					endif
				endif
				    
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)

				file$ = m.imageFileName$

				if file$ <> "" then
				
					m.stateMachine.imagePlayer.SetDefaultTransition(0)
		
					filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, file$)

					if m.useImageBuffer and type(m.bsp.imageBuffers) = "roAssociativeArray" and m.bsp.imageBuffers.DoesExist(filePath$) then
						
						m.bsp.diagnostics.PrintDebug("Use imageBuffer for " + file$ + " in STAudioInPlayingEventHandler: ")
						imageBuffer = m.bsp.imageBuffers.Lookup(filePath$)
						m.stateMachine.ClearImagePlane()
						m.stateMachine.imagePlayer.DisplayBuffer(imageBuffer, 0, 0)
					
					else

						aa = {}
						aa.filename = filePath$
						if m.bsp.encryptionByFile.DoesExist(file$)
						'if m.bsp.contentEncrypted then
							aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
							aa.AddReplace("EncryptionKey", file$)
						endif
						ok = m.stateMachine.imagePlayer.DisplayFile(aa)
						if not ok then
							m.bsp.diagnostics.PrintDebug("Error displaying file in STAudioInPlayingEventHandler: " + file$ + ", " + filePath$)
							m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
						else
							m.bsp.diagnostics.PrintDebug("Displayed file in STAudioInPlayingEventHandler: " + file$)
						endif

					endif

					m.stateMachine.ShowImageWidget()

				endif					

				if type(m.stateMachine.audioPlayer) = "roAudioPlayer" then
					player = m.stateMachine.audioPlayer
				else
					player = m.stateMachine.videoPlayer
				endif

				player.PlayFile(m.stateMachine.audioInput)

				m.LaunchTimer()    

		        m.bsp.SetTouchRegions(m)

				if file$ <> "" and type(m.imageUserVariable) = "roAssociativeArray" then
					m.imageUserVariable.Increment()
				endif
								
				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "audioIn")

				' playback logging
				m.stateMachine.LogPlayStart("audioIn", "")

				return "HANDLED"
				
            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
                
				if type(m.stateMachine.audioPlayer) = "roAudioPlayer" then
					player = m.stateMachine.audioPlayer
				else
					player = m.stateMachine.videoPlayer
				endif

				player.Stop()
                
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

            else
            
		        return m.MediaItemEventHandler(event, stateData)

			endif
			
		endif
		
    else
        return m.MediaItemEventHandler(event, stateData)
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Function STSuperStateEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.ConfigureBPButtons()

				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
    
				m.LaunchTimer()    

		        m.bsp.SetTouchRegions(m)

				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "superState")

				' playback logging
				m.stateMachine.LogPlayStart("superState", "")

				return "HANDLED"
				
            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

            else if event["EventType"] = "MEDIA_END" then

	            if type(m.mediaEndEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.mediaEndEvent, stateData, "")
				endif

			else
            
		        return m.MediaItemEventHandler(event, stateData)

			endif
			
		endif
		
    else

        return m.MediaItemEventHandler(event, stateData)
	
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Sub RestartXModemTimeoutTimer()

	m.sendTimeoutTimer = CreateObject("roTimer")
	m.sendTimeoutTimer.SetPort(m.stateMachine.msgPort)
	m.sendTimeoutTimer.SetElapsed(10, 0)

	m.sendTimeoutTimer.Start()

End Sub


Function RetryXModemTransfer() As Boolean

	if m.attemptedRetries >= m.numberOfRetries% then
		m.bsp.diagnostics.PrintDebug("retry count exceeded - transfer failure.")
		if type(m.failEvent) = "roAssociativeArray" then
			return false
		endif
	endif

	m.attemptedRetries = m.attemptedRetries + 1

	return true

End Function


Function STXModemEventHandler(event As Object, stateData As Object) As Object

	' serial constants
	SOH = 1
	EOT = 4
	ACK = 6
	NAK = 21
	CAN = 24

	' states
	WAITING_FOR_NAK = 0
	WAITING_FOR_TRANSFER_ACK = 1
	WAITING_FOR_FINAL_ACK = 2
	WAITING_FOR_CRC = 3

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.ConfigureBPButtons()
				m.ConfigureGPIOButtons()

				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
    
				m.LaunchTimer()    

		        m.bsp.SetTouchRegions(m)

				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "xModem")

				' playback logging
				m.stateMachine.LogPlayStart("xModem", "")

				' get relevant file information
				file$ = m.fileName$	
				filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, file$)

				m.xModemFile = CreateObject("roReadFile", filePath$)
				m.xModemFile.SeekToEnd()
				fileSize = m.xModemFile.CurrentPosition()
				m.xModemFile.SeekAbsolute(0)

				numFullBlocks% = fileSize / 128
				m.bytesInFinalBlock% = fileSize - (numFullBlocks% * 128)
				m.numFullBlocks% = numFullBlocks%
				m.totalNumBlocks% = m.numFullBlocks%
				if m.bytesInFinalBlock% > 0 then
					m.totalNumBlocks% = m.numFullBlocks% + 1
				endif

				m.bsp.diagnostics.PrintDebug("numFullBlocks = " + stri(m.numFullBlocks%))
				m.bsp.diagnostics.PrintDebug("bytesInFinalBlock = " + stri(m.bytesInFinalBlock%))

				m.serial = m.bsp.serial[m.port$]

				' start timeout timer
				m.RestartXModemTimeoutTimer()

				m.attemptedRetries = 0

				m.serial.SetByteEventPort(m.bsp.msgPort)

				' send file name command and wait for NAK
				m.state = WAITING_FOR_NAK

				return "HANDLED"
				
            else if event["EventType"] = "EXIT_SIGNAL" then

				m.serial.SetByteEventPort(invalid)

				m.serial.SetLineEventPort(m.bsp.msgPort)

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

            else
            
		        return m.MediaItemEventHandler(event, stateData)

			endif
			
		endif
		
    else if type(event) = "roTimerEvent" then

		if type(m.sendTimeoutTimer) = "roTimer" and event.GetSourceIdentity() = m.sendTimeoutTimer.GetIdentity() then
		    m.bsp.diagnostics.PrintDebug("Xmodem timeout")
			if type(m.failEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.failEvent, stateData, "")
			endif
		endif

    else if type(event) = "roStreamByteEvent" then

	    serialByte% = event.GetInt()

	    m.bsp.diagnostics.PrintDebug("Serial Byte Event " + str(serialByte%))

		if m.state = WAITING_FOR_NAK then

			if serialByte% = NAK then

				m.sendCommandTimeoutTimer = invalid
				m.attemptedRetries = 0

			    m.bsp.diagnostics.PrintDebug("Waiting for NAK, received NAK")

				' start sending data
				m.currentBlock% = 0
				m.blockNumToSend% = 1
				m.SendXModemBlock()
				m.state = WAITING_FOR_TRANSFER_ACK

			else if serialByte% = CAN then

				m.bsp.diagnostics.PrintDebug("CAN received while waiting for NAK")
				if type(m.failEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.failEvent, stateData, "")
				endif

			else

			    m.bsp.diagnostics.PrintDebug("Waiting for NAK, received " + stri(serialByte%))

			endif

			' restart timer
			m.RestartXModemTimeoutTimer()

		else if m.state = WAITING_FOR_TRANSFER_ACK then

			m.bsp.diagnostics.PrintDebug("Waiting for transfer ACK")

			if serialByte% = ACK then

				m.bsp.diagnostics.PrintDebug("Received ACK")

				m.attemptedRetries = 0

				' increment counters
				m.currentBlock% = m.currentBlock% + 1
				m.blockNumToSend% = m.blockNumToSend% + 1
				if m.blockNumToSend% > &HFF then
					m.blockNumToSend% = 0
				endif

				if m.currentBlock% < m.totalNumBlocks%
					m.SendXModemBlock()
				else
					' close file
					m.xModemFile = invalid

					' send EOT
					m.serial.SendByte(EOT)

					m.state = WAITING_FOR_FINAL_ACK

				endif

			else if serialByte% = NAK then

				m.bsp.diagnostics.PrintDebug("Received NAK, resend block")

				if not m.RetryXModemTransfer() then
					return m.ExecuteTransition(m.failEvent, stateData, "")
				endif

				m.SendXModemBlock()
			
			else if serialByte% = CAN then

				m.bsp.diagnostics.PrintDebug("CAN received while waiting for ACK")
				if type(m.failEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.failEvent, stateData, "")
				endif

			else

				m.bsp.diagnostics.PrintDebug("Waiting for transfer ACK, received " + stri(serialByte%) + ". Resend block.")

				if not m.RetryXModemTransfer() then
					return m.ExecuteTransition(m.failEvent, stateData, "")
				endif

				m.SendXModemBlock()

			endif

			' restart timer
			m.RestartXModemTimeoutTimer()

		else if m.state = WAITING_FOR_FINAL_ACK then

			if serialByte% = ACK then

				m.attemptedRetries = 0

				m.state = WAITING_FOR_CRC
				
				' switch to ASCII
				m.serial.SetLineEventPort(m.bsp.msgPort)

			else if serialByte% = CAN then

				m.bsp.diagnostics.PrintDebug("CAN received while waiting for final ACK")
				if type(m.failEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.failEvent, stateData, "")
				endif

			else

				m.bsp.diagnostics.PrintDebug("Waiting for final ACK, received " + stri(serialByte%) + ". Resend EOT.")

				if not m.RetryXModemTransfer() then
					return m.ExecuteTransition(m.failEvent, stateData, "")
				endif

				m.serial.SendByte(EOT)

			endif

			' restart timer
			m.RestartXModemTimeoutTimer()

		endif
		
    else if type(event) = "roStreamLineEvent" then
    
		event$ = event.GetString()

	    m.bsp.diagnostics.PrintDebug("Serial line event = " + event$ + ". Looking for crc " + m.crc16$)

		if m.state = WAITING_FOR_CRC then

			if instr(1, event$, m.crc16$) > 0 then
				if type(m.successEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.successEvent, stateData, "")
				endif
			endif

			' restart timeout timer
			m.RestartXModemTimeoutTimer()

		endif

    else
        return m.MediaItemEventHandler(event, stateData)
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Sub SendXModemBlock()

	SOH = 1

	blockData = CreateObject("roByteArray")

	m.serial.SendByte(SOH)
	m.serial.SendByte(m.blockNumToSend%)
	m.serial.SendByte(255 - m.blockNumToSend%)

	checkSum% = 0

	' seek to appropriate block (seek is required due to possible retries)
	m.xModemFile.SeekAbsolute(m.currentBlock% * 128)

	' read bytes
	if m.currentBlock% < m.numFullBlocks% then
		bytesToRead% = 128
	else
		bytesToRead% = m.bytesInFinalBlock%
	endif

	for i% = 0 to 127
		if i% < bytesToRead% then
			value% = m.xModemFile.ReadByte()
		else
			value% = &H1A
		endif
		blockData[i%] = value%
		checkSum% = checkSum% + value%
	end for

	checkSum% = checkSum% and &HFF

	m.serial.SendBlock(blockData)

	m.serial.SendByte(checkSum%)

End Sub


Function STEventHandlerEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.ConfigureBPButtons()
				m.ConfigureGPIOButtons()

				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
    
				if m.stopPlayback then

					if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
						m.stateMachine.videoPlayer.StopClear()
					endif

					m.stateMachine.ClearImagePlane()

					if IsAudioPlayer(m.stateMachine.audioPlayer) then
						m.stateMachine.audioPlayer.Stop()
					endif
	
				endif

				m.LaunchTimer()    

		        m.bsp.SetTouchRegions(m)

				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "eventHandler")

				' playback logging
				m.stateMachine.LogPlayStart("eventHandler", "")

				return "HANDLED"
				
            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

            else
            
		        return m.MediaItemEventHandler(event, stateData)

			endif
			
		endif
		
    else
        return m.MediaItemEventHandler(event, stateData)
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Function STRFScanHandler(event As Object, stateData As Object) As Object

    MEDIA_END = 8

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
    
				m.stateMachine.videoPlayer.StopClear()
				m.stateMachine.ClearImagePlane()

				m.channelManager = CreateObject("roChannelManager")
				' m.channelManager.EnableScanDebug("scanDebugOutput.txt") 
				m.channelManager.SetPort(m.bsp.msgPort)

				eventData$ = ""
				channelMap = m.scanSpec["ChannelMap"]
				if type(channelMap) = "roString" and channelMap <> "" then
					eventData$ = eventData$ + channelMap
				endif

				modulationType = m.scanSpec["ModulationType"]
				if type(modulationType) = "roString" and modulationType <> "" then
					eventData$ = eventData$ + " " + modulationType
				endif

				firstRFChannel = m.scanSpec["FirstRfChannel"]
				if type(firstRFChannel) = "roString" and firstRFChannel <> "" then
					eventData$ = eventData$ + " " + firstRFChannel
				endif

				lastRFChannel = m.scanSpec["LastRfChannel"]
				if type(lastRFChannel) = "roString" and lastRFChannel <> "" then
					eventData$ = eventData$ + " " + lastRFChannel
				endif

                m.bsp.diagnostics.PrintDebug("begin scan: " + eventData$)
			    m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_SCAN_START, eventData$)

				m.UpdateTunerScanPercentageComplete("0")

				m.channelManager.ClearChannelData()
				m.channelManager.AsyncScan(m.scanSpec)

				m.scanInProgress = true

				m.LaunchTimer()    

		        m.bsp.SetTouchRegions(m)

				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "tunerScan")

				' playback logging
				m.stateMachine.LogPlayStart("tunerScan", "")

				return "HANDLED"
				
            else if event["EventType"] = "EXIT_SIGNAL" then

				if m.scanInProgress then
					m.bsp.diagnostics.PrintDebug("Cancel tuner scan")
					m.channelManager.CancelScan()
				endif

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

            else
            
		        return m.MediaItemEventHandler(event, stateData)

			endif
			
		endif
		
    else if type(event) = "roChannelManagerEvent" then

		if event = 0 then

            m.bsp.diagnostics.PrintDebug("Scan complete")
		    m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_SCAN_COMPLETE, "")

			m.scanInProgress = false

			' save the current channel
			savedCurrentChannel = invalid
			if m.bsp.scannedChannels.Count() > m.stateMachine.currentChannelIndex%
				savedCurrentChannel = m.bsp.scannedChannels[m.stateMachine.currentChannelIndex%]
			endif

			m.ProcessScannedChannels(m.channelManager)
		
			' reset the current channel
			m.stateMachine.currentChannelIndex% = 0
			if type(savedCurrentChannel) = "roAssociativeArray" then
				for index% = 0 to m.bsp.scannedChannels.Count() - 1
					scannedChannel = m.bsp.scannedChannels[index%]
					if scannedChannel.VirtualChannel = savedCurrentChannel.VirtualChannel then
						m.stateMachine.currentChannelIndex% = index%
						exit for
					endif
				next
			endif

			m.UpdateTunerScanPercentageComplete("100")

            if type(m.videoEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.videoEndEvent, stateData, "")
			endif
			PostMediaEndEvent(m.bsp.msgPort)

		else if event = 1 then

			m.bsp.diagnostics.PrintDebug("channel manager progress event - percentage complete =" + stri(event.GetData()))

			m.UpdateTunerScanPercentageComplete(stri(event.GetData()))
		
		else if event = 2 then

			channelDescriptor = event.GetChannelDescriptor()
			m.bsp.diagnostics.PrintDebug("Found channel " + channelDescriptor.VirtualChannel)
		    m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_CHANNEL_FOUND, channelDescriptor.VirtualChannel)

		endif

	else
        return m.MediaItemEventHandler(event, stateData)
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Sub UpdateTunerScanPercentageComplete(percentageComplete$ As String)

	m.bsp.tunerScanPercentageComplete$ = percentageComplete$
	m.bsp.UpdateTunerScanPercentageCompleteUserVariables(true)
	systemVariableChanged = CreateObject("roAssociativeArray")
	systemVariableChanged["EventType"] = "SYSTEM_VARIABLE_UPDATED"
	m.bsp.msgPort.PostMessage(systemVariableChanged)			

End Sub


Sub ProcessScannedChannels(channelManager As Object)

	' Get channel descriptors for all the channels that were found
	channelCount% = channelManager.GetChannelCount()
	channelDescriptors = CreateObject("roArray", channelCount%, false)

	if channelCount% > 0 then
		channelInfo  = CreateObject("roAssociativeArray")
		for channelIndex% = 0 to channelCount% - 1
			channelInfo["ChannelIndex"] = channelIndex%
			channelDescriptor = channelManager.CreateChannelDescriptor(channelInfo)
			channelDescriptors.push(channelDescriptor)
		next
	endif

' Generate XML data for the results of the scan
	docName$ = "BrightSignRFChannels"

	root = CreateObject("roXMLElement")
	root.SetName(docName$)
	root.AddAttribute("version", "1.0")

	rfChannels = root.AddElement("rfChannels")

	' rebuild scanned channels data structure
	m.bsp.scannedChannels.Clear()

	for each channelDescriptor in channelDescriptors

		channelDescriptorElement		= rfChannels.AddElement("rfInputChannel")
		virtualChannelElement			= channelDescriptorElement.AddElement("virtualChannel")
		channelNameElement				= channelDescriptorElement.AddElement("channelName")

		virtualChannelElement.SetBody(channelDescriptor.VirtualChannel)
		channelNameElement.SetBody(channelDescriptor.ChannelName)

		scannedChannelDescriptor = { }
		scannedChannelDescriptor.VirtualChannel = channelDescriptor.VirtualChannel
		scannedChannelDescriptor.ChannelName = channelDescriptor.ChannelName

		m.bsp.scannedChannels.push(scannedChannelDescriptor)

	next

	xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

' Retrieve the XML data for the tuning data produced by system software
	scannedChannelData$ = channelManager.ExportToXml()

' Embed the scanned channel data in the xml as a CDATA element

	' find the insert point in the xml (is there a way to write out a proper element?)
	index% = instr(1, xml, "</" + docName$ + ">")
	if index% > 0 then
		xml = mid(xml, 1, index% - 1)
		xml = xml + " <![CDATA[" + chr(10) + scannedChannelData$ + " ]]>" + chr(10) + "</" + docName$ + ">"
	endif

	ok = WriteAsciiFile("ScannedChannels.xml", xml)

	m.bsp.UpdateRFChannelCountUserVariables(true)

	systemVariableChanged = CreateObject("roAssociativeArray")
	systemVariableChanged["EventType"] = "SYSTEM_VARIABLE_UPDATED"
	m.bsp.msgPort.PostMessage(systemVariableChanged)			

End Sub


Function STRFInputPlayingHandler(event As Object, stateData As Object) As Object

    MEDIA_END = 8

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then

            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.ConfigureIntraStateEventHandlerButton(m.channelUpEvent)
				m.ConfigureIntraStateEventHandlerButton(m.channelDownEvent)

				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
    
				m.stateMachine.videoPlayer.Stop()
				m.stateMachine.ClearImagePlane()

				m.stateMachine.videoPlayer.EnableSafeRegionTrimming(m.overscan)

				if m.stateMachine.firstTuneToChannel or m.reentryAction$ = "Retune" then
					tuneToOriginal = true
				else
					tuneToOriginal = false
				endif

				m.stateMachine.firstTuneToChannel = false

				channelDescriptor = invalid

				if tuneToOriginal then

					if m.firstScannedChannel then

						if m.bsp.scannedChannels.Count() > 0 then
							channelDescriptor = m.bsp.scannedChannels[0]
						else
							m.bsp.SendTuneFailureMessage("No scanned channels")
						endif

					else if type(m.channelDescriptor) = "roAssociativeArray" then

						channelDescriptor = m.channelDescriptor
					
					else if type(m.userVariable) = "roAssociativeArray" then
					
						channelSpec$ = m.userVariable.GetCurrentValue()

						' first look for match with virtual channels; then try channel names if no match found
						for each scannedChannel in m.bsp.scannedChannels
							if scannedChannel.VirtualChannel = channelSpec$ then
								channelDescriptor = scannedChannel
								exit for
							endif
						next

						if channelDescriptor = invalid then
							for each scannedChannel in m.bsp.scannedChannels
								if scannedChannel.ChannelName = channelSpec$ then
									channelDescriptor = scannedChannel
									exit for
								endif
							next
						endif

						if channelDescriptor = invalid then

							m.bsp.SendTuneFailureMessage("No channel found for user variable")

						endif

					else

						m.bsp.SendTuneFailureMessage("No valid channel descriptor")

					endif

					' set the current channel
					if not channelDescriptor = invalid then
						for index% = 0 to m.bsp.scannedChannels.Count() - 1
							scannedChannel = m.bsp.scannedChannels[index%]
							if scannedChannel.VirtualChannel = channelDescriptor.VirtualChannel then
								m.stateMachine.currentChannelIndex% = index%
								exit for
							endif
						next
					endif

				else

					if m.bsp.scannedChannels.Count() > m.stateMachine.currentChannelIndex% then
						channelDescriptor = m.bsp.scannedChannels[m.stateMachine.currentChannelIndex%]
					else
						m.bsp.SendTuneFailureMessage("No scanned channel matches selected channel.")
					endif
								
				endif

				if channelDescriptor = invalid then
					channelToLog$ = "error"
				else
					channelToLog$ = channelDescriptor.VirtualChannel
				endif

				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "RFIn " + channelToLog$)

				if type(channelDescriptor) = "roAssociativeArray" then
					m.bsp.TuneToChannel(m.stateMachine, channelDescriptor)
				endif

				m.LaunchTimer()    

		        m.bsp.SetTouchRegions(m)

				' playback logging
				m.stateMachine.LogPlayStart("RFIn", channelToLog$)

				return "HANDLED"
				
			else if event["EventType"] = "TuneFailureEvent" then

				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")    
				if type(m.videoEndEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.videoEndEvent, stateData, "")
				endif
				PostMediaEndEvent(m.bsp.msgPort)

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

			endif
			
		endif
		
	else if type(event) = "roVideoEvent" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() then            
		if event.GetInt() = MEDIA_END then
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if type(m.videoEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.videoEndEvent, stateData, "")
			endif
			PostMediaEndEvent(m.bsp.msgPort)
		endif
	
	endif

	if type(m.channelUpEvent) = "roAssociativeArray" and m.HandleIntraStateEvent(event, m.channelUpEvent) then
		
		if m.bsp.scannedChannels.Count() = 0 then
			m.bsp.SendTuneFailureMessage("No scanned channels.")
		else
			' perform channel up
			m.bsp.ChangeRFChannel(m.stateMachine, 1)
			channelDescriptor = m.bsp.scannedChannels[m.stateMachine.currentChannelIndex%]
			m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "RFIn " + channelDescriptor.VirtualChannel)
			m.bsp.TuneToChannel(m.stateMachine, channelDescriptor)
		endif
		return "HANDLED"
	
	endif

	if type(m.channelDownEvent) = "roAssociativeArray" and m.HandleIntraStateEvent(event, m.channelDownEvent) then

		if m.bsp.scannedChannels.Count() = 0 then
			m.bsp.SendTuneFailureMessage("No scanned channels.")
		else
			' perform channel down
			m.bsp.ChangeRFChannel(m.stateMachine, -1)
			channelDescriptor = m.bsp.scannedChannels[m.stateMachine.currentChannelIndex%]
			m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "RFIn " + channelDescriptor.VirtualChannel)
			m.bsp.TuneToChannel(m.stateMachine, channelDescriptor)
			return "HANDLED"
		endif

    endif

    return m.MediaItemEventHandler(event, stateData)

End Function


Sub SendTuneFailureMessage(debugMsg As String)

	m.diagnostics.PrintDebug(debugMsg)
	tuneFailure = CreateObject("roAssociativeArray")
	tuneFailure["EventType"] = "TuneFailureEvent"
	m.msgPort.PostMessage(tuneFailure)

End Sub


Function TuneToChannel(zone As Object, channelDescriptor As Object)

	m.diagnostics.PrintDebug("Tune to the following channel descriptor:")
    m.diagnostics.PrintDebug("VirtualChannel " + channelDescriptor.VirtualChannel)
'    m.diagnostics.PrintDebug("ChannelName " + channelDescriptor.ChannelName)
'    m.diagnostics.PrintDebug("RFChannel " + stri(channelDescriptor.RFChannel))

	m.rfVirtualChannel = channelDescriptor.VirtualChannel
	if IsString(channelDescriptor.ChannelName) then
		m.rfChannelName = channelDescriptor.ChannelName
	else
		m.rfChannelName = ""
	endif

	m.UpdateRFChannelUserVariables(true)

	systemVariableChanged = CreateObject("roAssociativeArray")
	systemVariableChanged["EventType"] = "SYSTEM_VARIABLE_UPDATED"
	m.msgPort.PostMessage(systemVariableChanged)			

	if type(zone.syncInfo) = "roAssociativeArray" then

		channelDescriptor.AddReplace("SyncDomain", zone.syncInfo.SyncDomain)
		channelDescriptor.AddReplace("SyncId", zone.syncInfo.SyncId)
		channelDescriptor.AddReplace("SyncIsoTimestamp", zone.syncInfo.SyncIsoTimestamp)

		if m.sign.isVideoWall and m.sign.videoWallType$ = "stretched" then
			channelDescriptor["MultiscreenWidth"] = m.sign.videoWallNumColumns%
			channelDescriptor["MultiscreenHeight"] = m.sign.videoWallNumRows%
			channelDescriptor["MultiscreenX"] = m.sign.videoWallColumnPosition%
			channelDescriptor["MultiscreenY"] = m.sign.videoWallRowPosition%
			channelDescriptor["Mode"] = zone.viewMode%
		endif

	endif

	ok = zone.videoPlayer.PlayFile(channelDescriptor)

	if not ok then
		m.SendTuneFailureMessage("Error tuning in TuneToChannel")
	    m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_TUNE_FAILURE, channelDescriptor.VirtualChannel)
	endif

End Function


Function STLiveVideoPlayingEventHandler(event As Object, stateData As Object) As Object

    MEDIA_END = 8

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
    
				m.stateMachine.videoPlayer.Stop()
				m.stateMachine.ClearImagePlane()

				if m.bsp.sysInfo.deviceFamily$ = "cheetah" or m.bsp.sysInfo.deviceFamily$ = "tiger"  or m.bsp.sysInfo.deviceFamily$ = "lynx" or m.bsp.sysInfo.deviceFamily$ = "impala" or m.bsp.sysInfo.deviceFamily$ = "malibu" or m.bsp.sysInfo.deviceFamily$ = "pantera" then
					' HDMI In
					m.stateMachine.videoPlayer.EnableSafeRegionTrimming(m.overscan)
				else
					m.stateMachine.videoPlayer.EnableSafeRegionTrimming(true)
				endif

				aa = { }
				aa.AddReplace("Capture", m.stateMachine.videoInput)

				if type(m.stateMachine.syncInfo) = "roAssociativeArray" then

					aa.AddReplace("SyncDomain", m.stateMachine.syncInfo.SyncDomain)
					aa.AddReplace("SyncId", m.stateMachine.syncInfo.SyncId)
					aa.AddReplace("SyncIsoTimestamp", m.stateMachine.syncInfo.SyncIsoTimestamp)

					if m.bsp.sign.isVideoWall and m.bsp.sign.videoWallType$ = "stretched" then
						aa["MultiscreenWidth"] = m.bsp.sign.videoWallNumColumns%
						aa["MultiscreenHeight"] = m.bsp.sign.videoWallNumRows%
						aa["MultiscreenX"] = m.bsp.sign.videoWallColumnPosition%
						aa["MultiscreenY"] = m.bsp.sign.videoWallRowPosition%
						aa["Mode"] = m.stateMachine.viewMode%
					endif

				endif

				if m.stateMachine.mosaicDecoderName <> "" then
					aa.Decoder = m.stateMachine.mosaicDecoderName
				endif

				ok = m.stateMachine.videoPlayer.PlayFile(aa)

				m.LaunchTimer()    

		        m.bsp.SetTouchRegions(m)

				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "liveVideo")

				' playback logging
				m.stateMachine.LogPlayStart("liveVideo", "")

				return "HANDLED"
				
            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

            else
            
		        return m.MediaItemEventHandler(event, stateData)

			endif
			
		endif
		
	else if type(event) = "roVideoEvent" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() then
        if event.GetInt() = MEDIA_END then
            m.bsp.diagnostics.PrintDebug("Video Event" + stri(event.GetInt()))
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif
            if type(m.videoEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.videoEndEvent, stateData, "")
			endif

			PostMediaEndEvent(m.bsp.msgPort)'
		endif

    else
        return m.MediaItemEventHandler(event, stateData)
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function

Sub SetUserAgentForHtmlWidget(bsp as Object, htmlWidget as Object)
	if bsp.httpWidgetGetUserAgentSupported and type(htmlWidget) = "roHtmlWidget" then
		ua$ = htmlWidget.GetUserAgent()
		' Prepend custom user agent string on to standard Html Widget string
		p% = instr(1, ua$, ")")
		if p% > 0 then
			ua$ = bsp.userAgent$ + mid(ua$, p%+1)
			htmlWidget.SetUserAgent(ua$)
		endif
	endif
End Sub

Function STHTML5PlayingEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.ConfigureBPButtons()
				m.ConfigureGPIOButtons()
	
				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)

				m.stateMachine.loadingHtmlWidget = CreateObject("roHtmlWidget", m.stateMachine.rectangle)
				SetUserAgentForHtmlWidget(m.bsp, m.stateMachine.loadingHtmlWidget)

				if m.bsp.sign.htmlEnableJavascriptConsole then
					m.stateMachine.loadingHtmlWidget.StartInspectorServer(2999)
				endif

				if CanUseScreenModes({}) then
				  ' no need to rotate per zone if already rotated by screen
				else if m.bsp.DeviceSupportsRotation() then
					if m.bsp.sign.monitorOrientation = "portrait" then
						m.stateMachine.loadingHtmlWidget.SetPortrait(true)
					else if m.bsp.htmlSetTransformSupported and m.bsp.sign.monitorOrientation = "portraitbottomonright" then
						m.stateMachine.loadingHtmlWidget.SetTransform("rot270")
					endif
				endif
				
				m.stateMachine.loadingHtmlWidget.SetPort(m.bsp.msgPort)

				securityParams = {}
				securityParams.websecurity = not m.enableExternalData
				securityParams.camera_enabled = m.enableCamera
				m.stateMachine.loadingHtmlWidget.EnableSecurity(securityParams)
				m.stateMachine.loadingHtmlWidget.EnableMouseEvents(m.enableMouseEvents)
				if m.hwzOn then
					m.stateMachine.loadingHtmlWidget.SetHWZDefault("on")
				endif
				m.stateMachine.loadingHtmlWidget.EnableScrollBars(m.enableScrollBars)
				
				m.stateMachine.loadingHtmlWidget.EnableJavascript(true)
				if m.enableExternalData then
					m.stateMachine.loadingHtmlWidget.AllowJavaScriptUrls({ all: "*" })
				endif

				' only used for obsolete format xml
				if m.bsp.sign.htmlEnableLocalStorage then
					m.stateMachine.loadingHtmlWidget.SetLocalStorageDir("localstorage")
					m.stateMachine.loadingHtmlWidget.SetLocalStorageQuota(m.bsp.sign.htmlLocalStorageSize% * 1024 * 1024)
				endif

				if m.contentIsLocal then

					syncSpec = ReadSyncSpec()
					if not type(syncSpec) = "roSyncSpec" stop

					assetCollection = syncSpec.GetAssets("download")

					presentationName$ = m.bsp.sign.name$
					stateName$ = m.name$
					prefix$ = m.prefix$

					m.stateMachine.loadingHtmlWidget.MapFilesFromAssetPool(m.bsp.assetPool, assetCollection, prefix$, "/" + prefix$ + "/")
					m.url$ = "file:///" + prefix$ + "/" + m.filePath$
				
				else

					m.url$ = m.url.GetCurrentParameterValue()

				endif

				m.url$ = m.url$ + m.queryString.GetCurrentParameterValue()

				for each customFont in m.customFonts
					customFontPath$ = GetPoolFilePath(m.bsp.assetPoolFiles, customFont)
					m.stateMachine.loadingHtmlWidget.AddFont(customFontPath$)
				next

				if m.useUserStylesheet then
					fileName$ = GetPoolFilePath(m.bsp.assetPoolFiles, m.userStylesheet)
					m.stateMachine.loadingHtmlWidget.SetUserStylesheet(fileName$)
				endif

				if not m.stateMachine.isVisible then
					m.stateMachine.loadingHtmlWidget.Hide()
				endif

				m.stateMachine.loadingHtmlWidget.SetUrl(m.url$)

				m.LaunchTimer()    

				m.bsp.SetTouchRegions(m)

				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "html5")

				' playback logging
				m.stateMachine.LogPlayStart("html5", m.name$)

				return "HANDLED"
				
            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

            else
            
		        return m.MediaItemEventHandler(event, stateData)

			endif
			
		endif

	else if type(event) = "roHtmlWidgetEvent" then

		eventData = event.GetData()
		if type(eventData) = "roAssociativeArray" and type(eventData.reason) = "roString" then
            m.bsp.diagnostics.PrintDebug("reason = " + eventData.reason)
			if eventData.reason = "load-error" then
				m.bsp.diagnostics.PrintDebug("message = " + eventData.message)
				m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_HTML5_LOAD_ERROR, eventData.message)

				if not m.contentIsLocal then
					m.htmlReloadTimer = CreateObject("roTimer")
					m.htmlReloadTimer.SetPort(m.bsp.msgPort)
					m.htmlReloadTimer.SetElapsed(30, 0)
					m.htmlReloadTimer.Start()
				endif
			else if eventData.reason = "load-finished" then
				if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
					m.stateMachine.videoPlayer.StopClear()
				endif

'				m.stateMachine.displayedHtmlWidget = m.stateMachine.loadingHtmlWidget
'				m.stateMachine.ShowHtmlWidget()
' Do a swap instead of just an assignment

				m.stateMachine.onDisplayHtmlWidget = m.stateMachine.displayedHtmlWidget
				m.stateMachine.displayedHtmlWidget = m.stateMachine.loadingHtmlWidget
				m.stateMachine.ShowHtmlWidget()
				m.stateMachine.onDisplayHtmlWidget = invalid
			endif
		endif

    else if type(event) = "roTimerEvent" then

		if type(m.htmlReloadTimer) = "roTimer" and event.GetSourceIdentity() = m.htmlReloadTimer.GetIdentity() then
			m.bsp.diagnostics.PrintDebug("Reload Html5 widget")
			m.stateMachine.loadingHtmlWidget.SetURL(m.url$)
			return "HANDLED"
		else
	        return m.MediaItemEventHandler(event, stateData)
		endif

	else
        return m.MediaItemEventHandler(event, stateData)
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Function IsPlayingClip() As Boolean

	return m.playingVideoClip or m.playingAudioClip or m.displayingImage
	
End Function


Sub ClearPlayingClip()

	m.playingVideoClip = false
	m.playingAudioClip = false
	m.displayingImage = false

End Sub


Sub ConfigureNavigationButton(navigation As Object)

	if type(navigation) = "roAssociativeArray" then
		if type(navigation.bpEvent) = "roAssociativeArray" then
			bpEvent = navigation.bpEvent
			bpEvent.configuration$ = "press"
			m.bsp.ConfigureBPButton(bpEvent.buttonPanelIndex%, bpEvent.buttonNumber$, bpEvent)
		else if type(navigation.gpioEvent) = "roAssociativeArray" then
			gpioEvent = navigation.gpioEvent
			gpioEvent.configuration$ = "press"
			m.bsp.ConfigureGPIOButton(gpioEvent.buttonNumber$, gpioEvent)
		endif
	endif

End Sub


Function STInteractiveMenuEventHandler(event As Object, stateData As Object) As Object

    MEDIA_END = 8

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.playingVideoClip = false
				m.playingAudioClip = false
				m.displayingImage = false

				if type(m.mstimeoutEvent) = "roAssociativeArray" then
					m.inactivityTimer = CreateObject("roTimer")
					m.inactivityTimer.SetPort(m.bsp.msgPort)
					m.RestartInteractiveMenuInactivityTimer()
				endif

				m.imageFileTimeoutTimer = invalid
				
				m.ConfigureBPButtons()
				m.ConfigureGPIOButtons()

				m.ConfigureNavigationButton(m.upNavigation)
				m.ConfigureNavigationButton(m.downNavigation)
				m.ConfigureNavigationButton(m.leftNavigation)
				m.ConfigureNavigationButton(m.rightNavigation)
				m.ConfigureNavigationButton(m.enterNavigation)
				m.ConfigureNavigationButton(m.backNavigation)
				m.ConfigureNavigationButton(m.nextClipNavigation)
				m.ConfigureNavigationButton(m.previousClipNavigation)

				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""
    
				m.stateMachine.imagePlayer.SetDefaultTransition(0)

				m.currentInteractiveMenuItem = invalid
				if m.interactiveMenuItems.Count() > 0 then
					m.currentInteractiveMenuNavigationIndex% = 0
					if m.navigateToLastSelectedOnEntry then
						' check to see if prior state was interactive menu (and is not referring to the current state)
						if type(m.stateMachine.previousStateName$) = "roString" then
							if m.stateMachine.previousStateName$ <> m.id$ then
								previousState = m.stateMachine.stateTable[m.stateMachine.previousStateName$]
								if type(previousState) = "roAssociativeArray" then
									if previousState.type$ = "interactiveMenuItem" then
										m.currentInteractiveMenuNavigationIndex% = m.lastInteractiveMenuNavigationIndex%
									endif
								endif
							endif
						endif
					endif
					m.currentInteractiveMenuItem = m.interactiveMenuItems[m.currentInteractiveMenuNavigationIndex%]
				endif
				
				m.DrawInteractiveMenu()
				
		        m.bsp.SetTouchRegions(m)

				' byte arrays to store stream byte input
				m.serialStreamInputBuffers = CreateObject("roArray", 8, true)
				for i% = 0 to 7
					m.serialStreamInputBuffers[i%] = CreateObject("roByteArray")
				next

				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "interactiveMenu")

				' playback logging
				m.stateMachine.LogPlayStart("interactiveMenu", "")

				return "HANDLED"
				
            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
                
				if type(m.inactivityTimer) = "roTimer" then
					m.inactivityTimer.Stop()
				endif

				if type(m.imageFileTimeoutTimer) = "roTimer" then
					m.imageFileTimeoutTimer.Stop()
				endif

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

                return "HANDLED"
            
			else if event["EventType"] = "SEND_ZONE_MESSAGE" and type(m.currentInteractiveMenuItem) = "roAssociativeArray" then

				zoneMessage$ = event["EventParameter"]
				m.bsp.diagnostics.PrintDebug("ZoneMessageEvent " + zoneMessage$)

				retVal = m.HandleInteractiveMenuEventInput(stateData, "zoneMessage$", zoneMessage$, "zoneMessage")
				if retVal <> invalid then
					return retVal
				endif
			
			else if event["EventType"] = "GPIOControlDown" and type(m.currentInteractiveMenuItem) = "roAssociativeArray" then
			
				gpioNum$ = event["ButtonNumber"]
				m.bsp.diagnostics.PrintDebug("GPIO Press" + gpioNum$)

				if not m.IsPlayingClip() then

					if type(m.upNavigation) = "roAssociativeArray" and type(m.upNavigation.gpioEvent) = "roAssociativeArray" and IsString(m.upNavigation.gpioEvent.buttonNumber$) and m.upNavigation.gpioEvent.buttonNumber$ = gpioNum$ then
						m.NavigateToMenuItem(m.currentInteractiveMenuItem.upNavigationIndex%)
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioButton", gpioNum$, "1")
						return "HANDLED"
					endif
			
					if type(m.downNavigation) = "roAssociativeArray" and type(m.downNavigation.gpioEvent) = "roAssociativeArray" and IsString(m.downNavigation.gpioEvent.buttonNumber$) and m.downNavigation.gpioEvent.buttonNumber$ = gpioNum$ then
						m.NavigateToMenuItem(m.currentInteractiveMenuItem.downNavigationIndex%)
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioButton", gpioNum$, "1")
						return "HANDLED"
					endif
			
					if type(m.leftNavigation) = "roAssociativeArray" and type(m.leftNavigation.gpioEvent) = "roAssociativeArray" and IsString(m.leftNavigation.gpioEvent.buttonNumber$) and m.leftNavigation.gpioEvent.buttonNumber$ = gpioNum$ then
						m.NavigateToMenuItem(m.currentInteractiveMenuItem.leftNavigationIndex%)
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioButton", gpioNum$, "1")
						return "HANDLED"
					endif
			
					if type(m.rightNavigation) = "roAssociativeArray" and type(m.rightNavigation.gpioEvent) = "roAssociativeArray" and IsString(m.rightNavigation.gpioEvent.buttonNumber$) and m.rightNavigation.gpioEvent.buttonNumber$ = gpioNum$ then
						m.NavigateToMenuItem(m.currentInteractiveMenuItem.rightNavigationIndex%)
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioButton", gpioNum$, "1")
						return "HANDLED"
					endif
			
					if type(m.enterNavigation) = "roAssociativeArray" and type(m.enterNavigation.gpioEvent) = "roAssociativeArray" and IsString(m.enterNavigation.gpioEvent.buttonNumber$) and m.enterNavigation.gpioEvent.buttonNumber$ = gpioNum$ then
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioButton", gpioNum$, "1")
						return m.ExecuteInteractiveMenuEnter(stateData)
					endif
			
				else
		
					if type(m.backNavigation) = "roAssociativeArray" and type(m.backNavigation.gpioEvent) = "roAssociativeArray" and IsString(m.backNavigation.gpioEvent.buttonNumber$) and m.backNavigation.gpioEvent.buttonNumber$ = gpioNum$ then
						m.DrawInteractiveMenu()
						m.ClearPlayingClip()
						m.RestartInteractiveMenuInactivityTimer()			
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioButton", gpioNum$, "1")
						return "HANDLED"
					endif

					if type(m.nextClipNavigation) = "roAssociativeArray" and type(m.nextClipNavigation.gpioEvent) = "roAssociativeArray" and IsString(m.nextClipNavigation.gpioEvent.buttonNumber$) and m.nextClipNavigation.gpioEvent.buttonNumber$ = gpioNum$ then
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioButton", gpioNum$, "1")
						return m.NextPrevInteractiveMenuLaunchMedia(stateData, 1)
					endif

					if type(m.previousClipNavigation) = "roAssociativeArray" and type(m.previousClipNavigation.gpioEvent) = "roAssociativeArray" and IsString(m.previousClipNavigation.gpioEvent.buttonNumber$) and m.previousClipNavigation.gpioEvent.buttonNumber$ = gpioNum$ then
						m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioButton", gpioNum$, "1")
						return m.NextPrevInteractiveMenuLaunchMedia(stateData, -1)
					endif

				endif
	
			else if event["EventType"] = "BPControlDown" and type(m.currentInteractiveMenuItem) = "roAssociativeArray" then
				bpIndex$ = event["ButtonPanelIndex"]
				bpIndex% = int(val(bpIndex$))
				bpNum$ = event["ButtonNumber"]
				bpNum% = int(val(bpNum$))
				m.bsp.diagnostics.PrintDebug("BP Press" + bpNum$ + " on button panel" + bpIndex$)
				
				if not m.IsPlayingClip() then
				
					if type(m.upNavigation) = "roAssociativeArray" and type(m.upNavigation.bpEvent) = "roAssociativeArray" then
						if m.upNavigation.bpEvent.buttonPanelIndex% = bpIndex% and m.upNavigation.bpEvent.buttonNumber$ = bpNum$ then
							m.NavigateToMenuItem(m.currentInteractiveMenuItem.upNavigationIndex%)
							m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "1")
							return "HANDLED"
						endif
					endif

					if type(m.downNavigation) = "roAssociativeArray" and type(m.downNavigation.bpEvent) = "roAssociativeArray" then
						if m.downNavigation.bpEvent.buttonPanelIndex% = bpIndex% and m.downNavigation.bpEvent.buttonNumber$ = bpNum$ then
							m.NavigateToMenuItem(m.currentInteractiveMenuItem.downNavigationIndex%)
							m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "1")
							return "HANDLED"
						endif
					endif
					
					if type(m.leftNavigation) = "roAssociativeArray" and type(m.leftNavigation.bpEvent) = "roAssociativeArray" then
						if m.leftNavigation.bpEvent.buttonPanelIndex% = bpIndex% and m.leftNavigation.bpEvent.buttonNumber$ = bpNum$ then
							m.NavigateToMenuItem(m.currentInteractiveMenuItem.leftNavigationIndex%)
							m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "1")
							return "HANDLED"
						endif
					endif
					
					if type(m.rightNavigation) = "roAssociativeArray" and type(m.rightNavigation.bpEvent) = "roAssociativeArray" then
						if m.rightNavigation.bpEvent.buttonPanelIndex% = bpIndex% and m.rightNavigation.bpEvent.buttonNumber$ = bpNum$ then
							m.NavigateToMenuItem(m.currentInteractiveMenuItem.rightNavigationIndex%)
							m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "1")
							return "HANDLED"
						endif
					endif
					
					if type(m.enterNavigation) = "roAssociativeArray" and type(m.enterNavigation.bpEvent) = "roAssociativeArray" then
						if m.enterNavigation.bpEvent.buttonPanelIndex% = bpIndex% and m.enterNavigation.bpEvent.buttonNumber$ = bpNum$ then
							m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "1")
							return m.ExecuteInteractiveMenuEnter(stateData)
						endif
					endif
				
				else
				
					if type(m.backNavigation) = "roAssociativeArray" and type(m.backNavigation.bpEvent) = "roAssociativeArray" then
						if m.backNavigation.bpEvent.buttonPanelIndex% = bpIndex% and m.backNavigation.bpEvent.buttonNumber$ = bpNum$ then
							m.DrawInteractiveMenu()
							m.ClearPlayingClip()
							m.RestartInteractiveMenuInactivityTimer()			
							m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "1")
							return "HANDLED"
						endif
					endif
					
					if type(m.nextClipNavigation) = "roAssociativeArray" and type(m.nextClipNavigation.bpEvent) = "roAssociativeArray" then
						if m.nextClipNavigation.bpEvent.buttonPanelIndex% = bpIndex% and m.nextClipNavigation.bpEvent.buttonNumber$ = bpNum$ then
							m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "1")
							return m.NextPrevInteractiveMenuLaunchMedia(stateData, 1)
						endif
					endif
					
					if type(m.previousClipNavigation) = "roAssociativeArray" and type(m.previousClipNavigation.bpEvent) = "roAssociativeArray" then
						if m.previousClipNavigation.bpEvent.buttonPanelIndex% = bpIndex% and m.previousClipNavigation.bpEvent.buttonNumber$ = bpNum$ then
							m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "1")
							return m.NextPrevInteractiveMenuLaunchMedia(stateData, -1)
						endif
					endif
					
				endif
				
			endif
			
		endif
		
	else if type(event) = "roVideoEvent" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() then
    
        m.bsp.diagnostics.PrintDebug("Video Event" + stri(event.GetInt()))

		if event.GetInt() = MEDIA_END and m.playingVideoClip then        
        
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif

			m.playingVideoClip = false

			m.DrawInteractiveMenu()
			m.RestartInteractiveMenuInactivityTimer()			

			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")

		else if event.GetInt() = MEDIA_END and m.playingAudioClip then

			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif

			m.playingAudioClip = false

			m.DrawInteractiveMenu()
			m.RestartInteractiveMenuInactivityTimer()			
        
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
        else
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "0")
		endif
        
        return "HANDLED"

	' this should no longer trigger - I think an interactive menu requires a VideoImages zone which doesn't include an audio player
    else if type(event) = "roAudioEvent" then

        m.bsp.diagnostics.PrintDebug("Audio Event" + stri(event.GetInt()))

		if event.GetInt() = MEDIA_END and m.playingAudioClip then
        
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif

			m.playingAudioClip = false

			m.DrawInteractiveMenu()
			m.RestartInteractiveMenuInactivityTimer()			
        
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
        else
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "0")
        endif
        
        return "HANDLED"

	else if type(event) = "roTimerEvent" then
	
		if type(m.inactivityTimer) = "roTimer" and event.GetSourceIdentity() = m.inactivityTimer.GetIdentity() then
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timer", "", "1")
			return m.ExecuteTransition(m.mstimeoutEvent, stateData, "")
		endif

		if type(m.imageFileTimeoutTimer) = "roTimer" and event.GetSourceIdentity() = m.imageFileTimeoutTimer.GetIdentity() then
			if m.displayingImage then
				m.displayingImage = false
				m.DrawInteractiveMenu()
				m.RestartInteractiveMenuInactivityTimer()			
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timer", "", "1")
			else
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timer", "", "0")
			endif
			return "HANDLED"
		endif

    else if type(event) = "roStreamByteEvent" and type(m.currentInteractiveMenuItem) = "roAssociativeArray" then

	    m.bsp.diagnostics.PrintDebug("Serial Byte Event " + str(event.GetInt()))
	    
	    serialByte% = event.GetInt()
		port$ = event.GetUserData()

		port% = int(val(port$))
		serialStreamInput = m.serialStreamInputBuffers[port%]
		while serialStreamInput.Count() >= 64
			serialStreamInput.Shift()
		end while
		serialStreamInput.push(serialByte%)

		if not m.IsPlayingClip() then
		
			status$ = m.ConsumeSerialByteInput(stateData, m.upNavigation, m.currentInteractiveMenuItem.upNavigationIndex%, serialStreamInput, port$, "navigate")
			if status$ <> "" return status$
			status$ = m.ConsumeSerialByteInput(stateData, m.downNavigation, m.currentInteractiveMenuItem.downNavigationIndex%, serialStreamInput, port$, "navigate")
			if status$ <> "" return status$
			status$ = m.ConsumeSerialByteInput(stateData, m.leftNavigation, m.currentInteractiveMenuItem.leftNavigationIndex%, serialStreamInput, port$, "navigate")
			if status$ <> "" return status$
			status$ = m.ConsumeSerialByteInput(stateData, m.rightNavigation, m.currentInteractiveMenuItem.rightNavigationIndex%, serialStreamInput, port$, "navigate")
			if status$ <> "" return status$
			status$ = m.ConsumeSerialByteInput(stateData, m.enterNavigation, -1, serialStreamInput, port$, "enter")
			if status$ <> "" return status$
						
		else
		
			status$ = m.ConsumeSerialByteInput(stateData, m.backNavigation, -1, serialStreamInput, port$, "back")
			if status$ <> "" return status$
			status$ = m.ConsumeSerialByteInput(stateData, m.nextClipNavigation, -1, serialStreamInput, port$, "nextClip")
			if status$ <> "" return status$
			status$ = m.ConsumeSerialByteInput(stateData, m.previousClipNavigation, -1, serialStreamInput, port$, "previousClip")
			if status$ <> "" return status$

		endif


    else if type(event) = "roStreamLineEvent" and type(m.currentInteractiveMenuItem) = "roAssociativeArray" then

	    m.bsp.diagnostics.PrintDebug("Serial Line Event " + event.GetString())

		port$ = event.GetUserData()
        serialEvent$ = event.GetString()

		if not m.IsPlayingClip() then

			if type(m.upNavigation) = "roAssociativeArray" and type(m.upNavigation.serialEvent) = "roAssociativeArray" then
				if m.upNavigation.serialEvent.protocol$ <> "Binary" and m.upNavigation.serialEvent.port$ = port$ and m.upNavigation.serialEvent.serial$ = serialEvent$ then
					m.NavigateToMenuItem(m.currentInteractiveMenuItem.upNavigationIndex%)
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "1")
					return "HANDLED"
				endif
			endif
	        
			if type(m.downNavigation) = "roAssociativeArray" and type(m.downNavigation.serialEvent) = "roAssociativeArray" then
				if m.downNavigation.serialEvent.protocol$ <> "Binary" and m.downNavigation.serialEvent.port$ = port$ and m.downNavigation.serialEvent.serial$ = serialEvent$ then
					m.NavigateToMenuItem(m.currentInteractiveMenuItem.downNavigationIndex%)
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "1")
					return "HANDLED"
				endif
			endif
	        
			if type(m.leftNavigation) = "roAssociativeArray" and type(m.leftNavigation.serialEvent) = "roAssociativeArray" then
				if m.leftNavigation.serialEvent.protocol$ <> "Binary" and m.leftNavigation.serialEvent.port$ = port$ and m.leftNavigation.serialEvent.serial$ = serialEvent$ then
					m.NavigateToMenuItem(m.currentInteractiveMenuItem.leftNavigationIndex%)
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "1")
					return "HANDLED"
				endif
			endif
	        
			if type(m.rightNavigation) = "roAssociativeArray" and type(m.rightNavigation.serialEvent) = "roAssociativeArray" then
				if m.rightNavigation.serialEvent.protocol$ <> "Binary" and m.rightNavigation.serialEvent.port$ = port$ and m.rightNavigation.serialEvent.serial$ = serialEvent$ then
					m.NavigateToMenuItem(m.currentInteractiveMenuItem.rightNavigationIndex%)
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "1")
					return "HANDLED"
				endif
			endif
	        
			if type(m.enterNavigation) = "roAssociativeArray" and type(m.enterNavigation.serialEvent) = "roAssociativeArray" then
				if m.enterNavigation.serialEvent.protocol$ <> "Binary" and m.enterNavigation.serialEvent.port$ = port$ and m.enterNavigation.serialEvent.serial$ = serialEvent$ then
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "1")
					return m.ExecuteInteractiveMenuEnter(stateData)
				endif
			endif
        
        else
        
			if type(m.backNavigation) = "roAssociativeArray" and type(m.backNavigation.serialEvent) = "roAssociativeArray" then
				if m.backNavigation.serialEvent.protocol$ <> "Binary" and m.backNavigation.serialEvent.port$ = port$ and m.backNavigation.serialEvent.serial$ = serialEvent$ then
					m.DrawInteractiveMenu()
					m.ClearPlayingClip()
					m.RestartInteractiveMenuInactivityTimer()			
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "1")
					return "HANDLED"
				endif
			endif
			
			if type(m.nextClipNavigation) = "roAssociativeArray" and type(m.nextClipNavigation.serialEvent) = "roAssociativeArray" then
				if m.nextClipNavigation.serialEvent.protocol$ <> "Binary" and m.nextClipNavigation.serialEvent.port$ = port$ and m.nextClipNavigation.serialEvent.serial$ = serialEvent$ then
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "1")
					return m.NextPrevInteractiveMenuLaunchMedia(stateData, 1)
				endif
			endif
			
			if type(m.previousClipNavigation) = "roAssociativeArray" and type(m.previousClipNavigation.serialEvent) = "roAssociativeArray" then
				if m.previousClipNavigation.serialEvent.protocol$ <> "Binary" and m.previousClipNavigation.serialEvent.port$ = port$ and m.previousClipNavigation.serialEvent.serial$ = serialEvent$ then
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "1")
					return m.NextPrevInteractiveMenuLaunchMedia(stateData, -1)
				endif
			endif
						
		endif			

	else if type(event) = "roIRRemotePress" and type(m.currentInteractiveMenuItem) = "roAssociativeArray" then
	
		remoteEvent% = event.GetInt()
		remoteEvent$ = ConvertToRemoteCommand(remoteEvent%)
		
		retVal = m.HandleInteractiveMenuEventInput(stateData, "remoteEvent$", remoteEvent$, "remote")
		if retVal <> invalid then
			return retVal
		endif

	else if type(event) = "roKeyboardPress" and type(m.currentInteractiveMenuItem) = "roAssociativeArray" then

		keyboardChar$ = chr(event.GetInt())

		' <any> is not supported here - it doesn't make sense.
		
		' if keyboard input is non printable character, convert it to the special code
		keyboardCode$ = m.bsp.GetNonPrintableKeyboardCode(event.GetInt())
		if keyboardCode$ <> "" then
			keyboardChar$ = keyboardCode$
		endif

		retVal = m.HandleInteractiveMenuEventInput(stateData, "keyboardChar$", keyboardChar$, "keyboard")
		if retVal <> invalid then
			return retVal
		endif

	endif

    return m.MediaItemEventHandler(event, stateData)

End Function


Function HandleInteractiveMenuEventInput(stateData As Object, navigationUserEvent As Object, eventData$ As String, eventType$ As String) As Object

	if not m.IsPlayingClip() then

		if type(m.upNavigation) = "roAssociativeArray" and IsString(m.upNavigation.Lookup(navigationUserEvent)) and m.upNavigation.Lookup(navigationUserEvent) = eventData$ then
			m.NavigateToMenuItem(m.currentInteractiveMenuItem.upNavigationIndex%)
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, eventType$, eventData$, "1")
			return "HANDLED"
		endif
			
		if type(m.downNavigation) = "roAssociativeArray" and IsString(m.downNavigation.Lookup(navigationUserEvent)) and m.downNavigation.Lookup(navigationUserEvent) = eventData$ then
			m.NavigateToMenuItem(m.currentInteractiveMenuItem.downNavigationIndex%)
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, eventType$, eventData$, "1")
			return "HANDLED"
		endif
			
		if type(m.leftNavigation) = "roAssociativeArray" and IsString(m.leftNavigation.Lookup(navigationUserEvent)) and m.leftNavigation.Lookup(navigationUserEvent) = eventData$ then
			m.NavigateToMenuItem(m.currentInteractiveMenuItem.leftNavigationIndex%)
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, eventType$, eventData$, "1")
			return "HANDLED"
		endif
			
		if type(m.rightNavigation) = "roAssociativeArray" and IsString(m.rightNavigation.Lookup(navigationUserEvent)) and m.rightNavigation.Lookup(navigationUserEvent) = eventData$ then
			m.NavigateToMenuItem(m.currentInteractiveMenuItem.rightNavigationIndex%)
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, eventType$, eventData$, "1")
			return "HANDLED"
		endif

		if type(m.enterNavigation) = "roAssociativeArray" and IsString(m.enterNavigation.Lookup(navigationUserEvent)) and m.enterNavigation.Lookup(navigationUserEvent) = eventData$ then
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, eventType$, eventData$, "1")
			return m.ExecuteInteractiveMenuEnter(stateData)
		endif
				
	else
		
		if type(m.backNavigation) = "roAssociativeArray" and IsString(m.backNavigation.Lookup(navigationUserEvent)) and m.backNavigation.Lookup(navigationUserEvent) = eventData$ then
			m.DrawInteractiveMenu()
			m.ClearPlayingClip()
			m.RestartInteractiveMenuInactivityTimer()			
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, eventType$, eventData$, "1")
			return "HANDLED"
		endif
			
		if type(m.nextClipNavigation) = "roAssociativeArray" and IsString(m.nextClipNavigation.Lookup(navigationUserEvent)) and m.nextClipNavigation.Lookup(navigationUserEvent) = eventData$ then
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, eventType$, eventData$, "1")
			return m.NextPrevInteractiveMenuLaunchMedia(stateData, 1)
		endif

		if type(m.previousClipNavigation) = "roAssociativeArray" and IsString(m.previousClipNavigation.Lookup(navigationUserEvent)) and m.previousClipNavigation.Lookup(navigationUserEvent) = eventData$ then
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, eventType$, eventData$, "1")
			return m.NextPrevInteractiveMenuLaunchMedia(stateData, -1)
		endif
			
	endif

	return invalid

End Function


Function ConsumeSerialByteInput(stateData As Object, navigation As Object, navigationIndex% As integer, serialStreamInput As Object, port$ As String, command$ As String) As String
			
	if type(navigation) = "roAssociativeArray" and type(navigation.serialEvent) = "roAssociativeArray" then
		if navigation.serialEvent.protocol$ = "Binary" and navigation.serialEvent.port$ = port$ then
			if ByteArraysMatch(serialStreamInput, navigation.serialEvent.inputSpec) then
				serialStreamInput.Clear()
				m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serialBytes", navigation.serialEvent.asciiSpec, "1")
				if command$ = "navigate" then
					m.NavigateToMenuItem(navigationIndex%)
				else if command$ = "enter" then
					return m.ExecuteInteractiveMenuEnter(stateData)
				else if command$ = "back" then
					m.DrawInteractiveMenu()
					m.ClearPlayingClip()
					m.RestartInteractiveMenuInactivityTimer()			
				else if command$ = "nextClip" then
					return m.NextPrevInteractiveMenuLaunchMedia(stateData, 1)
				else if command$ = "previousClip" then
					return m.NextPrevInteractiveMenuLaunchMedia(stateData, -1)
				endif
				return "HANDLED"
			endif
		endif
	endif

	return ""
	
End Function


Function ExecuteInteractiveMenuEnter(stateData As Object) As String

	if type(m.inactivityTimer) = "roTimer" then
		m.inactivityTimer.Stop()
	endif

	' execute commands
	if type(m.currentInteractiveMenuItem.enterCmds) = "roArray" then

		for each cmd in m.currentInteractiveMenuItem.enterCmds

			if cmd.name$ = "switchPresentation" then
				presentationName$ = cmd.parameters["presentationName"].GetCurrentParameterValue()
				switchToNewPresentation = m.bsp.ExecuteSwitchPresentationCommand(presentationName$)
				if switchToNewPresentation then
					return "HANDLED"
				endif
			endif

			m.bsp.ExecuteCmd(m.stateMachine, cmd.name$, cmd.parameters)

		next

	endif
	
	launchedPlayback = false
	if m.currentInteractiveMenuItem.targetType$ = "mediaFile" then
		if IsString(m.currentInteractiveMenuItem.targetVideoFile$) and m.currentInteractiveMenuItem.targetVideoFile$ <> "" then
			m.LaunchInteractiveMenuVideoClip(m.currentInteractiveMenuItem, m.currentInteractiveMenuItem.targetVideoFileUserVariable)
			launchedPlayback = true
		else if IsString(m.currentInteractiveMenuItem.targetAudioFile$) and m.currentInteractiveMenuItem.targetAudioFile$ <> "" then
			m.LaunchInteractiveMenuAudioClip(m.currentInteractiveMenuItem, m.currentInteractiveMenuItem.targetAudioFileUserVariable)
			launchedPlayback = true
		else if IsString(m.currentInteractiveMenuItem.targetImageFile$) and m.currentInteractiveMenuItem.targetImageFile$ <> "" then
			file$ = m.currentInteractiveMenuItem.targetImageFile$
			m.DisplayInteractiveMenuImage(file$, m.currentInteractiveMenuItem.targetImageFileUserVariable, m.currentInteractiveMenuItem.targetImageFileTimeout%, m.currentInteractiveMenuItem.targetImageFileUseImageBuffer, m.currentInteractiveMenuItem)
			launchedPlayback = true
		endif
	else if m.currentInteractiveMenuItem.targetType$ = "mediaState" then
	    stateData.nextState = m.stateMachine.stateTable[m.currentInteractiveMenuItem.targetMediaState$]
		if stateData.nextState.type$ = "superState" and stateData.nextState.firstState <> invalid then
			targetMediaState$ = stateData.nextState.firstState.id$
			stateData.nextState = m.stateMachine.stateTable[targetMediaState$]
		endif
		m.UpdatePreviousCurrentStateNames()
	    return "TRANSITION"
	else if m.currentInteractiveMenuItem.targetType$ = "previousState" then
	    stateData.nextState = m.stateMachine.stateTable[m.stateMachine.previousStateName$]
		m.UpdatePreviousCurrentStateNames()
	    return "TRANSITION"
	else ' currentState
	endif
	
	if not launchedPlayback and type(m.inactivityTimer) = "roTimer" then
		m.RestartInteractiveMenuInactivityTimer()
	endif
		
	return "HANDLED"
				
End Function


Function NextPrevInteractiveMenuLaunchMedia(stateData As Object, incrementValue% As Integer) As String
	
	while true
	
		m.currentInteractiveMenuNavigationIndex% = m.currentInteractiveMenuNavigationIndex% + incrementValue%
		if incrementValue% > 0 then
			if m.currentInteractiveMenuNavigationIndex% >= m.interactiveMenuItems.Count() then
				m.currentInteractiveMenuNavigationIndex% = 0
			endif
		else
			if m.currentInteractiveMenuNavigationIndex% < 0 then
				m.currentInteractiveMenuNavigationIndex% = m.interactiveMenuItems.Count() - 1
			endif
		endif
		
		m.lastInteractiveMenuNavigationIndex% = m.currentInteractiveMenuNavigationIndex%
		m.currentInteractiveMenuItem = m.interactiveMenuItems[m.currentInteractiveMenuNavigationIndex%]
		
		' execute commands
		if type(m.currentInteractiveMenuItem.enterCmds) = "roArray" then
			for each cmd in m.currentInteractiveMenuItem.enterCmds
				m.bsp.ExecuteCmd(m.stateMachine, cmd.name$, cmd.parameters)
			next
		endif
		
		if m.currentInteractiveMenuItem.targetType$ = "mediaFile" then
			if IsString(m.currentInteractiveMenuItem.targetVideoFile$) and m.currentInteractiveMenuItem.targetVideoFile$ <> "" then
				m.LaunchInteractiveMenuVideoClip(m.currentInteractiveMenuItem, m.currentInteractiveMenuItem.targetVideoFileUserVariable)
				return "HANDLED"
			else if IsString(m.currentInteractiveMenuItem.targetAudioFile$) and m.currentInteractiveMenuItem.targetAudioFile$ <> "" then
				m.LaunchInteractiveMenuAudioClip(m.currentInteractiveMenuItem, m.currentInteractiveMenuItem.targetAudioFileUserVariable)
				return "HANDLED"
			else if IsString(m.currentInteractiveMenuItem.targetImageFile$) and m.currentInteractiveMenuItem.targetImageFile$ <> "" then
				m.DisplayInteractiveMenuImage(m.currentInteractiveMenuItem.targetImageFile$, m.currentInteractiveMenuItem.targetImageFileUserVariable, m.currentInteractiveMenuItem.targetImageFileTimeout%, m.currentInteractiveMenuItem.targetImageFileUseImageBuffer, m.currentInteractiveMenuItem)
				return "HANDLED"
			endif
		else if m.currentInteractiveMenuItem.targetType$ = "mediaState" then
		    stateData.nextState = m.stateMachine.stateTable[m.currentInteractiveMenuItem.targetMediaState$]
			m.UpdatePreviousCurrentStateNames()
		    return "TRANSITION"
		else if m.currentInteractiveMenuItem.targetType$ = "previousState" then
		    stateData.nextState = m.stateMachine.stateTable[m.stateMachine.previousStateName$]
			m.UpdatePreviousCurrentStateNames()
			return "TRANSITION"
		else ' currentState
		endif
		
	end while

End Function


Sub LaunchInteractiveMenuVideoClip(interactiveMenuItem As Object, userVariable As Object)
	m.stateMachine.videoPlayer.SetLoopMode(0)
	m.stateMachine.videoPlayer.EnableSafeRegionTrimming(false)
	m.stateMachine.videoPlayer.StopClear()
'	m.stateMachine.audioPlayer.Stop()
	file$ = interactiveMenuItem.targetVideoFile$
	filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, file$)

	aa = { }
	aa.AddReplace("Filename", filePath$)
		
	if type(interactiveMenuItem.probeData) = "roString" then
		m.stateMachine.bsp.diagnostics.PrintDebug("LaunchInteractiveMenuVideoClip: probeData = " + interactiveMenuItem.probeData)
		aa.AddReplace("ProbeString", interactiveMenuItem.probeData)
	endif

	if ItemIsEncrypted(interactiveMenuItem) then
		aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
		aa.AddReplace("EncryptionKey", file$)
	endif

	if m.stateMachine.mosaicDecoderName <> "" then
		aa.Decoder = m.stateMachine.mosaicDecoderName
	endif

	ok = m.stateMachine.videoPlayer.PlayFile(aa)
	if ok = 0 then stop

	m.stateMachine.ClearImagePlane()
	
	if type(userVariable) = "roAssociativeArray" then
		userVariable.Increment()
	endif

	m.playingVideoClip = true
	m.playingAudioClip = false
	m.displayingImage = false
End Sub


Sub LaunchInteractiveMenuAudioClip(interactiveMenuItem As Object, userVariable As Object)

	if type(m.stateMachine.audioPlayer) = "roAudioPlayer" then
		player = m.stateMachine.audioPlayer
		m.stateMachine.audioPlayer.SetLoopMode(0)
		m.stateMachine.audioPlayer.Stop()
	else
		player = m.stateMachine.videoPlayer
		m.stateMachine.videoPlayer.StopClear()
	endif

	file$ = interactiveMenuItem.targetAudioFile$
	filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, file$)

	aa = { }
	aa.AddReplace("Filename", filePath$)
		
	if type(interactiveMenuItem.probeData) = "roString" then
		m.stateMachine.bsp.diagnostics.PrintDebug("LaunchInteractiveMenuAudioClip: probeData = " + interactiveMenuItem.probeData)
		aa.AddReplace("ProbeString", interactiveMenuItem.probeData)
	endif

	ok = player.PlayFile(aa)
	if ok = 0 then stop
		
	m.stateMachine.ClearImagePlane()
	
	if type(userVariable) = "roAssociativeArray" then
		userVariable.Increment()
	endif

	m.playingAudioClip = true
	m.playingVideoClip = false
	m.displayingImage = false
End Sub


Sub DisplayInteractiveMenuImage(file$ As String, userVariable As Object, timeout% As Integer, useImageBuffer As Boolean, interactiveMenuItem As Object)
'	m.stateMachine.audioPlayer.Stop()
	m.stateMachine.videoPlayer.StopClear()
	filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, file$)

	if useImageBuffer then
		m.bsp.diagnostics.PrintDebug("Use imageBuffer for " + file$ + " in DisplayInteractiveMenuImage.")
		imageBuffer = m.bsp.imageBuffers.Lookup(filePath$)
		m.stateMachine.imagePlayer.DisplayBuffer(imageBuffer, 0, 0)
	else
		aa = {}
		aa.filename = filePath$
		if ItemIsEncrypted(interactiveMenuItem) then
			aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
			aa.AddReplace("EncryptionKey", file$)
		endif
		ok = m.stateMachine.imagePlayer.DisplayFile(aa)
	endif

	m.stateMachine.ShowImageWidget()
	
	m.playingVideoClip = false
	m.playingAudioClip = false
	m.displayingImage = true

	if type(m.imageFileTimeoutTimer) = "roTimer" then
		m.imageFileTimeoutTimer.Stop()
	else
		m.imageFileTimeoutTimer = CreateObject("roTimer")
		m.imageFileTimeoutTimer.SetPort(m.bsp.msgPort)
	endif
	
	m.imageFileTimeoutTimer.SetElapsed(timeout%, 0)
	m.imageFileTimeoutTimer.Start()
	
	if type(userVariable) = "roAssociativeArray" then
		userVariable.Increment()
	endif
		
End Sub


Sub DrawInteractiveMenu()

	' execute entry commands any time the interactive menu is displayed
	m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)

	' display background image if it exists 
	if IsString(m.backgroundImage$) then
		file$ = m.backgroundImage$
		if file$ <> "" then
			filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, file$)
			m.stateMachine.videoPlayer.StopClear()
		    if m.backgroundImageUseImageBuffer and type(m.bsp.imageBuffers) = "roAssociativeArray" and m.bsp.imageBuffers.DoesExist(filePath$) then
				m.bsp.diagnostics.PrintDebug("Use imageBuffer for " + file$ + " in DrawInteractiveMenu.")
				imageBuffer = m.bsp.imageBuffers.Lookup(filePath$)
				m.stateMachine.imagePlayer.DisplayBuffer(imageBuffer, 0, 0)
			else
				m.bsp.diagnostics.PrintDebug("DisplayFile in STInteractiveMenuEventHandler: " + file$)

				isEncrypted = m.bsp.encryptionByFile.DoesExist(file$)
				'isEncrypted = m.bsp.contentEncrypted

				aa = {}
				aa.filename = filePath$
				if isEncrypted then
					aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
					aa.AddReplace("EncryptionKey", file$)
				endif
				ok = m.stateMachine.imagePlayer.DisplayFile(aa)
				if not ok then
					m.bsp.diagnostics.PrintDebug("Error preloading file in DrawInteractiveMenu: " + file$ + ", " + filePath$)
				else
					m.bsp.diagnostics.PrintDebug("Displayed file in DisplayImage: " + file$)
				endif
			endif 

			m.stateMachine.ShowImageWidget()

		else
			m.stateMachine.ClearImagePlane()
		endif
	endif
	
	' draw backgrounds
	for each interactiveMenuItem in m.interactiveMenuItems
		m.DisplayNavigationOverlay(interactiveMenuItem.unselectedImage$, interactiveMenuItem, interactiveMenuItem.unselectedImageUseImageBuffer)
	next
	
	' draw foreground for first menu item
	if type(m.currentInteractiveMenuItem) = "roAssociativeArray" then
		m.DisplayNavigationOverlay(m.currentInteractiveMenuItem.selectedImage$, m.currentInteractiveMenuItem, m.currentInteractiveMenuItem.selectedImageUseImageBuffer)
	endif

	if IsString(m.backgroundImage$) and m.backgroundImage$ <> "" and type(m.backgroundImageUserVariable) = "roAssociativeArray" then
		m.backgroundImageUserVariable.Increment()
	endif
				
End Sub


Sub NavigateToMenuItem(navigationIndex% As Integer)

	m.RestartInteractiveMenuInactivityTimer()

	if navigationIndex% >= 0 then
		m.DisplayNavigationOverlay(m.currentInteractiveMenuItem.unselectedImage$, m.currentInteractiveMenuItem, m.currentInteractiveMenuItem.unselectedImageUseImageBuffer)
		m.currentInteractiveMenuNavigationIndex% = navigationIndex%
		m.lastInteractiveMenuNavigationIndex% = m.currentInteractiveMenuNavigationIndex%
		m.currentInteractiveMenuItem = m.interactiveMenuItems[navigationIndex%]
		m.DisplayNavigationOverlay(m.currentInteractiveMenuItem.selectedImage$, m.currentInteractiveMenuItem, m.currentInteractiveMenuItem.selectedImageUseImageBuffer)
	endif

End Sub


Sub RestartInteractiveMenuInactivityTimer()

	if type(m.inactivityTimer) = "roTimer" then
		m.inactivityTimer.Stop()
		m.inactivityTimer.SetElapsed(0, m.mstimeoutValue%)
		m.inactivityTimer.Start()
	endif

End Sub


Sub DisplayNavigationOverlay(fileName$ As String, interactiveMenuItem As Object, useImageBuffer As Boolean)

	if IsString(fileName$) and fileName$ <> "" then
	
		filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, fileName$)
		
		if useImageBuffer then
			m.bsp.diagnostics.PrintDebug("Use imageBuffer for " + fileName$ + " in DisplayNavigationOverlay.")
			imageBuffer = m.bsp.imageBuffers.Lookup(filePath$)
			m.stateMachine.imagePlayer.DisplayBuffer(imageBuffer, interactiveMenuItem.x%,interactiveMenuItem.y%)
		else
			m.bsp.diagnostics.PrintDebug("Overlay image in DisplayNavigationOverlay: " + fileName$)

			if type(m.bsp.encryptionByFile) = "roAssociativeArray" and m.bsp.encryptionByFile.DoesExist(fileName$) and m.bsp.contentEncryptionSupported then
			'if m.bsp.contentEncrypted then
				aa = {}
				aa.filename = filePath$
				aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
				aa.AddReplace("EncryptionKey", fileName$)
				ok = m.stateMachine.imagePlayer.OverlayImage(aa, interactiveMenuItem.x%, interactiveMenuItem.y%)
			else
				ok = m.stateMachine.imagePlayer.OverlayImage(filePath$, interactiveMenuItem.x%, interactiveMenuItem.y%)
			endif

			if ok = 0 then
				m.bsp.diagnostics.PrintDebug("Error in OverlayImage in DisplayNavigationOverlay: " + fileName$ + ", " + filePath$)
			endif

		endif

		m.stateMachine.ShowImageWidget()

	endif
	
End Sub


Sub ScaleBackgroundImageToFit( backgroundImage As Object )

    xScale = m.backgroundImageWidth% / m.stateMachine.width%
    yScale = m.backgroundImageHeight% / m.stateMachine.height%

    if xScale > yScale then
        x% = 0
        y% = (m.stateMachine.height% - (m.backgroundImageHeight% / xScale)) / 2

        width% = m.backgroundImageWidth% / xScale
        height% = m.backgroundImageHeight% / xScale
    else
        x% = (m.stateMachine.width% - (m.backgroundImageWidth% / yScale)) / 2
        y% = 0

        width% = m.backgroundImageWidth% / yScale
        height% = m.backgroundImageHeight% / yScale
	endif

	backgroundImage["targetRect"] = { x: x%,  y: y%,  w: width%, h: height% }

End Sub


Sub SetBackgroundImageSizeLocation( backgroundImage As Object )

	if m.stateMachine.imageMode% = 0			' center image
        if m.backgroundImageWidth% > m.stateMachine.width% or m.backgroundImageHeight% > m.stateMachine.height% then
            m.ScaleBackgroundImageToFit(backgroundImage)
        else
			x% = (m.stateMachine.width% - m.backgroundImageWidth%) / 2
			y% = (m.stateMachine.height% - m.backgroundImageHeight%) / 2
			backgroundImage["targetRect"] = { x: x%,  y: y%,  w: m.backgroundImageWidth%, h: m.backgroundImageHeight% }
		endif
	else if m.stateMachine.imageMode% = 1		' scale to fit
        m.ScaleBackgroundImageToFit(backgroundImage)
	else if m.stateMachine.imageMode% = 2		' scale to fill and crop
        m.ScaleBackgroundImageToFit(backgroundImage)
	else if m.stateMachine.imageMode%			' scale to fill
		backgroundImage["targetRect"] = { x: 0,  y: 0,  w: m.stateMachine.width%, h: m.stateMachine.height% }
	endif

End Sub


Function TemplateUsesAnyUserVariable() As Boolean

	for each templateItem in m.templateItems
		if templateItem.type$ = "userVariableTemplateItem" then
			return true
		endif
	next

End Function


Function TemplateUsesUserVariable(userVariable As Object) As Boolean

	for each templateItem in m.templateItems
		if templateItem.type$ = "userVariableTemplateItem" then
			if type(templateItem.userVariable) = "roAssociativeArray" then
				if templateItem.userVariable.name$ = userVariable.name$ then
					return true
				endif
			endif
		endif
	next

End Function


Function TemplateUsesSystemVariable() As Boolean

	for each templateItem in m.templateItems
		if templateItem.type$ = "systemVariableTextTemplateItem"
			return true
		endif
	next

End Function


Sub BuildBaseTemplateItem(templateItem As Object, content As Object)

	content["targetRect"] = { x: templateItem.x%,  y: templateItem.y%,  w: templateItem.width%, h: templateItem.height%  }

End Sub


Sub BuildTextTemplateItem(templateItem As Object, content As Object)

	BuildBaseTemplateItem(templateItem, content)

	textAttrs = { }
	textAttrs.color = templateItem.foregroundTextColor$
			
	textAttrs.fontSize = templateItem.fontSize%
			
	if templateItem.font$ <> "System" then
		textAttrs.fontFile = GetPoolFilePath(m.bsp.assetPoolFiles, templateItem.font$)
	endif

	textAttrs.vAlign = "Top"
	textAttrs.hAlign = templateItem.alignment$
	textAttrs.rotation = templateItem.rotation$
						
	content.textAttrs = textAttrs

End Sub


Sub ClearTemplateItems()

	m.stateMachine.canvasWidget.EnableAutoRedraw(0)
	numLayers% = m.stateMachine.templateObjectsByLayer.Count()
	for i% = 0 to numLayers% - 1
	    m.stateMachine.canvasWidget.ClearLayer(i% + 1)
    next
	m.stateMachine.canvasWidget.EnableAutoRedraw(1)

End Sub


Sub RedisplayTemplateItems()

	m.BuildTemplateItems()

	m.stateMachine.canvasWidget.EnableAutoRedraw(0)

	numLayers% = m.stateMachine.templateObjectsByLayer.Count()
	for i% = 0 to numLayers% - 1
		if type(m.stateMachine.templateObjectsByLayer[i%]) = "roArray" then

			templateObjects = m.stateMachine.templateObjectsByLayer[i%]

			' must support multiple objects per layer??!!
			templateObject = templateObjects[0]

			if type(templateObject) = "roAssociativeArray" and templateObject.DoesExist("name") then
				name$ = templateObject["name"]
				if type(m.bsp.encryptionByFile) = "roAssociativeArray" and m.bsp.encryptionByFile.DoesExist(name$) then
					templateObject.EncryptionAlgorithm = "AesCtrHmac"
					templateObject.EncryptionKey = name$
				endif
			endif

			if m.bsp.contentEncrypted and type(templateObject) = "roAssociativeArray" and templateObject.DoesExist("fileNameForEncryption") then
				name$ = templateObject["fileNameForEncryption"]
				if name$ <> "" and type(m.currentFeed) = "roAssociativeArray" and type(m.currentFeed.liveDataFeed) = "roAssociativeArray" then
					if (m.currentFeed.liveDataFeed.isDynamicPlaylist or m.currentFeed.liveDataFeed.isLiveMediaFeed) then
						templateObject.EncryptionAlgorithm = "AesCtrHmac"
						templateObject.EncryptionKey = name$
					endif
				endif
			endif

			m.stateMachine.canvasWidget.SetLayer(templateObject, i% + 1)
		else
			m.stateMachine.canvasWidget.ClearLayer(i% + 1)
		endif
	next

	for i% = numLayers% to numLayers% + 2
		m.stateMachine.canvasWidget.ClearLayer(i% + 1)
	next

	m.stateMachine.canvasWidget.EnableAutoRedraw(1)

End Sub


Sub BuildTemplateItems()

	m.stateMachine.templateObjectsByLayer = CreateObject("roArray", 1, true)

	for each templateItem in m.templateItems

		text = invalid
		image = invalid

		backgroundLayer% = (templateItem.layer% - 1) * 2 + 1
		contentLayer% = backgroundLayer% + 1

		if templateItem.type$ = "constantTextTemplateItem" then

			text = { }
			text["text"] = templateItem.textString$
			m.BuildTextTemplateItem(templateItem, text)
		
		else if templateItem.type$ = "systemVariableTextTemplateItem" then

			text = { }

			if templateItem.systemVariableType$ = "SerialNumber" then
				text["text"] = m.bsp.sysInfo.deviceUniqueID$
			else if templateItem.systemVariableType$ = "IPAddressWired" then
				text["text"] = m.bsp.sysInfo.ipAddressWired$
			else if templateItem.systemVariableType$ = "IPAddressWireless" then
				text["text"] = m.bsp.sysInfo.ipAddressWireless$
			else if templateItem.systemVariableType$ = "FirmwareVersion" then
				text["text"] = m.bsp.sysInfo.deviceFWVersion$
			else if templateItem.systemVariableType$ = "ScriptVersion" then
				text["text"] = m.bsp.sysInfo.autorunVersion$
			else if templateItem.systemVariableType$ = "RFChannelCount" then
				text["text"] = StripLeadingSpaces(stri(m.bsp.scannedChannels.Count()))
			else if templateItem.systemVariableType$ = "RFChannelName" then
				text["text"] = StripLeadingSpaces(m.bsp.rfChannelName)
			else if templateItem.systemVariableType$ = "RFVirtualChannel" then
				text["text"] = StripLeadingSpaces(m.bsp.rfVirtualChannel)
			else if templateItem.systemVariableType$ = "TunerScanPercentageComplete" then
				text["text"] = m.bsp.tunerScanPercentageComplete$
			else if templateItem.systemVariableType$ = "EdidMonitorSerialNumber" then
				text["text"] = m.bsp.sysInfo.edidMonitorSerialNumber$
			else if templateItem.systemVariableType$ = "EdidYearOfManufacture" then
				text["text"] = m.bsp.sysInfo.edidYearOfManufacture$
			else if templateItem.systemVariableType$ = "EdidMonitorName" then
				text["text"] = m.bsp.sysInfo.edidMonitorName$
			else if templateItem.systemVariableType$ = "EdidManufacturer" then
				text["text"] = m.bsp.sysInfo.edidManufacturer$
			else if templateItem.systemVariableType$ = "EdidUnspecifiedText" then
				text["text"] = m.bsp.sysInfo.edidUnspecifiedText$
			else if templateItem.systemVariableType$ = "EdidSerialNumber" then
				text["text"] = m.bsp.sysInfo.edidSerialNumber$
			else if templateItem.systemVariableType$ = "EdidManufacturerProductCode" then
				text["text"] = m.bsp.sysInfo.edidManufacturerProductCode$
			else if templateItem.systemVariableType$ = "EdidWeekOfManufacture" then
				text["text"] = m.bsp.sysInfo.edidWeekOfManufacture$
			else if templateItem.systemVariableType$ = "ActivePresentation" then
				if IsString(m.bsp.activePresentation$) then
					text["text"] = m.bsp.activePresentation$
				else
					text["text"] = ""
				endif
			else if templateItem.systemVariableType$ = "BrightAuthorVersion" then
				text["text"] = m.bsp.sysInfo.baVersion$
			endif

			m.BuildTextTemplateItem(templateItem, text)

		else if templateItem.type$ = "mediaCounterTemplateItem" or templateItem.type$ = "userVariableTemplateItem" then

			if type(templateItem.userVariable) = "roAssociativeArray" then
				text = { }
				text["text"] = templateItem.userVariable.GetCurrentValue()
				m.BuildTextTemplateItem(templateItem, text)
			endif

		else if templateItem.type$ = "indexedLiveTextDataEntryTemplateItem" or templateItem.type$ = "titledLiveTextDataEntryTemplateItem" then

			liveDataFeed = templateItem.liveDataFeed
			if m.liveDataFeeds.DoesExist(liveDataFeed.name$) then
				if templateItem.type$ = "indexedLiveTextDataEntryTemplateItem" then
					indexStr$ = templateItem.index.GetCurrentParameterValue()
					index% = int(val(indexStr$))
					if index% > 0 then
						index% = index% - 1
					endif
					if type(liveDataFeed.articles) = "roArray" then
						if index% <= (liveDataFeed.articles.count() - 1) then
							textValue$ = liveDataFeed.articles[index%]
						else
							textValue$ = ""
						endif

						text = { }
						text["text"] = textValue$
						m.BuildTextTemplateItem(templateItem, text)						
					endif
				else
					title$ = templateItem.title.GetCurrentParameterValue()
					if type(liveDataFeed.articlesByTitle) = "roAssociativeArray" then
						if liveDataFeed.articlesByTitle.DoesExist(title$) then
							textValue$ = liveDataFeed.articlesByTitle.Lookup(title$)
						else
							textValue$ = ""
						endif

						text = { }
						text["text"] = textValue$
						m.BuildTextTemplateItem(templateItem, text)					
					endif
				endif
			endif

		else if templateItem.type$ = "imageTemplateItem" then

			image = { }
			
			image["name"] = templateItem.fileName$
			image["filename"] = GetPoolFilePath(m.bsp.assetPoolFiles, templateItem.fileName$)
			image["CompositionMode"] = "source_over"

			' TemplateToDo - do the images care about the stretch/crop mode?
			' m.SetBackgroundImageSizeLocation(backgroundImage)
			BuildBaseTemplateItem(templateItem, image)

		else if templateItem.type$ = "mrssTextTemplateItem" then

			if type(templateItem.textString$) <> "Invalid" then
				text = { }
				text["text"] = templateItem.textString$
				m.BuildTextTemplateItem(templateItem, text)
			endif

		else if templateItem.type$ = "mrssImageTemplateItem" or templateItem.type$ = "mrssMediaTemplateItem" then

			if type(templateItem.fileName$) <> "Invalid" and templateItem.fileName$ <> "" then
				image = { }
				
				if type(templateItem.fileNameForEncryption) = "roString" then
					image["fileNameForEncryption"] = templateItem.fileNameForEncryption
				else
					image["fileNameForEncryption"] = ""
				endif

				image["filename"] = templateItem.fileName$
				image["CompositionMode"] = "source-over"

				' TemplateToDo - do the images care about the stretch/crop mode?
				' m.SetBackgroundImageSizeLocation(backgroundImage)
				BuildBaseTemplateItem(templateItem, image)
			endif

		endif

		m.BuildTemplateItem(text, image, templateItem)

	next

' now add any simple rss items
	if type(m.simpleRSSTextTemplateItems) = "roAssociativeArray" then
		for each simpleRSSId in m.simpleRSSTextTemplateItems
			simpleRSS = m.simpleRSSTextTemplateItems.Lookup(simpleRSSId)
			if type(simpleRSS) = "roAssociativeArray" then
				liveDataFeed = simpleRSS.rssLiveDataFeeds[simpleRSS.currentLiveDataFeedIndex%]
				if m.liveDataFeeds.DoesExist(liveDataFeed.name$) then
					if type(liveDataFeed.articles) = "roArray" then
						if simpleRSS.currentIndex% >= liveDataFeed.articles.count() then
							simpleRSS.currentIndex% = 0
						endif
						index% = simpleRSS.currentIndex%

						' remove the next conditional - it's not needed
						if index% <= (liveDataFeed.articles.count() - 1) then
							for each templateItem in simpleRSS.items
								if templateItem.elementName$ = "title" then
									textValue$ = liveDataFeed.articleTitles[index%]
								else
									textValue$ = liveDataFeed.articles[index%]
								endif
							
								text = { }
								text["text"] = textValue$
								m.BuildTextTemplateItem(templateItem, text)

								m.BuildTemplateItem(text, image, templateItem)
							next

							' first time display - start timer to display next item
							if type(simpleRSS.rssItemTimer) <> "roTimer" then
								simpleRSS.rssItemTimer = CreateObject("roTimer")
							    simpleRSS.rssItemTimer.SetPort(m.stateMachine.msgPort)
								simpleRSS.rssItemTimer.SetElapsed(simpleRSS.displayTime%, 0)
								simpleRSS.rssItemTimer.Start()
							endif
						endif
					endif
				endif
			endif
		next
	endif

End Sub


Sub BuildTemplateItem(text As Object, image As Object, templateItem As Object)

	if type(text) = "roAssociativeArray" or type(image) = "roAssociativeArray" then

		backgroundLayer% = (templateItem.layer% - 1) * 2 + 1
		contentLayer% = backgroundLayer% + 1

		if type(m.stateMachine.templateObjectsByLayer[contentLayer%]) <> "roArray" then
			m.stateMachine.templateObjectsByLayer[contentLayer%] = CreateObject("roArray", 1, true)
		endif

		if type(text) = "roAssociativeArray" then

			m.stateMachine.templateObjectsByLayer[contentLayer%].push(text)

			if templateItem.backgroundColorSpecified then
			
				backgroundColor = { }
				backgroundColor["color"] = templateItem.backgroundTextColor$
				backgroundColor["targetRect"] = { x: templateItem.x%,  y: templateItem.y%,  w: templateItem.width%, h: templateItem.height%  }

				if type(m.stateMachine.templateObjectsByLayer[backgroundLayer%]) <> "roArray" then
					m.stateMachine.templateObjectsByLayer[backgroundLayer%] = CreateObject("roArray", 1, true)
				endif

				m.stateMachine.templateObjectsByLayer[backgroundLayer%].push(backgroundColor)
			
			endif

		else

			m.stateMachine.templateObjectsByLayer[contentLayer%].push(image)

		endif

	endif

End Sub


Sub SetupTemplateMRSS()

	if type(m.mrssTitleTemplateItem) = "roAssociativeArray" then
		m.mrssTitleTemplateItem.textString$ = invalid
	endif

	if type(m.mrssDescriptionTemplateItem) = "roAssociativeArray" then
		m.mrssDescriptionTemplateItem.textString$ = invalid
	endif

	if type(m.mrssImageTemplateItem) = "roAssociativeArray" or type(m.mrssMediaTemplateItem) = "roAssociativeArray" then
		m.mrssMediaTemplateItem.fileName$ = invalid
	endif

	if type(m.mrssCustomFieldTemplateItems) = "roAssociativeArray" then
		for each customFieldName in m.mrssCustomFieldTemplateItems
			customFieldTemplateItem = m.mrssCustomFieldTemplateItems[customFieldName]
			customFieldTemplateItem.textString$ = invalid
		next
	endif

End Sub


Function STTemplatePlayingEventHandler(event As Object, stateData As Object) As Object

    MEDIA_END = 8
    
	stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				if m.mrssActive then

					m.SetupTemplateMRSS()

					m.currentFeed = invalid
					m.pendingFeed = invalid

					m.mrssItemTimer = CreateObject("roTimer")
					m.mrssItemTimer.SetPort(m.stateMachine.msgPort)

					if type(m.mrssLiveDataFeeds) = "roArray" and m.mrssLiveDataFeeds.Count() > 0 then
						m.mrssLiveDataFeed = invalid
						m.currentFeed = invalid
						m.LaunchWaitForContentTimer()
					endif

				endif

				' reset indices on entry to the state
				if type(m.simpleRSSTextTemplateItems) = "roAssociativeArray" then
					for each simpleRSSId in m.simpleRSSTextTemplateItems
						simpleRSS = m.simpleRSSTextTemplateItems.Lookup(simpleRSSId)
						if type(simpleRSS) = "roAssociativeArray" then
							simpleRSS.currentIndex% = 0
							simpleRSS.currentLiveDataFeedIndex% = 0
							simpleRSS.rssItemTimer = invalid
						endif
					next
				endif

				m.liveDataFeeds = CreateObject("roAssociativeArray")
				for each liveDataFeedName in m.bsp.liveDataFeeds
					liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedName)
					m.liveDataFeeds.AddReplace(liveDataFeedName, liveDataFeed)
				next

				m.ConfigureBPButtons()
				m.ConfigureGPIOButtons()
	
				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
    
				if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
					m.stateMachine.videoPlayer.StopClear()
				endif

				if type(m.stateMachine.canvasWidget) <> "roCanvasWidget" then
					r = CreateObject("roRectangle", m.stateMachine.x%, m.stateMachine.y%, m.stateMachine.width%, m.stateMachine.height%)
					m.stateMachine.canvasWidget = CreateObject("roCanvasWidget", r)
				endif

				m.stateMachine.canvasWidget.EnableAutoRedraw(0)

				maxLayer% = 1
				if type(m.stateMachine.templateObjectsByLayer) = "roArray" then
					maxLayer% = m.stateMachine.templateObjectsByLayer.Count()
				endif

				for i% = 1 to maxLayer%
					m.stateMachine.canvasWidget.ClearLayer(i%)
				next

				' display background image if it exists 
				file$ = m.backgroundImage$
				if file$ <> "" then
					backgroundImage = {}
					backgroundImage["name"] = file$
					backgroundImage["filename"] = GetPoolFilePath(m.bsp.assetPoolFiles, file$)
					backgroundImage["CompositionMode"] = "source"

					if m.backgroundImageWidth% <= 0 or m.backgroundImageHeight% <= 0 then
						backgroundImage["targetRect"] = { x: 0,  y: 0,  w: m.stateMachine.width%, h: m.stateMachine.height% }
					else
						m.SetBackgroundImageSizeLocation(backgroundImage)
					endif

					if backgroundImage.DoesExist("name") then
						name$ = backgroundImage["name"]
						if type(m.bsp.encryptionByFile) = "roAssociativeArray" and m.bsp.encryptionByFile.DoesExist(name$) then
						'if m.bsp.contentEncrypted then
							backgroundImage.EncryptionAlgorithm = "AesCtrHmac"
							backgroundImage.EncryptionKey = name$
						endif
					endif

					m.stateMachine.canvasWidget.SetLayer(backgroundImage, 0)
				
				else
				
					m.stateMachine.canvasWidget.ClearLayer(0)
				
				endif

				' build arrays of template items & background colors
				m.BuildTemplateItems()

				numLayers% = m.stateMachine.templateObjectsByLayer.Count()

				for i% = 0 to numLayers% - 1
					if type(m.stateMachine.templateObjectsByLayer[i%]) = "roArray" then

						templateObjects = m.stateMachine.templateObjectsByLayer[i%]

						' must support multiple objects per layer??!!
						templateObject = templateObjects[0]

						if type(templateObject) = "roAssociativeArray" and templateObject.DoesExist("name") then
							name$ = templateObject["name"]
							if type(m.bsp.encryptionByFile) = "roAssociativeArray" and m.bsp.encryptionByFile.DoesExist(name$) then
								templateObject.EncryptionAlgorithm = "AesCtrHmac"
								templateObject.EncryptionKey = name$
							endif
						endif

						m.stateMachine.canvasWidget.SetLayer(templateObject, i% + 1)
					endif
				next

				m.stateMachine.canvasWidget.EnableAutoRedraw(1)

				m.stateMachine.ShowCanvasWidget()

				m.LaunchTimer()    

		        m.bsp.SetTouchRegions(m)

				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "liveText")

				' playback logging
				m.stateMachine.LogPlayStart("liveText", "")

                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")

				' resize the video player in case it was used by an MRSS feed within this state
				if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
					m.stateMachine.videoPlayer.StopClear()
					m.stateMachine.videoPlayer.SetRectangle(m.stateMachine.rectangle)
				endif

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

            else if event["EventType"] = "USER_VARIABLES_UPDATED" then

				if m.TemplateUsesAnyUserVariable() then
					m.RedisplayTemplateItems()
				endif

				return "HANDLED"

            else if event["EventType"] = "SYSTEM_VARIABLE_UPDATED" then

				if m.TemplateUsesSystemVariable() then
					m.RedisplayTemplateItems()
				endif

				return "HANDLED"

            else if event["EventType"] = "USER_VARIABLE_CHANGE" then

				userVariable =  event["UserVariable"]

				if m.TemplateUsesUserVariable(userVariable) then
					m.RedisplayTemplateItems()
				endif

				return "HANDLED"
				
			else if event["EventType"] = "USER_VARIABLES_RESET" then

				m.RedisplayTemplateItems()

				return "HANDLED"

            else if event["EventType"] = "LIVE_DATA_FEED_UPDATE" then

				liveDataFeed = event["EventData"]

				m.liveDataFeeds.AddReplace(liveDataFeed.name$, liveDataFeed)

				m.RedisplayTemplateItems()

				return "HANDLED"

            else
            
		        return m.MediaItemEventHandler(event, stateData)

			endif

		endif

	else if type(event) = "roVideoEvent" and type(m.stateMachine.videoPlayer) = "roVideoPlayer" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() and event.GetInt() = MEDIA_END then

		m.GetMRSSTemplateItem()
		return "HANDLED"

	else if type(event) = "roTimerEvent" then
    
		if type(m.waitForContentTimer) = "roTimer" and event.GetSourceIdentity() = m.waitForContentTimer.GetIdentity() then

			if m.FindMRSSContent() then
				m.GetMRSSTemplateItem()
			endif

			return "HANDLED"
			
		endif

		if type(m.mrssItemTimer) = "roTimer" and event.GetSourceIdentity() = m.mrssItemTimer.GetIdentity() then

			m.GetMRSSTemplateItem()
			return "HANDLED"

		endif

		if type(m.simpleRSSTextTemplateItems) = "roAssociativeArray" then
			for each simpleRSSId in m.simpleRSSTextTemplateItems
				simpleRSS = m.simpleRSSTextTemplateItems.Lookup(simpleRSSId)
				if type(simpleRSS) = "roAssociativeArray" then
					if type(simpleRSS.rssItemTimer) = "roTimer" then
						if event.GetSourceIdentity() = simpleRSS.rssItemTimer.GetIdentity() then

							itemExists = false

							liveDataFeed = simpleRSS.rssLiveDataFeeds[simpleRSS.currentLiveDataFeedIndex%]
							if m.liveDataFeeds.DoesExist(liveDataFeed.name$) then
								if type(liveDataFeed.articles) = "roArray" then
									simpleRSS.currentIndex% = simpleRSS.currentIndex% + 1
									if simpleRSS.currentIndex% >= liveDataFeed.articles.count() then
										simpleRSS.currentIndex% = 0
										simpleRSS.currentLiveDataFeedIndex% = simpleRSS.currentLiveDataFeedIndex% + 1
										if simpleRSS.currentLiveDataFeedIndex% >= simpleRSS.rssLiveDataFeeds.Count() then
											simpleRSS.currentLiveDataFeedIndex% = 0
										endif
										liveDataFeed = simpleRSS.rssLiveDataFeeds[simpleRSS.currentLiveDataFeedIndex%]
										if m.liveDataFeeds.DoesExist(liveDataFeed.name$) then
											if type(liveDataFeed.articles) = "roArray" then
												itemExists = true
											endif
										endif
									else
										itemExists = true
									endif
								endif
							endif

							if itemExists then
								m.RedisplayTemplateItems()

								' restart timer
								simpleRSS.rssItemTimer.SetElapsed(simpleRSS.displayTime%, 0)
								simpleRSS.rssItemTimer.Start()
							endif

							return "HANDLED"

						endif			
					endif
				endif
			next
		endif

	    return m.MediaItemEventHandler(event, stateData)
	
	else

        return m.MediaItemEventHandler(event, stateData)
	
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Function FindMRSSContent() As Boolean

	' get the mrss live data feed to start searching with
	if m.mrssLiveDataFeed = invalid then
		mrssLiveDataFeedIndex% = 0
	else
		mrssLiveDataFeedIndex% = m.mrssLiveDataFeedIndex%
	endif

	startingIndex% = mrssLiveDataFeedIndex%

	while true

		mrssLiveDataFeed = m.mrssLiveDataFeeds[mrssLiveDataFeedIndex%]

		if type(mrssLiveDataFeed.assetPoolFiles) = "roAssetPoolFiles" then
			currentFeed = mrssLiveDataFeed.feed
			if currentFeed.AllContentExists( mrssLiveDataFeed.assetPoolFiles ) then
				m.mrssLiveDataFeed = mrssLiveDataFeed
				m.currentFeed = m.mrssLiveDataFeed.feed
				m.mrssLiveDataFeedIndex% = mrssLiveDataFeedIndex%
				m.displayIndex = 0
				return true
			endif
		endif

		mrssLiveDataFeedIndex% = mrssLiveDataFeedIndex% + 1
		if mrssLiveDataFeedIndex% >= m.mrssLiveDataFeeds.Count() then
			mrssLiveDataFeedIndex% = 0
		endif

		if mrssLiveDataFeedIndex% = startingIndex% then
			' search has wrapped around - nothing was found - continue waiting
			m.LaunchWaitForContentTimer()
			return false
		endif

	end while

End Function


Function GetNextMRSSTemplateItem() As Object

    if m.currentFeed.items.Count() = 0 then
        return invalid
    endif
	foundItem = false

	while not foundItem

		if m.displayIndex >= m.currentFeed.items.Count() then

			m.mrssLiveDataFeedIndex% = m.mrssLiveDataFeedIndex% + 1
			if m.mrssLiveDataFeedIndex% >= m.mrssLiveDataFeeds.Count() then
				m.mrssLiveDataFeedIndex% = 0
			endif

			contentFound = m.FindMRSSContent()

			if not contentFound then
				return invalid
			endif

		endif

		displayItem = m.currentFeed.items[m.displayIndex]

		if displayItem = invalid then
		    return invalid
		endif

		filePath$ = GetPoolFilePath(m.mrssLiveDataFeed.assetPoolFiles, displayItem.url)
		if filePath$ <> "" then
			foundItem = true
			displayItem.filePath$ = filePath$
		endif

		m.displayIndex = m.displayIndex + 1

	end while
	
	return displayItem

End Function


Sub GetMRSSTemplateItem()

	item = m.GetNextMRSSTemplateItem()

	if type(item) <> "roAssociativeArray" then

        ' no valid content - clear old content
        if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
            m.stateMachine.videoPlayer.StopClear()
        endif
        if type(m.stateMachine.imagePlayer) = "roImageWidget" then
            m.stateMachine.imagePlayer.StopDisplay()
        endif

        m.ClearTemplateItems()

        m.LaunchWaitForContentTimer()
		return

	endif

	' check to see if the item is an image or a video.
	' if an image, set a timer
	' if a video, need to wait for media end event
	if isImage(item) then
	    m.mrssItemTimer.SetElapsed(item.duration, 0)
		m.mrssItemTimer.Start()
	endif

	if type(m.mrssTitleTemplateItem) = "roAssociativeArray" then
		m.mrssTitleTemplateItem.textString$ = item.title
	endif

	if type(m.mrssDescriptionTemplateItem) = "roAssociativeArray" then
		m.mrssDescriptionTemplateItem.textString$ = item.description
	endif

	' need to distinguish here between image items and video items
	m.mrssVideoTemplateItem = {}
	
	if type(m.mrssMediaTemplateItem) = "roAssociativeArray" then
		if isImage(item) then
			m.mrssMediaTemplateItem.fileName$ = item.filePath$
			m.mrssMediaTemplateItem.fileNameForEncryption = item.title
			m.mrssVideoTemplateItem.fileName$ = ""
		else
			m.mrssMediaTemplateItem.fileName$ = ""
			m.mrssVideoTemplateItem.fileName$ = item.filePath$
			m.mrssVideoTemplateItem.fileNameForEncryption = item.title
		endif
	endif

	if type(m.mrssCustomFieldTemplateItems) = "roAssociativeArray" then

		for each customFieldName in m.mrssCustomFieldTemplateItems

			customFieldTemplateItem = m.mrssCustomFieldTemplateItems[customFieldName]
			
			' see if the corresponding custom field exists in this item
			customFieldValue = item.mrssCustomFields.lookup(customFieldName)
			if type(customFieldValue) = "roString" then
				customFieldTemplateItem.textString$ = customFieldValue
			endif

		next

	endif

	' different/additional path needed to display video
	if type(m.mrssVideoTemplateItem) = "roAssociativeArray" and m.mrssVideoTemplateItem.fileName$ <> invalid and m.mrssVideoTemplateItem.fileName$ <> "" and type(m.stateMachine.videoPlayer) = "roVideoPlayer" then

		' ensure that the image gets cleared

		item = m.mrssMediaTemplateItem

		r = CreateObject("roRectangle", item.x% + m.stateMachine.rectangle.getX(), item.y% + m.stateMachine.rectangle.getY(), item.width%, item.height%)
		m.stateMachine.videoPlayer.SetRectangle(r)
		
		m.videoItem = {}
		m.videoItem.videoDisplayMode% = 0
		m.videoItem.automaticallyLoop = false
		m.videoItem.fileName$ = m.mrssVideoTemplateItem.fileNameForEncryption
		m.videoItem.filePath$ = m.mrssVideoTemplateItem.fileName$
		m.stateMachine.preloadState = invalid
		m.stateMachine.syncInfo = invalid
		m.videoItem.userVariable = invalid
		m.videoTimeCodeEvents = invalid

		m.videoItem.isEncrypted = false
		if m.bsp.contentEncrypted and type(m.currentFeed) = "roAssociativeArray" and type(m.currentFeed.liveDataFeed) = "roAssociativeArray" then
			if m.currentFeed.liveDataFeed.isDynamicPlaylist or m.currentFeed.liveDataFeed.isLiveMediaFeed then
				m.videoItem.isEncrypted = true
			endif
		endif

		m.PlayVideo(false, true)
	else
		if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
			m.stateMachine.videoPlayer.StopClear()
		endif
	endif

	m.RedisplayTemplateItems()

End Sub


Function STPlayingMediaRSSEventHandler(event As Object, stateData As Object) As Object

    MEDIA_START = 3
    MEDIA_END = 8

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
			            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				' execute entry commands; perform other setup functions
				m.firstItemDisplayed = false
				m.PreDrawImage()

				if not m.stateMachine.useVideoPlayerForImages then
					' set default transition
					if type(m.stateMachine.imagePlayer) = "roImageWidget" then
						m.stateMachine.imagePlayer.SetDefaultTransition(m.slideTransition%)
					endif
				endif

				m.currentFeed = invalid
				m.pendingFeed = invalid

				' see if the designated feed has already been downloaded (doesn't imply content exists)
				if type(m.liveDataFeed) <> "roAssociativeArray" stop

				if type(m.liveDataFeed.assetPoolFiles) = "roAssetPoolFiles" then

					' create local versions of key objects
					m.assetCollection = m.liveDataFeed.assetCollection
					m.assetPoolFiles = m.liveDataFeed.assetPoolFiles
					m.currentFeed = m.liveDataFeed.feed

					' protect the feed that is getting displayed
					m.ProtectMRSSFeed( "display-" + m.liveDataFeed.name$, m.assetCollection )

					m.displayIndex = 0

					' distinguish between a feed that has no content and a feed in which no content has been downloaded
					if m.currentFeed.items.Count() = 0 or not m.currentFeed.AllContentExists( m.assetPoolFiles ) then
						' no content in feed - send a message to self to trigger exit from state (like video playback failure)
						mrssNotFullyLoadedPlaybackEvent = CreateObject("roAssociativeArray")
						mrssNotFullyLoadedPlaybackEvent["EventType"] = "MRSSNotFullyLoadedPlaybackEvent"
						mrssNotFullyLoadedPlaybackEvent["EventParameter"] = m.liveDataFeed.name$
						m.stateMachine.msgPort.PostMessage(mrssNotFullyLoadedPlaybackEvent)
					else
						m.AdvanceToNextMRSSItem()
					endif
				else
					' this situation will occur when the feed itself has not downloaded yet - send a message to self to trigger exit from state (like video playback failure)
					mrssNotFullyLoadedPlaybackEvent = CreateObject("roAssociativeArray")
					mrssNotFullyLoadedPlaybackEvent["EventType"] = "MRSSNotFullyLoadedPlaybackEvent"
					mrssNotFullyLoadedPlaybackEvent["EventParameter"] = m.liveDataFeed.name$
					m.stateMachine.msgPort.PostMessage(mrssNotFullyLoadedPlaybackEvent)
					return "HANDLED"
				endif

				m.LaunchTimer()
				m.bsp.SetTouchRegions(m)
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "mrss")

                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

                return "HANDLED"

			else if event["EventType"] = "VideoPlaybackFailureEvent" then      
				
				if type(m.currentFeed) <> "roAssociativeArray" then
					return "HANDLED"
				endif

				if m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray" then
					return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
				else
					m.AdvanceToNextMRSSItem()
					return "HANDLED"
				endif

			else if event["EventType"] = "MRSSNotFullyLoadedPlaybackEvent" then
        
				liveDataFeedName$ = event["EventParameter"]
				if liveDataFeedName$ = m.liveDataFeed.name$ then
					if type(m.signChannelEndEvent) = "roAssociativeArray" then
						return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
					else if type(m.currentFeed) = "roAssociativeArray" and m.currentFeed.ContentExists( m.assetPoolFiles ) then
  					m.AdvanceToNextMRSSItem()
'' redundant check
''					else if type(m.currentFeed) = "roAssociativeArray" and type(m.currentFeed.items) = "roArray" and m.currentFeed.items.Count() = 0 then
''						m.LaunchWaitForContentTimer()
					else
						m.LaunchWaitForContentTimer()
					endif
				endif
				return "HANDLED"

            else if event["EventType"] = "MRSS_SPEC_UPDATED" then
				updatedLiveDataFeed = event["LiveDataFeed"]

				if updatedLiveDataFeed.name$ = m.liveDataFeed.name$ then

					m.liveDataFeed = updatedLiveDataFeed	' this seems completely unnecessary

					if type( m.currentFeed ) <> "roAssociativeArray" or (not m.currentFeed.ContentExists( m.assetPoolFiles ) ) then

						' this is the first time that data is available
						m.pendingFeed = invalid
						m.currentFeed = m.liveDataFeed.feed
						m.assetCollection = m.liveDataFeed.assetCollection
						m.assetPoolFiles = m.liveDataFeed.assetPoolFiles

						' protect the feed that is getting displayed
						m.ProtectMRSSFeed( "display-" + m.liveDataFeed.name$, m.assetCollection )

						' feed may have been downloaded but it might not have content yet (empty mrss feed)
						' or feed has been downloaded but not all of its content has been downloaded yet - in this case, move on to the next item if possible
						if m.currentFeed.items.Count() = 0 or not m.currentFeed.AllContentExists( m.assetPoolFiles )
							if type(m.currentFeed) = "roAssociativeArray" and m.currentFeed.ContentExists( m.assetPoolFiles ) then
  								m.AdvanceToNextMRSSItem()
							else if type(m.signChannelEndEvent) = "roAssociativeArray" then
								return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
							else
								m.LaunchWaitForContentTimer()
								return "HANDLED"
							endif
						endif

						' all content exists - display an item
						m.displayIndex = 0
						m.AdvanceToNextMRSSItem()

					else

						' feed was updated. play through existing feed until it reaches the end; then switch to new feed.
						' note - this does not imply that the feed actually changed.
						m.pendingFeed = m.liveDataFeed.feed
						m.pendingAssetCollection = m.liveDataFeed.assetCollection
						m.pendingAssetPoolFiles = m.liveDataFeed.assetPoolFiles

					endif

				endif

                return "HANDLED"
				
            else
            
		        return m.MediaItemEventHandler(event, stateData)

            endif
            
        endif

	else if type(event) = "roHtmlWidgetEvent" and type(m.stateMachine.loadingHtmlWidget) = "roHtmlWidget" then

		eventData = event.GetData()
		if type(eventData) = "roAssociativeArray" and type(eventData.reason) = "roString" then
			userData = event.GetUserData()
			if userData <> invalid and userData.stateId$ <> invalid and userData.stateId$ = m.id$ then
				m.bsp.diagnostics.PrintDebug("reason = " + eventData.reason)
				if eventData.reason = "load-error" then
					m.bsp.diagnostics.PrintDebug("message = " + eventData.message)
					m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_HTML5_LOAD_ERROR, eventData.message)

					m.AdvanceToNextMRSSItem()

				else if eventData.reason = "load-finished" then
					if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
						m.stateMachine.videoPlayer.StopClear()
					endif

'					m.stateMachine.displayedHtmlWidget = m.stateMachine.loadingHtmlWidget
'					m.stateMachine.ShowHtmlWidget()
' Do a swap instead of just an assignment

					m.stateMachine.onDisplayHtmlWidget = m.stateMachine.displayedHtmlWidget
					m.stateMachine.displayedHtmlWidget = m.stateMachine.loadingHtmlWidget
					m.stateMachine.ShowHtmlWidget()
					m.stateMachine.onDisplayHtmlWidget = invalid

				endif
			endif
		endif

    else if type(event) = "roTimerEvent" then

        if type(m.imageTimeoutTimer) = "roTimer" and event.GetSourceIdentity() = m.imageTimeoutTimer.GetIdentity() then

			if m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
			else
				m.AdvanceToNextMRSSItem()
				return "HANDLED"
			endif

		else if type(m.waitForContentTimer) = "roTimer" and event.GetSourceIdentity() = m.waitForContentTimer.GetIdentity() then

			if type(m.currentFeed) <> "roAssociativeArray" or not m.currentFeed.AllContentExists( m.assetPoolFiles ) then
				if type(m.currentFeed) = "roAssociativeArray" and m.currentFeed.ContentExists( m.assetPoolFiles ) then
					if m.displayIndex = invalid then
						m.displayIndex = 0
					endif
					m.AdvanceToNextMRSSItem()
				else
					m.LaunchWaitForContentTimer()
				endif
			else if type(m.currentFeed) = "roAssociativeArray" and type(m.currentFeed.items) = "roArray" and m.currentFeed.items.Count() = 0 then
				m.LaunchWaitForContentTimer()
			else
				m.displayIndex = 0
				m.AdvanceToNextMRSSItem()
			endif

			return "HANDLED"

		endif

        return m.MediaItemEventHandler(event, stateData)
	else if type(event) = "roVideoEvent" and type(m.stateMachine.videoPlayer) = "roVideoPlayer" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() and event.GetInt() = MEDIA_END then
		if m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray" then
			return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
		else
			m.AdvanceToNextMRSSItem()
			return "HANDLED"
		endif

	else if m.stateMachine.type$ = "EnhancedAudio" and type(event) = "roAudioEventMx" then

		if event.GetInt() = MEDIA_START then

			if not (m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray") then
				m.AdvanceToNextMRSSItem()
				return "HANDLED"
			endif

		else if event.GetInt() = MEDIA_END then

			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")

			if m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
			else
				m.AdvanceToNextMRSSItem()
				return "HANDLED"
			endif

		endif
            
    else if IsAudioEvent(m.stateMachine, event) and event.GetInt() = MEDIA_END then

		if m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray" then
			return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
		else
			m.AdvanceToNextMRSSItem()
			return "HANDLED"
		endif

	else
    
        return m.MediaItemEventHandler(event, stateData)
	
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Sub LaunchWaitForContentTimer()

	if type(m.waitForContentTimer) = "roTimer" then
		m.waitForContentTimer.Stop()
	else
		m.waitForContentTimer = CreateObject("roTimer")
		m.waitForContentTimer.SetPort(m.bsp.msgPort)
	endif

	m.waitForContentTimer.SetElapsed(1, 0)
	m.waitForContentTimer.Start()

End Sub


' need to also consider the case where it's not at the end but there's no more content.
Function AtEndOfFeed() As Boolean

	return m.displayIndex >= m.currentFeed.items.Count()

End Function


Sub AdvanceToNextMRSSItem()

	displayedItem = false

	while not displayedItem

		if m.displayIndex >= m.currentFeed.items.Count() then

			m.displayIndex = 0

			' switch to new feed if available
			if type(m.pendingFeed) = "roAssociativeArray" then

				m.currentFeed = m.pendingFeed
				m.assetCollection = m.pendingAssetCollection
				m.assetPoolFiles = m.pendingAssetPoolFiles

				m.pendingFeed = invalid

				' protect the feed that we're switching to
				m.ProtectMRSSFeed( "display-" + m.liveDataFeed.name$, m.assetCollection )

				if m.currentFeed.items.Count() = 0 or not m.currentFeed.AllContentExists( m.assetPoolFiles ) then

					if type(m.currentFeed) = "roAssociativeArray" and m.currentFeed.ContentExists( m.assetPoolFiles ) then
						if m.displayIndex = invalid then
							m.displayIndex = 0
						endif
						m.AdvanceToNextMRSSItem()
					else
						m.LaunchWaitForContentTimer()
					endif
					return
				endif
			endif

		endif

		displayItem = m.currentFeed.items[m.displayIndex]

		if isHtml(displayItem) then
			if displayItem.type = "application/widget" then
				filePath$ = GetPoolFilePath(m.assetPoolFiles, displayItem.url)
				if filePath$ <> "" then
					widgetPath$ = m.GetHtmlWidgetFilePath(displayItem, filePath$)
					if widgetPath$ <> invalid then
						m.DisplayMRSSItem( displayItem, widgetPath$ )
						displayedItem = true
					end if
				end if
			else if displayItem.type = "text/html" then
				m.DisplayMRSSItem( displayItem, displayItem.url )
				displayedItem = true
			endif
		else
			filePath$ = GetPoolFilePath(m.assetPoolFiles, displayItem.url)
			if filePath$ <> "" then
				m.ProtectMRSSItem( displayItem )	' with the current code, this may be unnecessary since the entire feed is protected.
				m.DisplayMRSSItem( displayItem, filePath$ )
				' check return value before doing this??
				displayedItem = true
			endif
		endif

		m.displayIndex = m.displayIndex + 1

	end while

End Sub


Sub ProtectMRSSFeed( name$ As String, assets As Object )

	if not m.bsp.feedPool.ProtectAssets( name$, assets ) then
		m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, m.bsp.feedPool.GetFailureReason())
		m.bsp.logging.FlushLogFile()
		m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + m.bsp.feedPool.GetFailureReason())
		stop
	endif

End Sub


Sub ProtectMRSSItem( displayItem As Object )

	m.playingItemAssetCollection = CreateObject("roAssetCollection")

	asset = CreateObject("roAssociativeArray")
	asset.link = displayItem.url
	asset.name = displayItem.url
	if IsNonEmptyString(displayItem.guid) then
		asset.change_hint = displayItem.guid
	else if IsString(displayItem.url) then
		asset.change_hint = displayItem.url
	endif
	m.playingItemAssetCollection.AddAsset(asset)

'	m.ProtectMRSSFeed( "playing-" + m.currentFeed.title, m.playingItemAssetCollection )
	m.ProtectMRSSFeed( "playing-" + m.liveDataFeed.name$, m.playingItemAssetCollection )

End Sub


Sub DisplayMRSSItem( displayItem As Object, filePath$ As String )

	if ItemIsEncrypted(displayItem) and (m.liveDataFeed.isDynamicPlaylist or m.liveDataFeed.isLiveMediaFeed)
		isEncrypted = true
	else
		isEncrypted = false
	endif

	if isImage(displayItem) then
		m.imageItem = {}

		if IsString(displayItem.title) then
			m.imageItem.fileName$ = displayItem.title
		else
			m.imageItem.fileName$ = "noTitle"
		endif

		m.imageItem.filePath$ = filePath$
		m.imageItem.userVariable = invalid
		m.imageItem.slideDelayInterval% = displayItem.duration
		m.imageItem.useImageBuffer = false
		m.imageItem.isEncrypted = isEncrypted
		
		' m.DisplayImage("mrss")
		m.DrawImage(false)

		if not m.stateMachine.useVideoPlayerForImages then
			if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
				m.stateMachine.videoPlayer.StopClear()
			endif
		endif

		if type(m.imageTimeoutTimer) = "roTimer" then
			m.imageTimeoutTimer.Stop()
		endif

		if type(m.imageTimeoutTimer) <> "roTimer" then
			m.imageTimeoutTimer = CreateObject("roTimer")
			m.imageTimeoutTimer.SetPort(m.stateMachine.msgPort)
		endif
		m.imageTimeoutTimer.SetElapsed(m.imageItem.slideDelayInterval%, 0)
		m.imageTimeoutTimer.Start()

		' Set transition after image display to be default transition
		if type(m.stateMachine.imagePlayer) = "roImageWidget" then
			m.stateMachine.imagePlayer.SetDefaultTransition(m.slideTransition%)
		endif

	else if isAudio(displayItem)

		fileName$ = displayItem.title

		if type(m.stateMachine.audioPlayer) = "roAudioPlayerMx" then

			track = {}
			track["Filename"] = filePath$
			track["QueueNext"] = 1

			fadeLength% = m.stateMachine.fadelength% * 1000
			track["FadeInLength"] = fadeLength%
			track["FadeOutLength"] = fadeLength%

			if not m.firstItemDisplayed then
				track["FadeCurrentPlayNext"] = 0
				m.firstItemDisplayed = true
			endif

			ok = m.stateMachine.audioPlayer.PlayFile(track)
			
			m.stateMachine.LogPlayStart("audioMx", fileName$)
			
		else

			player = invalid
			if type(m.stateMachine.audioPlayer) = "roAudioPlayer" then
				player = m.stateMachine.audioPlayer
			else if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
				player = m.stateMachine.videoPlayer
			endif

			if player <> invalid then

			    player.SetLoopMode(0)

				aa = { }
				aa.AddReplace("Filename", filePath$)
		
				if type(displayItem.probeData) = "roString" and displayItem.probeData <> "" then
					aa.AddReplace("ProbeString", displayItem.probeData)
				endif

				ok = player.PlayFile(aa)

				if ok = 0 then
					m.bsp.diagnostics.PrintDebug("Error playing audio file: " + fileName$ + ", " + filePath$)
					m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, fileName$)
				endif

				' playback logging
				m.stateMachine.LogPlayStart("audio", fileName$)

			endif

		endif

	else if isHtml(displayItem)

		m.stateMachine.loadingHtmlWidget = CreateObject("roHtmlWidget", m.stateMachine.rectangle)
		SetUserAgentForHtmlWidget(m.bsp, m.stateMachine.loadingHtmlWidget)
	
		userData = {}
		userData.stateId$ = m.id$
		m.stateMachine.loadingHtmlWidget.SetUserData(userData)

		if m.bsp.sign.htmlEnableJavascriptConsole then
			m.stateMachine.loadingHtmlWidget.StartInspectorServer(2999)
		endif

		if CanUseScreenModes({}) then
		  ' no need to rotate per zone if already rotated by screen
		else if m.bsp.DeviceSupportsRotation() then
			if m.bsp.sign.monitorOrientation = "portrait" then
				m.stateMachine.loadingHtmlWidget.SetPortrait(true)
			else if m.bsp.htmlSetTransformSupported and m.bsp.sign.monitorOrientation = "portraitbottomonright" then
				m.stateMachine.loadingHtmlWidget.SetTransform("rot270")
			endif
		endif
		
		m.stateMachine.loadingHtmlWidget.SetPort(m.bsp.msgPort)

		if m.bsp.sign.htmlEnableLocalStorage then
			m.stateMachine.loadingHtmlWidget.SetLocalStorageDir("localstorage")
			m.stateMachine.loadingHtmlWidget.SetLocalStorageQuota(m.bsp.sign.htmlLocalStorageSize% * 1024 * 1024)
		endif
		
		m.stateMachine.loadingHtmlWidget.SetUrl(filePath$)
		'print "Loading MRSS HTML path: ";filePath$
		' ?? call LaunchTimer here to handle load timeout ??

		if type(m.imageTimeoutTimer) = "roTimer" then
			m.imageTimeoutTimer.Stop()
		endif

		if type(m.imageTimeoutTimer) <> "roTimer" then
			m.imageTimeoutTimer = CreateObject("roTimer")
			m.imageTimeoutTimer.SetPort(m.stateMachine.msgPort)
		endif
		m.imageTimeoutTimer.SetElapsed(displayItem.duration, 0)
		m.imageTimeoutTimer.Start()

	else
		m.videoItem = {}
		m.videoItem.fileName$ = displayItem.title
		m.videoItem.filePath$ = filePath$
		m.videoItem.userVariable = invalid
		m.videoItem.probeData = invalid
		m.videoItem.automaticallyLoop = true
		m.videoItem.videoDisplayMode% = 0
		m.videoItem.isEncrypted = isEncrypted

		' m.LaunchVideo("mrss")
		m.PlayVideo(false, true)

		m.stateMachine.ClearImagePlane()

		' Set transition to put image up immediately after video is finished
		if type(m.stateMachine.imagePlayer) = "roImageWidget" then
			m.stateMachine.imagePlayer.SetDefaultTransition(0)
		endif

	endif

	if not m.firstItemDisplayed then
		m.firstItemDisplayed = true
	endif

End Sub

Function LastPathComponent(path$ As String) As String
	p% = instr(1, path$, "/")
	t% = p%
	while t% > 0
		t% = instr(p%+1, path$, "/")
		if t% > 0 then
			p% = t%
		endif
	end while
	if p% > 0 then
		return mid(path$, p%+1)
	endif
	return path$
End Function

Function GetHtmlWidgetFilePath( displayItem As Object, filePath$ As String) As String
	' Get file name from url as widget name
	widgetName$ = LastPathComponent(displayItem.url)
	widgetDir$ = "/htmlWidgets/" + widgetName$ + "/"
	CreateDirectory(widgetDir$)
	' TODO - check to see if files have already been unzipped and skip unzip if so ??
	unzipper = CreateObject("roBrightPackage", filePath$)
	unzipper.Unpack(widgetDir$)
	return "file:" + widgetDir$ + "index.html"
End Function


Sub PlayMixerAudio(executeEntryCmds As Boolean, playbackIndex% As Integer, playImmediate As Boolean)

    loopMode% = 1
    if type(m.audioEndEvent) = "roAssociativeArray" then loopMode% = 0
    m.stateMachine.audioPlayer.SetLoopMode(loopMode%)

' can't stop since this is code is often just queuing up a track - will this cause any repercussions?
'	m.stateMachine.audioPlayer.Stop()

' for now, never stop playback - always use crossfade
'	if playImmediate then
'		m.stateMachine.audioPlayer.Stop()
'	endif

	if executeEntryCmds then
		m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
	endif

    file$ = m.audioItem.fileName$

	if type(m.audioItem.filePath$) = "roString" then
		filePath$ = m.audioItem.filePath$
	else
		filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, m.audioItem.fileName$)
	endif

    track = CreateObject("roAssociativeArray")
    track["Filename"] = filePath$
    track["QueueNext"] = 1
'    track["Algorithm"] = "muzak"
'    track["EncryptionKey"] = playlistPath$
	track["UserString"]= playbackIndex%

	fadeLength% = m.stateMachine.fadelength% * 1000
    track["FadeInLength"] = fadeLength%
    track["FadeOutLength"] = fadeLength%

	if playImmediate then
		track["FadeCurrentPlayNext"] = 0
	endif

	ok = m.stateMachine.audioPlayer.PlayFile(track)

	m.stateMachine.ClearImagePlane()

	if type(m.audioItem.userVariable) = "roAssociativeArray" then
		m.audioItem.userVariable.Increment()
	endif
    
	' playback logging
	m.stateMachine.LogPlayStart("audioMx", file$)

End Sub


Sub LaunchMixerAudio(playbackIndex% As Integer, playImmediate As Boolean)

	m.PrePlayAudio()

	m.PlayMixerAudio(true, playbackIndex%, playImmediate)

	m.PostPlayAudio("audioMx")

End Sub


Function STAudioPlayingEventHandler(event As Object, stateData As Object) As Object

    MEDIA_END = 8
	AUDIO_TIME_CODE = 12
	
    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				if m.stateMachine.type$ = "EnhancedAudio" then
					m.LaunchMixerAudio(-1, true)
				else		
					m.LaunchAudio("audio")
				endif

                return "HANDLED"

            else if event["EventType"] = "AudioPlaybackFailureEvent" then

				if m.bsp.ProcessMediaEndEvent() then
					return "HANDLED"
				endif
                if type(m.audioEndEvent) = "roAssociativeArray" then
                    return m.ExecuteTransition(m.audioEndEvent, stateData, "")
                endif

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

            else
            
		        return m.MediaItemEventHandler(event, stateData)

            endif
            
        endif

	else if m.stateMachine.type$ = "EnhancedAudio" and type(event) = "roAudioEventMx" then
        if event.GetInt() = MEDIA_END then
            m.bsp.diagnostics.PrintDebug("Audio Event" + stri(event.GetInt()))
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif
            if type(m.audioEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.audioEndEvent, stateData, "")
            endif
			PostMediaEndEvent(m.bsp.msgPort)
        endif
            
    else if IsAudioEvent(m.stateMachine, event) then
        if event.GetInt() = MEDIA_END then
            m.bsp.diagnostics.PrintDebug("Audio Event" + stri(event.GetInt()))
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
			if m.bsp.ProcessMediaEndEvent() then
				return "HANDLED"
			endif
            if type(m.audioEndEvent) = "roAssociativeArray" then
				return m.ExecuteTransition(m.audioEndEvent, stateData, "")
            endif
			PostMediaEndEvent(m.bsp.msgPort)
        else if event.GetInt() = AUDIO_TIME_CODE then
			audioTimeCodeIndex$ = str(event.GetData())
			m.bsp.diagnostics.PrintDebug("Audio TimeCode Event " + audioTimeCodeIndex$)
            if type(m.audioTimeCodeEvents) = "roAssociativeArray" then
                audioTimeCodeEvent = m.audioTimeCodeEvents[audioTimeCodeIndex$]
                if type(audioTimeCodeEvent) = "roAssociativeArray" then
					m.bsp.ExecuteTransitionCommands(m.stateMachine, audioTimeCodeEvent)
					m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "audioTimeCode", "", "1")
					return "HANDLED"      
                endif
			m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "audioTimeCode", "", "0")
            endif
        endif
    else
        return m.MediaItemEventHandler(event, stateData)
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function

'endregion

'region Background Image State Machine
' *************************************************
'
' Background Image State Machine
'
' *************************************************
Function newBackgroundImageZoneHSM(bsp As Object, zoneXML As Object) As Object

	zoneHSM = newHSM()
	zoneHSM.ConstructorHandler = BackgroundImageZoneConstructor
	zoneHSM.InitialPseudostateHandler = BackgroundImageZoneGetInitialState
	
	newZoneCommon(bsp, zoneXML, zoneHSM)

    return zoneHSM

End Function


Sub BackgroundImageZoneConstructor()

	m.InitializeZoneCommon(m.bsp.msgPort)

    zoneHSM = m
    
    ' create players
    
    videoPlayer = CreateObject("roVideoPlayer")
    if type(videoPlayer) <> "roVideoPlayer" then print "videoPlayer creation failed" : stop

	if CanUseScreenModes({}) then
		' no need to rotate per zone if already rotated by screen
		else if m.bsp.DeviceSupportsRotation() then
		if m.bsp.sign.monitorOrientation = "portrait" then
			videoPlayer.SetTransform("rot90")
		else if m.bsp.sign.monitorOrientation = "portraitbottomonright" then
			videoPlayer.SetTransform("rot270")
		endif
	endif

    videoPlayer.SetRectangle(zoneHSM.rectangle)
    
    videoPlayer.SetPort(zoneHSM.msgPort)

	' refactor to 'not puma, not panther'
	if m.bsp.sysInfo.deviceFamily$ = "cheetah" or m.bsp.sysInfo.deviceFamily$ = "tiger" or m.bsp.sysInfo.deviceFamily$ = "lynx" or m.bsp.sysInfo.deviceFamily$ = "impala" or m.bsp.sysInfo.deviceFamily$ = "malibu" or m.bsp.sysInfo.deviceFamily$ = "pantera" then
		videoPlayer.ToBack()
	endif

    zoneHSM.videoPlayer = videoPlayer
    zoneHSM.isVideoZone = true

	m.activeState = m.playlist.firstState
	if type(m.playlist.firstState) = "roAssociativeArray" then
		m.previousStateName$ = m.playlist.firstState.id$
	else
		m.previousStateName$ = ""
	endif
	
	m.CreateObjects()
            
End Sub


Function BackgroundImageZoneGetInitialState() As Object

	return m.activeState

End Function


Function STDisplayingBackgroundImageEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.usbInputBuffer$ = ""
				m.usbInputLogBuffer$ = ""

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)

				file$ = m.backgroundImageItem.fileName$
				filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, file$)

				aa = {}
				m.bsp.SetEncryptionAttributes(aa, file$, filePath$)

				if m.bsp.contentEncryptionSupported then
					ok = m.stateMachine.videoPlayer.PlayStaticImage(aa)
				else
					ok = m.stateMachine.videoPlayer.PlayStaticImage(filePath$)
				endif

				if ok = 0 then
					m.bsp.diagnostics.PrintDebug("Error displaying file in LaunchBackgroundImage: " + file$ + ", " + filePath$)
				else
					m.bsp.diagnostics.PrintDebug("LaunchBackgroundImage: display file " + file$)
				endif   

				if type(m.backgroundImageItem.userVariable) = "roAssociativeArray" then
					m.backgroundImageItem.userVariable.Increment()
				endif
    
				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "backgroundImage")

                return "HANDLED"

            else if event["EventType"] = "PREPARE_FOR_RESTART" then

				m.stateMachine.videoPlayer = invalid
				return "HANDLED"
				
            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)

            else
            
		        return m.MediaItemEventHandler(event, stateData)

            endif
            
        endif
    else
        return m.MediaItemEventHandler(event, stateData)
    endif
            
    stateData.nextState = m.superState
    return "SUPER"
    
End Function

'endregion

'region Clock State Machine
' *************************************************
'
' Clock State Machine
'
' *************************************************
Function newClockZoneHSM(bsp As Object, zoneXML As Object) As Object

	zoneHSM = newHSM()
	zoneHSM.ConstructorHandler = ClockZoneConstructor
	zoneHSM.InitialPseudostateHandler = ClockZoneGetInitialState
	
	newZoneCommon(bsp, zoneXML, zoneHSM)

    displayTime$ = zoneXML.zoneSpecificParameters.displayTime.GetText()
    if lcase(displayTime$) = "true" then
        zoneHSM.displayTime% = 1
    else
        zoneHSM.displayTime% = 0
    endif
    
    REM code below doesn't work with BS 2.0
    ' rotation$ = zoneXML.zoneSpecificParameters.rotation.GetText()
	zoneHSM.rotation% = 0
    ele = zoneXML.zoneSpecificParameters.GetNamedElements("rotation")
    if ele.Count() = 1 then
		rotation$ = ele[0].GetText()
		if rotation$ = "90" then
			zoneHSM.rotation% = 3
		else if rotation$ = "180" then
			zoneHSM.rotation% = 2
		else if rotation$ = "270" then
			zoneHSM.rotation% = 1
		endif
    endif
    
    widget = zoneXML.zoneSpecificParameters.widget
    foregroundTextColor = widget.foregroundTextColor
    zoneHSM.foregroundTextColor% = GetColor(foregroundTextColor.GetAttributes())
    backgroundTextColor = widget.backgroundTextColor
    zoneHSM.backgroundTextColor% = GetColor(backgroundTextColor.GetAttributes())
    zoneHSM.font$ = widget.font.GetText()

    backgroundBitmap = widget.backgroundBitmap
    if backgroundBitmap.Count() = 1 then
        backgroundBitmapAttrs = backgroundBitmap.GetAttributes()
        zoneHSM.backgroundBitmapFile$ = backgroundBitmapAttrs["file"]
        stretchStr = backgroundBitmapAttrs["stretch"]
        if stretchStr = "true" then
            zoneHSM.stretch% = 1
        else
            zoneHSM.stretch% = 0
        endif
    endif
    
    safeTextRegion = widget.safeTextRegion
    if safeTextRegion.Count() = 1 then
        zoneHSM.safeTextRegionX% = int(val(safeTextRegion.safeTextRegionX.GetText()))
        zoneHSM.safeTextRegionY% = int(val(safeTextRegion.safeTextRegionY.GetText()))
        zoneHSM.safeTextRegionWidth% = int(val(safeTextRegion.safeTextRegionWidth.GetText()))
        zoneHSM.safeTextRegionHeight% = int(val(safeTextRegion.safeTextRegionHeight.GetText()))
    endif
    
    zoneHSM.stClock = zoneHSM.newHState(bsp, "Clock")
    zoneHSM.stClock.HStateEventHandler = STClockEventHandler
	zoneHSM.stClock.superState = zoneHSM.stTop
        
	return zoneHSM
		
End Function


Sub ClockZoneConstructor()

	m.InitializeZoneCommon(m.bsp.msgPort)
	
    zoneHSM = m

	globalAA = GetGlobalAA()
    resourceManager = CreateObject("roResourceManager", globalAA.resourcesFilePath$)
    if type(resourceManager) = "roResourceManager" then
        ok = resourceManager.SetLanguage(zoneHSM.language$)
        if not ok then print "No resources for language ";zoneHSM.language$ : stop
        
	    a=CreateObject("roAssociativeArray")
	    if zoneHSM.displayTime% = 1 then
			a["Time"] = 1
			a["Date"] = 0
		else
			a["Time"] = 0
			a["Date"] = 1
		endif
	    a["Rotation"] = zoneHSM.rotation%

        clockWidget = CreateObject("roClockWidget", zoneHSM.rectangle, resourceManager, a)

		if type(clockWidget) = "roClockWidget" then
        
			zoneHSM.widget = clockWidget
		
			if type(zoneHSM.foregroundTextColor%) = "roInt" then
				zoneHSM.widget.SetForegroundColor(zoneHSM.foregroundTextColor%)
			endif

			if type(zoneHSM.backgroundTextColor%) = "roInt" then
				zoneHSM.widget.SetBackgroundColor(zoneHSM.backgroundTextColor%)
			endif
        
			if zoneHSM.font$ <> "" and zoneHSM.font$ <> "System" then
				fontPath$ = GetPoolFilePath(m.bsp.assetPoolFiles, zoneHSM.font$)
				zoneHSM.widget.SetFont(fontPath$)
			endif
                
			if IsString(zoneHSM.backgroundBitmapFile$) then

				backgroundBitmapFilePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, zoneHSM.backgroundBitmapFile$)

				aa = {}
				m.bsp.SetEncryptionAttributes(aa, zoneHSM.backgroundBitmapFile$, backgroundBitmapFilePath$)

				if m.bsp.contentEncryptionSupported then
					zoneHSM.widget.SetBackgroundBitmap(aa, zoneHSM.stretch%)
				else
					zoneHSM.widget.SetBackgroundBitmap(backgroundBitmapFilePath$, zoneHSM.stretch%)
				endif

			endif
                
			if type(zoneHSM.safeTextRegionX%) = "roInt" then
				r = CreateObject("roRectangle", zoneHSM.safeTextRegionX%, zoneHSM.safeTextRegionY%, zoneHSM.safeTextRegionWidth%, zoneHSM.safeTextRegionHeight%)
				zoneHSM.widget.SetSafeTextRegion(r)
				r = 0
			endif

		endif

    endif
    
End Sub


Function ClockZoneGetInitialState() As Object

	return m.stClock

End Function


Function STClockEventHandler(event As Object, stateData As Object) As Object
    
    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				if type(m.stateMachine.widget) = "roClockWidget" and m.stateMachine.isVisible then
					m.stateMachine.widget.Show()
				endif

                return "HANDLED"

            else if event["EventType"] = "PREPARE_FOR_RESTART" then

				m.stateMachine.widget = invalid
				
				return "HANDLED"
				
            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
            endif
            
        endif
        
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function

'endregion
	
'region Ticker State Machine
' *************************************************
'
' Ticker State Machine
'
' *************************************************
Function newTickerZoneHSM(bsp As Object, sign As Object, zoneXML As Object) As Object

    zoneHSM = newHSM()
	zoneHSM.ConstructorHandler = TickerZoneConstructor
	zoneHSM.InitialPseudostateHandler = TickerZoneGetInitialState

    newZoneCommon(bsp, zoneXML, zoneHSM)
    
    zoneHSM.rssDownloadPeriodicValue% = sign.rssDownloadPeriodicValue%
    zoneHSM.rssDownloadTimer = CreateObject("roTimer")

    zoneHSM.numberOfLines% = int(val(zoneXML.zoneSpecificParameters.textWidget.numberOfLines.GetText()))
    zoneHSM.delay% = int(val(zoneXML.zoneSpecificParameters.textWidget.delay.GetText()))
    
    ' below line doesn't work in BS2.0
    ' rotation$ = zoneXML.zoneSpecificParameters.textWidget.rotation.GetText()
	zoneHSM.rotation% = 0
    ele = zoneXML.zoneSpecificParameters.textWidget.GetNamedElements("rotation")
    if ele.Count() = 1 then
		rotation$ = ele[0].GetText()
		if rotation$ = "90" then
			zoneHSM.rotation% = 3
		else if rotation$ = "180" then
			zoneHSM.rotation% = 2
		else if rotation$ = "270" then
			zoneHSM.rotation% = 1
		endif
    endif
    
    alignment$ = zoneXML.zoneSpecificParameters.textWidget.alignment.GetText()
    if alignment$ = "center" then
        zoneHSM.alignment% = 1
    else if alignment$ = "right" then
        zoneHSM.alignment% = 2
    else
        zoneHSM.alignment% = 0
    endif
    
    zoneHSM.scrollingMethod% = int(val(zoneXML.zoneSpecificParameters.textWidget.scrollingMethod.GetText()))

	zoneHSM.scrollSpeed% = 100
	if type(zoneXML.zoneSpecificParameters.scrollSpeed) = "roXMLList" then
		scrollSpeed$ = zoneXML.zoneSpecificParameters.scrollSpeed[0].GetText()
		if scrollSpeed$ <> "" then
			zoneHSM.scrollSpeed% = int(val(scrollSpeed$))
		endif
	endif
    
    widget = zoneXML.zoneSpecificParameters.widget
    foregroundTextColor = widget.foregroundTextColor
    zoneHSM.foregroundTextColor% = GetColor(foregroundTextColor.GetAttributes())
    backgroundTextColor = widget.backgroundTextColor
    zoneHSM.backgroundTextColor% = GetColor(backgroundTextColor.GetAttributes())
    zoneHSM.font$ = widget.font.GetText()

    backgroundBitmap = widget.backgroundBitmap
    if backgroundBitmap.Count() = 1 then
        backgroundBitmapAttrs = backgroundBitmap.GetAttributes()
        zoneHSM.backgroundBitmapFile$ = backgroundBitmapAttrs["file"]
        stretchStr = backgroundBitmapAttrs["stretch"]
        if stretchStr = "true" then
            zoneHSM.stretch% = 1
        else
            zoneHSM.stretch% = 0
        endif
    endif
    
    safeTextRegion = widget.safeTextRegion
    if safeTextRegion.Count() = 1 then
        zoneHSM.safeTextRegionX% = int(val(safeTextRegion.safeTextRegionX.GetText()))
        zoneHSM.safeTextRegionY% = int(val(safeTextRegion.safeTextRegionY.GetText()))
        zoneHSM.safeTextRegionWidth% = int(val(safeTextRegion.safeTextRegionWidth.GetText()))
        zoneHSM.safeTextRegionHeight% = int(val(safeTextRegion.safeTextRegionHeight.GetText()))
    endif
    
    zoneHSM.stRSSDataFeedInitialLoad = zoneHSM.newHState(bsp, "RSSDataFeedInitialLoad")
    zoneHSM.stRSSDataFeedInitialLoad.HStateEventHandler = STRSSDataFeedInitialLoadEventHandler
	zoneHSM.stRSSDataFeedInitialLoad.superState = zoneHSM.stTop
        
    zoneHSM.stRSSDataFeedPlaying = zoneHSM.newHState(bsp, "RSSDataFeedPlaying")
	zoneHSM.stRSSDataFeedPlaying.PopulateRSSDataFeedWidget = PopulateRSSDataFeedWidget
    zoneHSM.stRSSDataFeedPlaying.HStateEventHandler = STRSSDataFeedPlayingEventHandler
	zoneHSM.stRSSDataFeedPlaying.superState = zoneHSM.stTop
			    	
    return zoneHSM
        
End Function


Sub TickerZoneConstructor()

	m.InitializeZoneCommon(m.bsp.msgPort)
	
    zoneHSM = m
    
    a=CreateObject("roAssociativeArray")
    a["PauseTime"] = zoneHSM.delay%
    a["Rotation"] = zoneHSM.rotation%
    a["Alignment"] = zoneHSM.alignment%
    textWidget = CreateObject("roTextWidget", zoneHSM.rectangle, zoneHSM.numberOfLines%, zoneHSM.scrollingMethod%, a)

    zoneHSM.widget = textWidget
    
	if zoneHSM.scrollingMethod% = 3 then
		zoneHSM.widget.SetAnimationSpeed(zoneHSM.scrollSpeed%)
	endif

    if type(zoneHSM.foregroundTextColor%) = "roInt" then
        zoneHSM.widget.SetForegroundColor(zoneHSM.foregroundTextColor%)
    endif
    
    if type(zoneHSM.backgroundTextColor%) = "roInt" then
        zoneHSM.widget.SetBackgroundColor(zoneHSM.backgroundTextColor%)
    endif
    
    if zoneHSM.font$ <> "" and zoneHSM.font$ <> "System" then
        fontPath$ = GetPoolFilePath(m.bsp.assetPoolFiles, zoneHSM.font$)
        zoneHSM.widget.SetFont(fontPath$)
    endif
            
    if IsString(zoneHSM.backgroundBitmapFile$) then

        backgroundBitmapFilePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, zoneHSM.backgroundBitmapFile$)

		aa = {}
		m.bsp.SetEncryptionAttributes(aa, zoneHSM.backgroundBitmapFile$, backgroundBitmapFilePath$)

		if m.bsp.contentEncryptionSupported then
			zoneHSM.widget.SetBackgroundBitmap(aa, zoneHSM.stretch%)
		else
			zoneHSM.widget.SetBackgroundBitmap(backgroundBitmapFilePath$, zoneHSM.stretch%)
		endif

	endif
            
    if type(zoneHSM.safeTextRegionX%) = "roInt" then
        r = CreateObject("roRectangle", zoneHSM.safeTextRegionX%, zoneHSM.safeTextRegionY%, zoneHSM.safeTextRegionWidth%, zoneHSM.safeTextRegionHeight%)
        zoneHSM.widget.SetSafeTextRegion(r)
        r = invalid
    endif

	m.includesRSSFeeds = false
	for each rssDataFeedItem in m.rssDataFeedItems
		if rssDataFeedItem.isRSSFeed then
			m.includesRSSFeeds = true
		endif
	next

End Sub


Sub SetEncryptionAttributes(aa As Object, fileName$ As String, filePath$ As String)

	aa.fileName = filePath$
	if type(m.encryptionByFile) = "roAssociativeArray" then
		if m.encryptionByFile.DoesExist(fileName$) then
		' if m.bsp.contentEncrypted
			aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
			aa.AddReplace("EncryptionKey", fileName$)
		endif
	endif

End Sub


Function TickerZoneGetInitialState() As Object

	if m.includesRSSFeeds then
		return m.stRSSDataFeedInitialLoad
	else
		return m.stRSSDataFeedPlaying
	endif

End Function


Function GetRSSTempFilename()
	fileName$ = "tmp:/rss" + StripLeadingSpaces(stri(m.rssFileIndex%)) + ".xml"
	m.rssFileIndex% = m.rssFileIndex% + 1
	return fileName$
End Function


Function STRSSDataFeedInitialLoadEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				for each rssDataFeedItem in m.stateMachine.rssDataFeedItems
					rssDataFeedItem.loadAttemptComplete = not rssDataFeedItem.isRSSFeed
				next

				return "HANDLED"

            else if event["EventType"] = "LIVE_DATA_FEED_UPDATE" or event["EventType"] = "LIVE_DATA_FEED_UPDATE_FAILURE" then

				liveDataFeed = event["EventData"]

				allLoadsComplete = true

				for each rssDataFeedItem in m.stateMachine.rssDataFeedItems
					if rssDataFeedItem.isRSSFeed then
						if liveDataFeed.name$ = rssDataFeedItem.liveDataFeed.name$ then
							rssDataFeedItem.loadAttemptComplete = true
						else if not rssDataFeedItem.loadAttemptComplete then
							allLoadsComplete = false
						endif
					endif
				next

				if allLoadsComplete then
					stateData.nextState = m.stateMachine.STRSSDataFeedPlaying
					return "TRANSITION"
				else
					return "HANDLED"
				endif

            else if event["EventType"] = "PREPARE_FOR_RESTART" then

                m.bsp.diagnostics.PrintDebug(m.id$ + " - PREPARE_FOR_RESTART")
				m.stateMachine.widget = invalid
				return "HANDLED"
				
			endif

		endif

	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Sub PopulateRSSDataFeedWidget()

	' clear existing strings
	rssStringCount = m.stateMachine.widget.GetStringCount()
	m.stateMachine.widget.PopStrings(rssStringCount)

	' populate widget with new strings
	for each rssDataFeedItem in m.stateMachine.rssDataFeedItems
		if type(rssDataFeedItem.textStrings) = "roArray" then
			for each textString in rssDataFeedItem.textStrings
				m.stateMachine.widget.PushString(textString)
			next
		else
			for each article in rssDataFeedItem.liveDataFeed.articles
				m.stateMachine.widget.PushString(article)
			next
		endif
	next
				
	if m.stateMachine.isVisible then m.stateMachine.widget.Show()

End Sub


Function STRSSDataFeedPlayingEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.PopulateRSSDataFeedWidget()

				return "HANDLED"

            else if event["EventType"] = "LIVE_DATA_FEED_UPDATE" then

				liveDataFeed = event["EventData"]

				' check that the live data feed is for one of the rss feeds

				rssDataFeedItemLoaded = false

				for each rssDataFeedItem in m.stateMachine.rssDataFeedItems
					if rssDataFeedItem.isRSSFeed then
						if liveDataFeed.name$ = rssDataFeedItem.liveDataFeed.name$ then
							rssDataFeedItemLoaded = true
							exit for
						endif
					endif
				next

				if rssDataFeedItemLoaded then

					m.PopulateRSSDataFeedWidget()

				endif

            else if event["EventType"] = "USER_VARIABLES_UPDATED" then

                rssDataFeedsUpdated = false

	            userVariables = m.bsp.currentUserVariables

				for each rssDataFeedItem in m.stateMachine.rssDataFeedItems
					if rssDataFeedItem.isUserVariable then
                        for each userVariableKey in userVariables
                            userVariable = userVariables.Lookup(userVariableKey)
                            if userVariable.name$ = rssDataFeedItem.userVariableName then
                                tickerItemValue = userVariable.GetCurrentValue()
                                rssDataFeedItem.textStrings = []
                                rssDataFeedItem.textStrings.push(tickerItemValue)
                                rssDataFeedsUpdated = true
                            endif
                        next
					endif
                next

                if rssDataFeedsUpdated then
                    m.PopulateRSSDataFeedWidget()
                endif

				return "HANDLED"

            else if event["EventType"] = "PREPARE_FOR_RESTART" then

                m.bsp.diagnostics.PrintDebug(m.id$ + " - PREPARE_FOR_RESTART")
				m.stateMachine.widget = invalid
				return "HANDLED"
				
			endif

		endif

	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function

'endregion

'region Player State Machine
' *************************************************
'
' Player State Machine
'
' *************************************************
Function newPlayerStateMachine(bsp As Object) As Object

    PlayerStateMachine = newHSM()
    PlayerStateMachine.InitialPseudostateHandler = InitializePlayerStateMachine

	PlayerStateMachine.bsp = bsp
	PlayerStateMachine.msgPort = bsp.msgPort
	PlayerStateMachine.logging = bsp.logging
	
	PlayerStateMachine.SetSystemInfo = SetSystemInfo
	PlayerStateMachine.CheckForUSBUpdate = CheckForUSBUpdate
	PlayerStateMachine.DisplayUSBUpdateStatus = DisplayUSBUpdateStatus

    PlayerStateMachine.dataFeedRetryInterval% = 30

    PlayerStateMachine.POOL_EVENT_FILE_DOWNLOADED = 1
    PlayerStateMachine.POOL_EVENT_FILE_FAILED = -1
    PlayerStateMachine.POOL_EVENT_ALL_DOWNLOADED = 2
    PlayerStateMachine.POOL_EVENT_ALL_FAILED = -2

    PlayerStateMachine.SYNC_ERROR_CANCELLED = -10001
    PlayerStateMachine.SYNC_ERROR_CHECKSUM_MISMATCH = -10002
    PlayerStateMachine.SYNC_ERROR_EXCEPTION = -10003
    PlayerStateMachine.SYNC_ERROR_DISK_ERROR = -10004
    PlayerStateMachine.SYNC_ERROR_POOL_UNSATISFIED = -10005
    
    PlayerStateMachine.EVENT_REALIZE_SUCCESS = 101

    PlayerStateMachine.stTop = PlayerStateMachine.newHState(bsp, "Top")
    PlayerStateMachine.stTop.HStateEventHandler = STTopEventHandler
    
    PlayerStateMachine.stPlayer = PlayerStateMachine.newHState(bsp, "Player") 
    PlayerStateMachine.stPlayer.HStateEventHandler = STPlayerEventHandler
	PlayerStateMachine.stPlayer.superState = PlayerStateMachine.stTop

    PlayerStateMachine.stPlaying = PlayerStateMachine.newHState(bsp, "Playing") 
    PlayerStateMachine.stPlaying.HStateEventHandler = STPlayingEventHandler
	PlayerStateMachine.stPlaying.superState = PlayerStateMachine.stPlayer
	PlayerStateMachine.stPlaying.UpdateTimeClockEvents = UpdateTimeClockEvents

    PlayerStateMachine.stWaiting = PlayerStateMachine.newHState(bsp, "Waiting") 
    PlayerStateMachine.stWaiting.HStateEventHandler = STWaitingEventHandler
	PlayerStateMachine.stWaiting.superState = PlayerStateMachine.stPlayer

    PlayerStateMachine.stUpdatingFromUSB = PlayerStateMachine.newHState(bsp, "UpdatingFromUSB") 
    PlayerStateMachine.stUpdatingFromUSB.HStateEventHandler = STUpdatingFromUSBEventHandler
	PlayerStateMachine.stUpdatingFromUSB.superState = PlayerStateMachine.stPlayer
	PlayerStateMachine.stUpdatingFromUSB.BuildFileUpdateList = BuildFileUpdateList
    PlayerStateMachine.stUpdatingFromUSB.StartUpdateSyncListDownload = StartUpdateSyncListDownload
	PlayerStateMachine.stUpdatingFromUSB.HandleUSBAssetFetcherEvent = HandleUSBAssetFetcherEvent

    PlayerStateMachine.stWaitForStorageDetached = PlayerStateMachine.newHState(bsp, "WaitForStorageDetached")
    PlayerStateMachine.stWaitForStorageDetached.HStateEventHandler = STWaitForStorageDetachedEventHandler
	PlayerStateMachine.stWaitForStorageDetached.superState = PlayerStateMachine.stTop

	PlayerStateMachine.topState = PlayerStateMachine.stTop
	
	return PlayerStateMachine
	
End Function


Function InitializePlayerStateMachine() As Object

	m.bsp.Restart("")

	' check for the presence of a USB drive with an update
	for n% = 1 to 9
		usb$ = "USB" + StripLeadingSpaces(stri(n%)) + ":"
		du = CreateObject("roStorageInfo", usb$)
		if type(du) = "roStorageInfo" then
			m.bsp.diagnostics.PrintDebug("### Disc mounted at " + usb$)
			if m.CheckForUSBUpdate(usb$) then
				m.storagePath$ = usb$
				return m.stUpdatingFromUSB
			endif
		endif
	next

	activeScheduledPresentation = m.bsp.schedule.activeScheduledEvent
	if type(activeScheduledPresentation) = "roAssociativeArray" then
	    return m.stPlaying
	else
	    return m.stWaiting
	endif

End Function


Sub InitiateRemoteSnapshotTimer()

	m.remoteSnapshotTimer = CreateObject("roTimer")
	m.remoteSnapshotTimer.SetPort(m.msgPort)
	m.remoteSnapshotTimer.SetElapsed(m.globalAA.remoteSnapshotInterval, 0) 
	m.remoteSnapshotTimer.Start()

End Sub


Sub RemoveRemoteSnapshotTimer()

	if type(m.remoteSnapshotTimer) = "roTimer" then
		m.remoteSnapshotTimer.Stop()
		m.remoteSnapshotTimer = invalid
	endif

End Sub


Function STPlayerEventHandler(event As Object, stateData As Object) As Object
    
    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
				
				if m.bsp.globalAA.enableRemoteSnapshot then
					m.bsp.InitiateRemoteSnapshotTimer()
				endif

                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
            else if event["EventType"] = "PREPARE_FOR_RESTART" then

                m.bsp.diagnostics.PrintDebug("STPlayerEventHandler - PREPARE_FOR_RESTART")

			    m.bsp.touchScreen = invalid
			    m.bsp.btManager.ResetPresentationBeacons()
                
                return "HANDLED"

			else if event["EventType"] = "SWITCH_PRESENTATION" then

				presentationName$ = event["Presentation"]

                m.bsp.diagnostics.PrintDebug("STPlayerEventHandler - Switch to presentation " + presentationName$)

				m.bsp.Restart(presentationName$)

				stateData.nextState = m.bsp.playerHSM.stPlaying

				return "TRANSITION"

            else if event["EventType"] = "CONTENT_UPDATED" then

                ' new content was downloaded from the network
                
                m.bsp.diagnostics.PrintDebug("STPlayerEventHandler - CONTENT_UPDATED")

				currentSyncSpec = CreateObject("roSyncSpec")
	
				if currentSyncSpec.ReadFromFile("current-sync.xml") then
	
					m.bsp.assetCollection = currentSyncSpec.GetAssets("download")
					m.bsp.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.assetPool, m.bsp.assetCollection)

				endif
				
				m.bsp.Restart("")
    
				activeScheduledPresentation = m.bsp.schedule.activeScheduledEvent
				if type(activeScheduledPresentation) = "roAssociativeArray" then
	    		    stateData.nextState = m.stateMachine.stPlaying
				else
	    		    stateData.nextState = m.stateMachine.stWaiting
				endif

		        return "TRANSITION"

            endif
            
        endif
        
	else if type(event) = "roControlDisconnected" then

    userData$ = "Port unspecified"
    if type(event.getUserData()) = "roString" then
      userData$ = event.getUserData()
    endif

		m.bsp.diagnostics.PrintDebug("### Control port disconnected: " + userData$ )
		m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_CONTROL_PORT_DISCONNECTED, userData$)
		m.bsp.logging.FlushLogFile()
  	RebootSystem()
		return "HANDLED"

	else if type(event) = "roDiskErrorEvent" then

		aa = event.GetDiskError()

		diskErrorReport$ = "Time: " + aa["Time"] + " Error: " + aa["source"] + " " + aa["error"] + " " + aa["device"] + " " + aa["param"]
		m.bsp.diagnostics.PrintDebug("STPlayerEventHandler: Disk error event received: " + diskErrorReport$)		
		m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_DISK_ERROR, diskErrorReport$)

		diskErrorMsg = CreateObject("roAssociativeArray")
		diskErrorMsg["EventType"] = "DISK_ERROR"
		diskErrorMsg["DiskError"] = aa
		m.bsp.msgPort.PostMessage(diskErrorMsg)

    else if type(event) = "roTimerEvent" then

		if type(m.bsp.remoteSnapshotTimer) = "roTimer" and stri(event.GetSourceIdentity()) = stri(m.bsp.remoteSnapshotTimer.GetIdentity()) then

			presentationName$ = ""
			if m.bsp.activePresentation$ <> invalid then
				presentationName$ = m.bsp.activePresentation$
			endif

			ok = TakeSnapshot(m.bsp.systemTime, presentationName$)

			m.bsp.remoteSnapshotTimer.SetElapsed(m.bsp.globalAA.remoteSnapshotInterval, 0) 
			m.bsp.remoteSnapshotTimer.Start()

		endif

		if type(m.stateMachine.timer) = "roTimer" and stri(event.GetSourceIdentity()) = stri(m.stateMachine.timer.GetIdentity()) then
			m.bsp.diagnostics.PrintDebug("STPlayerEventHandler timer event")
			
			if not m.bsp.PostponeRestart() then

				' send internal message to prepare for restart
				prepareForRestartEvent = CreateObject("roAssociativeArray")
				prepareForRestartEvent["EventType"] = "PREPARE_FOR_RESTART"
				m.bsp.msgPort.PostMessage(prepareForRestartEvent)

				' send internal message indicating that new content is available
				contentUpdatedEvent = CreateObject("roAssociativeArray")
				contentUpdatedEvent["EventType"] = "CONTENT_UPDATED"
				m.bsp.msgPort.PostMessage(contentUpdatedEvent)

            endif
			
			return "HANDLED"
		endif
			
        if type(m.bsp.logging.cutoverTimer) = "roTimer" then
        
            if stri(event.GetSourceIdentity()) = stri(m.bsp.logging.cutoverTimer.GetIdentity()) then

				m.bsp.diagnostics.PrintDebug("STPlayerEventHandler cutover logs timer event")

				m.bsp.logging.HandleTimerEvent()
				                
				m.bsp.LogActivePresentation()

                return "HANDLED"
                
            endif
            
        endif
		
		if type(m.bsp.serialPortsToRetry) = "roAssociativeArray" then

			for each serialPortToRetryName in m.bsp.serialPortsToRetry
				serialPortToRetry = m.bsp.serialPortsToRetry[serialPortToRetryName]
				timer = serialPortToRetry.timer
				if stri(event.GetSourceIdentity()) = stri(timer.GetIdentity()) then
					m.bsp.diagnostics.PrintDebug("RetryCreateSerial timeout")		
					ok = m.bsp.RetryCreateSerial(serialPortToRetry.port$, serialPortToRetry.outputOnly)
					if not ok then
						m.bsp.diagnostics.PrintDebug("RetryCreateSerial failure, restart timer")		
						timer.SetElapsed(15, 0)
						timer.Start()
					endif
				endif
			next
		endif

	else if type(event) = "roHdmiEdidChanged" then

		edid = m.bsp.videoMode.GetEdidIdentity(true)
		UpdateEdidValues(edid, m.bsp.sysInfo)
		edid = invalid

		m.bsp.UpdateEdidUserVariables(true)

		systemVariableChanged = CreateObject("roAssociativeArray")
		systemVariableChanged["EventType"] = "SYSTEM_VARIABLE_UPDATED"
		m.bsp.msgPort.PostMessage(systemVariableChanged)

    else if type(event) = "roAssetFetcherEvent" then

		userData$ = event.GetUserData()

		for each liveDataFeedName in m.bsp.liveDataFeeds
			if userData$ = liveDataFeedName then
				liveDataFeed = m.bsp.liveDataFeeds.Lookup(userData$)
				liveDataFeed.HandleLiveDataFeedContentDownloadAssetFetcherEvent(event)
				return "HANDLED"
			endif
		next

	else if type(event) = "roAssetFetcherProgressEvent" then

	    print "### File download progress ";event.GetFileName();" unknown"

		userData$ = event.GetUserData()

		for each liveDataFeedName in m.bsp.liveDataFeeds
			if userData$ = liveDataFeedName then
				liveDataFeed = m.bsp.liveDataFeeds.Lookup(userData$)
				liveDataFeed.HandleLiveDataFeedContentDownloadAssetFetcherProgressEvent(event)
				return "HANDLED"
			endif
		next

	else if type(event) = "roNetworkAttached" or type(event) = "roNetworkDetached" then
	
		networkInterface% = event.GetInt()

		if type(event) = "roNetworkAttached" then
			nc = CreateObject("roNetworkConfiguration", networkInterface%)
			if type(nc) = "roNetworkConfiguration" then
				currentConfig = nc.GetCurrentConfig()
				if type(currentConfig) = "roAssociativeArray" then
					if currentConfig.ip4_address <> "" then
						if networkInterface% = 0 then
							m.bsp.sysInfo.ipAddressWired$ = currentConfig.ip4_address
						else if networkInterface% = 1 then
							m.bsp.sysInfo.ipAddressWireless$ = currentConfig.ip4_address
						endif
					endif
				endif
			endif
			nc = invalid
		else
			if networkInterface% = 0 then
				m.bsp.sysInfo.ipAddressWired$ = "Invalid"
			else if networkInterface% = 1 then
				m.bsp.sysInfo.ipAddressWireless$ = "Invalid"
			endif
		endif

		m.bsp.UpdateIPAddressUserVariables(true)

		systemVariableChanged = CreateObject("roAssociativeArray")
		systemVariableChanged["EventType"] = "SYSTEM_VARIABLE_UPDATED"
		m.bsp.msgPort.PostMessage(systemVariableChanged)			

	else if type(event) = "roControlEvent" then
		
		eventIdentity = stri(event.GetSourceIdentity())

		blcIndex% = -1
		if type(m.bsp.blcDiagnostics[0]) = "roControlPort" and stri(m.bsp.blcDiagnostics[0].GetIdentity()) = eventIdentity then
			blcIndex% = 0
		else if type(m.bsp.blcDiagnostics[1]) = "roControlPort" and stri(m.bsp.blcDiagnostics[1].GetIdentity()) = eventIdentity then
			blcIndex% = 1
		else if type(m.bsp.blcDiagnostics[2]) = "roControlPort" and stri(m.bsp.blcDiagnostics[2].GetIdentity()) = eventIdentity then
			blcIndex% = 2
		endif

		if blcIndex% <> -1 then
			blcIdentifier$ = "BLC" + stri(blcIndex%) + ":"
		endif

		' event types coming back from the blc400
		REPORT_UNDER_EVENT%      = &h20
		REPORT_OVER_EVENT%       = &h21
		REPORT_MISSING%          = &h22
		REPORT_NORMAL%           = &h23

		' event ADC channels
		MAIN_ADC%        = 0
		LED_ADC_COMP1%   = 1
		LED_ADC_COMP2%   = 2
		LED_ADC_COMP3%   = 3
		LED_ADC_COMP4%   = 4
		LED_ADC_OCOMP1%  = 5
		LED_ADC_OCOMP2%  = 6
		LED_ADC_OCOMP3%  = 7
		LED_ADC_OCOMP4%  = 8

		ch% = event.GetEventByte(1)
		adc% = event.GetEventWord(2)

		if (event.GetEventByte(0) = REPORT_UNDER_EVENT%) then
			event$ = "REPORT_UNDER_EVENT: "
		else if (event.GetEventByte(0) = REPORT_OVER_EVENT%) then
			event$ = "REPORT_OVER_EVENT: "
		else if (event.GetEventByte(0) = REPORT_MISSING%) then
			event$ = "REPORT_MISSING: "
		else if (event.GetEventByte(0) = REPORT_NORMAL%) then
			event$ = "REPORT_NORMAL: "
		else
			event$ = ""
		endif

		nextChannel% = -1

		if event$ = "" then
			msg$ = blcIdentifier$ + "Unexpected control event(0): " + Stri(event.GetEventByte(0))
		else if (ch% = MAIN_ADC%) then
			msg$ = blcIdentifier$ + event$ + "Main Power: " + Stri(adc%)
			nextChannel% = &h01
		else if (ch% = LED_ADC_COMP1%) or (ch% = LED_ADC_OCOMP1%) then
			msg$ = blcIdentifier$ + event$ + "Channel A: " + Stri(adc%)
			nextChannel% = &h02
		else if (ch% = LED_ADC_COMP2%) or (ch% = LED_ADC_OCOMP2%) then
			msg$ = blcIdentifier$ + event$ + "Channel B: " + Stri(adc%)
			nextChannel% = &h04
		else if (ch% = LED_ADC_COMP3%) or (ch% = LED_ADC_OCOMP3%) then
			msg$ = blcIdentifier$ + event$ + "Channel C: " + Stri(adc%)
			nextChannel% = &h08
		else if (ch% = LED_ADC_COMP4%) or (ch% = LED_ADC_OCOMP4%) then
			msg$ = blcIdentifier$ + event$ + "Channel D: " + Stri(adc%)
		else
			msg$ = blcIdentifier$ + "Unknown Power Error: " + Stri(ch%) + ": " + Stri(adc%)
		endif

		m.bsp.diagnostics.PrintDebug(msg$)
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BLC400_STATUS, msg$)

		if nextChannel% <> -1 and blcIndex% <> -1 then
			m.bsp.CheckBLCStatus(m.bsp.blcs[blcIndex%], nextChannel%)
		endif

	else if type(event) = "roStorageAttached" then

		storagePath$ = event.GetString()

		' check for existence of upgrade file
		if m.stateMachine.CheckForUSBUpdate(storagePath$) then

			m.stateMachine.storagePath$ = storagePath$
			stateData.nextState = m.stateMachine.stUpdatingFromUSB
			return "TRANSITION"

		else
			actionsXMLFilePath$ = event.GetString() + "actions.xml"

			actionsSpec$ = ReadAsciiFile(actionsXMLFilePath$)
			if actionsSpec$ <> "" then

		        actionsXML = CreateObject("roXMLElement")
				actionsXML.Parse(actionsSpec$)

				if type(actionsXML.action) = "roXMLList" then

					if actionsXML.action.Count() > 0 then
					
						attributes = actionsXML.GetAttributes()
						displayStatus$ = attributes.Lookup("displayStatus")
						if lcase(displayStatus$) = "true" then

							videoMode = CreateObject("roVideoMode")
							resX = videoMode.GetResX()
							resY = videoMode.GetResY()
							videoMode = invalid

							r = CreateObject("roRectangle", 0, 0, resX, resY)
							twParams = CreateObject("roAssociativeArray")
							twParams.LineCount = 1
							twParams.TextMode = 2
							twParams.Rotation = 0
							twParams.Alignment = 1
							tw=CreateObject("roTextWidget",r,1,2,twParams)
							tw.PushString("")
							tw.Show()

							r=CreateObject("roRectangle",0,resY/2-resY/64,resX,resY/32)
							tw=CreateObject("roTextWidget",r,1,2,twParams)

							displayStatus = true

						else

							displayStatus = false
						
						endif

						deletedLogFiles = false

						errorEncountered = false

						for each action in actionsXML.action

							action$ = action.GetText()

							if action$ = "copyLogs" then

								if displayStatus then
									tw.Clear()
									tw.PushString("Copying log files.")
									tw.Show()
								endif

								ok = m.bsp.logging.CopyAllLogFiles(storagePath$)
								if ok then
									m.bsp.diagnostics.PrintDebug("CopyAllLogFiles completed successfully")
								else
									errorEncountered = true
									m.bsp.diagnostics.PrintDebug("CopyAllLogFiles failed")

									if displayStatus then

										tw.Clear()
										tw.PushString("Error encountered while copying log files.")
										tw.Show()

										sleep(5000)

									endif

									exit for
								endif

							else if action$ = "deleteLogs" then

								if displayStatus then
									tw.Clear()
									tw.PushString("Deleting log files.")
									tw.Show()
								endif

								m.bsp.logging.DeleteAllLogFiles()
								m.bsp.diagnostics.PrintDebug("DeleteAllLogFiles complete")
								deletedLogFiles = true

							else if action$ = "resetVariables" then

								if displayStatus then
									tw.Clear()
									tw.PushString("Resetting variables.")
									tw.Show()
								endif

								m.bsp.ResetVariables()

								m.bsp.diagnostics.PrintDebug("Resetting variables complete")

							else if action$ = "copyVariablesDB" then

								if m.bsp.variablesDBExists then

									if displayStatus then
										tw.Clear()
										tw.PushString("Copying variables database.")
										tw.Show()
									endif

									serialNumber$ = m.bsp.sysInfo.deviceUniqueID$

									dtLocal = m.bsp.systemTime.GetLocalDateTime()
									year$ = Right(stri(dtLocal.GetYear()), 2)
									month$ = StripLeadingSpaces(stri(dtLocal.GetMonth()))
									if len(month$) = 1 then
										month$ = "0" + month$
									endif
									day$ = StripLeadingSpaces(stri(dtLocal.GetDay()))
									if len(day$) = 1 then
										day$ = "0" + day$
									endif
									hour$ = StripLeadingSpaces(stri(dtLocal.GetHour()))
									if len(hour$) = 1 then
										hour$ = "0" + hour$
									endif
									minute$ = StripLeadingSpaces(stri(dtLocal.GetMinute()))
									if len(minute$) = 1 then
										minute$ = "0" + minute$
									endif
									'date$ = year$ + month$ + day$ + hour$ + minute$
									date$ = year$ + month$ + day$

									fileName$ = "BrightSignVariables." + serialNumber$ + "-" + date$ + ".txt"
									filePath$ = storagePath$ + fileName$

									variablesFile = CreateObject("roCreateFile", filePath$)
									
									if type(variablesFile) = "roCreateFile" then

										m.bsp.ExportVariablesDBToAsciiFile(variablesFile)

										' determine if write was successful
										' partial fix - only works if card was full before this step
										variablesFile.SeekToEnd()
										position% = variablesFile.CurrentPosition()
										if position% = 0 then
											errorEncountered = true
											m.bsp.diagnostics.PrintDebug("copyVariablesDB failed - fileLength = 0")
										else
											m.bsp.diagnostics.PrintDebug("Wrote variables file to " + filePath$)
										endif

										variablesFile = invalid

									else

										errorEncountered = true
										m.bsp.diagnostics.PrintDebug("copyVariablesDB failed - create file failed")

									endif

									if errorEncountered then

										if displayStatus then

											tw.Clear()
											tw.PushString("Error encountered while copying variables database.")
											tw.Show()

											sleep(5000)

										endif

										exit for

									endif

								else if displayStatus then

									tw.Clear()
									tw.PushString("No variables to copy.")
									tw.Show()

									sleep(3000)

								endif
							
							else if action$ = "reboot" then

								if displayStatus then
									tw.Clear()
									tw.PushString("Finalizing data writes, do not remove your drive yet.")
									tw.Show()

									EjectStorage(storagePath$)

									tw.Clear()
									tw.PushString("Data capture complete - you may remove your drive. The system will reboot shortly.")
									tw.Show()

									sleep(5000)
									tw.Clear()
								else
									EjectStorage(storagePath$)
								endif

								RebootSystem()

								return "HANDLED"

							endif
						next

						if displayStatus then

							tw.Clear()
							tw.PushString("Finalizing data writes, do not remove your drive yet.")
							tw.Show()

							EjectStorage(storagePath$)

							tw.Clear()

							if errorEncountered then
								tw.PushString("Data capture failed  - you may remove your drive.")
							else
								tw.PushString("Data capture completed successfully  - you may remove your drive.")
							endif

							tw.Show()

							sleep(5000)

							tw = invalid

						endif

						' if the log files were deleted but the system is not rebooting, open a log file
						if m.bsp.logging.loggingEnabled then m.bsp.logging.OpenOrCreateCurrentLog()

					endif
				endif
			endif
		endif

	else if type(event) = "roBtEvent" then

		m.bsp.btManager.HandleEvent(event)
		return "HANDLED"

	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Sub DeleteExcessSnapshots(globalAA As Object)

	while globalAA.listOfSnapshotFiles.Count() >= globalAA.remoteSnapshotMaxImages and globalAA.listOfSnapshotFiles.Count() > 0

		fileToDelete = globalAA.listOfSnapshotFiles.Shift()
		
		if type(globalAA.bsp.networkingHSM) = "roAssociativeArray" then
			if type(globalAA.bsp.networkingHSM.uploadSnapshotUrl) = "roUrlTransfer" then
				if lcase(globalAA.bsp.networkingHSM.uploadSnapshotUrl.getUserData().lookup("name")) = lcase(fileToDelete) then
					globalAA.bsp.diagnostics.PrintDebug("DeleteExcessSnapshots: roUrlTransfer in process for "+fileToDelete)
					globalAA.bsp.NetworkingHSM.uploadSnapshotUrl.AsyncCancel()

					globalAA.bsp.NetworkingHSM.uploadSnapshotUrl = invalid

					' and setup the retry timer
					if type(globalAA.bsp.networkingHSM.retrySnapshotUploadTimer) <> "roTimer" then
						globalAA.bsp.networkingHSM.retrySnapshotUploadTimer = CreateObject("roTimer")
						globalAA.bsp.networkingHSM.retrySnapshotUploadTimer.SetPort(globalAA.bsp.networkingHSM.msgPort)
						globalAA.bsp.networkingHSM.retrySnapshotUploadTimer.SetElapsed(30, 0)			'30 seconds seems to be what this gets set to other places.
						globalAA.bsp.networkingHSM.retrySnapshotUploadTimer.Start()
					endif
				endif
			endif

			if globalAA.bsp.networkingHSM.pendingSnapshotsToUpload.DoesExist(fileToDelete) then
				globalAA.bsp.diagnostics.PrintDebug("DeleteExcessSnapshots: removed from pendingSnapshotsToUpload: "+fileToDelete)
				globalAA.bsp.networkingHSM.pendingSnapshotsToUpload.Delete(fileToDelete)
			endif
		endif

		ok = DeleteFile("snapshots/" + fileToDelete)

	end while

End Sub


Sub TakeSnapshot(systemTime As Object, activePresentation$ As String)

	globalAA = GetGlobalAA()

	' before taking snapshot, delete the oldest if necessary
	DeleteExcessSnapshots(globalAA)

	videoMode = CreateObject("roVideoMode")

	' create a file name based on the current date/time
	dtLocal = systemTime.GetLocalDateTime()

	' strip illegal characters from string
	dateTime$ = GetISODateTimeString(dtLocal)

	aa = {}
	aa.filename = "snapshots/" + dateTime$ + ".jpg"
	aa.Width = videoMode.GetResX()
	aa.Height = videoMode.GetResY()
	aa.filetype = "JPEG"
	aa.quality = globalAA.remoteSnapshotJpegQualityLevel
	aa.Async = 0

	if type(activePresentation$) = "roString" then
		aa.description = activePresentation$
	endif

	if type(globalAA.bsp) = "roAssociativeArray" then
		globalAA.bsp.diagnostics.PrintDebug( "------------------------------------------- SCREENSHOT " + aa.filename + "-------------------------------------")
	endif
	ok = videoMode.Screenshot(aa)
	if not ok then
		if type(globalAA.bsp) = "roAssociativeArray" then
			globalAA.bsp.diagnostics.PrintDebug("TakeSnapshot: not ok returned from Screenshot")		
			globalAA.bsp.logging.WriteDiagnosticLogEntry(globalAA.bsp.diagnosticCodes.EVENT_SCREENSHOT_ERROR, "")
		endif
	else
		globalAA.listOfSnapshotFiles.push(dateTime$ + ".jpg")

		' upload snapshot to BSN if it's initialized
		if type(globalAA.bsp) = "roAssociativeArray" then
			snapshotCaptured = CreateObject("roAssociativeArray")
			snapshotCaptured["EventType"] = "SNAPSHOT_CAPTURED"
			snapshotCaptured["SnapshotName"] = dateTime$ + ".jpg"
			globalAA.bsp.msgPort.PostMessage(snapshotCaptured)			
		endif

	endif

	videoMode = invalid

End Sub


Function GetISODateTimeString(date As Object) As String

	isoDateTime$ = date.ToIsoString()

	index = instr(1, isoDateTime$, ",")
	if index >= 1 then
		isoDateTime$ = mid(isoDateTime$, 1, index - 1)
	endif

	return isoDateTime$

End Function


Sub QueueSnapshotForBSN(snapshotName$ As String, url As String)

'	systemTime = m.stateMachine.systemTime

	utcDateTime = m.stateMachine.systemTime.GetUtcDateTime()
	localDateTime = m.stateMachine.systemTime.GetLocalDateTime()

	headers = {}
	headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
	headers["host"] = m.stateMachine.AwsSqsHost
	headers["User-Agent"] = m.bsp.userAgent$
	headers["X-Amz-Content-SHA256"] = ""
	xAmzDate$ = Left(utcDateTime.ToIsoString(), 15) + "Z"
	headers["X-Amz-Date"] = xAmzDate$
	headers["x-amz-security-token"] = m.stateMachine.awsSessionToken$

	s0 = chr(34) + ":" + chr(34)
	s1 = chr(34) + ", " + chr(34)

	localTimestamp$ = FormatDateTime(localDateTime)	+ "0000"
	utcTimestamp$ = FormatDateTime(utcDateTime) + "0000Z"

	account$ = m.stateMachine.currentSync.LookupMetadata("server", "account")
	user$ = m.stateMachine.currentSync.LookupMetadata("server", "user")
	password$ = m.stateMachine.currentSync.LookupMetadata("server", "password")
	group$ = m.stateMachine.currentSync.LookupMetadata("server", "group")
	serialNumber$ = m.bsp.sysInfo.deviceUniqueID$

	if type(m.bsp.activePresentation$) = "roString" then
		presentationName$ = m.bsp.activePresentation$
	else
		presentationName$ = ""
	endif

	jsonString = "{" + chr(34) + "AccountName" + s0 + account$ + s1 + "DeviceSerial" + s0 + serialNumber$ + s1 + "Login" + s0 + user$ + s1 + "Password" + s0 + password$ + s1 + "GroupName" + s0 + group$ + s1 + "PresentationName" + s0 + presentationName$ + s1 + "ScreenshotUrl" + s0 + url + s1 + "LocalTimestamp" + s0 + localTimestamp$ + s1 + "UTCTimestamp" + s0 + utcTimestamp$ + chr(34) + " }"

	parameters = {}
	parameters["Action"] = "SendMessage"
	parameters["MessageBody"] = jsonString
	parameters["Version"] = "2012-11-05"

	payload = GetRequestPayload(parameters)

	headers["X-Amz-Content-SHA256"] = ComputeSHA256Hash(payload)

    canonicalRequest = CanonicalizeRequest(m.stateMachine.AwsSqsAbsolutePath, "POST", headers, headers["X-Amz-Content-SHA256"])

	signature = ComputeSignature(m.stateMachine.awsAccessKeyId$, m.stateMachine.awsSecretAccessKey$, m.stateMachine.AwsSqsRegion, utcDateTime, m.stateMachine.AwsSqsService, CanonicalizeHeaderNames(headers), canonicalRequest)

	authorizationHeader$ = "AWS4-HMAC-SHA256"
	authorizationHeader$ = authorizationHeader$ + " Credential" + "=" + m.stateMachine.awsAccessKeyId$ + "/" + FormatDateTimeyyyyMMdd(utcDateTime) + "/" + m.stateMachine.AwsSqsRegion + "/" + m.stateMachine.AwsSqsService + "/" + "aws4_request" + ","
	authorizationHeader$ = authorizationHeader$ + " SignedHeaders" + "=" + "content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-security-token" + ","
	authorizationHeader$ = authorizationHeader$ + " Signature" + "=" + signature
	
    headers["Authorization"] = authorizationHeader$

	m.stateMachine.queueSnapshotUrl = CreateObject("roUrlTransfer")
	m.stateMachine.queueSnapshotUrl.SetUserData(snapshotName$)
	m.stateMachine.queueSnapshotUrl.SetPort(m.stateMachine.msgPort)
	m.stateMachine.queueSnapshotUrl.SetUserAgent(m.bsp.userAgent$)

	requestHeaders = {}

	if not m.stateMachine.queueSnapshotUrl.AddHeader("Authorization", headers["Authorization"]) then stop
	if not m.stateMachine.queueSnapshotUrl.AddHeader("Content-Type", headers["Content-Type"]) then stop
	if not m.stateMachine.queueSnapshotUrl.AddHeader("x-amz-security-token", headers["x-amz-security-token"]) then stop
	if not m.stateMachine.queueSnapshotUrl.AddHeader("X-Amz-Date", headers["X-Amz-Date"]) then stop
	if not m.stateMachine.queueSnapshotUrl.AddHeader("X-Amz-Content-SHA256", headers["X-Amz-Content-SHA256"]) then stop

	if not m.stateMachine.queueSnapshotUrl.SetUrl(m.stateMachine.incomingDeviceScreenshotsQueue$) then stop
	
	aa = {}
	aa.method = "POST"
	aa.request_body_string = payload
	aa.response_body_string = true

	ok = m.stateMachine.queueSnapshotUrl.AsyncMethod(aa)
	if not ok then stop

End Sub


Function FormatDateTimeyyyyMMdd(dateTime As Object) As String

	dt$ = StripLeadingSpaces(stri(dateTime.GetYear())) + AddLeadingZeros(StripLeadingSpaces(stri(dateTime.GetMonth())), 2) + AddLeadingZeros(StripLeadingSpaces(stri(dateTime.GetDay())), 2) 
	return dt$

End Function


Function FormatDateTime(dt As Object) As String

	dt$ = StripLeadingSpaces(stri(dt.GetYear()))
	dt$ = dt$ + "-" + AddLeadingZeros(StripLeadingSpaces(stri(dt.GetMonth())), 2)
	dt$ = dt$ + "-" + AddLeadingZeros(StripLeadingSpaces(stri(dt.GetDay())), 2)
	dt$ = dt$ + "T" + AddLeadingZeros(StripLeadingSpaces(stri(dt.GetHour())), 2)
	dt$ = dt$ + ":" + AddLeadingZeros(StripLeadingSpaces(stri(dt.GetMinute())), 2)
	dt$ = dt$ + ":" + AddLeadingZeros(StripLeadingSpaces(stri(dt.GetSecond())), 2)
	dt$ = dt$ + "." + AddLeadingZeros(StripLeadingSpaces(stri(dt.GetMillisecond())), 3)

	return dt$

End Function


Function AddLeadingZeros(str$ As String, numDigits% As Integer) As String

	while len(str$) < numDigits%
		str$ = "0" + str$
	end while

	return str$

End Function


Function GetRequestPayload(parameters As Object)

' Action
' MessageBody
' Version

	xfer = CreateObject("roUrlTransfer")

	payload$ = ""

	payload$ = payload$ + GetPayloadItem(xfer, "Action", parameters)
	payload$ = payload$ + GetPayloadItem(xfer, "MessageBody", parameters)
	payload$ = payload$ + GetPayloadItem(xfer, "Version", parameters)

	payload$ = Left(payload$, len(payload$) - 1)

	return payload$

End Function


Function GetPayloadItem(xfer As Object, key As String, values As Object) As String

	payload$ = key
	payload$ = payload$ + "="
	payload$ = payload$ + xfer.Escape(values[key])
	payload$ = payload$ + "&"

	return payload$

End Function


Function ComputeSHA256Hash(str$ As String)

	bytes = CreateObject("roByteArray")
	bytes.FromAsciiString(str$)

	hashGenerator = CreateObject("roHashGenerator", "SHA256")
	hash = hashGenerator.hash(bytes)

	hex$ = hash.ToHexString()
	hex$ = lcase(hex$)

	return hex$

End Function


Function CanonicalizeRequest(resourcePath As String, httpMethod As String, headers As Object, precomputedBodyHash As String) As String

	canonicalRequest$ = ""
	canonicalRequest$ = canonicalRequest$ + httpMethod + chr(10)
	canonicalRequest$ = canonicalRequest$ + resourcePath + chr(10)
	canonicalRequest$ = canonicalRequest$ + chr(10)
	canonicalRequest$ = canonicalRequest$ + CanonicalizeHeaders(headers) + chr(10)
	canonicalRequest$ = canonicalRequest$ + CanonicalizeHeaderNames(headers) + chr(10)
	canonicalRequest$ = canonicalRequest$ + precomputedBodyHash

	return canonicalRequest$

End Function


Function CanonicalizeHeaders(headers As Object) As String

	' Content-Type
	' host
	' User-Agent
	' X-Amz-Content-SHA256
	' X-Amz-Date
	' x-amz-security-token

	canonicalHeaders$ = ""

	canonicalHeaders$ = canonicalHeaders$ + AddCanonicalHeader(headers, "Content-Type")
	canonicalHeaders$ = canonicalHeaders$ + AddCanonicalHeader(headers, "host")
	canonicalHeaders$ = canonicalHeaders$ + AddCanonicalHeader(headers, "User-Agent")
	canonicalHeaders$ = canonicalHeaders$ + AddCanonicalHeader(headers, "X-Amz-Content-SHA256")
	canonicalHeaders$ = canonicalHeaders$ + AddCanonicalHeader(headers, "X-Amz-Date")
	canonicalHeaders$ = canonicalHeaders$ + AddCanonicalHeader(headers, "x-amz-security-token")

	return canonicalHeaders$

End Function


Function AddCanonicalHeader(headers As Object, entry As String) As String

	canonicalHeader$ = lcase(entry)
	canonicalHeader$ = canonicalHeader$ + ":"
	canonicalHeader$ = canonicalHeader$ + headers[entry]
	canonicalHeader$ = canonicalHeader$ + chr(10)

	return canonicalHeader$

End Function


Function CanonicalizeHeaderNames(headers As Object) As String

' Content-Type
' host
' User-Agent
' X-Amz-Content-SHA256
' X-Amz-Date
' x-amz-security-token
' 
	canonicalHeaderNames$ = ""

	canonicalHeaderNames$ = canonicalHeaderNames$ + AddCanonicalHeaderName("Content-Type")
	canonicalHeaderNames$ = canonicalHeaderNames$ + AddCanonicalHeaderName("host")
	canonicalHeaderNames$ = canonicalHeaderNames$ + AddCanonicalHeaderName("User-Agent")
	canonicalHeaderNames$ = canonicalHeaderNames$ + AddCanonicalHeaderName("X-Amz-Content-SHA256")
	canonicalHeaderNames$ = canonicalHeaderNames$ + AddCanonicalHeaderName("X-Amz-Date")
	canonicalHeaderNames$ = canonicalHeaderNames$ + AddCanonicalHeaderName("x-amz-security-token")

	canonicalHeaderNames$ = Left(canonicalHeaderNames$, len(canonicalHeaderNames$) - 1)

	return canonicalHeaderNames$

End Function


Function AddCanonicalHeaderName(entry As String) As String

	canonicalHeaderName$ = lcase(entry)
	canonicalHeaderName$ = canonicalHeaderName$ + ";"

	return canonicalHeaderName$

End Function


Function ComputeSignature(awsAccessKey As String, awsSecretAccessKey As String, region As String, signedAt As Object, service As String, signedHeaders as String, canonicalRequest As String) As String

	dateStamp1 = FormatDateTimeyyyyMMdd(signedAt)
	scope = dateStamp1 + "/" + region + "/" + service + "/aws4_request"

	dateStamp2 = dateStamp1 + "T" + AddLeadingZeros(StripLeadingSpaces(stri(signedAt.GetHour())), 2) + AddLeadingZeros(StripLeadingSpaces(stri(signedAt.GetMinute())), 2) + AddLeadingZeros(StripLeadingSpaces(stri(signedAt.GetSecond())), 2) + "Z"
	stringToSign = "AWS4-HMAC-SHA256" + chr(10) + dateStamp2 + chr(10) + scope + chr(10)

	canonicalRequestHash = ComputeSHA256Hash(canonicalRequest)
	stringToSign = stringToSign + canonicalRequestHash

	key = ComposeSigningKey(awsSecretAccessKey, region, dateStamp1, service)

	stringToSignBytes = CreateObject("roByteArray")
	stringToSignBytes.FromAsciiString(stringToSign)
	keyedHash = ComputeKeyedHash(key, stringToSignBytes)
	keyedHash$ = lcase(keyedHash.ToHexString())

	return keyedHash$

End Function


Function ComposeSigningKey(awsSecretAccessKey As String, region As String, date As String, service As String) As Object

	ksecretBytes = CreateObject("roByteArray")
	ksecretBytes.FromAsciiString("AWS4" + awsSecretAccessKey)

	dateBytes = CreateObject("roByteArray")
	dateBytes.FromAsciiString(date)

	hashDate = ComputeKeyedHash(ksecretBytes, dateBytes)
	
	regionBytes = CreateObject("roByteArray")
	regionBytes.FromAsciiString(region)
	hashRegion = ComputeKeyedHash(hashDate, regionBytes)

	serviceBytes = CreateObject("roByteArray")
	serviceBytes.FromAsciiString(service)
	hashService = ComputeKeyedHash(hashRegion, serviceBytes)

	aws4RequestBytes = CreateObject("roByteArray")
	aws4RequestBytes.FromAsciiString("aws4_request")
	keyedHash = ComputeKeyedHash(hashService, aws4RequestBytes)

	return keyedHash

End Function


Function ComputeKeyedHash(key As Object, data As Object) As Object

	hashGenerator = CreateObject("roHashGenerator", "SHA256")
	ok = hashGenerator.SetHmacKey(key)
	if not ok then stop

	hash = hashGenerator.hash(data)

	return hash

End Function


Sub UploadSnapshotToSFN(snapshotName$ As String)

	headers = {}
	headers["Content-Type"] = "image/jpeg"

	headers["SFN-DeviceSerial"] = m.bsp.sysInfo.deviceUniqueID$

	if type(m.bsp.activePresentation$) = "roString" then
		presentationName$ = m.bsp.activePresentation$
	else
		presentationName$ = ""
	endif
	headers["SFN-PresentationName"] = presentationName$
	
	localDateTime = m.stateMachine.systemTime.GetLocalDateTime()
	headers["SFN-LocalTimestamp"] = FormatDateTime(localDateTime)

	utcDateTime = m.stateMachine.systemTime.GetUtcDateTime()
	headers["SFN-UTCTimestamp"] = FormatDateTime(utcDateTime) + "Z"

	snapshotFilePath$ = "snapshots/" + snapshotName$
	fileSize% = GetFileSize( snapshotFilePath$ )

	headers["Content-Length"] = stri(fileSize%)

	m.stateMachine.uploadSnapshotToSFNUrl = CreateObject("roUrlTransfer")
	m.stateMachine.uploadSnapshotToSFNUrl.SetPort(m.stateMachine.msgPort)
	m.stateMachine.uploadSnapshotToSFNUrl.SetTimeout(5000)
	m.stateMachine.uploadSnapshotToSFNUrl.SetUrl(m.stateMachine.uploadSnapshotsURL$)
	m.stateMachine.uploadSnapshotToSFNUrl.SetHeaders(headers)

	ok = m.stateMachine.uploadSnapshotToSFNUrl.AsyncPutFromFile(snapshotFilePath$)
	
End Sub


Sub UploadSnapshotToBSNEE(snapshotName$ As String)

	headers = {}
	headers["Content-Type"] = "image/jpeg"
'    request.KeepAlive = true;
'   headers["Connection"] = "Keep-Alive"

	headers["BSN-DeviceSerial"] = m.bsp.sysInfo.deviceUniqueID$
	headers["BSN-AccountName"] = m.stateMachine.currentSync.LookupMetadata("server", "account")
	headers["BSN-Login"] = m.stateMachine.currentSync.LookupMetadata("server", "user")
	headers["BSN-Password"] = m.stateMachine.currentSync.LookupMetadata("server", "password")
	headers["BSN-GroupName"] = m.stateMachine.currentSync.LookupMetadata("server", "group")

	if type(m.bsp.activePresentation$) = "roString" then
		presentationName$ = m.bsp.activePresentation$
	else
		presentationName$ = ""
	endif
	headers["BSN-PresentationName"] = presentationName$
	
	localDateTime = m.stateMachine.systemTime.GetLocalDateTime()
	headers["BSN-LocalTimestamp"] = FormatDateTime(localDateTime)

	utcDateTime = m.stateMachine.systemTime.GetUtcDateTime()
	headers["BSN-UTCTimestamp"] = FormatDateTime(utcDateTime) + "Z"

	headers["BSN-SecurityToken"] = m.stateMachine.securityToken

	snapshotFilePath$ = "snapshots/" + snapshotName$
	fileSize% = GetFileSize( snapshotFilePath$ )

	headers["Content-Length"] = stri(fileSize%)

	m.stateMachine.uploadSnapshotToBSNEEUrl = CreateObject("roUrlTransfer")
	m.stateMachine.uploadSnapshotToBSNEEUrl.SetPort(m.stateMachine.msgPort)
	m.stateMachine.uploadSnapshotToBSNEEUrl.SetTimeout(5000)
	m.stateMachine.uploadSnapshotToBSNEEUrl.SetUrl(m.stateMachine.uploadDeviceScreenshotHandlerAddress)
	m.stateMachine.uploadSnapshotToBSNEEUrl.SetHeaders(headers)
	m.stateMachine.uploadSnapshotToBSNEEUrl.SetUserAgent(m.bsp.userAgent$)
    if not m.stateMachine.uploadSnapshotToBSNEEUrl.AsyncPostFromFile(snapshotFilePath$) then stop

End Sub


Sub UploadSnapshotToBSN(snapshotName$ As String)

	if type(m.stateMachine.uploadSnapshotUrl) = "roUrlTransfer" then
		m.stateMachine.pendingSnapshotsToUpload.AddReplace(snapshotName$, snapshotName$)
		return
	endif

	globalAA = GetGlobalAA()

	snapshotFilePath$ = "snapshots/" + snapshotName$

	serialNumber$ = m.bsp.sysInfo.deviceUniqueID$

	photoTimestamp$ = GetPhotoTimestamp(snapshotName$)

	AWSResourceKey = m.stateMachine.AwsIncomingDirectory + serialNumber$ + "/" + photoTimestamp$ + ".jpg"

	headers = {}
	headers["Content-Type"] = "image/jpeg"
	headers["Content-Length"] = StripLeadingSpaces(stri(GetFileSize(snapshotFilePath$)))
	headers["x-amz-security-token"] = m.stateMachine.awsSessionToken$
	headers["X-Amz-Date"] = GetDateTime(m.stateMachine.systemTime.GetUtcDateTime())

	stringToSign$ = m.BuildStringToSign(headers, AWSResourceKey)

	auth$ = GetHMACSign(stringToSign$, m.stateMachine.awsSecretAccessKey$)

	authorization$ = "AWS " + m.stateMachine.awsAccessKeyId$ + ":" + auth$
	headers["Authorization"] = authorization$

	m.stateMachine.uploadSnapshotUrl = CreateObject("roUrlTransfer")

	' url to send snapshots to
	url = m.stateMachine.deviceScreenShotsTemporaryStorage$ + serialNumber$ + "/" + photoTimestamp$ + ".jpg"
	if not m.stateMachine.uploadSnapshotUrl.SetUrl(url) then stop

	snapshot = {}
	snapshot.name = snapshotName$
	snapshot.url = url
	m.stateMachine.uploadSnapshotUrl.SetUserData(snapshot)

	if not m.stateMachine.uploadSnapshotUrl.AddHeader("Authorization", headers["Authorization"]) then stop
	if not m.stateMachine.uploadSnapshotUrl.AddHeader("Content-Type", "image/jpeg") then stop
	if not m.stateMachine.uploadSnapshotUrl.AddHeader("x-amz-security-token", headers["x-amz-security-token"]) then stop
	if not m.stateMachine.uploadSnapshotUrl.AddHeader("X-Amz-Date", headers["X-Amz-Date"]) then stop

	m.stateMachine.uploadSnapshotUrl.SetPort(m.stateMachine.msgPort)
	m.stateMachine.uploadSnapshotUrl.SetTimeout(5000)
	m.stateMachine.uploadSnapshotUrl.SetUserAgent(m.bsp.userAgent$)

	ok = m.stateMachine.uploadSnapshotUrl.AsyncPutFromFile(snapshotFilePath$)
	if not ok then
        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SNAPSHOT_PUT_TO_SERVER_ERROR, m.stateMachine.uploadSnapshotUrl.GetFailureReason())
        m.bsp.diagnostics.PrintDebug("### AsyncPutFromFile failed: file " + snapshotFilePath$ + ", reason " + m.stateMachine.uploadSnapshotUrl.GetFailureReason())
	endif

End Sub


Function BuildStringToSign(headers As Object, AWSResourceKey As String) As String

	result$ = ""
	result$ = result$ + "PUT"
	result$ = result$ + chr(10)
	result$ = result$ + chr(10)
	result$ = result$ + headers["Content-Type"]
	result$ = result$ + chr(10)
	result$ = result$ + chr(10)

	result$ = result$ + "x-amz-date" + ":" + headers["X-Amz-Date"] + chr(10)
	result$ = result$ + "x-amz-security-token" + ":" + headers["x-amz-security-token"] + chr(10)

	result$ = result$ + "/" + m.stateMachine.AwsBucketName + "/" + AwsResourceKey

	return result$

End Function


Function GetHMACSign(data As String, key As String) As String

	binaryData = CreateObject("roByteArray")
	binaryData.FromAsciiString(data)

	keyData = CreateObject("roByteArray")
	keyData.FromAsciiString(key)

	hashGenerator = CreateObject("roHashGenerator", "SHA1")
	ok = hashGenerator.SetHmacKey(keyData)

	bytes = hashGenerator.hash(binaryData)
	signature = bytes.ToBase64String()
	return signature

End Function

' Return RFC1123 date/time string from roDateTime

Function GetDateTime(currentTime As Object) As String

	daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
	months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

	dayOfWeek$ = daysOfWeek[currentTime.GetDayOfWeek()]
	
	day$ = StripLeadingSpaces(stri(currentTime.GetDay()))
	if len(day$) = 1 then day$ = "0" + day$

	month$ = months[currentTime.GetMonth() - 1]

	year$ = StripLeadingSpaces(stri(currentTime.GetYear()))

	hour$ = StripLeadingSpaces(stri(currentTime.GetHour()))
	if len(hour$) = 1 then hour$ = "0" + hour$

	minute$ = StripLeadingSpaces(stri(currentTime.GetMinute()))
	if len(minute$) = 1 then minute$ = "0" + minute$

	second$ = StripLeadingSpaces(stri(currentTime.GetSecond()))
	if len(second$) = 1 then second$ = "0" + second$

	currentDateTime = dayOfWeek$ + ", " + day$ + " " + month$ + " " + year$ + " " + hour$ + ":" + minute$ + ":" + second$ + " GMT"

	return currentDateTime

End Function

' Parse an HTTP date/time value, as specified in RFC 2616 sec 3.3
' Preferred format is RFC1123, e.g., "Sun, 15 Mar 2016 08:15:10 GMT"

Function ParseHTTPDateTime(httpDateTime As String) As Object

	months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

	' Currently, handle only RFC 1123
	' TODO - handle the other 'obsolete' HTTP formats, RFC 850/1036, and ANSI C asctime()format

	' return current time if time string not valid
	systemTime = CreateObject("roSystemTime")
	dateTime = systemTime.GetUtcDateTime()
	dateTime.SetMillisecond(0)

    if mid(httpDateTime,4,1) = "," then
    	day% = val(mid(httpDateTime,6,2))
    	if day% > 0 and day% < 32 then
    		dateTime.SetDay(day%)
    	endif
    	httpMonth = mid(httpDateTime,9,3)
    	for month% = 0 to 11
    		if months[month%] = httpMonth then
    			dateTime.SetMonth(month% + 1)
    			exit for
    		endif
    	next
    	year% = val(mid(httpDateTime,13,4))
    	if year% > 0 and year% < dateTime.GetYear() then
    		dateTime.SetYear(year%)
    	endif
    	hour% = val(mid(httpDateTime,18,2))
    	if hour% < 24 then
    		dateTime.SetHour(hour%)
    	endif
    	minute% = val(mid(httpDateTime,21,2))
    	if minute% < 60 then
    		dateTime.SetMinute(minute%)
    	endif
    	second% = val(mid(httpDateTime,24,2))
    	if second% < 60 then
    		dateTime.SetSecond(second%)
    	endif
    endif

    return dateTime

End Function

Function GetPhotoTimestamp(snapshotName$ As String) As String

	year$ = mid(snapshotName$, 1, 4)
	month$ = mid(snapshotName$, 5, 2)
	day$ = mid(snapshotName$, 7, 2)
	hour$ = mid(snapshotName$, 10, 2)
	minute$ = mid(snapshotName$, 12, 2)
	second$ = mid(snapshotName$, 14, 2)
	photoTimestamp$ = year$ + "-" + month$ + "-" + day$ + "T" + hour$ + "-" + minute$ + "-" + second$ + ".0000000Z"
	return photoTimestamp$

End Function


Sub EjectStorage(storagePath$ As String)

	ok = EjectDrive(storagePath$)
	if not ok then
		sleep(30000)
	endif

End Sub


Function STWaitingEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				' set a timer for when the system should become active again
				if type(m.bsp.schedule.nextScheduledEventTime) = "roDateTime" then
					dateTime = m.bsp.schedule.nextScheduledEventTime
					newTimer = CreateObject("roTimer")
					newTimer.SetTime(dateTime.GetHour(), dateTime.GetMinute(), 0)
					newTimer.SetDate(dateTime.GetYear(), dateTime.GetMonth(), dateTime.GetDay())
					newTimer.SetDayOfWeek(dateTime.GetDayOfWeek())
					newTimer.SetPort(m.stateMachine.msgPort)
					newTimer.Start()
		            m.stateMachine.timer = newTimer
				endif
				
                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
            endif
            
        endif
        
    endif
            
    stateData.nextState = m.superState
    return "SUPER"
    
End Function


Function STPlayingEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				' set a timer for when the current presentation should end
				activeScheduledPresentation = m.bsp.schedule.activeScheduledEvent
			    
				if type(activeScheduledPresentation) = "roAssociativeArray" then
			        
					if m.bsp.schedule.activeScheduledEventEndDateTime <> invalid then
			        
						endDateTime = m.bsp.schedule.activeScheduledEventEndDateTime

						newTimer = CreateObject("roTimer")
						newTimer.SetTime(endDateTime.GetHour(), endDateTime.GetMinute(), 0)
						newTimer.SetDate(endDateTime.GetYear(), endDateTime.GetMonth(), endDateTime.GetDay())
						newTimer.SetDayOfWeek(endDateTime.GetDayOfWeek())
						newTimer.SetPort(m.stateMachine.msgPort)
						newTimer.Start()
			                            
						m.stateMachine.timer = newTimer

						m.bsp.diagnostics.PrintDebug("Set STPlayingEventHandler timer to " + endDateTime.GetString())

					endif

					' check for live data feeds that include content (either MRSS or content for Media Lists / PlayFiles). for each of them, check to see if the feed and/or content already exists.
					for each liveDataFeedName in m.bsp.liveDataFeeds
						liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedName)
						liveDataFeed.ReadFeedContent()
					next

					' load live data feeds
					m.liveDataFeeds = CreateObject("roAssociativeArray")
					m.bsp.liveDataFeedsByTimer = CreateObject("roAssociativeArray")

					' queue live data feeds for downloading
					m.bsp.liveDataFeedsToDownload = []
					for each liveDataFeedName in m.bsp.liveDataFeeds
						liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedName)
						m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
					next

					' launch playback
					m.bsp.StartPlayback()
					
				endif
				
                return "HANDLED"

            else if event["EventType"] = "UPDATE_DATA_FEED" then

				dataFeedName$ = event["Name"]
				for each liveDataFeedName in m.bsp.liveDataFeeds
					liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedName)
					if dataFeedName$ = liveDataFeedName then
						liveDataFeed.forceUpdate = true
						m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
					endif
				next

            else if event["EventType"] = "UPDATE_DATA_FEED_BY_CATEGORY" then

				categoryName$ = event["Name"]
				for each liveDataFeedName in m.bsp.liveDataFeeds
					liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedName)
					if liveDataFeed.title$ = categoryName$ then
						liveDataFeed.forceUpdate = true
						m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
					endif
				next

            else if event["EventType"] = "UPDATE_ALL_DATA_FEEDS" then

				for each liveDataFeedName in m.bsp.liveDataFeeds
					liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedName)
					if liveDataFeed.autoGenerateUserVariables then
						liveDataFeed.forceUpdate = true
						m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
					endif
				next

			else if event["EventType"] = "CONTENT_DATA_FEED_LOADED" then

				sign = m.bsp.sign

				for each zone in sign.zonesHSM
					for each stateName in zone.stateTable
						state = zone.stateTable[stateName]
						if state.type$ = "playFile" then
							if type(state.liveDataFeed) = "roAssociativeArray" and event["Name"] = state.liveDataFeed.name$ then
								if type(state.liveDataFeed.assetPoolFiles) = "roAssetPoolFiles" then
									state.PopulatePlayFileFromLiveDataFeed()
' following line is commented out as the code should fall through to the next if statement
'									return "HANDLED"
								endif
							endif
						endif
					next
				next

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
            endif
            
			if event["EventType"] = "MRSS_DATA_FEED_LOADED" or event["EventType"] = "CONTENT_DATA_FEED_LOADED" or event["EventType"] = "CONTENT_DATA_FEED_UNCHANGED" then

				m.bsp.AdvanceToNextLiveDataFeedInQueue(m.liveDataFeeds)

			endif

        endif

	endif

    if type(event) = "roTimerEvent" then
        
		eventIdentity$ = stri(event.GetSourceIdentity())

		if m.bsp.liveDataFeedsByTimer.DoesExist(eventIdentity$) then

			liveDataFeed = m.bsp.liveDataFeedsByTimer.Lookup(eventIdentity$)

			' if this feed's download failed, launch download of any pending feeds
			' if this isn't done, no new downloads are ever retrieved as the queue is never empty and there's never an event that causes the
			' next feed to be retrieved
			if lcase(type(liveDataFeed.lastDownloadedFailed)) = "roboolean" or lcase(type(liveDataFeed.lastDownloadedFailed)) = "boolean" then
			    if liveDataFeed.lastDownloadedFailed then
        			m.bsp.RetrievePendingLiveDataFeed(m.liveDataFeeds)
        		endif
            endif

            ' requeue the feed that failed
			m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)

		endif

    else if type(event) = "roUrlEvent" then

		eventIdentity$ = stri(event.GetSourceIdentity())

		if m.liveDataFeeds.DoesExist(eventIdentity$) then

			liveDataFeed = m.liveDataFeeds.Lookup(eventIdentity$)
			m.liveDataFeeds.Delete(eventIdentity$)
			if event.GetResponseCode() = 200 or event.GetResponseCode() = 0 then

				updateInterval% = 0

				headers = event.GetResponseHeaders()
				lastModifiedTime = invalid
				lastModifiedTimeStr = headers["Last-Modified"]
				if lastModifiedTimeStr <> invalid then
					lastModifiedTime = ParseHTTPDateTime(lastModifiedTimeStr)
				endif

                ' indicate that last download was successful
                liveDataFeed.lastDownloadedFailed = false

				if liveDataFeed.headRequest then

					getFeed = false
					if type(liveDataFeed.currentModifiedTime) <> "roDateTime" then
						' retrieve the feed if there is no currentModifiedTime stamp for the feed
						m.bsp.diagnostics.PrintDebug("### Check live data feed - no current timestamp - updating")
						getFeed = true
					else
						headers = event.GetResponseHeaders()
						lastModifiedTimeStr = headers["Last-Modified"]
						if lastModifiedTimeStr <> invalid then
							lastModifiedTime = ParseHTTPDateTime(lastModifiedTimeStr)
							if lastModifiedTime <> liveDataFeed.currentModifiedTime then
								m.bsp.diagnostics.PrintDebug("### Check live data feed - updating '" + liveDataFeed.name$ + "', last modified = " + lastModifiedTime.GetString() + ", currentModified = " + liveDataFeed.currentModifiedTime.GetString())
								' retrieve the feed if there is a Last-Modified header and it is greater than the modified time of the current feed
								getFeed = true
							else
								m.bsp.diagnostics.PrintDebug("### Check live data feed - no update needed for '" + liveDataFeed.name$ + "', last modified = " + lastModifiedTime.GetString() + ", currentModified = " + liveDataFeed.currentModifiedTime.GetString())
							endif
						else
							' retrieve the feed if there is no Last-Modified header
							m.bsp.diagnostics.PrintDebug("### Check live data feed - no Last-Modified header - updating")
							getFeed = true
						endif
					endif

					if getFeed then
						liveDataFeed.headRequest = false
						m.bsp.RetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
					else
						' No feed download needed, pop the item off the queue and see if something else is pending
						m.bsp.AdvanceToNextLiveDataFeedInQueue(m.liveDataFeeds)
						' Send next HEAD request after interval
						updateInterval% = liveDataFeed.updateInterval%
					endif

				else

					userVariables = m.bsp.currentUserVariables

					liveDataFeed.currentModifiedTime = lastModifiedTime

					if liveDataFeed.usage$ <> "mrss" and liveDataFeed.usage$ <> "mrsswith4k" then
						' simple RSS or content

						if liveDataFeed.usage$ = "content" and (liveDataFeed.isDynamicPlaylist or liveDataFeed.isLiveMediaFeed) then
							liveDataFeed.ParseMRSSFeed(liveDataFeed.rssFileName$)
							liveDataFeed.ConvertMRSSFormatToContent()
						else
							liveDataFeed.ParseSimpleRSSFeed(liveDataFeed.rssFileName$)
						endif

						' parsing for autogenerated user variables - make it conditional on using autogenerated user variables
						' check for uv parser. if exists, send rss feed, array of elements where each element is title, description, mediaURL. also need to get the title back (not sure how).
						' if no parser exists, parse it here, filling in the array as above. determine title of feed.
						liveDataFeed.items = CreateObject("roArray", 1, true)
						if liveDataFeed.autoGenerateUserVariables then
							if liveDataFeed.uvParser$ <> "" then
								ERR_NORMAL_END = &hFC
								retVal = Eval(liveDataFeed.uvParser$ + "(liveDataFeed.rssFileName$, liveDataFeed.items, userVariables, m.bsp)")
								if retVal <> ERR_NORMAL_END then
									' log the failure
									m.bsp.diagnostics.PrintDebug("Failure invoking Eval to parse live data feed for user variables: return value = " + stri(retVal) + ", parser is " + liveDataFeed.uvParser$)
									m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_LIVE_TEXT_PLUGIN_FAILURE, stri(retVal) + chr(9) + liveDataFeed.uvParser$)
								endif
							else
								dataFeedXML = CreateObject("roXMLElement")
								dataFeedXML.Parse(ReadAsciiFile(liveDataFeed.rssFileName$))
								liveDataFeed.title$ = dataFeedXML.channel.title.gettext()

								allItemsXML = dataFeedXML.channel.item
								position% = 0
								for each itemXML in allItemsXML
									item = {}
									item.title$ = itemXML.title.gettext()
									item.description$ = itemXML.description.gettext()

									mediaContent=itemXML.GetNamedElements("media:content")[0]
									if mediaContent = invalid then
										item.mediaUrl$ = ""
									else
										item.mediaUrl$= mediaContent.GetAttributes()["url"]
									endif

									item.position% = position%
									position% = position% + 1

									liveDataFeed.items.push(item)
								end for
							endif
						endif
					else
						' These must be valid objects even for MRSS feeds (at least for now)
						liveDataFeed.articles = CreateObject("roArray", 1, true)
						liveDataFeed.articleTitles = CreateObject("roArray", 1, true)
						liveDataFeed.articlesByTitle = CreateObject("roAssociativeArray")
					endif

					liveDataFeed.isMRSSFeed = liveDataFeed.FeedIsMRSS( liveDataFeed.rssFileName$ )

					if liveDataFeed.usage$ = "content" then
						liveDataFeed.DownloadLiveFeedContent()
					else if liveDataFeed.usage$ = "mrss" or liveDataFeed.usage$ = "mrsswith4k" and (liveDataFeed.parser$ <> "" or liveDataFeed.isMRSSFeed) then
						liveDataFeed.DownloadMRSSContent()
					endif

					if liveDataFeed.autoGenerateUserVariables then
						m.bsp.CreateUserVariablesFromDataFeed(liveDataFeed)
					endif

					DeleteFile(liveDataFeed.rssFileName$)

					' update user variables
					if type(userVariables) = "roAssociativeArray" then

						updatedUserVariables = { }

						for each title in liveDataFeed.articlesByTitle
							' update user variable if appropriate
							if userVariables.DoesExist(title) then
								userVariable = userVariables.Lookup(title)
								if type(userVariable.liveDataFeed) = "roAssociativeArray" and userVariable.liveDataFeed.name$ = liveDataFeed.name$ then
									description = liveDataFeed.articlesByTitle[title]
									userVariable.SetCurrentValue(description, true)
									updatedUserVariables.AddReplace(title, userVariable)
								endif
							endif
						next

						m.UpdateTimeClockEvents(updatedUserVariables)

					endif

					' send internal message indicating that the data feed has been updated
					liveTextDataUpdatedEvent = CreateObject("roAssociativeArray")
					liveTextDataUpdatedEvent["EventType"] = "LIVE_DATA_FEED_UPDATE"
					liveTextDataUpdatedEvent["EventData"] = liveDataFeed
					m.bsp.msgPort.PostMessage(liveTextDataUpdatedEvent)

					liveDataFeed.forceUpdate = false

					if liveDataFeed.useHeadRequest then
						' Set headRequest so that next call will be a HEAD call
						liveDataFeed.headRequest = true
					endif

					updateInterval% = liveDataFeed.updateInterval%

				end if   'feed download, not HEAD

			else
				url$ = liveDataFeed.url.GetCurrentParameterValue()
                m.bsp.diagnostics.PrintDebug("Failure downloading Live Text Data feed " + url$ + ", responseCode = " + stri(event.GetResponseCode()))
		        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_LIVE_TEXT_FEED_DOWNLOAD_FAILURE, url$ + chr(9) + stri(event.GetResponseCode()) + chr(9) + event.GetFailureReason())
			
				' send internal message indicating that the data feed download failed
				liveTextDataUpdatedEvent = CreateObject("roAssociativeArray")
				liveTextDataUpdatedEvent["EventType"] = "LIVE_DATA_FEED_UPDATE_FAILURE"
				liveTextDataUpdatedEvent["EventData"] = liveDataFeed
				m.bsp.msgPort.PostMessage(liveTextDataUpdatedEvent)

			    ' remove the failed feed from the queue in case there's a problem with the feed, but only if it was on the queue
			    if liveDataFeed.usage$ <> "text" then
				    m.bsp.RemoveFailedFeedFromQueue()
                endif

			    ' start a timer before attempting to retrieve the next feed in case there's a problem with the network
				updateInterval% = m.stateMachine.dataFeedRetryInterval%

                ' indicate that last download failed
                liveDataFeed.lastDownloadedFailed = true

			endif

			' set a timer to update this live data feed
			liveDataFeed.RestartLiveDataFeedDownloadTimer(updateInterval%)
		endif
		
    endif

    stateData.nextState = m.superState
    return "SUPER"
    
End Function


Sub UpdateDataFeed(parameters As Object)

	updateDataFeedParameter = parameters["dataFeed"]
	dataFeedName$ = updateDataFeedParameter.GetCurrentParameterValue()

	updateDataFeedMsg = CreateObject("roAssociativeArray")
	updateDataFeedMsg["EventType"] = "UPDATE_DATA_FEED"
	updateDataFeedMsg["Name"] = dataFeedName$
	m.msgPort.PostMessage(updateDataFeedMsg)

End Sub


Sub CreateUserVariablesFromDataFeed(liveDataFeed As Object)
	
	' get the section name
	if lcase(liveDataFeed.userVariableAccess$) = "shared" then
		sectionName$ = "Shared"
	else
		sectionName$ = m.activePresentation$
	endif

	sectionId% = m.GetDBSectionId(sectionName$)
	if sectionId% < 0 then
		m.AddDBSection(sectionName$)
		sectionId% = m.GetDBSectionId(sectionName$)
	endif

	categoryName$ = liveDataFeed.title$
	categoryId% = m.GetDBCategoryId(sectionId%, categoryName$)
	
	' desired behavour
	'	if the category does not exist, create it and populate it
	'	if the category does exist and the update interval is not Once
	'		add new user variables
	'		for matching user variables, update the current value but leave the default alone
	'		remove deleted user variables
	
	if categoryId% < 0 then

		m.AddDBCategory(sectionId%, categoryName$)
		categoryId% = m.GetDBCategoryId(sectionId%, categoryName$)

		for each item in liveDataFeed.items
			m.AddDBVariable(categoryId%, item.title$, item.description$, item.mediaUrl$, item.position%)
		next

	else if liveDataFeed.updateInterval% > 0 or liveDataFeed.forceUpdate then

		' get existing user variables for this section (and Shared) / category
		userVariablesList = m.GetUserVariablesByCategoryList(categoryName$)

		' populate an associative array with the items in the live data feed
		dataFeedItems = {}
		for each item in liveDataFeed.items
			dataFeedItems.AddReplace(item.title$, item)
		next

		' delete each user variable that exists but is not in the data feed; update variables that remain
		userVariables = {}
		for each userVariable in userVariablesList
			userVariableName$ = userVariable.name$
			userVariables.AddReplace(userVariableName$, userVariable)
			if not dataFeedItems.DoesExist(userVariableName$) then
				' delete variable
				m.DeleteDBVariable(categoryId%, userVariableName$)
			else
				' update variable
				dataFeedItem = dataFeedItems.Lookup(userVariableName$)
				m.UpdateDBVariable(categoryId%, dataFeedItem.title$, dataFeedItem.description$)
				m.UpdateDBVariableMediaUrl(categoryId%, dataFeedItem.title$, dataFeedItem.mediaUrl$)
				m.UpdateDBVariablePosition(categoryId%, dataFeedItem.title$, dataFeedItem.position%)
			endif
		next

		' add new variables
		for each item in liveDataFeed.items
			if not userVariables.DoesExist(item.title$) then
				m.AddDBVariable(categoryId%, item.title$, item.description$, item.mediaUrl$, item.position%)			
			endif
		next
			
	endif

End Sub


Function STUpdatingFromUSBEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

' stop all playback, clear screen and background
				if type(m.bsp.sign) = "roAssociativeArray" and type(m.bsp.sign.zonesHSM) = "roArray" then
					for each zoneHSM in m.bsp.sign.zonesHSM

						if IsAudioPlayer(zoneHSM.audioPlayer) then
							zoneHSM.audioPlayer.Stop()
							zoneHSM.audioPlayer = invalid
						endif

						if type(zoneHSM.videoPlayer) = "roVideoPlayer" then
							zoneHSM.videoPlayer.Stop()
							zoneHSM.videoPlayer = invalid
						endif

						zoneHSM.ClearImagePlane()

					next
				endif

				m.bsp.sign = invalid

				videoMode = CreateObject("roVideoMode")
				resX = videoMode.GetResX()
				resY = videoMode.GetResY()
				videoMode.SetBackgroundColor(0)                     
				videoMode = invalid

' display update message on the screen
				twParams = CreateObject("roAssociativeArray")
				twParams.LineCount = 1
				twParams.TextMode = 2
				twParams.Rotation = 0
				twParams.Alignment = 1

				r=CreateObject("roRectangle",0,resY/2-resY/64,resX,resY/32)
				m.stateMachine.usbUpdateTW = CreateObject("roTextWidget",r,1,2,twParams)

'				m.stateMachine.DisplayUSBUpdateStatus("Content update in progress. Do not remove the drive.")
				m.stateMachine.DisplayUSBUpdateStatus("Update in progress. Do not remove the drive.")

' read the sync specs and proceed with update if appropriate
				performingFWUpdate = false
				syncSpecFile$ = "/update/local-sync.xml"
				syncSpecFilePath$ = m.stateMachine.storagePath$ + syncSpecFile$
				if not FileExists(syncSpecFilePath$) then
					performingFWUpdate = true
					syncSpecFile$ = "/fwUpdate/fw-sync.xml"
					syncSpecFilePath$ = m.stateMachine.storagePath$ + syncSpecFile$
				endif

				m.stateMachine.newSync = CreateObject("roSyncSpec")
				ok = m.stateMachine.newSync.ReadFromFile(syncSpecFilePath$)
				if not ok then
					m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_READ_SYNCSPEC_FAILURE, "newSync")
					m.bsp.diagnostics.PrintDebug("### USB drive has an invalid sync spec.")
					usbUpdateErrorEvent = CreateObject("roAssociativeArray")
					usbUpdateErrorEvent["EventType"] = "USB_UPDATE_ERROR"
					usbUpdateErrorEvent["Message"] = "Update files are corrupt."
					m.stateMachine.msgPort.PostMessage(usbUpdateErrorEvent)
					return "HANDLED"
				endif

' perform security check
				usbContentUpdatePassword$ = m.bsp.registrySettings.usbContentUpdatePassword$

				' check for signature file
				signaturePath$ = m.stateMachine.storagePath$ + "/update/signature.txt"
				signatureFile = CreateObject("roReadFile", signaturePath$)
				if type(signatureFile) = "roReadFile" then
					signatureFileExists = true
					signature$ = ReadAsciiFile(signaturePath$)
				else
					signatureFileExists = false
				endif
				signatureFile = invalid

				securityError = false

				if not signatureFileExists then
					' no signature file and passphrase => error; no signature file and no passphrase => proceed with update
					if usbContentUpdatePassword$ <> "" then
						securityError = true
					endif
				else if usbContentUpdatePassword$ <> "" then
					ok = m.stateMachine.newSync.VerifySignature(signature$, usbContentUpdatePassword$)
					if not ok then
						securityError = true
					endif
				endif

				if securityError then
					m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_USB_UPDATE_SECURITY_ERROR, "local-sync")
					m.bsp.diagnostics.PrintDebug("### USB update security error.")
					usbUpdateErrorEvent = CreateObject("roAssociativeArray")
					usbUpdateErrorEvent["EventType"] = "USB_UPDATE_ERROR"
					usbUpdateErrorEvent["Message"] = "Update failed - an incorrect password was provided."
					m.stateMachine.msgPort.PostMessage(usbUpdateErrorEvent)
					return "HANDLED"
				endif

				if performingFWUpdate then

					globalAA = GetGlobalAA()
					globalAA.bsp.msgPort.DeferWatchdog(120)

					assetCollection = m.stateMachine.newSync.GetAssets("download")

					path$ = m.stateMachine.storagePath$ + "/fwUpdate/pool"
					pool = CreateObject("roAssetPool", path$)

					realizer = CreateObject("roAssetRealizer", pool, "/")

					event = realizer.Realize(assetCollection)

					' check return value
					if event.GetEvent() <> m.stateMachine.EVENT_REALIZE_SUCCESS then
						m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_REALIZE_FAILURE, stri(event.GetEvent()) + chr(9) + event.GetName() + chr(9) + event.GetFailureReason())
						m.bsp.diagnostics.PrintDebug("### Realize failed " + stri(event.GetEvent()) + chr(9) + event.GetName() + chr(9) + event.GetFailureReason() )
						m.stateMachine.waitForStorageDetachedMsg$ = "Update failure (Realize). Remove the drive and the system will reboot."

						usbTransitionEvent = CreateObject("roAssociativeArray")
						usbTransitionEvent["EventType"] = "USB_PERFORM_TRANSITION"
						m.stateMachine.msgPort.PostMessage(usbTransitionEvent)
						return "HANDLED"

					endif

					' write out a new sync spec file??

					m.bsp.diagnostics.PrintTimestamp()
					m.bsp.diagnostics.PrintDebug("### USB FIRWMARE UPDATE FILE DOWNLOAD COMPLETE")

					m.stateMachine.waitForStorageDetachedMsg$ = "Firmware update complete. Remove the drive and the system will reboot."

					usbTransitionEvent = CreateObject("roAssociativeArray")
					usbTransitionEvent["EventType"] = "USB_PERFORM_TRANSITION"
					m.stateMachine.msgPort.PostMessage(usbTransitionEvent)
					return "HANDLED"

				else

					m.stateMachine.currentSync = CreateObject("roSyncSpec")
					ok = m.stateMachine.currentSync.ReadFromFile("local-sync.xml") or m.stateMachine.currentSync.ReadFromFile("localSetupToStandalone-sync.xml")
					if not ok then
						m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_READ_SYNCSPEC_FAILURE, "local-sync")
						m.bsp.diagnostics.PrintDebug("### Unable to read local-sync.xml.")
						usbUpdateErrorEvent = CreateObject("roAssociativeArray")
						usbUpdateErrorEvent["EventType"] = "USB_UPDATE_ERROR"
						usbUpdateErrorEvent["Message"] = "Unable to perform update."
						m.stateMachine.msgPort.PostMessage(usbUpdateErrorEvent)
						return "HANDLED"
					endif

					if m.stateMachine.newSync.EqualTo(m.stateMachine.currentSync) then
						m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_RECEIVED, "NO")
						m.bsp.diagnostics.PrintDebug("### USB drive has a spec that matches current-sync. Nothing more to do.")
						m.stateMachine.newSync = invalid

						updateSyncSpecMatchesEvent = CreateObject("roAssociativeArray")
						updateSyncSpecMatchesEvent["EventType"] = "UPDATE_SYNC_SPEC_MATCHES"
						m.stateMachine.msgPort.PostMessage(updateSyncSpecMatchesEvent)
						return "HANDLED"
					endif

					m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_RECEIVED, "YES")

					m.BuildFileUpdateList(m.stateMachine.newSync)

					errorMsg = m.StartUpdateSyncListDownload()
					if type(errorMsg) = "roString" then
						usbUpdateErrorEvent = CreateObject("roAssociativeArray")
						usbUpdateErrorEvent["EventType"] = "USB_UPDATE_ERROR"
						usbUpdateErrorEvent["Message"] = errorMsg
						m.stateMachine.msgPort.PostMessage(usbUpdateErrorEvent)
					endif
				
				endif

				return "HANDLED"

            else if event["EventType"] = "USB_PERFORM_TRANSITION" then

 				stateData.nextState = m.stateMachine.stWaitForStorageDetached
				return "TRANSITION"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
            else if event["EventType"] = "UPDATE_SYNC_SPEC_MATCHES" then

				m.stateMachine.waitForStorageDetachedMsg$ = "The content on the USB drive matches the content on the card. Remove the drive and the system will reboot."
	            stateData.nextState = m.stateMachine.stWaitForStorageDetached
				return "TRANSITION"

            else if event["EventType"] = "USB_UPDATE_ERROR" then

				errorMsg$ = event["Message"]

				m.stateMachine.waitForStorageDetachedMsg$ = errorMsg$ + " Remove the drive and the system will reboot."
	            stateData.nextState = m.stateMachine.stWaitForStorageDetached
				return "TRANSITION"

            else if event["EventType"] = "PREPARE_FOR_RESTART" or event["EventType"] = "SWITCH_PRESENTATION" or event["EventType"] = "CONTENT_UPDATED" then ' consume these events during USB updates

				return "HANDLED"

            endif
            
        endif

    else if type(event) = "roTimerEvent" or type(event) = "roUrlEvent" then	' consume these events during USB updates

		return "HANDLED"

    else if type(event) = "roAssetFetcherProgressEvent" then

		m.bsp.diagnostics.PrintDebug("### File update progress " + event.GetFileName() + str(event.GetCurrentFilePercentage()))

		m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_FILE_DOWNLOAD_PROGRESS, event.GetFileName() + chr(9) + str(event.GetCurrentFilePercentage()))

		fileIndex% = event.GetFileIndex()
		fileItem = m.stateMachine.newSync.GetFile("download", fileIndex%)

		if event.GetCurrentFilePercentage() = 0 then
			m.stateMachine.DisplayUSBUpdateStatus("Downloading " + event.GetFileName() + " (" + StripLeadingSpaces(stri(fileIndex%)) + " of " + StripLeadingSpaces(stri(m.listOfUpdateFiles.Count())) + "). Do not remove the drive.")
		endif

        return "HANDLED"
        
    else if (type(event) = "roAssetFetcherEvent") then

		if event.GetUserData() = "USB" then	    

	        nextState = m.HandleUSBAssetFetcherEvent(event)
	        
            if type(nextState) = "roAssociativeArray" then
                stateData.nextState = nextState
	            return "TRANSITION"
            endif

            return "HANDLED"
            
	    endif

' this event is currently not received - the script gets the typical roAssetPoolEvent errors.	            
'	else if type(event) = "roStorageDetached" then

'		m.stateMachine.DisplayUSBUpdateStatus("The drive was removed before the update was complete - the system will reboot shortly.")
'		sleep(5000)
'	    RebootSystem()

    endif
            
    stateData.nextState = m.superState
    return "SUPER"
    
End Function


Sub BuildFileUpdateList(syncSpec As Object)

    fileInPoolStatus = m.bsp.assetPool.QueryFiles(syncSpec)

    m.listOfUpdateFiles = CreateObject("roArray", 10, true)

	for each fileName in fileInPoolStatus
	
		fileInPool = fileInPoolStatus.Lookup(fileName)
		if not fileInPool then
			m.listOfUpdateFiles.push(fileName)
		endif

	next

End Sub


Function StartUpdateSyncListDownload() As Object

    m.bsp.diagnostics.PrintDebug("### Start usb update sync list download")
    m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_DOWNLOAD_START, "")

	m.bsp.assetPool.ReserveMegabytes(50)

	m.assetFetcher = CreateObject("roAssetFetcher", m.bsp.assetPool)
	m.assetFetcher.SetUserData("USB")
    m.assetFetcher.SetPort(m.stateMachine.msgPort)
	m.assetFetcher.AddHeader("User-Agent",m.bsp.userAgent$)

	if not m.bsp.assetPool.ProtectAssets("USB", m.stateMachine.currentSync) then ' don't allow download to delete current files
		m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, "AssetPool Protect Failure")
		m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + "AssetPool Protect Failure")
        return "Update failure (ProtectFiles)."
    endif

	prefix$ = "file:///" + m.stateMachine.storagePath$ + "/update/"
	m.assetFetcher.SetRelativeLinkPrefix(prefix$)

    if not m.assetFetcher.AsyncDownload(m.stateMachine.newSync) then
        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_IMMEDIATE_FAILURE, m.assetFetcher.GetFailureReason())
        m.bsp.diagnostics.PrintDebug("### AsyncDownload failed: " + m.assetFetcher.GetFailureReason())
        return "Update failure (AsyncDownload)."
    endif

	return invalid

End Function


Function HandleUSBAssetFetcherEvent(event As Object) As Object

    m.bsp.diagnostics.PrintTimestamp()
    m.bsp.diagnostics.PrintDebug("### usb update pool_event")

	if (event.GetEvent() = m.stateMachine.POOL_EVENT_FILE_DOWNLOADED) then
        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_FILE_DOWNLOAD_COMPLETE, event.GetName())
        m.bsp.diagnostics.PrintDebug("### File downloaded " + event.GetName())
	else if (event.GetEvent() = m.stateMachine.POOL_EVENT_FILE_FAILED) then
        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_FILE_DOWNLOAD_FAILURE, event.GetName() + chr(9) + event.GetFailureReason())
        m.bsp.diagnostics.PrintDebug("### File failed " + event.GetName() + ": " + event.GetFailureReason())
	else if (event.GetEvent() = m.stateMachine.POOL_EVENT_ALL_FAILED) then
        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_FAILURE, event.GetFailureReason())		
        m.bsp.diagnostics.PrintDebug("### Sync failed: " + event.GetFailureReason())
		m.stateMachine.waitForStorageDetachedMsg$ = "Update failure (file failure). Remove the drive and the system will reboot."
        return m.stateMachine.stWaitForStorageDetached
	else if (event.GetEvent() = m.stateMachine.POOL_EVENT_ALL_DOWNLOADED) then
        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_DOWNLOAD_COMPLETE, "")		
        m.bsp.diagnostics.PrintDebug("### All files downloaded")

		oldSyncSpecScriptsOnly  = m.stateMachine.currentSync.FilterFiles("download", { group: "script" } )
		newSyncSpecScriptsOnly  = m.stateMachine.newSync.FilterFiles("download", { group: "script" } )

		rebootRequired = false

		if not oldSyncSpecScriptsOnly.FilesEqualTo(newSyncSpecScriptsOnly) then

			' Protect all the media files that the current sync spec is using in case we fail part way through and need to continue using it. 
			if not (m.bsp.assetPool.ProtectAssets("current", m.stateMachine.currentSync) and m.bsp.assetPool.ProtectAssets("new", m.stateMachine.newSync)) then
				m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, "AssetPool Protect Failure")
				m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + "AssetPool Protect Failure")
				m.stateMachine.waitForStorageDetachedMsg$ = "Update failure (ProtectFiles). Remove the drive and the system will reboot."
				return m.stateMachine.stWaitForStorageDetached
			endif   

			realizer = CreateObject("roAssetRealizer", m.bsp.assetPool, "/")
			globalAA = GetGlobalAA()
			globalAA.bsp.msgPort.DeferWatchdog(120)
			event = realizer.Realize(newSyncSpecScriptsOnly)
			realizer = invalid

			if event.GetEvent() <> m.stateMachine.EVENT_REALIZE_SUCCESS then
		        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_REALIZE_FAILURE, stri(event.GetEvent()) + chr(9) + event.GetName() + chr(9) + event.GetFailureReason())
				m.bsp.diagnostics.PrintDebug("### Realize failed " + stri(event.GetEvent()) + chr(9) + event.GetName() + chr(9) + event.GetFailureReason() )
				m.stateMachine.waitForStorageDetachedMsg$ = "Update failure (Realize). Remove the drive and the system will reboot."
				return m.stateMachine.stWaitForStorageDetached
			endif

		endif

' Save to current-sync.xml then do cleanup
	    if not m.stateMachine.newSync.WriteToFile("local-sync.xml") then stop

        m.bsp.diagnostics.PrintTimestamp()
        m.bsp.diagnostics.PrintDebug("### USB UPDATE FILE DOWNLOAD COMPLETE")

		m.stateMachine.waitForStorageDetachedMsg$ = "Content update complete. Remove the drive and the system will reboot."
        return m.stateMachine.stWaitForStorageDetached

	endif

	return invalid

End Function


Function STWaitForStorageDetachedEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

' check to see if the drive is still in the device
				du = CreateObject("roStorageInfo", m.stateMachine.storagePath$)
				if type(du) = "roStorageInfo" then
					m.stateMachine.DisplayUSBUpdateStatus(m.stateMachine.waitForStorageDetachedMsg$)
				else
					m.stateMachine.DisplayUSBUpdateStatus("The drive was removed before the update was complete - the system will reboot shortly.")
					sleep(5000)
					RebootSystem()
				endif

                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
            else if event["EventType"] = "PREPARE_FOR_RESTART" or event["EventType"] = "SWITCH_PRESENTATION" or event["EventType"] = "CONTENT_UPDATED" then ' consume these events during USB updates

				return "HANDLED"

			endif
			            
        endif
        
    else if type(event) = "roTimerEvent" or type(event) = "roUrlEvent" then	' consume these events during USB updates

		return "HANDLED"

	else if type(event) = "roStorageDetached" then

		m.stateMachine.logging.FlushLogFile()
	    RebootSystem()

	endif
	            
    stateData.nextState = m.superState
    return "SUPER"
    
End Function


Function CheckForUSBUpdate(storagePath$ As String) As Object

	syncSpecFilePath$ = storagePath$ + "/update/local-sync.xml"
	if FileExists(syncSpecFilePath$) return true

	syncSpecFilePath$ = storagePath$ + "/fwUpdate/fw-sync.xml"
	if FileExists(syncSpecFilePath$) return true

	return false

End Function


Sub DisplayUSBUpdateStatus(status$ As String)

	m.usbUpdateTW.Clear()
	m.usbUpdateTW.PushString(status$)
	m.usbUpdateTW.Show()

End Sub


Sub UpdateTimeClockEvents(updatedUserVariables As Object)

' m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_LIVE_TEXT_FEED_DOWNLOAD_FAILURE, url$ + chr(9) + stri(event.GetResponseCode()) + chr(9) + event.GetFailureReason())

	if type(m.bsp.sign) = "roAssociativeArray" then
		sign = m.bsp.sign
		if type(sign.zonesHSM) = "roArray" then
			for each zoneHSM in sign.zonesHSM
				if type(zoneHSM.activeState) = "roAssociativeArray" then
					activeState = zoneHSM.activeState
					if type(activeState.timeClockEvents) = "roArray" then
						for each timeClockEvent in activeState.timeClockEvents
							if type(timeClockEvent.userVariable) = "roAssociativeArray" then
								updatedUserVariable = updatedUserVariables.Lookup(timeClockEvent.userVariableName$)
								if type(updatedUserVariable) = "roAssociativeArray" then
									dateTime$ = updatedUserVariable.GetCurrentValue()
									dateTime = FixDateTime(dateTime$)
									if type(dateTime) = "roDateTime" then
										' if timer is in the future, set it.
										if IsTimeoutInFuture(dateTime)
											setTimer = true
									        m.bsp.diagnostics.PrintDebug("Set timeout to " + dateTime.GetString())
										else
											setTimer = false
										endif

										if type(timeClockEvent.timer) = "roTimer" then
											timeClockEvent.timer.Stop()
										else if setTimer then
											timeClockEvent.timer = CreateObject("roTimer")
										endif

										if setTimer then
											timeClockEvent.timer.SetDateTime(dateTime)
											timeClockEvent.timer.SetPort(zoneHSM.msgPort)
											timeClockEvent.timer.Start()
										endif
									else
								        m.bsp.diagnostics.PrintDebug("Timeout specification " + dateTime$ + " is invalid")
										m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_INVALID_DATE_TIME_SPEC, dateTime$)
									endif
								endif
							endif
						next
					endif
				endif
			next
		endif
	endif

End Sub

Function GenerateNonce(systemTime As Object) As String

	' Nonce just needs to be a reasonably unique alphanumeric string
	bytes = CreateObject("roByteArray")
	bytes.FromAsciiString(systemTime.GetUtcDateTime().GetString())
	nonce = bytes.ToBase64String()
	' Remove non word chars - just replace with arbitrary character
	rx = CreateObject("roRegEx","\W","")
	return rx.ReplaceAll(nonce,"z")

End Function


Function GenerateTimestamp(systemTime As Object) As String

	return systemTime.GetUtcDateTime().ToSecondsSinceEpoch().ToStr()
	
End Function


Function GenerateOAuthSignature(urlTransfer As Object, authenticationData As Object, nonce As String, timestamp As String) As String

	url$ = urlTransfer.GetUrl()
	' Generate sorted array of all parameters (header and query string)
	paramArray = CreateObject("roArray", 8, TRUE)
	' First, get parameters from URL query string
	queryIndex = instr(1, url$, "?")
	if queryIndex > 0 then
		params = mid(url$, queryIndex+1).tokenize("&")
		for each param in params
			nameval = param.tokenize("=")
			if nameval.Count() > 1 then
				paramItem = CreateObject("roAssociativeArray")
				paramItem.name = nameVal[0]
				paramItem.value = nameVal[1]
				paramArray.push(paramItem)
			endif
		next
	endif
	' Next, add the oauth parameters
	paramArray.push( { name: "oauth_consumer_key", value: authenticationData.ConsumerKey } )
	paramArray.push( { name: "oauth_nonce", value: urlTransfer.Escape(nonce) } )
	paramArray.push( { name: "oauth_signature_method", value: "HMAC-SHA1" } )
	paramArray.push( { name: "oauth_timestamp", value: timestamp } )
	paramArray.push( { name: "oauth_token", value: urlTransfer.Escape(authenticationData.AuthToken) } )
	paramArray.push( { name: "oauth_version", value: "1.0" } )
	
	' Now sort the parameter array
	max = paramArray.Count()
	sortedParamArray = CreateObject("roArray", max, FALSE)
	while (paramArray.Count() > 0)
		index = 0
		for i = 1 to paramArray.Count()-1
			if paramArray[i].name < paramArray[index].name then
				index = i
			endif
		end for
		sortedParamArray.push(paramArray[index])
		paramArray.Delete(index)
	end while
	
	' normalized parameter string
	normParams$ = ""
	for i = 0 to sortedParamArray.Count()-1
		normParams$ = normParams$ + urlTransfer.Escape(sortedParamArray[i].name) + "=" + urlTransfer.Escape(sortedParamArray[i].value)
		if i < sortedParamArray.Count()-1 then
			normParams$ = normParams$ + "&"
		endif
	end for
	
	' create signature base string
	if authenticationData.DoesExist("HttpMethod") and type(authenticationData.HttpMethod) = "roString" then
		sigBase$ = authenticationData.HttpMethod + "&"
	else
		sigBase$ = "GET&"
	endif
	
	if (queryIndex > 0)
		normUrl$ = left(url$,queryIndex-1)
	else
		normUrl$ = url$
	endif
	sigBase$ = sigBase$ + urlTransfer.Escape(normUrl$) + "&" + urlTransfer.Escape(normParams$) 
	
	'print "OAuth base string: " + sigBase$
	
	hashGen = CreateObject("roHashGenerator", "SHA1")
	hashGen.SetObfuscatedHmacKey(authenticationData.EncryptedTwitterSecrets)
	' get hash - we will NOT escape this here - that will be done when we generate the header
	hashStr$ = hashGen.hash(sigBase$).ToBase64String()
	
	return hashStr$

End Function

Function GetOAuthAuthorizationHeader(urlTransfer As Object, authenticationData As Object) As String

	systemTime = CreateObject("roSystemTime")    
	nonce = GenerateNonce(systemTime)
	timestamp = GenerateTimestamp(systemTime)

	s = "OAuth "
	s = s + "oauth_consumer_key=" + chr(34) + urlTransfer.Escape(authenticationData.ConsumerKey) + chr(34) + ","
	s = s + "oauth_nonce=" + chr(34) + nonce + chr(34) + ","
	s = s + "oauth_signature=" + chr(34) + urlTransfer.Escape(GenerateOAuthSignature(urlTransfer, authenticationData, nonce, timestamp)) + chr(34) + ","
	s = s + "oauth_signature_method=" + chr(34) + "HMAC-SHA1" + chr(34) + ","
	s = s + "oauth_timestamp=" + chr(34) + timestamp + chr(34) + ","
	s = s + "oauth_token=" + chr(34) + urlTransfer.Escape(authenticationData.AuthToken) + chr(34) + ","
	s = s + "oauth_version=" + chr(34) + "1.0" + chr(34)
	
	'print "Auth header: " + s

	return s

End Function


Sub RemoveFailedFeedFromQueue()
	' remove failed feed - it will get added back to queue when retry timeout occurs
	failedFeed = m.liveDataFeedsToDownload.Shift()
End Sub


Sub RetrievePendingLiveDataFeed(liveDataFeeds As Object)
	if m.liveDataFeedsToDownload.Count() > 0 then
		m.RetrieveLiveDataFeed(liveDataFeeds, m.liveDataFeeds[m.liveDataFeedsToDownload[0]])
	endif
End Sub


Sub QueueRetrieveLiveDataFeed(liveDataFeeds As Object, liveDataFeed As Object)

' for download feeds that are neither MRSS nor content immediately (simple RSS)
	if liveDataFeed.usage$ = "text" then
		m.RetrieveLiveDataFeed(liveDataFeeds, liveDataFeed)
	else
		m.liveDataFeedsToDownload.push(liveDataFeed.name$)

		' launch download of first feed
		if m.liveDataFeedsToDownload.Count() = 1 then
			m.RetrieveLiveDataFeed(liveDataFeeds, liveDataFeed)
		endif
	endif

End Sub

Sub AdvanceToNextLiveDataFeedInQueue(liveDataFeeds as Object)
	' Remove top entry
	m.liveDataFeedsToDownload.Shift()

	if m.liveDataFeedsToDownload.Count() > 0 then
		liveDataFeedName = m.liveDataFeedsToDownload[0]
		liveDataFeed = m.liveDataFeeds.Lookup(liveDataFeedName)
		m.RetrieveLiveDataFeed(liveDataFeeds, liveDataFeed)
	endif
End Sub

Sub RetrieveLiveDataFeed(liveDataFeeds As Object, liveDataFeed As Object)

	url$ = liveDataFeed.url.GetCurrentParameterValue()
	auth = liveDataFeed.authenticationData

	if liveDataFeed.headRequest then
    	m.diagnostics.PrintDebug("### Checking live text data feed from " + url$)    
    	m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_CHECK_LIVE_TEXT_FEED_HEAD, url$)
	else
    	m.diagnostics.PrintDebug("### Retrieve live text data feed from " + url$)    
    	m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_RETRIEVE_LIVE_TEXT_FEED, url$)
    endif

	liveDataFeed.rssURLXfer = CreateObject("roUrlTransfer")
	liveDataFeed.rssURLXfer.SetUrl(url$)
	liveDataFeed.rssURLXfer.SetPort(m.msgPort)
	if not liveDataFeed.headRequest then
		liveDataFeed.rssFileName$ = m.GetRSSTempFilename()
	end if
	liveDataFeed.rssURLXfer.SetTimeout(55000) ' 55 second timeout

	' Set User agent string - see in there is a custom parser function for the user agent
	userAgent$ = ""
	if liveDataFeed.customUserAgent$ <> "" then
		data = {userAgent: m.userAgent$}
		retVal = Eval(liveDataFeed.customUserAgent$ + "(m, data)")
		ERR_NORMAL_END = &hFC
		if retVal = ERR_NORMAL_END then
			if IsString(data.userAgent) then
				userAgent$ = data.userAgent
				m.diagnostics.PrintDebug("Using custom user agent string for " + liveDataFeed.name$ + ": "+ userAgent$)
			endif
		else
			' log the failure
			m.diagnostics.PrintDebug("Failure invoking Eval to parse custom User Agent for data feed: return value = " + stri(retVal) + ", parser is " + liveDataFeed.customUserAgent$)
			m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_CUSTOM_USER_AGENT_FAILURE, stri(retVal) + chr(9) + liveDataFeed.customUserAgent$)
		endif
	endif
	if userAgent$ <> "" then
		liveDataFeed.rssURLXfer.SetUserAgent(userAgent$)
	else
		liveDataFeed.rssURLXfer.SetUserAgent(m.userAgent$)
	endif

	' Set authorization header, if authentication data is present
	if type(auth) = "roAssociativeArray" and type(auth.AuthType) = "roString" then
		if auth.AuthType = "OAuth 1.0a" then
			' Set OAuth header
			if not liveDataFeed.rssURLXfer.AddHeader("Authorization", GetOAuthAuthorizationHeader(liveDataFeed.rssURLXfer, auth)) then
				m.diagnostics.PrintDebug("Failed to set authorization header, reason: " + liveDataFeed.rssURLXfer.GetFailureReason())
			endif
		endif
	endif

	binding% = GetBinding(m.textFeedsXfersEnabledWired, m.textFeedsXfersEnabledWireless)
    m.diagnostics.PrintDebug("### Binding for RetrieveLiveDataFeed is " + stri(binding%))
	ok = liveDataFeed.rssURLXfer.BindToInterface(binding%)
	if not ok then stop
	if liveDataFeed.headRequest then
		liveDataFeed.rssURLXfer.AsyncHead()
	else
		liveDataFeed.rssURLXfer.AsyncGetToFile(liveDataFeed.rssFileName$)
	end if
	liveDataFeeds.AddReplace(stri(liveDataFeed.rssURLXfer.GetIdentity()), liveDataFeed)

End Sub


Function GetRSSDownloadInterval(rssDownloadSpec As Object) As Integer

	rssDownloadPeriodicValue% = 86400

    if type(rssDownloadSpec) = "roXMLList" then
    
        if rssDownloadSpec.Count() > 0 then
        
            rssDownloadSpecAttrs = rssDownloadSpec.GetAttributes()
            rssDownloadSpecType = rssDownloadSpecAttrs["type"]
        
            if rssDownloadSpecType = "periodic" then
                rssDownloadPeriodicValue% = val(rssDownloadSpecAttrs["value"])
            endif
        endif
    endif

	return rssDownloadPeriodicValue%

End Function

'endregion

'region Networking State Machine
' *************************************************
'
' Networking State Machine
'
' *************************************************
Function newNetworkingStateMachine(bsp As Object, msgPort As Object) As Object

    NetworkingStateMachine = newHSM()
    NetworkingStateMachine.InitialPseudostateHandler = InitializeNetworkingStateMachine

	NetworkingStateMachine.bsp = bsp
	NetworkingStateMachine.msgPort = msgPort
	NetworkingStateMachine.systemTime = bsp.systemTime
	NetworkingStateMachine.diagnostics = bsp.diagnostics
	NetworkingStateMachine.logging = bsp.logging

    NetworkingStateMachine.RestartContentDownloadWindowStartTimer = RestartContentDownloadWindowStartTimer
    NetworkingStateMachine.RestartContentDownloadWindowEndTimer = RestartContentDownloadWindowEndTimer
    NetworkingStateMachine.RestartHeartbeatsWindowStartTimer = RestartHeartbeatsWindowStartTimer
    NetworkingStateMachine.RestartHeartbeatsWindowEndTimer = RestartHeartbeatsWindowEndTimer
	NetworkingStateMachine.RestartWindowStartTimer = RestartWindowStartTimer
	NetworkingStateMachine.RestartWindowEndTimer = RestartWindowEndTimer

    NetworkingStateMachine.SetSystemInfo = SetSystemInfo
    NetworkingStateMachine.AddMiscellaneousHeaders = AddMiscellaneousHeaders
	NetworkingStateMachine.AddLocalToBSNHeaders = AddLocalToBSNHeaders

    NetworkingStateMachine.DeviceDownloadItems = CreateObject("roArray", 8, true)
    NetworkingStateMachine.DeviceDownloadItemsPendingUpload = CreateObject("roArray", 8, true)
    NetworkingStateMachine.AddDeviceDownloadItem = AddDeviceDownloadItem
    NetworkingStateMachine.UploadDeviceDownload = UploadDeviceDownload

	NetworkingStateMachine.FileListPendingUpload = true
    NetworkingStateMachine.DeviceDownloadProgressItems = CreateObject("roAssociativeArray")
    NetworkingStateMachine.DeviceDownloadProgressItemsPendingUpload = CreateObject("roAssociativeArray")
	NetworkingStateMachine.PushDeviceDownloadProgressItem = PushDeviceDownloadProgressItem
    NetworkingStateMachine.AddDeviceDownloadProgressItem = AddDeviceDownloadProgressItem
    NetworkingStateMachine.UploadDeviceDownloadProgressItems = UploadDeviceDownloadProgressItems
    NetworkingStateMachine.UploadDeviceDownloadProgressFileList = UploadDeviceDownloadProgressFileList
	NetworkingStateMachine.BuildFileDownloadList = BuildFileDownloadList
    
	NetworkingStateMachine.SendTrafficUpload = SendTrafficUpload
    NetworkingStateMachine.UploadTrafficDownload = UploadTrafficDownload
    NetworkingStateMachine.UploadMRSSTrafficDownload = UploadMRSSTrafficDownload
	NetworkingStateMachine.pendingMRSSContentDownloaded# = 0
    NetworkingStateMachine.lastMRSSContentDownloaded# = 0
    
    NetworkingStateMachine.EventItems = CreateObject("roArray", 8, true)
    NetworkingStateMachine.AddEventItem = AddEventItem
    NetworkingStateMachine.UploadEvent = UploadEvent
        
    NetworkingStateMachine.DeviceErrorItems = CreateObject("roArray", 8, true)
    NetworkingStateMachine.AddDeviceErrorItem = AddDeviceErrorItem
    NetworkingStateMachine.UploadDeviceError = UploadDeviceError
       
    NetworkingStateMachine.deviceDownloadProgressUploadURL = invalid
    NetworkingStateMachine.deviceDownloadUploadURL = invalid
    NetworkingStateMachine.trafficDownloadUploadURL = invalid
    NetworkingStateMachine.mrssTrafficDownloadUploadURL = invalid
    NetworkingStateMachine.eventUploadURL = invalid
    NetworkingStateMachine.deviceErrorUploadURL = invalid

	NetworkingStateMachine.LogProtectFilesFailure = LogProtectFilesFailure

' logging
    NetworkingStateMachine.UploadLogFiles = UploadLogFiles
    NetworkingStateMachine.UploadLogFileHandler = UploadLogFileHandler
    NetworkingStateMachine.uploadLogFileURLXfer = invalid
    NetworkingStateMachine.uploadLogFileURL$ = ""
    NetworkingStateMachine.uploadLogFolder = "logs"
    NetworkingStateMachine.uploadLogArchiveFolder = "archivedLogs"
    NetworkingStateMachine.uploadLogFailedFolder = "failedLogs"
    NetworkingStateMachine.enableLogDeletion = true
    
    NetworkingStateMachine.SendHeartbeat = SendHeartbeat

    NetworkingStateMachine.AddUploadHeaders = AddUploadHeaders

    NetworkingStateMachine.RebootAfterEventsSent = RebootAfterEventsSent
    NetworkingStateMachine.WaitForTransfersToComplete = WaitForTransfersToComplete

	NetworkingStateMachine.ParseAWSURLs = ParseAWSURLs
	NetworkingStateMachine.SetRemoteSnapshotUrls = SetRemoteSnapshotUrls
	NetworkingStateMachine.SetBASParameters = SetBASParameters

    NetworkingStateMachine.ResetDownloadTimerToDoRetry = ResetDownloadTimerToDoRetry
    NetworkingStateMachine.retryInterval% = 60
    NetworkingStateMachine.numRetries% = 0
    NetworkingStateMachine.maxRetries% = 3

    NetworkingStateMachine.ResetHeartbeatTimerToDoRetry = ResetHeartbeatTimerToDoRetry
    NetworkingStateMachine.heartbeatRetryInterval% = 60
    NetworkingStateMachine.numHeartbeatRetries% = 0
    NetworkingStateMachine.maxHeartbeatRetries% = 3

	NetworkingStateMachine.fileDownloadFailureCount% = 0
	NetworkingStateMachine.maxFileDownloadFailures% = 3

    NetworkingStateMachine.POOL_EVENT_FILE_DOWNLOADED = 1
    NetworkingStateMachine.POOL_EVENT_FILE_FAILED = -1
    NetworkingStateMachine.POOL_EVENT_ALL_DOWNLOADED = 2
    NetworkingStateMachine.POOL_EVENT_ALL_FAILED = -2

    NetworkingStateMachine.SYNC_ERROR_CANCELLED = -10001
    NetworkingStateMachine.SYNC_ERROR_CHECKSUM_MISMATCH = -10002
    NetworkingStateMachine.SYNC_ERROR_EXCEPTION = -10003
    NetworkingStateMachine.SYNC_ERROR_DISK_ERROR = -10004
    NetworkingStateMachine.SYNC_ERROR_POOL_UNSATISFIED = -10005
    
    NetworkingStateMachine.EVENT_REALIZE_SUCCESS = 101

    NetworkingStateMachine.stTop = NetworkingStateMachine.newHState(bsp, "Top")
    NetworkingStateMachine.stTop.HStateEventHandler = STTopEventHandler
    
    NetworkingStateMachine.stNetworkScheduler = NetworkingStateMachine.newHState(bsp, "NetworkScheduler")
    NetworkingStateMachine.stNetworkScheduler.HStateEventHandler = STNetworkSchedulerEventHandler
	NetworkingStateMachine.stNetworkScheduler.QueueSnapshotForBSN = QueueSnapshotForBSN
	NetworkingStateMachine.stNetworkScheduler.UploadSnapshotToBSN = UploadSnapshotToBSN
	NetworkingStateMachine.stNetworkScheduler.UploadSnapshotToBSNEE = UploadSnapshotToBSNEE
	NetworkingStateMachine.stNetworkScheduler.UploadSnapshotToSFN = UploadSnapshotToSFN
	NetworkingStateMachine.stNetworkScheduler.BuildStringToSign = BuildStringToSign
	NetworkingStateMachine.stNetworkScheduler.superState = NetworkingStateMachine.stTop

    NetworkingStateMachine.stWaitForTimeout = NetworkingStateMachine.newHState(bsp, "WaitForTimeout")
    NetworkingStateMachine.stWaitForTimeout.HStateEventHandler = STWaitForTimeoutEventHandler
	NetworkingStateMachine.stWaitForTimeout.superState = NetworkingStateMachine.stNetworkScheduler

    NetworkingStateMachine.stRetrievingSyncList = NetworkingStateMachine.newHState(bsp, "RetrievingSyncList")
    NetworkingStateMachine.stRetrievingSyncList.StartSync = StartSync 
    NetworkingStateMachine.stRetrievingSyncList.SyncSpecXferEvent = SyncSpecXferEvent 
    NetworkingStateMachine.stRetrievingSyncList.HStateEventHandler = STRetrievingSyncListEventHandler
	NetworkingStateMachine.stRetrievingSyncList.superState = NetworkingStateMachine.stNetworkScheduler
	NetworkingStateMachine.stRetrievingSyncList.ConfigureNetwork = ConfigureNetwork
	NetworkingStateMachine.stRetrievingSyncList.UpdateRegistrySetting = UpdateRegistrySetting

    NetworkingStateMachine.stDownloadingSyncFiles = NetworkingStateMachine.newHState(bsp, "DownloadingSyncFiles")
    NetworkingStateMachine.stDownloadingSyncFiles.StartSyncListDownload = StartSyncListDownload
    NetworkingStateMachine.stDownloadingSyncFiles.HandleAssetFetcherEvent = HandleAssetFetcherEvent
    NetworkingStateMachine.stDownloadingSyncFiles.HStateEventHandler = STDownloadingSyncFilesEventHandler
	NetworkingStateMachine.stDownloadingSyncFiles.superState = NetworkingStateMachine.stNetworkScheduler

	NetworkingStateMachine.topState = NetworkingStateMachine.stTop
	
	return NetworkingStateMachine
	
End Function


Function InitializeNetworkingStateMachine() As Object

    ' determine whether or not to enable proxy mode support
	m.proxy_mode = false
	
    ' if caching is enabled, set parameter indicating whether downloads are only allowed from the cache
    m.downloadOnlyIfCached = false   
    
    ' combination of proxies and wireless not yet supported
    nc = CreateObject("roNetworkConfiguration", 0)
    if type(nc) = "roNetworkConfiguration" then
        if nc.GetProxy() <> "" then
	        m.proxy_mode = true
            OnlyDownloadIfCached$ = m.bsp.registrySettings.OnlyDownloadIfCached$
            if OnlyDownloadIfCached$ = "true" then m.downloadOnlyIfCached = true
        endif
    endif
    nc = invalid

    ' Load up the current sync specification so we have it ready
	m.currentSync = CreateObject("roSyncSpec")
	if type(m.currentSync) <> "roSyncSpec" then return false
	if not m.currentSync.ReadFromFile("current-sync.xml") and not m.currentSync.ReadFromFile("localToBSN-sync.xml") then
	    m.diagnostics.PrintDebug("### No current sync state available")
	    return false
	endif

    m.accountName$ = m.currentSync.LookupMetadata("server", "account")

    base$ = m.currentSync.LookupMetadata("client", "base")
    nextURL = GetURL(base$, m.currentSync.LookupMetadata("client", "next"))
	m.eventURL = GetURL(base$, m.currentSync.LookupMetadata("client", "event"))
    m.deviceDownloadProgressURL = GetURL(base$, m.currentSync.LookupMetadata("client", "devicedownloadprogress"))
	m.deviceDownloadURL = GetURL(base$, m.currentSync.LookupMetadata("client", "devicedownload"))
	m.trafficDownloadURL = GetURL(base$, m.currentSync.LookupMetadata("client", "trafficdownload"))
	m.deviceErrorURL = GetURL(base$, m.currentSync.LookupMetadata("client", "deviceerror"))
    m.uploadLogFileURL$ = GetURL(base$, m.currentSync.LookupMetadata("client", "uploadlogs"))
	m.heartbeatURL$ = GetURL(base$,  m.currentSync.LookupMetadata("client", "heartbeat"))

	timezone = m.currentSync.LookupMetadata("client", "timezone")
    if timezone <> "" then
        m.systemTime.SetTimeZone(timezone)
    endif

    m.diagnostics.PrintTimestamp()
    m.diagnostics.PrintDebug("### Current active sync list suggests next URL of " + nextURL)
    
	if nextURL = "" then stop
    if m.eventURL = "" then stop
    
    m.user$ = m.currentSync.LookupMetadata("server", "user")
    m.password$ = m.currentSync.LookupMetadata("server", "password")
    if m.user$ <> "" or m.password$ <> "" then
        m.setUserAndPassword = true
		enableUnsafeAuthentication$ = m.currentSync.LookupMetadata("server", "enableUnsafeAuthentication")
		if lcase(enableUnsafeAuthentication$) = "true" then
			m.enableUnsafeAuthentication = true
		else
			m.enableUnsafeAuthentication = false
		endif
    else
        m.setUserAndPassword = false
		m.enableUnsafeAuthentication = false
    endif

    useWireless$ = m.currentSync.LookupMetadata("client", "useWireless")
    if not m.modelSupportsWifi then useWireless$ = "no"

	if useWireless$ = "yes" then
		m.useWireless = true
	else
		m.useWireless = false
	endif
	        
' get net connect parameters, setup timer, and rate limits
    timeBetweenNetConnects$ = m.currentSync.LookupMetadata("client", "timeBetweenNetConnects")
    contentDownloadsRestricted = m.currentSync.LookupMetadata("client", "contentDownloadsRestricted")
    contentDownloadRangeStart = m.currentSync.LookupMetadata("client", "contentDownloadRangeStart")
    contentDownloadRangeLength = m.currentSync.LookupMetadata("client", "contentDownloadRangeLength")

    timeBetweenHeartbeats$ = m.currentSync.LookupMetadata("client", "timeBetweenHeartbeats")
    heartbeatsRestricted = m.currentSync.LookupMetadata("client", "heartbeatsRestricted")
    heartbeatsRangeStart = m.currentSync.LookupMetadata("client", "heartbeatsRangeStart")
    heartbeatsRangeLength = m.currentSync.LookupMetadata("client", "heartbeatsRangeLength")

	m.wiredRateLimits = {}
	if m.useWireless then
		rateLimitModeOutsideWindowWired$ = m.currentSync.LookupMetadata("client", "rateLimitModeOutsideWindow_2")
		rateLimitRateOutsideWindowWired$ = m.currentSync.LookupMetadata("client", "rateLimitRateOutsideWindow_2")
		rateLimitModeInWindowWired$ = m.currentSync.LookupMetadata("client", "rateLimitModeInWindow_2")
		rateLimitRateInWindowWired$ = m.currentSync.LookupMetadata("client", "rateLimitRateInWindow_2")
	else
		rateLimitModeOutsideWindowWired$ = m.currentSync.LookupMetadata("client", "rateLimitModeOutsideWindow")
		rateLimitRateOutsideWindowWired$ = m.currentSync.LookupMetadata("client", "rateLimitRateOutsideWindow")
		rateLimitModeInWindowWired$ = m.currentSync.LookupMetadata("client", "rateLimitModeInWindow")
		rateLimitRateInWindowWired$ = m.currentSync.LookupMetadata("client", "rateLimitRateInWindow")
	endif
	SetRateLimitValues(true, m.wiredRateLimits, rateLimitModeOutsideWindowWired$, rateLimitRateOutsideWindowWired$, rateLimitModeInWindowWired$, rateLimitRateInWindowWired$)

	m.wirelessRateLimits = {}
	if m.useWireless then
		rateLimitModeOutsideWindowWireless$ = m.currentSync.LookupMetadata("client", "rateLimitModeOutsideWindow")
		rateLimitRateOutsideWindowWireless$ = m.currentSync.LookupMetadata("client", "rateLimitRateOutsideWindow")
		rateLimitModeInWindowWireless$ = m.currentSync.LookupMetadata("client", "rateLimitModeInWindow")
		rateLimitRateInWindowWireless$ = m.currentSync.LookupMetadata("client", "rateLimitRateInWindow")
	else
		rateLimitModeOutsideWindowWireless$ = m.currentSync.LookupMetadata("client", "rateLimitModeOutsideWindow_2")
		rateLimitRateOutsideWindowWireless$ = m.currentSync.LookupMetadata("client", "rateLimitRateOutsideWindow_2")
		rateLimitModeInWindowWireless$ = m.currentSync.LookupMetadata("client", "rateLimitModeInWindow_2")
		rateLimitRateInWindowWireless$ = m.currentSync.LookupMetadata("client", "rateLimitRateInWindow_2")
	endif
	SetRateLimitValues(true, m.wirelessRateLimits, rateLimitModeOutsideWindowWireless$, rateLimitRateOutsideWindowWireless$, rateLimitModeInWindowWireless$, rateLimitRateInWindowWireless$)

' if the values above are not found, try to retrieve them from the registry. for simple networking (pre BA 2.3), only the
' initial sync spec will include these values
    if timeBetweenNetConnects$ = "" then
		timeBetweenNetConnects$ = m.bsp.registrySettings.timeBetweenNetConnects$
        if timeBetweenNetConnects$ = "" then print "Error: timeBetweenNetConnects not found in either the sync spec or the registry":stop
        contentDownloadsRestricted = m.bsp.registrySettings.contentDownloadsRestricted
        if contentDownloadsRestricted = "" then print "Error: contentDownloadsRestricted not set in registry":stop
        contentDownloadRangeStart = m.bsp.registrySettings.contentDownloadRangeStart
        contentDownloadRangeLength = m.bsp.registrySettings.contentDownloadRangeLength
    endif

' check for timeBetweenNetConnects override
	tbnco$ = m.bsp.registrySettings.tbnco$
	if tbnco$ <> "" then
		timeBetweenNetConnects$	= tbnco$
	endif    
    
    m.timeBetweenNetConnects% = val(timeBetweenNetConnects$)
    m.diagnostics.PrintDebug("### Time between net connects = " + timeBetweenNetConnects$)
    m.currentTimeBetweenNetConnects% = m.timeBetweenNetConnects%
    m.networkTimerDownload = CreateObject("roAssociativeArray")
    m.networkTimerDownload.timerType = "TIMERTYPEPERIODIC"
    m.networkTimerDownload.timerInterval = m.timeBetweenNetConnects%
    
	if timeBetweenHeartbeats$ = "" then
		m.timeBetweenHearbeats% = 0
	else
	    m.timeBetweenHearbeats% = val(timeBetweenHeartbeats$)
    endif
    m.currentTimeBetweenHeartbeats% = m.timeBetweenHearbeats%
	m.diagnostics.PrintDebug("### Time between heartbeats = " + timeBetweenHeartbeats$)

    newTimer = CreateObject("roTimer")
    newTimer.SetPort(m.msgPort)
    
    m.networkTimerDownload.timer = newTimer

' get time range for when net connects can occur
    if contentDownloadsRestricted = "yes" then
        m.contentDownloadsRestricted = true
        m.contentDownloadRangeStart% = val(contentDownloadRangeStart)
        m.contentDownloadRangeLength% = val(contentDownloadRangeLength)
        m.diagnostics.PrintDebug("### Content downloads are restricted to the time from " + contentDownloadRangeStart + " for " + contentDownloadRangeLength + " minutes.")
    else
        m.diagnostics.PrintDebug("### Content downloads are unrestricted")
        m.contentDownloadsRestricted = false
    endif

' get time range for when heartbeats can occur
    if heartbeatsRestricted = "yes" then
        m.heartbeatsRestricted = true
        m.heartbeatsRangeStart% = val(heartbeatsRangeStart)
        m.heartbeatsRangeLength% = val(heartbeatsRangeLength)
        m.diagnostics.PrintDebug("### Heartbeats are restricted to the time from " + heartbeatsRangeStart + " for " + heartbeatsRangeLength + " minutes.")
    else
        m.diagnostics.PrintDebug("### Heartbeats are unrestricted")
        m.heartbeatsRestricted = false
    endif

' program the rate limit for networking
    if m.contentDownloadsRestricted then
        currentTime = m.systemTime.GetLocalDateTime()
        startOfRange% = m.contentDownloadRangeStart%
        endOfRange% = startOfRange% + m.contentDownloadRangeLength%
        
        notInDownloadWindow = OutOfDownloadWindow(currentTime, startOfRange%, endOfRange%)
        
		if notInDownloadWindow then
			wiredRL% = m.wiredRateLimits.rlOutsideWindow%
			wirelessRL% = m.wirelessRateLimits.rlOutsideWindow%
		else
			wiredRL% = m.wiredRateLimits.rlInWindow%
			wirelessRL% = m.wirelessRateLimits.rlInWindow%
		endif
    else
		wiredRL% = m.wiredRateLimits.rlOutsideWindow%
		wirelessRL% = m.wirelessRateLimits.rlOutsideWindow%
	endif

'  remote snapshot values
	m.SetRemoteSnapshotUrls(m.currentSync)
	m.pendingSnapshotsToUpload = {}

' diagnostic web server
	dwsParams = GetDWSParams(m.currentSync, m.bsp.registrySettings)
	
	dwsAA = CreateObject("roAssociativeArray")
	if dwsParams.dwsEnabled$ = "yes" then
		dwsAA["port"] = "80"
		dwsAA["password"] = dwsParams.dwsPassword$
	else if dwsParams.dwsEnabled$ = "no" then
		dwsAA["port"] = 0
	endif

	SetDownloadRateLimit(m.diagnostics, 0, wiredRL%)

	if m.useWireless then
		SetDownloadRateLimit(m.diagnostics, 1, wirelessRL%)
	endif
                
    nc = CreateObject("roNetworkConfiguration", 0)
    if type(nc) = "roNetworkConfiguration"
		dwsRebootRequired = nc.SetupDWS(dwsAA)
		if dwsRebootRequired then RebootSystem()
	endif

	m.bsp.obfuscatedEncryptionKey = ""

    return m.stRetrievingSyncList
	
End Function


Sub SetRemoteSnapshotUrls(syncSpec As Object)

	m.deviceScreenShotsTemporaryStorage$ = syncSpec.LookupMetadata("client", "deviceScreenShotsTemporaryStorage")
	m.incomingDeviceScreenshotsQueue$ = syncSpec.LookupMetadata("client", "incomingDeviceScreenshotsQueue")
	m.awsAccessKeyId$ = syncSpec.LookupMetadata("client", "awsAccessKeyId")
	m.awsSecretAccessKey$ = syncSpec.LookupMetadata("client", "awsSecretAccessKey")
	m.awsSessionToken$ = syncSpec.LookupMetadata("client", "awsSessionToken")
	m.ParseAWSURLs()
    m.uploadSnapshotsURL$ = syncSpec.LookupMetadata("client", "uploadsnapshots")

	m.uploadDeviceScreenshotHandlerAddress = syncSpec.LookupMetadata("client", "uploadDeviceScreenshotHandlerAddress")
	m.securityToken = syncSpec.LookupMetadata("client", "securityToken")

End Sub


Sub ParseAWSURLs()

	m.AwsBucketName = ""			' bsnm2
	m.AwsBaseAddress = ""			' https://s3.amazonaws.com/
	m.AwsIncomingDirectory = ""		' DeviceScreenShots/Incoming/
	m.AwsSqsHost = ""				' sqs.us-east-1.amazonaws.com
	m.AwsSqsAbsolutePath = ""		' /965175186373/bsn-QA-RS-IDSS
	m.AwsSqsService = ""			' sqs
	m.AwsSqsRegion = ""				' us-east-1

	if m.deviceScreenShotsTemporaryStorage$ <> "" then

		regexSlash = CreateObject("roRegEx","/","i")
		regexDot = CreateObject("roRegEx","\.","i")
		urlItems = regexSlash.Split(m.deviceScreenShotsTemporaryStorage$)
		hostItems = regexDot.Split(urlItems[2])
		sqsUrlItems = regexSlash.Split(m.incomingDeviceScreenshotsQueue$)

		' Get Bucket
		m.AwsBucketName = hostItems[0]
		
		' Get Base Address
		m.AwsBaseAddress = urlItems[0] + "//" + hostItems[1]
		for i = 2 to (hostItems.Count() - 1)
			m.AwsBaseAddress = m.AwsBaseAddress + "." + hostItems[i]
        next
        m.AwsBaseAddress = m.AwsBaseAddress + "/"

        ' Get Resource Key
        m.AwsIncomingDirectory = ""
        for i = 3 to (urlItems.Count() - 1)
            m.AwsIncomingDirectory = m.AwsIncomingDirectory + urlItems[i] + "/"
        next

		' sqs url host
		m.AwsSqsHost = sqsUrlItems[2]

		' sqs absolute path
		for i = 3 to (urlItems.Count() - 1)
			m.AwsSqsAbsolutePath = m.AwsSqsAbsolutePath + "/" + sqsUrlItems[i]
		next

		' sqs service,region
		sqsHostItems = regexDot.Split(m.AwsSqsHost)
		m.AwsSqsService = sqsHostItems[0]
		m.AwsSqsRegion = sqsHostItems[1]

	endif

End Sub


Function GetDwsParams(syncSpec As Object, registrySettings As Object)

	dwsEnabled$ = syncSpec.LookupMetadata("client", "dwsEnabled")
	
	if dwsEnabled$ = "" then
		' simple file networking case
		dwsEnabled$ = registrySettings.dwsEnabled$
		dwsPassword$ = registrySettings.dwsPassword$
	else
		dwsPassword$ = syncSpec.LookupMetadata("client", "dwsPassword")
	endif
	
	dwsParams = CreateObject("roAssociativeArray")
	dwsParams.dwsEnabled$ = dwsEnabled$
	dwsParams.dwsPassword$ = dwsPassword$
	
	return dwsParams
	
End Function


Function OutOfDownloadWindow(currentTime As Object, startOfRangeInMinutes% As Integer, endOfRangeInMinutes% As Integer)

	secondsPerDay% = 24 * 60 * 60
	
	secondsSinceMidnight% = currentTime.GetHour() * 3600 + currentTime.GetMinute() * 60 + currentTime.GetSecond()
	startOfRangeInSeconds% = startOfRangeInMinutes% * 60
	endOfRangeInSeconds% = endOfRangeInMinutes% * 60
	
	notInDownloadWindow = false
	if endOfRangeInSeconds% <= secondsPerDay% then
		if not(secondsSinceMidnight% >= startOfRangeInSeconds% and secondsSinceMidnight% <= endOfRangeInSeconds%) then
			notInDownloadWindow = true
		endif
	else
		if not(((secondsSinceMidnight% >= startOfRangeInSeconds%) and (secondsSinceMidnight% < secondsPerDay%)) or (secondsSinceMidnight% < (endOfRangeInSeconds% - secondsPerDay%))) then
			notInDownloadWindow = true
		endif
	endif
	
	return notInDownloadWindow
	
End Function


Sub SetRateLimitValues(updateIfNotSpecified As boolean, rateLimits As Object, rateLimitModeOutsideWindow$ As String, rateLimitRateOutsideWindow$ As String, rateLimitModeInWindow$ As String, rateLimitRateInWindow$ As String)

	if rateLimitModeOutsideWindow$ = "unlimited" then
		rateLimits.rlOutsideWindow% = 0
	else if rateLimitModeOutsideWindow$ = "specified" then
		if rateLimitRateOutsideWindow$ <> "" then
			rateLimits.rlOutsideWindow% = int(val(rateLimitRateOutsideWindow$))
		endif
	else if updateIfNotSpecified or rateLimitModeOutsideWindow$ <> "" then
		rateLimits.rlOutsideWindow% = -1
	endif

	if rateLimitModeInWindow$ = "unlimited" then
		rateLimits.rlInWindow% = 0
	else if rateLimitModeInWindow$ = "specified" then
		if rateLimitRateInWindow$ <> "" then
			rateLimits.rlInWindow% = int(val(rateLimitRateInWindow$))
		endif
	else if updateIfNotSpecified or rateLimitModeInWindow$ <> "" then
		rateLimits.rlInWindow% = -1
	endif

End Sub


Function GetURL(base$ As String, urlFromSyncSpec$ As String) As String

    if instr(1, urlFromSyncSpec$, ":") > 0 then
        url$ = urlFromSyncSpec$
    else if urlFromSyncSpec$ = "" then
        url$ = ""
    else
        url$ = base$ + urlFromSyncSpec$
    endif
    
    return url$
    
End Function


Sub AddUploadHeaders(url As Object, contentDisposition$)

'    url.SetHeaders({})
    url.SetHeaders(m.currentSync.GetMetadata("server"))

' Add device unique identifier, timezone
    url.AddHeader("DeviceID", m.deviceUniqueID$)
    
    url.AddHeader("DeviceModel", m.deviceModel$)
    url.AddHeader("DeviceFamily", m.deviceFamily$)
    url.AddHeader("DeviceFWVersion", m.firmwareVersion$)
    url.AddHeader("DeviceSWVersion", m.autorunVersion$)
    url.AddHeader("CustomAutorunVersion", m.customAutorunVersion$)
    
    url.AddHeader("utcTime", m.systemTime.GetUtcDateTime().GetString())

    url.AddHeader("Content-Type", "application/octet-stream")
    
    url.AddHeader("Content-Disposition", contentDisposition$)

End Sub


Function GetContentDisposition(file As String) As String

'Content-Disposition: form-data; name="file"; filename="UploadPlaylog.xml"

    contentDisposition$ = "form-data; name="
    contentDisposition$ = contentDisposition$ + chr(34)
    contentDisposition$ = contentDisposition$ + "file"
    contentDisposition$ = contentDisposition$ + chr(34)
    contentDisposition$ = contentDisposition$ + "; filename="
    contentDisposition$ = contentDisposition$ + chr(34)
    contentDisposition$ = contentDisposition$ + file
    contentDisposition$ = contentDisposition$ + chr(34)

    return contentDisposition$
    
End Function


Sub BuildFileDownloadList(syncSpec As Object)

	listOfDownloadFiles = syncSpec.GetFileList("download")
    fileInPoolStatus = m.bsp.assetPool.QueryFiles(syncSpec)
        
    m.filesToDownload = {}
    m.chargeableFiles = {}
                
    for each downloadFile in listOfDownloadFiles
        
        if not m.filesToDownload.DoesExist(downloadFile.hash) then
            fileToDownload = CreateObject("roAssociativeArray")
            fileToDownload.name = downloadFile.name
            fileToDownload.size = downloadFile.size
            fileToDownload.hash = downloadFile.hash
                
            fileToDownload.currentFilePercentage$ = ""
            fileToDownload.status$ = ""

            ' check to see if this file is already in the pool (and therefore doesn't need to be downloaded)
            if fileInPoolStatus.DoesExist(downloadFile.name) then
                fileInPool = fileInPoolStatus.Lookup(downloadFile.name)
                if fileInPool then
                    fileToDownload.currentFilePercentage$ = "100"
                    fileToDownload.status$ = "ok"
                endif
            endif
                
            m.filesToDownload.AddReplace(downloadFile.hash, fileToDownload)
        endif
            
        if IsString(downloadFile.chargeable) then
            if lcase(downloadFile.chargeable) = "yes" then
                m.chargeableFiles[downloadFile.name] = true
            endif
        endif
            
    next
                                
End Sub


Sub PushDeviceDownloadProgressItem(fileItem As Object, type$ As String, currentFilePercentage$ As String, status$ As String)

	if type(fileItem) <> "roAssociativeArray" return

    deviceDownloadProgressItem = CreateObject("roAssociativeArray")
    deviceDownloadProgressItem.type$ = type$
    deviceDownloadProgressItem.name$ = fileItem.name
    deviceDownloadProgressItem.hash$ = fileItem.hash
    deviceDownloadProgressItem.size$ = fileItem.size
    deviceDownloadProgressItem.currentFilePercentage$ = currentFilePercentage$
    deviceDownloadProgressItem.status$ = status$
    deviceDownloadProgressItem.utcTime$ = m.systemTime.GetUtcDateTime().GetString()

	if m.DeviceDownloadProgressItems.DoesExist(fileItem.name)
		existingDeviceDownloadProgressItem = m.DeviceDownloadProgressItems.Lookup(fileItem.name)
		deviceDownloadProgressItem.type$ = existingDeviceDownloadProgressItem.type$
	endif

	m.DeviceDownloadProgressItems.AddReplace(fileItem.name, deviceDownloadProgressItem)

End Sub


Sub AddDeviceDownloadProgressItem(fileItem As Object, currentFilePercentage$ As String, status$ As String)

	if type(fileItem) <> "roAssociativeArray" return

	m.PushDeviceDownloadProgressItem(fileItem, "deviceDownloadProgressItem", currentFilePercentage$, status$)
    m.UploadDeviceDownloadProgressItems()
    
End Sub


Sub UploadDeviceDownloadProgressItems()

    if m.deviceDownloadProgressURL = "" then
        m.diagnostics.PrintDebug("### UploadDeviceDownloadProgressItems - deviceDownloadProgressURL not set, return")
        return
    else
        m.diagnostics.PrintDebug("### UploadDeviceDownloadProgressItems")
    endif

' verify that there is content to upload
    if m.DeviceDownloadProgressItems.IsEmpty() and m.DeviceDownloadProgressItemsPendingUpload.IsEmpty() then return
    
' create roUrlTransfer if needed
	if type(m.deviceDownloadProgressUploadURL) <> "roUrlTransfer" then
		m.deviceDownloadProgressUploadURL = CreateObject("roUrlTransfer")
        m.deviceDownloadProgressUploadURL.SetUrl(m.deviceDownloadProgressURL)
        m.deviceDownloadProgressUploadURL.SetPort(m.msgPort)
		m.deviceDownloadProgressUploadURL.SetTimeout(900000)
		m.deviceDownloadProgressUploadURL.SetUserAgent(m.bsp.userAgent$)
	endif
	    
' if a transfer is in progress, return
	if not m.deviceDownloadProgressUploadURL.SetUrl(m.deviceDownloadProgressURL) then
        m.diagnostics.PrintDebug("### UploadDeviceDownloadProgressItems - upload already in progress")
		return 
	else
        m.diagnostics.PrintDebug("### UploadDeviceDownloadProgressItems - proceed with post")
	end if

' merge new items into pending items
	for each deviceDownloadProgressItemKey in m.DeviceDownloadProgressItems
		deviceDownloadProgressItem = m.DeviceDownloadProgressItems.Lookup(deviceDownloadProgressItemKey)
		if m.DeviceDownloadProgressItemsPendingUpload.DoesExist(deviceDownloadProgressItem.name$)
			existingDeviceDownloadProgressItem = m.DeviceDownloadProgressItemsPendingUpload.Lookup(deviceDownloadProgressItem.name$)
			deviceDownloadProgressItem.type$ = existingDeviceDownloadProgressItem.type$
		endif
		m.DeviceDownloadProgressItemsPendingUpload.AddReplace(deviceDownloadProgressItem.name$, deviceDownloadProgressItem)
	next

' generate the XML and upload the data
    root = CreateObject("roXMLElement")
    
    root.SetName("DeviceDownloadProgressItems")

	for each deviceDownloadProgressItemKey in m.DeviceDownloadProgressItemsPendingUpload
		deviceDownloadProgressItem = m.DeviceDownloadProgressItemsPendingUpload.Lookup(deviceDownloadProgressItemKey)
		BuildDeviceDownloadProgressItemXML(root, deviceDownloadProgressItem)
    next

    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

' prepare the upload    
    contentDisposition$ = GetContentDisposition("UploadDeviceDownloadProgressItems.xml")
    m.AddUploadHeaders(m.deviceDownloadProgressUploadURL, contentDisposition$)
    m.deviceDownloadProgressUploadURL.AddHeader("updateDeviceLastDownload", "true")

	binding% = GetBinding(m.bsp.contentXfersEnabledWired, m.bsp.contentXfersEnabledWireless)
    m.diagnostics.PrintDebug("### Binding for UploadDeviceDownloadProgressItems is " + stri(binding%))
	ok = m.deviceDownloadProgressUploadURL.BindToInterface(binding%)
	if not ok then stop

	ok = m.deviceDownloadProgressUploadURL.AsyncPostFromString(xml)
    if not ok then
        m.diagnostics.PrintDebug("### UploadDeviceDownloadProgressItems - AsyncPostFromString failed")
    endif
    
	m.DeviceDownloadProgressItems.Clear()

End Sub


Sub BuildDeviceDownloadProgressItemXML(root As Object, deviceDownloadProgressItem As Object)

    item = root.AddBodyElement()
    item.SetName(deviceDownloadProgressItem.type$)

    elem = item.AddElement("name")
    elem.SetBody(deviceDownloadProgressItem.name$)
    
    elem = item.AddElement("hash")
    elem.SetBody(deviceDownloadProgressItem.hash$)
    
    elem = item.AddElement("size")
    elem.SetBody(deviceDownloadProgressItem.size$)
    
    elem = item.AddElement("currentFilePercentage")
    elem.SetBody(deviceDownloadProgressItem.currentFilePercentage$)
    
    elem = item.AddElement("status")
    elem.SetBody(deviceDownloadProgressItem.status$)
        
    elem = item.AddElement("utcTime")
    elem.SetBody(deviceDownloadProgressItem.utcTime$)

End Sub


Sub UploadDeviceDownloadProgressFileList()

    if m.deviceDownloadProgressURL = "" then
        m.diagnostics.PrintDebug("### UploadDeviceDownloadProgressFileList - deviceDownloadProgressURL not set, return")
        return
    else
        m.diagnostics.PrintDebug("### UploadDeviceDownloadProgressFileList")
    endif

' create roUrlTransfer if needed
	if type(m.deviceDownloadProgressUploadURL) <> "roUrlTransfer" then
		m.deviceDownloadProgressUploadURL = CreateObject("roUrlTransfer")
        m.deviceDownloadProgressUploadURL.SetUrl(m.deviceDownloadProgressURL)
        m.deviceDownloadProgressUploadURL.SetPort(m.msgPort)
		m.deviceDownloadProgressUploadURL.SetTimeout(900000)
		m.deviceDownloadProgressUploadURL.SetUserAgent(m.bsp.userAgent$)
	else
' cancel any uploads of this type that are in progress
		m.deviceDownloadProgressUploadURL.AsyncCancel()
	endif

' this data will overwrite any pending data so clear the existing data structures
    m.DeviceDownloadProgressItems.Clear()
    m.DeviceDownloadProgressItemsPendingUpload.Clear()

' create progress items for each file in the sync spec
    for each fileToDownloadKey in m.filesToDownload
        fileToDownload = m.filesToDownload.Lookup(fileToDownloadKey)
		m.PushDeviceDownloadProgressItem(fileToDownload, "fileInSyncSpec", fileToDownload.currentFilePercentage$, fileToDownload.status$)
	next

' create progress items for each file in each feed
	for each liveDataFeedName in m.bsp.liveDataFeeds
		liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedName)
	    for each fileToDownloadKey in liveDataFeed.feedContentFilesToDownload
		    fileToDownload = liveDataFeed.feedContentFilesToDownload.Lookup(fileToDownloadKey)
		    if type(fileToDownload) = "roAssociativeArray" then
			    m.PushDeviceDownloadProgressItem(fileToDownload, "fileInSyncSpec", fileToDownload.currentFilePercentage$, fileToDownload.status$)
			  endif
		next
	next

	m.UploadDeviceDownloadProgressItems()

End Sub



Sub AddDeviceDownloadItem(downloadEvent$ As String, fileName$ As String, downloadData$ As String)
    
    ' Make sure the array doesn't get too big.
    while m.DeviceDownloadItems.Count() > 100
        m.DeviceDownloadItems.Shift()
    end while

    deviceDownloadItem = CreateObject("roAssociativeArray")
    deviceDownloadItem.downloadEvent$ = downloadEvent$
    deviceDownloadItem.fileName$ = fileName$
    deviceDownloadItem.downloadData$ = downloadData$
    m.DeviceDownloadItems.push(deviceDownloadItem)

    m.UploadDeviceDownload()
    
End Sub


Sub UploadDeviceDownload()

    if m.deviceDownloadURL = "" then
        m.diagnostics.PrintDebug("### UploadDeviceDownload - deviceDownloadURL not set, return")
        return
    else
        m.diagnostics.PrintDebug("### UploadDeviceDownload")
    endif

' verify that there is content to upload
    if m.DeviceDownloadItems.Count() = 0 and m.DeviceDownloadItemsPendingUpload.Count() = 0 then return

' create roUrlTransfer if needed
	if type(m.deviceDownloadUploadURL) <> "roUrlTransfer" then
		m.deviceDownloadUploadURL = CreateObject("roUrlTransfer")
        m.deviceDownloadUploadURL.SetUrl(m.deviceDownloadURL)
        m.deviceDownloadUploadURL.SetPort(m.msgPort)
		m.deviceDownloadUploadURL.SetTimeout(900000)
		m.deviceDownloadUploadURL.SetUserAgent(m.bsp.userAgent$)
	endif
	    
' if a transfer is in progress, return
	if not m.deviceDownloadUploadURL.SetUrl(m.deviceDownloadURL) then
        m.diagnostics.PrintDebug("### UploadDeviceDownload - upload already in progress")
        if m.DeviceDownloadItemsPendingUpload.Count() > 100 then
            m.diagnostics.PrintDebug("### UploadDeviceDownload - clear pending items from queue")
            m.DeviceDownloadItemsPendingUpload.Clear()
        endif        
        if m.DeviceDownloadItems.Count() > 100 then
            m.diagnostics.PrintDebug("### UploadDeviceDownload - clear items from queue")
            m.DeviceDownloadItems.Clear()
        endif        
		return 
	else
        m.diagnostics.PrintDebug("### UploadDeviceDownload - proceed with post")
	end if

' generate the XML and upload the data
    root = CreateObject("roXMLElement")
    
    root.SetName("DeviceDownloadBatch")

	' first add the items that failed the last time
	for each deviceDownloadItem in m.DeviceDownloadItemsPendingUpload
		BuildDeviceDownloadItemXML(root, deviceDownloadItem)
    next

	' now add the new items
	for each deviceDownloadItem in m.DeviceDownloadItems    
		BuildDeviceDownloadItemXML(root, deviceDownloadItem)
    next

    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

' prepare the upload    
    contentDisposition$ = GetContentDisposition("UploadDeviceDownload.xml")
    m.AddUploadHeaders(m.deviceDownloadUploadURL, contentDisposition$)

	binding% = GetBinding(m.bsp.contentXfersEnabledWired, m.bsp.contentXfersEnabledWireless)
    m.diagnostics.PrintDebug("### Binding for UploadDeviceDownload is " + stri(binding%))
	ok = m.deviceDownloadUploadURL.BindToInterface(binding%)
	if not ok then stop

	ok = m.deviceDownloadUploadURL.AsyncPostFromString(xml)
    if not ok then
        m.diagnostics.PrintDebug("### UploadDeviceDownload - AsyncPostFromString failed")
    endif
        
	for each deviceDownloadItem in m.DeviceDownloadItems
		m.DeviceDownloadItemsPendingUpload.push(deviceDownloadItem)
	next

	m.DeviceDownloadItems.Clear()

End Sub


Sub BuildDeviceDownloadItemXML(root As Object, deviceDownloadItem As Object)

        item = root.AddBodyElement()
        item.SetName("deviceDownload")

        elem = item.AddElement("downloadEvent")
        elem.SetBody(deviceDownloadItem.downloadEvent$)
    
        elem = item.AddElement("fileName")
        elem.SetBody(deviceDownloadItem.fileName$)
    
        elem = item.AddElement("downloadData")
        elem.SetBody(deviceDownloadItem.downloadData$)

End Sub


Sub UploadLogFiles()

    if m.uploadLogFileURL$ = "" then return
    
' create roUrlTransfer if needed
	if type(m.uploadLogFileURLXfer) <> "roUrlTransfer" then
		m.uploadLogFileURLXfer = CreateObject("roUrlTransfer")
        m.uploadLogFileURLXfer.SetUrl(m.uploadLogFileURL$)
        m.uploadLogFileURLXfer.SetPort(m.msgPort)
	    m.uploadLogFileURLXfer.SetMinimumTransferRate(1,300)
		m.uploadLogFileURLXfer.SetUserAgent(m.bsp.userAgent$)
	endif
	    
' if a transfer is in progress, return
    m.diagnostics.PrintDebug("### Upload " + m.uploadLogFolder)
	if not m.uploadLogFileURLXfer.SetUrl(m.uploadLogFileURL$) then
        m.diagnostics.PrintDebug("### Upload " + m.uploadLogFolder + " - upload already in progress")
		return 
	end if

' see if there are any files to upload
    listOfLogFiles = MatchFiles("/" + m.uploadLogFolder, "*.log")
    if listOfLogFiles.Count() = 0 then return
    
	binding% = GetBinding(m.bsp.logUploadsXfersEnabledWired, m.bsp.logUploadsXfersEnabledWireless)
    m.diagnostics.PrintDebug("### Binding for UploadLogFiles is " + stri(binding%))
	ok = m.uploadLogFileURLXfer.BindToInterface(binding%)

' upload the first file    
    for each file in listOfLogFiles
        m.diagnostics.PrintDebug("### UploadLogFiles " + file + " to " + m.uploadLogFileURL$)
        fullFilePath = m.uploadLogFolder + "/" + file
                
        contentDisposition$ = GetContentDisposition(file)
        m.AddUploadHeaders(m.uploadLogFileURLXfer, contentDisposition$)

        ok = m.uploadLogFileURLXfer.AsyncPostFromFile(fullFilePath)
        if not ok then
	        m.diagnostics.PrintDebug("### UploadLogFiles - AsyncPostFromFile failed")
        else
			m.logFileUpload = fullFilePath
			m.logFile$ = file
			return
        endif
    next
    
End Sub


Sub UploadLogFileHandler(msg As Object)
	    	    
    if msg.GetResponseCode() = 200 then

        if IsString(m.logFileUpload) then
            m.diagnostics.PrintDebug("###  UploadLogFile XferEvent - successfully uploaded " + m.logFileUpload)
            if m.enableLogDeletion then
                DeleteFile(m.logFileUpload)
            else
                target$ = m.uploadLogArchiveFolder + "/" + m.logFile$
                ok = MoveFile(m.logFileUpload, target$)
            endif
            m.logFileUpload = invalid		    
        endif
        
    else
        
        if IsString(m.logFileUpload) then
            m.diagnostics.PrintDebug("### Failed to upload log file " + m.logFileUpload + ", error code = " + str(msg.GetResponseCode()))

            ' move file so that the script doesn't try to upload it again immediately
            target$ = m.uploadLogFailedFolder + "/" + m.logFile$
            ok = MoveFile(m.logFileUpload, target$)

        endif

        m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_LOGFILE_UPLOAD_FAILURE, str(msg.GetResponseCode()))
        
	endif
	
	m.UploadLogFiles()
		
End Sub


Function UploadTrafficDownload(contentDownloaded# As Double) As Boolean

    if m.trafficDownloadURL = "" then
        m.diagnostics.PrintDebug("### UploadTrafficDownload - trafficDownloadURL not set, return")
        return false
    else
        m.diagnostics.PrintDebug("### UploadTrafficDownload")
    endif
    
' create roUrlTransfer if needed
	if type(m.trafficDownloadUploadURL) <> "roUrlTransfer" then
		m.trafficDownloadUploadURL = CreateObject("roUrlTransfer")
        m.trafficDownloadUploadURL.SetUrl(m.trafficDownloadURL)
        m.trafficDownloadUploadURL.SetPort(m.msgPort)
	    m.trafficDownloadUploadURL.SetTimeout(900000)
		m.trafficDownloadUploadURL.SetUserAgent(m.bsp.userAgent$)
	endif
	    
' if a transfer is in progress, return
	if not m.trafficDownloadUploadURL.SetUrl(m.trafficDownloadURL) then
        m.diagnostics.PrintDebug("### UploadTrafficDownload - upload already in progress")
		return false
	end if

    m.lastContentDownloaded# = contentDownloaded#
    
	binding% = GetBinding(m.bsp.contentXfersEnabledWired, m.bsp.contentXfersEnabledWireless)
    m.diagnostics.PrintDebug("### Binding for UploadTrafficDownload is " + stri(binding%))
	ok = m.trafficDownloadUploadURL.BindToInterface(binding%)
	if not ok then stop

	return m.SendTrafficUpload(m.trafficDownloadUploadURL, contentDownloaded#, false)

End Function


Function UploadMRSSTrafficDownload(contentDownloaded# As Double) As Boolean

    if m.trafficDownloadURL = "" then
        m.diagnostics.PrintDebug("### UploadMRSSTrafficDownload - trafficDownloadURL not set, return")
        return false
    else
        m.diagnostics.PrintDebug("### UploadMRSSTrafficDownload")
    endif
    
' create roUrlTransfer if needed
	if type(m.mrssTrafficDownloadUploadURL) <> "roUrlTransfer" then
		m.mrssTrafficDownloadUploadURL = CreateObject("roUrlTransfer")
        m.mrssTrafficDownloadUploadURL.SetUrl(m.trafficDownloadURL)
        m.mrssTrafficDownloadUploadURL.SetPort(m.msgPort)
	    m.mrssTrafficDownloadUploadURL.SetTimeout(900000)
		m.mrssTrafficDownloadUploadURL.SetUserAgent(m.bsp.userAgent$)
	endif
	    
' if a transfer is in progress, return
	if not m.mrssTrafficDownloadUploadURL.SetUrl(m.trafficDownloadURL) then
        m.diagnostics.PrintDebug("### UploadMRSSTrafficDownload - upload already in progress")

		totalContentDownloaded# = m.pendingMRSSContentDownloaded#
		totalContentDownloaded# = totalContentDownloaded# + contentDownloaded#
		m.pendingMRSSContentDownloaded# = totalContentDownloaded#

		return false
	end if

	contentDownloaded# = contentDownloaded# + m.pendingMRSSContentDownloaded#
	m.pendingMRSSContentDownloaded# = 0
    m.lastMRSSContentDownloaded# = contentDownloaded#
    
	binding% = GetBinding(m.bsp.mediaFeedsXfersEnabledWired, m.bsp.mediaFeedsXfersEnabledWireless)
    m.diagnostics.PrintDebug("### Binding for UploadMRSSTrafficDownload is " + stri(binding%))
	ok = m.mrssTrafficDownloadUploadURL.BindToInterface(binding%)
	if not ok then stop

	m.diagnostics.PrintDebug("### UploadMRSSTrafficDownload: Content downloaded = " + str(contentDownloaded#))
	return m.SendTrafficUpload(m.mrssTrafficDownloadUploadURL, contentDownloaded#, true)

End Function


Sub SendTrafficUpload(url As Object, contentDownloaded# As Double, intermediateTrafficReport As Boolean) As Boolean

' convert contentDownloaded# to contentDownloaded in KBytes which can be stored in an integer
	contentDownloaded% = contentDownloaded# / 1024

	url.SetHeaders(m.currentSync.GetMetadata("server"))
    url.AddHeader("DeviceID", m.deviceUniqueID$)
    url.AddHeader("contentDownloadedInKBytes", StripLeadingSpaces(stri(contentDownloaded%)))    
    url.AddHeader("DeviceFWVersion", m.firmwareVersion$)
    url.AddHeader("DeviceSWVersion", m.autorunVersion$)
    url.AddHeader("CustomAutorunVersion", m.customAutorunVersion$)
    url.AddHeader("timezone", m.systemTime.GetTimeZone())
    url.AddHeader("utcTime", m.systemTime.GetUtcDateTime().GetString())
	if intermediateTrafficReport then
	    url.AddHeader("intermediateTrafficReport", "yes")
	endif

	ok = url.AsyncPostFromString("UploadTrafficDownload")
	if not ok then
        m.diagnostics.PrintDebug("### SendTrafficUpload - AsyncPostFromString failed")
		return false
	endif	

    return ok

End Sub


Sub AddEventItem(eventType$ As String, eventData$ As String, eventResponseCode$ As String)
    
    ' Make sure the array doesn't get too big.
    while m.EventItems.Count() > 50
        m.EventItems.Shift()
    end while

    eventItem = CreateObject("roAssociativeArray")
    eventItem.eventType$ = eventType$
    eventItem.eventData$ = eventData$
    eventItem.eventResponseCode$ = eventResponseCode$
    m.EventItems.push(eventItem)

    m.UploadEvent()
    
End Sub


Sub AddDeviceErrorItem(event$ As String, name$ As String, failureReason$ As String, responseCode$ As String)
    
    ' Make sure the array doesn't get too big.
    while m.DeviceErrorItems.Count() > 50
        m.DeviceErrorItems.Shift()
    end while

    deviceErrorItem = CreateObject("roAssociativeArray")
    deviceErrorItem.event$ = event$
    deviceErrorItem.name$ = name$
    deviceErrorItem.failureReason$ = failureReason$
    deviceErrorItem.responseCode$ = responseCode$
    m.DeviceErrorItems.push(deviceErrorItem)

    m.UploadDeviceError()
    
End Sub


Sub UploadEvent()

    m.diagnostics.PrintDebug("### UploadEvent")

' verify that there is content to upload
    if m.EventItems.Count() = 0 then return
    
' create roUrlTransfer if needed
	if type(m.eventUploadURL) <> "roUrlTransfer" then
		m.eventUploadURL = CreateObject("roUrlTransfer")
	    m.eventUploadURL.SetUrl(m.eventURL)
        m.eventUploadURL.SetPort(m.msgPort)
	    m.eventUploadURL.SetTimeout(900000)
		m.eventUploadURL.SetUserAgent(m.bsp.userAgent$)
	endif
	    
' if a transfer is in progress, return
	if not m.eventUploadURL.SetUrl(m.eventURL) then
        m.diagnostics.PrintDebug("### UploadEvent - upload already in progress")
        if m.EventItems.Count() > 50 then
            m.diagnostics.PrintDebug("### UploadEvent - clear items from queue")
            m.EventItems.Clear()
        endif        
		return 
	end if

' generate the XML and upload the data
    root = CreateObject("roXMLElement")
    
    root.SetName("EventBatch")

    for each eventItem in m.EventItems
    
        item = root.AddBodyElement()
        item.SetName("event")

        elem = item.AddElement("eventType")
        elem.SetBody(eventItem.eventType$)
    
        elem = item.AddElement("eventData")
        elem.SetBody(eventItem.eventData$)
    
        elem = item.AddElement("eventResponseCode")
        elem.SetBody(eventItem.eventResponseCode$)
    
    next

    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

' prepare the upload    
    contentDisposition$ = GetContentDisposition("UploadEvent.xml")
    m.AddUploadHeaders(m.eventUploadURL, contentDisposition$)

	binding% = GetBinding(m.bsp.contentXfersEnabledWired, m.bsp.contentXfersEnabledWireless)
    m.diagnostics.PrintDebug("### Binding for UploadEvent is " + stri(binding%))
	ok = m.eventUploadURL.BindToInterface(binding%)
	if not ok then stop

	ok = m.eventUploadURL.AsyncPostFromString(xml)
    if not ok then
        m.diagnostics.PrintDebug("### UploadEvent - AsyncPostFromString failed")
	else
		' clear out EventItems - no big deal if the post fails
		m.EventItems.Clear()
	endif
        
End Sub


Sub UploadDeviceError()

    m.diagnostics.PrintDebug("### UploadDeviceError")

' verify that there is content to upload
    if m.DeviceErrorItems.Count() = 0 then return
    
' create roUrlTransfer if needed
	if type(m.deviceErrorUploadURL) <> "roUrlTransfer" then
		m.deviceErrorUploadURL = CreateObject("roUrlTransfer")
	    m.deviceErrorUploadURL.SetUrl(m.deviceErrorURL)
        m.deviceErrorUploadURL.SetPort(m.msgPort)
	    m.deviceErrorUploadURL.SetTimeout(900000)
		m.deviceErrorUploadURL.SetUserAgent(m.bsp.userAgent$)
	endif
	    
' if a transfer is in progress, return
	if not m.deviceErrorUploadURL.SetUrl(m.deviceErrorURL) then
        m.diagnostics.PrintDebug("### UploadDeviceError - upload already in progress")
        if m.DeviceErrorItems.Count() > 50 then
            m.diagnostics.PrintDebug("### UploadDeviceError - clear items from queue")
            m.DeviceErrorItems.Clear()
        endif        
		return 
	end if

' generate the XML and upload the data
    root = CreateObject("roXMLElement")
    
    root.SetName("DeviceErrorBatch")

    for each deviceErrorItem in m.DeviceErrorItems
    
        item = root.AddBodyElement()
        item.SetName("deviceError")

        elem = item.AddElement("event")
        elem.SetBody(deviceErrorItem.event$)
    
        elem = item.AddElement("name")
        elem.SetBody(deviceErrorItem.name$)
    
        elem = item.AddElement("failureReason")
        elem.SetBody(deviceErrorItem.failureReason$)
    
        elem = item.AddElement("responseCode")
        elem.SetBody(deviceErrorItem.responseCode$)
    
    next

    xml = root.GenXML({ indent: " ", newline: chr(10), header: true })

' prepare the upload    
    contentDisposition$ = GetContentDisposition("UploadDeviceError.xml")
    m.AddUploadHeaders(m.deviceErrorUploadURL, contentDisposition$)

	binding% = GetBinding(m.bsp.contentXfersEnabledWired, m.bsp.contentXfersEnabledWireless)
    m.diagnostics.PrintDebug("### Binding for UploadDeviceError is " + stri(binding%))
	ok = m.deviceErrorUploadURL.BindToInterface(binding%)
	if not ok then stop

	ok = m.deviceErrorUploadURL.AsyncPostFromString(xml)
    if not ok then
        m.diagnostics.PrintDebug("### UploadDeviceError - AsyncPostFromString failed")
    else
		' clear out DeviceErrorItems - no big deal if the post fails
		m.DeviceErrorItems.Clear()
    endif
        
End Sub


Function STNetworkSchedulerEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				currentTime = m.stateMachine.systemTime.GetLocalDateTime()

				' set timer for when content download window starts / ends
				if m.stateMachine.contentDownloadsRestricted then
					startOfRange% = m.stateMachine.contentDownloadRangeStart%
					endOfRange% = startOfRange% + m.stateMachine.contentDownloadRangeLength%
            
					notInDownloadWindow = OutOfDownloadWindow(currentTime, startOfRange%, endOfRange%)

					if notInDownloadWindow then
						m.stateMachine.RestartContentDownloadWindowStartTimer(currentTime, startOfRange%)
					else
						m.stateMachine.RestartContentDownloadWindowEndTimer(currentTime, endOfRange%)
					endif

				endif

				' set timer for when heartbeat window starts / ends
				if m.stateMachine.timeBetweenHearbeats% > 0 then

					if m.stateMachine.heartbeatsRestricted then
						startOfRange% = m.stateMachine.heartbeatsRangeStart%
						endOfRange% = startOfRange% + m.stateMachine.heartbeatsRangeLength%
            
						notInHeartbeatWindow = OutOfDownloadWindow(currentTime, startOfRange%, endOfRange%)

						if notInHeartbeatWindow then
							m.stateMachine.RestartHeartbeatsWindowStartTimer(currentTime, startOfRange%)
						else
							' in window, send initial heartbeat
							m.stateMachine.SendHeartbeat()		
							m.stateMachine.RestartHeartbeatsWindowEndTimer(currentTime, endOfRange%)
						endif
					else
						m.stateMachine.SendHeartbeat()		
					endif

				endif

                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
            else if event["EventType"] = "DISK_ERROR" then

				errorEvent =  event["DiskError"]
		        m.stateMachine.AddDeviceErrorItem("diskError", errorEvent["source"] + " " + errorEvent["device"], errorEvent["error"], errorEvent["param"])
				return "HANDLED"

			else if event["EventType"] = "SNAPSHOT_CAPTURED" then
				
				snapshotName$ = event["SnapshotName"]

				if type(m.stateMachine.awsAccessKeyId$) = "roString" and m.stateMachine.awsAccessKeyId$ <> "" then
					m.UploadSnapshotToBSN(snapshotName$)
				else if type(m.stateMachine.securityToken) = "roString" and m.stateMachine.securityToken <> "" then
					m.UploadSnapshotToBSNEE(snapshotName$)
				else if m.stateMachine.uploadSnapshotsURL$ <> "" then
					m.UploadSnapshotToSFN(snapshotName$)
				endif

			else if event["EventType"] = "SEND_HEARTBEAT" then
				m.stateMachine.SendHeartbeat()
			endif
            
        endif
        
    else if type(event) = "roTimerEvent" then
    
		if type(m.heartbeatTimer) = "roTimer" then

            if stri(event.GetSourceIdentity()) = stri(m.heartbeatTimer.GetIdentity()) then

				time = m.stateMachine.systemTime.GetLocalDateTime()
				if m.stateMachine.heartbeatsRestricted then
					startOfRange% = m.stateMachine.heartbeatsRangeStart%
					endOfRange% = startOfRange% + m.stateMachine.heartbeatsRangeLength%
            
					notInHeartbeatWindow = OutOfDownloadWindow(time, startOfRange%, endOfRange%)

					if not notInHeartbeatWindow then
						m.stateMachine.SendHeartbeat()		
					endif
				else
					m.stateMachine.SendHeartbeat()		
				endif

				return "HANDLED"

			endif
					
		endif

        if type(m.stateMachine.heartbeatsWindowStartTimer) = "roTimer" then
        
            if stri(event.GetSourceIdentity()) = stri(m.stateMachine.heartbeatsWindowStartTimer.GetIdentity()) then

				' start window end timer
				if m.stateMachine.heartbeatsRestricted then

					currentTime = m.stateMachine.systemTime.GetLocalDateTime()
					startOfRange% = m.stateMachine.heartbeatsRangeStart%
					endOfRange% = startOfRange% + m.stateMachine.heartbeatsRangeLength%

					m.stateMachine.RestartHeartbeatsWindowEndTimer(currentTime, endOfRange%)

				endif

		        return "HANDLED"
		    endif
		    
		endif

        if type(m.stateMachine.heartbeatsWindowEndTimer) = "roTimer" then
        
            if stri(event.GetSourceIdentity()) = stri(m.stateMachine.heartbeatsWindowEndTimer.GetIdentity()) then

				' start window start timer
				currentTime = m.stateMachine.systemTime.GetLocalDateTime()
				startOfRange% = m.stateMachine.heartbeatsRangeStart%
				endOfRange% = startOfRange% + m.stateMachine.heartbeatsRangeLength%

				m.stateMachine.RestartHeartbeatsWindowStartTimer(currentTime, startOfRange%)

		        return "HANDLED"
		    endif
		    
		endif

        if type(m.stateMachine.contentDownloadWindowStartTimer) = "roTimer" then
        
            if stri(event.GetSourceIdentity()) = stri(m.stateMachine.contentDownloadWindowStartTimer.GetIdentity()) then

				SetDownloadRateLimit(m.bsp.diagnostics, 0, m.stateMachine.wiredRateLimits.rlInWindow%)

				if m.stateMachine.useWireless then
					SetDownloadRateLimit(m.bsp.diagnostics, 1, m.stateMachine.wirelessRateLimits.rlInWindow%)
				endif
                
				' start window end timer
				if m.stateMachine.contentDownloadsRestricted then

					currentTime = m.stateMachine.systemTime.GetLocalDateTime()
					startOfRange% = m.stateMachine.contentDownloadRangeStart%
					endOfRange% = startOfRange% + m.stateMachine.contentDownloadRangeLength%

					m.stateMachine.RestartContentDownloadWindowEndTimer(currentTime, endOfRange%)

				endif

		        return "HANDLED"
		    endif
		    
		endif

        if type(m.stateMachine.contentDownloadWindowEndTimer) = "roTimer" then
        
            if stri(event.GetSourceIdentity()) = stri(m.stateMachine.contentDownloadWindowEndTimer.GetIdentity()) then

				' send internal message to indicate that any in-progress sync pool downloads should stop
				cancelDownloadsEvent = CreateObject("roAssociativeArray")
				cancelDownloadsEvent["EventType"] = "CANCEL_DOWNLOADS"
				m.stateMachine.msgPort.PostMessage(cancelDownloadsEvent)

				' change rate limit values - outside window
				SetDownloadRateLimit(m.bsp.diagnostics, 0, m.stateMachine.wiredRateLimits.rlOutsideWindow%)

				if m.stateMachine.useWireless then
					SetDownloadRateLimit(m.bsp.diagnostics, 1, m.stateMachine.wirelessRateLimits.rlOutsideWindow%)
				endif

				' start window start timer
				currentTime = m.stateMachine.systemTime.GetLocalDateTime()
				startOfRange% = m.stateMachine.contentDownloadRangeStart%
				endOfRange% = startOfRange% + m.stateMachine.contentDownloadRangeLength%

				m.stateMachine.RestartContentDownloadWindowStartTimer(currentTime, startOfRange%)

		        return "HANDLED"
		    endif
		    
		endif

        if type(m.stateMachine.retrySnapshotUploadTimer) = "roTimer" then        
            if stri(event.GetSourceIdentity()) = stri(m.stateMachine.retrySnapshotUploadTimer.GetIdentity()) then
				if type(m.stateMachine.uploadSnapshotUrl) = "roUrlTransfer" then
					' a snapshot upload is currently in progress - return and wait for completion event handler
				else
					' make sure there's an outstanding snapshot to upload
					if not m.stateMachine.pendingSnapshotsToUpload.IsEmpty() then
						m.stateMachine.pendingSnapshotsToUpload.Reset()
						snapshotName = m.stateMachine.pendingSnapshotsToUpload.Next()
						m.stateMachine.pendingSnapshotsToUpload.Delete(snapshotName)
						m.UploadSnapshotToBSN(snapshotName)
					endif
				endif
				m.stateMachine.retrySnapshotUploadTimer = invalid
				return "HANDLED"
			endif

		endif

		if type(m.bsp.overrideBSNTimer) = "roTimer" then
            if stri(event.GetSourceIdentity()) = stri(m.bsp.overrideBSNTimer.GetIdentity()) then
			    m.bsp.diagnostics.PrintDebug("### BSN override expired.")
				m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BSN_OVERRIDE_EXPIRED, "")
				m.bsp.overrideBSNTimer = invalid
				m.stateMachine.SetBASParameters(m.stateMachine.currentSync, false, false, true, true)
			endif
		endif

    else if type(event) = "roUrlEvent" then
        
		if type(m.stateMachine.uploadSnapshotToBSNEEUrl) = "roUrlTransfer" then
			if event.GetSourceIdentity() = m.stateMachine.uploadSnapshotToBSNEEUrl.GetIdentity() then
				if event.GetResponseCode() = 200 then
					m.bsp.diagnostics.PrintDebug("### Snapshot file uploaded to BSNEE")
				else
					' log failure
					m.bsp.diagnostics.PrintDebug("### snapshot upload failure " + stri(event.GetResponseCode()) + " " + event.GetFailureReason())
					m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SCREENSHOT_UPLOAD_ERROR, stri(event.GetResponseCode()) + " " + event.GetFailureReason())
				endif

				m.stateMachine.uploadSnapshotToBSNEEUrl = invalid

				return "HANDLED"

			endif
		endif

		if type(m.stateMachine.uploadSnapshotToSFNUrl) = "roUrlTransfer" then
			if event.GetSourceIdentity() = m.stateMachine.uploadSnapshotToSFNUrl.GetIdentity() then
				if event.GetResponseCode() = 200 then
					m.bsp.diagnostics.PrintDebug("### snapshot sucessfully uploaded to simple file networking handler: " + m.stateMachine.uploadSnapshotsURL$)
					m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SCREENSHOT_UPLOADED, " ")
				else
					m.bsp.diagnostics.PrintDebug("### snapshot upload failure " + stri(event.GetResponseCode()) + " " + event.GetFailureReason())
					m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SCREENSHOT_UPLOAD_ERROR, stri(event.GetResponseCode()) + " " + event.GetFailureReason())
				endif
			endif
		endif

		if type(m.stateMachine.uploadSnapshotUrl) = "roUrlTransfer" then
			if event.GetSourceIdentity() = m.stateMachine.uploadSnapshotUrl.GetIdentity() then

				snapshot = m.stateMachine.uploadSnapshotUrl.GetUserData()
				snapshotName$ = snapshot.name
				url = snapshot.url

				if event.GetResponseCode() = 200 then

					' note - don't check for prior upload failures here - go ahead and queue this upload so that users can see
					' this latest snapshots as soon as possible.

					m.QueueSnapshotForBSN(snapshotName$, url)

				else

					' log failure
					m.bsp.diagnostics.PrintDebug("### snapshot upload failure " + snapshotName$ + stri(event.GetResponseCode()) + " " + event.GetFailureReason())
					m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SCREENSHOT_UPLOAD_ERROR, snapshotName$ + stri(event.GetResponseCode()) + " " + event.GetFailureReason())

					' retry
					m.stateMachine.pendingSnapshotsToUpload.AddReplace(snapshotName$, snapshotName$)
				
					if type(m.stateMachine.retrySnapshotUploadTimer) <> "roTimer" then
						m.stateMachine.retrySnapshotUploadTimer = CreateObject("roTimer")
						m.stateMachine.retrySnapshotUploadTimer.SetPort(m.stateMachine.msgPort)
						m.stateMachine.retrySnapshotUploadTimer.SetElapsed(30, 0)
						m.stateMachine.retrySnapshotUploadTimer.Start()
					endif

				endif

				' indicate that transfer is no longer in progress
				m.stateMachine.uploadSnapshotUrl = invalid

				return "HANDLED"
			endif
		endif

		if type(m.stateMachine.queueSnapshotUrl) = "roUrlTransfer" then
			if event.GetSourceIdentity() = m.stateMachine.queueSnapshotUrl.GetIdentity() then

				snapshotName$ = m.stateMachine.queueSnapshotUrl.GetUserData()

				if event.GetResponseCode() = 200 then

					m.bsp.diagnostics.PrintDebug("### snapshot uploaded and queued" + snapshotName$)
					m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SCREENSHOT_UPLOADED_AND_QUEUED, snapshotName$)

					' if prior uploads failed and another upload is not in progress, retry upload
					if not m.stateMachine.pendingSnapshotsToUpload.IsEmpty() and type(m.stateMachine.uploadSnapshotUrl) <> "roUrlTransfer" then
						m.stateMachine.pendingSnapshotsToUpload.Reset()
						snapshotName = m.stateMachine.pendingSnapshotsToUpload.Next()
						m.stateMachine.pendingSnapshotsToUpload.Delete(snapshotName)
						m.UploadSnapshotToBSN(snapshotName)
					endif

				else

					' queue operation failed - retry entire upload / queue sequence

					' log failure
					m.bsp.diagnostics.PrintDebug("### snapshot queue failure " + snapshotName$ + stri(event.GetResponseCode()) + " " + event.GetFailureReason())
					m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SCREENSHOT_QUEUE_ERROR, snapshotName$ + stri(event.GetResponseCode()) + " " + event.GetFailureReason())

					m.stateMachine.pendingSnapshotsToUpload.AddReplace(snapshotName$, snapshotName$)				
					if type(m.stateMachine.retrySnapshotUploadTimer) <> "roTimer" then
						m.stateMachine.retrySnapshotUploadTimer = CreateObject("roTimer")
						m.stateMachine.retrySnapshotUploadTimer.SetPort(m.stateMachine.msgPort)
						m.stateMachine.retrySnapshotUploadTimer.SetElapsed(30, 0)
						m.stateMachine.retrySnapshotUploadTimer.Start()
					endif

				endif

				return "HANDLED"
			endif
		endif

		if type (m.stateMachine.sendHeartbeatUrl) = "roUrlTransfer" then
	        if event.GetSourceIdentity() = m.stateMachine.sendHeartbeatUrl.GetIdentity() then

				m.bsp.diagnostics.PrintDebug("###  SendHeartbeatUrlEvent: " + stri(event.GetResponseCode()))

				if event.GetResponseCode() = 200 then
					m.stateMachine.numHeartbeatRetries% = 0
				    m.stateMachine.currentTimeBetweenHeartbeats% = m.stateMachine.timeBetweenHearbeats%
				else
					m.stateMachine.ResetHeartbeatTimerToDoRetry()
				endif

				' start heartbeat timer
				if type(m.heartbeatTimer) <> "roTimer" then
					m.heartbeatTimer = CreateObject("roTimer")
					m.heartbeatTimer.SetPort(m.stateMachine.msgPort)
				endif
				m.heartbeatTimer.SetElapsed(m.stateMachine.currentTimeBetweenHeartbeats%, 0)
				m.heartbeatTimer.Start()

				return "HANDLED"
	        endif
        endif

        if type (m.stateMachine.deviceDownloadUploadURL) = "roUrlTransfer" then
	        if event.GetSourceIdentity() = m.stateMachine.deviceDownloadUploadURL.GetIdentity() then
				if event.GetResponseCode() = 200 then
					m.stateMachine.DeviceDownloadItemsPendingUpload.Clear()
				else
					m.bsp.diagnostics.PrintDebug("###  DeviceDownloadURLEvent: " + stri(event.GetResponseCode()))
				endif
				m.stateMachine.deviceDownloadUploadURL = invalid
				m.stateMachine.UploadDeviceDownload()
				return "HANDLED"
	        endif
        endif

        if type (m.stateMachine.deviceDownloadProgressUploadURL) = "roUrlTransfer" then
	        if event.GetSourceIdentity() = m.stateMachine.deviceDownloadProgressUploadURL.GetIdentity() then
				if event.GetResponseCode() = 200 then
					m.stateMachine.DeviceDownloadProgressItemsPendingUpload.Clear()
				else
					m.bsp.diagnostics.PrintDebug("###  DeviceDownloadProgressURLEvent: " + stri(event.GetResponseCode()))
				endif
				m.stateMachine.deviceDownloadProgressUploadURL = invalid
				m.stateMachine.UploadDeviceDownloadProgressItems()
				return "HANDLED"
	        endif
        endif

        if type (m.stateMachine.uploadLogFileURLXfer) = "roUrlTransfer" then
	        if event.GetSourceIdentity() = m.stateMachine.uploadLogFileURLXfer.GetIdentity() then
				m.stateMachine.uploadLogFileURLXfer = invalid
	            m.stateMachine.UploadLogFileHandler(event)
                return "HANDLED"
	        endif
        endif

        if type (m.stateMachine.eventUploadURL) = "roUrlTransfer" then
	        if event.GetSourceIdentity() = m.stateMachine.eventUploadURL.GetIdentity() then
			    m.stateMachine.eventUploadURL = invalid
	            m.stateMachine.UploadEvent()
                return "HANDLED"
	        endif
        endif

        if type (m.stateMachine.deviceErrorUploadURL) = "roUrlTransfer" then
	        if event.GetSourceIdentity() = m.stateMachine.deviceErrorUploadURL.GetIdentity() then
			    m.stateMachine.deviceErrorUploadURL = invalid
	            m.stateMachine.UploadDeviceError()
                return "HANDLED"
	        endif
        endif

		if type (m.stateMachine.trafficDownloadUploadURL) = "roUrlTransfer" then
			if event.GetSourceIdentity() = m.stateMachine.trafficDownloadUploadURL.GetIdentity() then
				if event.GetInt() = m.URL_EVENT_COMPLETE then
					m.bsp.diagnostics.PrintDebug("###  URLTrafficDownloadXferEvent: " + stri(event.GetResponseCode()))
					m.stateMachine.trafficDownloadUploadURL = invalid
					if event.GetResponseCode() <> 200 then
						m.stateMachine.UploadTrafficDownload(m.lastContentDownloaded#)
					endif
			    endif
                return "HANDLED"
			endif
		endif
    
		if type (m.stateMachine.mrssTrafficDownloadUploadURL) = "roUrlTransfer" then
			if event.GetSourceIdentity() = m.stateMachine.mrssTrafficDownloadUploadURL.GetIdentity() then
				if event.GetInt() = m.URL_EVENT_COMPLETE then
					m.bsp.diagnostics.PrintDebug("###  URLMRSSTrafficDownloadXferEvent: " + stri(event.GetResponseCode()))
					m.stateMachine.mrssTrafficDownloadUploadURL = invalid
					if event.GetResponseCode() <> 200 then
						m.stateMachine.UploadMRSSTrafficDownload(m.lastMRSSContentDownloaded#)
					endif
			    endif
                return "HANDLED"
			endif
		endif

    endif
            
    stateData.nextState = m.superState
    return "SUPER"
    
End Function


Sub RestartWindowStartTimer(timer As Object, currentTime As Object, startOfRange% As Integer)

	hour% = startOfRange% / 60
	minute% = startOfRange% - (hour% * 60)
	timeoutTime = CopyDateTime(currentTime)
	timeoutTime.SetHour(hour%)
	timeoutTime.SetMinute(minute%)
	timeoutTime.SetSecond(0)
	timeoutTime.SetMillisecond(0)
	GetNextTimeout(m.systemTime, timeoutTime)
	timer.SetDateTime(timeoutTime)
	timer.SetPort(m.msgPort)
	timer.Start()

	m.bsp.diagnostics.PrintDebug("RestartWindowStartTimer: set timer to start of window - " + timeoutTime.GetString())

End Sub


Sub RestartContentDownloadWindowStartTimer(currentTime As Object, startOfRange% As Integer)
	m.contentDownloadWindowStartTimer = CreateObject("roTimer")
	m.RestartWindowStartTimer(m.contentDownloadWindowStartTimer, currentTime, startOfRange%)
End Sub


Sub RestartHeartbeatsWindowStartTimer(currentTime As Object, startOfRange% As Integer)
	m.heartbeatsWindowStartTimer = CreateObject("roTimer")
	m.RestartWindowStartTimer(m.heartbeatsWindowStartTimer, currentTime, startOfRange%)
End Sub


Function RestartWindowEndTimer(currentTime As Object, endOfRange% As Integer) As Object

	currentTime.SetHour(0)
	currentTime.SetMinute(0)
	currentTime.SetSecond(0)
	currentTime.SetMillisecond(0)
	currentTime.AddSeconds(endOfRange% * 60)
	currentTime.Normalize()
	GetNextTimeout(m.systemTime, currentTime)
	timer = CreateObject("roTimer")
	timer.SetDateTime(currentTime)
	timer.SetPort(m.msgPort)
	timer.Start()

	m.bsp.diagnostics.PrintDebug("RestartWindowEndTimer: set timer to end of window - " + currentTime.GetString())

	return timer

End Function


Sub RestartContentDownloadWindowEndTimer(currentTime As Object, endOfRange% As Integer)
	m.contentDownloadWindowEndTimer = m.RestartWindowEndTimer(currentTime, endOfRange%)
End Sub


Sub RestartHeartbeatsWindowEndTimer(currentTime As Object, endOfRange% As Integer)
	m.heartbeatsWindowEndTimer = m.RestartWindowEndTimer(currentTime, endOfRange%)
End Sub


Sub GetNextTimeout(systemTime As Object, timerDateTime As object) As Object

	currentDateTime = systemTime.GetLocalDateTime()

	if timerDateTime.GetString() <= currentDateTime.GetString() then
		timerDateTime.AddSeconds(24 * 60 * 60)
		timerDateTime.Normalize()
	endif
	
End Sub


Sub WaitForTransfersToComplete()

    if type(m.trafficDownloadUploadURL) = "roUrlTransfer" then    
        ' check to see if the trafficUpload call has been processed - if not, wait 5 seconds
        if not m.trafficDownloadUploadURL.SetUrl(m.trafficDownloadURL) then
            m.diagnostics.PrintDebug("### RebootAfterEventsSent - traffic upload still in progress - wait")
            sleep(5000)
            m.diagnostics.PrintDebug("### RebootAfterEventsSent - proceed after waiting 5 seconds for traffic upload to complete")
        else
            m.diagnostics.PrintDebug("### RebootAfterEventsSent - traffic upload must be complete - proceed")
        end if
    endif

    if type(m.deviceDownloadProgressUploadURL) = "roUrlTransfer" then    
        ' check to see if the device download progress call has been processed - if not, wait 5 seconds
	    if not m.deviceDownloadProgressUploadURL.SetUrl(m.deviceDownloadProgressURL) then
            sleep(5000)
            m.diagnostics.PrintDebug("### RebootAfterEventsSent - proceed after waiting 5 seconds for device download progress item upload to complete")
        else
            m.diagnostics.PrintDebug("### RebootAfterEventsSent - device download progress item upload must be complete - proceed")
        end if
    endif
    
    if type(m.deviceDownloadUploadURL) = "roUrlTransfer" then
        ' check to see if the device download call has been processed - if not, wait 5 seconds
	    if not m.deviceDownloadUploadURL.SetUrl(m.deviceDownloadURL) then
            sleep(5000)
            m.diagnostics.PrintDebug("### RebootAfterEventsSent - proceed after waiting 5 seconds for device download upload to complete")
        else
            m.diagnostics.PrintDebug("### RebootAfterEventsSent - device download upload must be complete - proceed")
        end if
    endif

End Sub


Sub RebootAfterEventsSent()

    ' temporary
    sleep(2000)
    
    m.WaitForTransfersToComplete()
    
	m.UploadDeviceDownloadProgressItems()    
	m.UploadDeviceDownload()
    
    m.WaitForTransfersToComplete()
    
    RebootSystem()

End Sub


Sub ResetDownloadTimerToDoRetry()

	if m.numRetries% >= m.maxRetries% then
	    m.diagnostics.PrintDebug("### reset_download_timer_to_do_retry - max retries attempted - wait until next regularly scheduled download.")
		m.numRetries% = 0
		m.currentTimeBetweenNetConnects% = m.timeBetweenNetConnects%
	else
		m.numRetries% = m.numRetries% + 1
		m.currentTimeBetweenNetConnects% = m.retryInterval% * m.numRetries%
	    m.diagnostics.PrintDebug("### reset_download_timer_to_do_retry - wait " + stri(m.currentTimeBetweenNetConnects%) + " seconds.")
	endif
	    
	m.assetFetcher = invalid

End Sub


Sub ResetHeartbeatTimerToDoRetry()

	if m.numHeartbeatRetries% >= m.maxHeartbeatRetries% then
	    m.diagnostics.PrintDebug("### reset_heartbeat_timer_to_do_retry - max retries attempted - wait until next regularly scheduled heartbeat.")
		m.numHeartbeatRetries% = 0
		m.currentTimeBetweenHeartbeats% = m.timeBetweenHearbeats%
	else
		m.numHeartbeatRetries% = m.numHeartbeatRetries% + 1
		m.currentTimeBetweenHeartbeats% = m.heartbeatRetryInterval% * m.numHeartbeatRetries%
	    m.diagnostics.PrintDebug("### reset_heartbeat_timer_to_do_retry - wait " + stri(m.currentTimeBetweenHeartbeats%) + " seconds.")
	endif
	    
End Sub


Function STWaitForTimeoutEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.stateMachine.networkTimerDownload.timer.SetElapsed(m.stateMachine.currentTimeBetweenNetConnects%, 0)
				m.stateMachine.networkTimerDownload.timer.Start()
				
                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
            endif
            
        endif
        
    else if type(event) = "roTimerEvent" then
    
        if type(m.stateMachine.networkTimerDownload.timer) = "roTimer" then
        
            if stri(event.GetSourceIdentity()) = stri(m.stateMachine.networkTimerDownload.timer.GetIdentity()) then
        	    stateData.nextState = m.stateMachine.stRetrievingSyncList
		        return "TRANSITION"
    		    
		    endif
		    
		endif

    endif
            
    stateData.nextState = m.superState
    return "SUPER"
    
End Function


Function STRetrievingSyncListEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

                m.StartSync("download")

                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
            endif
            
        endif
        
    else if type(event) = "roUrlEvent" then
    
        m.bsp.diagnostics.PrintDebug("STRetrievingSyncListEventHandler: roUrlEvent")

        if stri(event.GetSourceIdentity()) = stri(m.xfer.GetIdentity()) then
        
            stateData.nextState = m.SyncSpecXferEvent(event)
		    return "TRANSITION"
		    
		endif

    endif
            
    stateData.nextState = m.superState
    return "SUPER"
    
End Function


Sub SendHeartbeat()

	if m.heartbeatURL$ = "" then
        m.bsp.diagnostics.PrintDebug("### SendHeartbeat - heartbeatURL not set, return")
		return
	endif
		
    m.bsp.diagnostics.PrintTimestamp()
    m.bsp.diagnostics.PrintDebug("### SendHeartbeat")

	m.sendHeartbeatUrl = CreateObject("roUrlTransfer")
	m.sendHeartbeatUrl.SetPort(m.msgPort)
	m.sendHeartbeatUrl.SetUserAgent(m.bsp.userAgent$)

	m.sendHeartbeatUrl.SetUrl(m.heartbeatURL$)
	m.sendHeartbeatUrl.SetTimeout(90000) ' 90 second timeout

' Add minimum number of headers
	m.sendHeartbeatUrl.SetHeaders(m.currentSync.GetMetadata("server"))
	m.sendHeartbeatUrl.AddHeader("BrightWallName", m.bsp.registrySettings.brightWallName$)
	m.sendHeartbeatUrl.AddHeader("BrightWallScreenNumber", m.bsp.registrySettings.brightWallScreenNumber$)
	m.sendHeartbeatUrl.AddHeader("act", m.currentSync.LookupMetadata("server", "account"))
	m.sendHeartbeatUrl.AddHeader("pwd", m.currentSync.LookupMetadata("server", "password"))
	m.sendHeartbeatUrl.AddHeader("g", m.currentSync.LookupMetadata("server", "group"))
	m.sendHeartbeatUrl.AddHeader("u", m.currentSync.LookupMetadata("server", "user"))
	m.sendHeartbeatUrl.AddHeader("id", m.deviceUniqueID$)
	m.sendHeartbeatUrl.AddHeader("model", m.deviceModel$)
	m.sendHeartbeatUrl.AddHeader("family", m.deviceFamily$)
	m.sendHeartbeatUrl.AddHeader("fw", m.firmwareVersion$)
	m.sendHeartbeatUrl.AddHeader("sw", m.autorunVersion$)
	m.sendHeartbeatUrl.AddHeader("ca", m.customAutorunVersion$)
	m.sendHeartbeatUrl.AddHeader("tz", m.systemTime.GetTimeZone())

' card size
	du = CreateObject("roStorageInfo", "./")
	m.sendHeartbeatUrl.AddHeader("sz", str(du.GetSizeInMegabytes()))
	du = invalid

' presentation name
    if type(m.bsp.sign) = "roAssociativeArray" then
        m.sendHeartbeatUrl.AddHeader("pName", m.bsp.sign.name$)
    else
        m.sendHeartbeatUrl.AddHeader("pName", "none")
    endif
    
	binding% = GetBinding(m.bsp.healthXfersEnabledWired, m.bsp.healthXfersEnabledWireless)
    m.bsp.diagnostics.PrintDebug("### Binding for Heartbeat is " + stri(binding%))
	ok = m.sendHeartbeatUrl.BindToInterface(binding%)
	if not ok then stop

    if not m.sendHeartbeatUrl.AsyncGetToString() then stop

End Sub


Sub StartSync(syncType$ As String)

' Call when you want to start a sync operation

    m.bsp.diagnostics.PrintTimestamp()
    
    m.bsp.diagnostics.PrintDebug("### start_sync " + syncType$)
    
	if type(m.stateMachine.assetFetcher) = "roAssetFetcher" then
' This should be improved in the future to work out
' whether the sync spec we're currently satisfying
' matches the one that we're currently downloading or
' not.
        m.bsp.diagnostics.PrintDebug("### sync already active so we'll let it continue")
        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNC_ALREADY_ACTIVE, "")
		return
	endif

    if syncType$ = "cache" then
        if not m.stateMachine.proxy_mode then
            m.bsp.diagnostics.PrintDebug("### cache download requested but the BrightSign is not configured to use a cache server")
            return
        endif
    endif
    
	m.xfer = CreateObject("roUrlTransfer")
	m.xfer.SetPort(m.stateMachine.msgPort)
	m.xfer.SetUserAgent(m.bsp.userAgent$)
	
	m.stateMachine.syncType$ = syncType$

    m.bsp.diagnostics.PrintDebug("### xfer created - identity = " + stri(m.xfer.GetIdentity()) + " ###")

' We've read in our current sync. Talk to the server to get
' the next sync. Note that we use the current-sync.xml because
' we need to tell the server what we are _currently_ running not
' what we might be running at some point in the future.

    base$ = m.stateMachine.currentSync.LookupMetadata("client", "base")
    nextURL = GetURL(base$, m.stateMachine.currentSync.LookupMetadata("client", "next"))
    m.bsp.diagnostics.PrintDebug("### Looking for new sync list from " + nextURL)    
    m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_CHECK_CONTENT, nextURL)

	m.xfer.SetUrl(nextURL)
    if m.stateMachine.setUserAndPassword then m.xfer.SetUserAndPassword(m.stateMachine.user$, m.stateMachine.password$)
	m.xfer.EnableUnsafeAuthentication(m.stateMachine.enableUnsafeAuthentication)
    m.xfer.SetMinimumTransferRate(10,240)
	m.xfer.SetHeaders(m.stateMachine.currentSync.GetMetadata("server"))

' Add presentation name to header
    if type(m.bsp.sign) = "roAssociativeArray" then
        m.xfer.AddHeader("presentationName", m.bsp.sign.name$)
    else
        m.xfer.AddHeader("presentationName", "none")
    endif
    
' Add device unique identifier, timezone
    m.xfer.AddHeader("DeviceID", m.stateMachine.deviceUniqueID$)
    m.xfer.AddHeader("DeviceModel", m.stateMachine.deviceModel$)
    m.xfer.AddHeader("DeviceFamily", m.stateMachine.deviceFamily$)
    m.xfer.AddHeader("DeviceFWVersion", m.stateMachine.firmwareVersion$)
    m.xfer.AddHeader("DeviceSWVersion", m.stateMachine.autorunVersion$)
    m.xfer.AddHeader("CustomAutorunVersion", m.stateMachine.customAutorunVersion$)
    m.xfer.AddHeader("timezone", m.stateMachine.systemTime.GetTimeZone())
    m.xfer.AddHeader("localTime", m.stateMachine.systemTime.GetLocalDateTime().GetString())

    m.stateMachine.AddMiscellaneousHeaders(m.xfer, m.bsp.assetPool)

' Add headers associated with remote snapshot - TODO - shouldn't have to do this!!

    m.xfer.AddHeader("deviceScreenShotsEnabled", m.stateMachine.currentSync.LookupMetadata("client", "deviceScreenShotsEnabled"))
    m.xfer.AddHeader("deviceScreenShotsInterval", m.stateMachine.currentSync.LookupMetadata("client", "deviceScreenShotsInterval"))
    m.xfer.AddHeader("deviceScreenShotsCountLimit", m.stateMachine.currentSync.LookupMetadata("client", "deviceScreenShotsCountLimit"))
    m.xfer.AddHeader("deviceScreenShotsQuality", m.stateMachine.currentSync.LookupMetadata("client", "deviceScreenShotsQuality"))

'	globalAA = GetGlobalAA()
'	if globalAA.enableRemoteSnapshot then
'		deviceScreenShotsEnabled$ = "yes"
'		deviceScreenShotsInterval$ = StripLeadingSpaces(stri(globalAA.remoteSnapshotInterval))
'		deviceScreenShotsCountLimit$ = StripLeadingSpaces(stri(globalAA.remoteSnapshotMaxImages))
'		deviceScreenShotsQuality$ = StripLeadingSpaces(stri(globalAA.remoteSnapshotJpegQualityLevel))
'	else
'		deviceScreenShotsEnabled$ = "no"
'		deviceScreenShotsInterval$ = ""
'		deviceScreenShotsCountLimit$ = ""
'		deviceScreenShotsQuality$ = ""
'	endif

'    m.xfer.AddHeader("deviceScreenShotsEnabled", deviceScreenShotsEnabled$)
'    m.xfer.AddHeader("deviceScreenShotsInterval", deviceScreenShotsInterval$)
'    m.xfer.AddHeader("deviceScreenShotsCountLimit", deviceScreenShotsCountLimit$)
'    m.xfer.AddHeader("deviceScreenShotsQuality", deviceScreenShotsQuality$)

' Add headers for BrightWall
	m.xfer.AddHeader("BrightWallName", m.bsp.registrySettings.brightWallName$)
	m.xfer.AddHeader("BrightWallScreenNumber", m.bsp.registrySettings.brightWallScreenNumber$)

' Add headers if this is a local system trying to connect to BSN for the first time
	localToBSN = false
	if not FileExists("current-sync.xml") then
		if not FileExists("local-sync.xml") then
			if FileExists("localToBSN-sync.xml") then
				m.stateMachine.AddLocalToBSNHeaders(m.xfer)
			endif
		endif
	endif

	binding% = GetBinding(m.bsp.contentXfersEnabledWired, m.bsp.contentXfersEnabledWireless)
    m.bsp.diagnostics.PrintDebug("### Binding for StartSync is " + stri(binding%))
	ok = m.xfer.BindToInterface(binding%)
	if not ok then stop

    if not m.xfer.AsyncGetToObject("roSyncSpec") then stop
    
End Sub


Sub AddLocalToBSNHeaders(xfer As Object)

   	xfer.AddHeader("setup", "yes")
    xfer.AddHeader("synclistid", "0")
	xfer.AddHeader("unitName", m.bsp.registrySettings.unitName$)
    xfer.AddHeader("unitNamingMethod", m.bsp.registrySettings.unitNamingMethod$)
	xfer.AddHeader("unitDescription", m.bsp.registrySettings.unitDescription$)
	xfer.AddHeader("playbackLoggingEnabled", m.bsp.registrySettings.playbackLoggingEnabled)
	xfer.AddHeader("eventLoggingEnabled", m.bsp.registrySettings.eventLoggingEnabled)
	xfer.AddHeader("diagnosticLoggingEnabled", m.bsp.registrySettings.diagnosticLoggingEnabled)
	xfer.AddHeader("variableLoggingEnabled", m.bsp.registrySettings.variableLoggingEnabled)
	xfer.AddHeader("stateLoggingEnabled", m.bsp.registrySettings.stateLoggingEnabled)
	xfer.AddHeader("uploadLogFilesAtBoot", m.bsp.registrySettings.uploadLogFilesAtBoot)
	xfer.AddHeader("uploadLogFilesAtSpecificTime", m.bsp.registrySettings.uploadLogFilesAtSpecificTime)
	xfer.AddHeader("uploadLogFilesTime", m.bsp.registrySettings.uploadLogFilesTime$)
    xfer.AddHeader("timeBetweenNetConnects", m.bsp.registrySettings.timeBetweenNetConnects$)
    xfer.AddHeader("contentDownloadsRestricted", m.bsp.registrySettings.contentDownloadsRestricted)
    if m.bsp.registrySettings.contentDownloadsRestricted = "yes" then
		xfer.AddHeader("contentDownloadRangeStart", m.bsp.registrySettings.contentDownloadRangeStart)
		xfer.AddHeader("contentDownloadRangeLength", m.bsp.registrySettings.contentDownloadRangeLength)
	endif
    xfer.AddHeader("timeBetweenHeartbeats", m.bsp.registrySettings.timeBetweenHeartbeats$)
	xfer.AddHeader("heartbeatsRestricted", m.bsp.registrySettings.heartbeatsRestricted)
    if m.bsp.registrySettings.heartbeatsRestricted = "yes" then
	    xfer.AddHeader("heartbeatsRangeStart", m.bsp.registrySettings.heartbeatsRangeStart)
		xfer.AddHeader("heartbeatsRangeLength", m.bsp.registrySettings.heartbeatsRangeLength)
	endif

    xfer.AddHeader("dwsEnabled", m.bsp.registrySettings.dwsEnabled$)
    xfer.AddHeader("dwsPassword", m.bsp.registrySettings.dwsPassword$)

	xfer.AddHeader("proxy", m.bsp.registrySettings.proxy$)

	xfer.AddHeader("useWireless", m.bsp.registrySettings.useWireless$)    
    if m.bsp.registrySettings.useWireless$ = "yes" then
        xfer.AddHeader("ssid", m.bsp.registrySettings.ssid$)    
        xfer.AddHeader("passphrase", m.bsp.registrySettings.passphrase$)    
    endif

    xfer.AddHeader("timeServer", m.bsp.registrySettings.timeServer$)    

' first network configuration
	useDHCP$ = m.bsp.registrySection.Read("dhcp")
    xfer.AddHeader("useDHCP", useDHCP$)    
    if useDHCP$ = "no" then
        xfer.AddHeader("staticIPAddress", m.bsp.registrySection.Read("sip"))    
        xfer.AddHeader("subnetMask", m.bsp.registrySection.Read("sm"))    
        xfer.AddHeader("gateway", m.bsp.registrySection.Read("gw"))    
        xfer.AddHeader("broadcast", m.bsp.registrySection.Read("bc"))    
        xfer.AddHeader("dns1", m.bsp.registrySection.Read("d1"))    
        xfer.AddHeader("dns2", m.bsp.registrySection.Read("d2"))    
        xfer.AddHeader("dns3", m.bsp.registrySection.Read("d3"))
    endif

    ' rate limit parameters
    xfer.AddHeader("rateLimitModeOutsideWindow", m.bsp.registrySettings.rateLimitModeOutsideWindow$)    
    xfer.AddHeader("rateLimitRateOutsideWindow", m.bsp.registrySettings.rateLimitRateOutsideWindow$)    
    xfer.AddHeader("rateLimitModeInWindow", m.bsp.registrySettings.rateLimitModeInWindow$)    
    xfer.AddHeader("rateLimitRateInWindow", m.bsp.registrySettings.rateLimitRateInWindow$)    
    xfer.AddHeader("rateLimitModeInitialDownloads", m.bsp.registrySettings.rateLimitModeInitialDownloads$)    
    xfer.AddHeader("rateLimitRateInitialDownloads", m.bsp.registrySettings.rateLimitRateInitialDownloads$)    

' second network configuration
	useDHCP_2$ = m.bsp.registrySection.Read("dhcp2")
	if useDHCP_2$ = "" then
		useDHCP_2$ = "yes"
	endif
    xfer.AddHeader("useDHCP_2", useDHCP_2$)
    if useDHCP_2$ = "no" then
        xfer.AddHeader("staticIPAddress_2", m.bsp.registrySection.Read("sip2"))    
        xfer.AddHeader("subnetMask_2", m.bsp.registrySection.Read("sm2"))    
        xfer.AddHeader("gateway_2", m.bsp.registrySection.Read("gw2"))    
        xfer.AddHeader("broadcast_2", m.bsp.registrySection.Read("bc2"))    
        xfer.AddHeader("dns1_2", m.bsp.registrySection.Read("d12"))    
        xfer.AddHeader("dns2_2", m.bsp.registrySection.Read("d22"))    
        xfer.AddHeader("dns3_2", m.bsp.registrySection.Read("d32"))    
    endif

    xfer.AddHeader("rateLimitModeOutsideWindow_2", m.bsp.registrySettings.rateLimitModeOutsideWindow_2$)    
    xfer.AddHeader("rateLimitRateOutsideWindow_2", m.bsp.registrySettings.rateLimitRateOutsideWindow_2$)    
    xfer.AddHeader("rateLimitModeInWindow_2", m.bsp.registrySettings.rateLimitModeInWindow_2$)    
    xfer.AddHeader("rateLimitRateInWindow_2", m.bsp.registrySettings.rateLimitRateInWindow_2$)    
    xfer.AddHeader("rateLimitModeInitialDownloads_2", m.bsp.registrySettings.rateLimitModeInitialDownloads_2$)    
    xfer.AddHeader("rateLimitRateInitialDownloads_2", m.bsp.registrySettings.rateLimitRateInitialDownloads_2$)    

	xfer.AddHeader("networkConnectionPriorityWired", m.bsp.registrySettings.networkConnectionPriorityWired$)    
	xfer.AddHeader("contentXfersEnabledWired", m.bsp.registrySettings.contentXfersEnabledWired$)
	xfer.AddHeader("textFeedsXfersEnabledWired", m.bsp.registrySettings.textFeedsXfersEnabledWired$)
	xfer.AddHeader("healthXfersEnabledWired", m.bsp.registrySettings.healthXfersEnabledWired$)
	xfer.AddHeader("mediaFeedsXfersEnabledWired", m.bsp.registrySettings.mediaFeedsXfersEnabledWired$)
	xfer.AddHeader("logUploadsXfersEnabledWired", m.bsp.registrySettings.logUploadsXfersEnabledWired$)

    if m.bsp.registrySettings.useWireless$ = "yes" then
		xfer.AddHeader("networkConnectionPriorityWireless", m.bsp.registrySettings.networkConnectionPriorityWireless$)    
		xfer.AddHeader("contentXfersEnabledWireless", m.bsp.registrySettings.contentXfersEnabledWireless$)
		xfer.AddHeader("textFeedsXfersEnabledWireless", m.bsp.registrySettings.textFeedsXfersEnabledWireless$)
		xfer.AddHeader("healthXfersEnabledWireless", m.bsp.registrySettings.healthXfersEnabledWireless$)
		xfer.AddHeader("mediaFeedsXfersEnabledWireless", m.bsp.registrySettings.mediaFeedsXfersEnabledWireless$)
		xfer.AddHeader("logUploadsXfersEnabledWireless", m.bsp.registrySettings.logUploadsXfersEnabledWireless$)
	endif

End Sub


Sub AddMiscellaneousHeaders(urlXfer As Object, assetPool As Object)

' Add card size
	du = CreateObject("roStorageInfo", "./")
	urlXfer.AddHeader("storage-size", str(du.GetSizeInMegabytes()))
	urlXfer.AddHeader("storage-fs", du.GetFileSystemType())

' Add estimated realized size
	tempRealizer = CreateObject("roAssetRealizer", assetPool, "/")
	tempSpec = CreateObject("roSyncSpec")

	if tempSpec.ReadFromFile("current-sync.xml") or tempSpec.ReadFromFile("localToBSN-sync.xml") then
	    urlXfer.AddHeader("storage-current-used", str(tempRealizer.EstimateRealizedSizeInMegabytes(tempSpec)))
	endif
	tempRealizer = invalid
	tempSpec = invalid
    
End Sub


Function SyncSpecXferEvent(event As Object) As Object

    nextState = invalid
    
	xferInUse = false
	
	if event.GetResponseCode() = 200 then
	
        m.stateMachine.newSync = event.GetObject()

        m.bsp.diagnostics.PrintDebug("### Spec received from server")

		' save last successful connection to BSN in local time
		currentTime = m.bsp.systemTime.GetLocalDateTime()
		m.bsp.WriteRegistrySetting("lastBSNConnectionTime", currentTime)
		m.bsp.registrySettings.lastBSNConnectionTime = currentTime

		headers=event.getResponseHeaders()
		if headers.DoesExist("bsn-content-passphrase") and m.bsp.contentEncryptionSupported then
			m.bsp.obfuscatedEncryptionKey = headers["bsn-content-passphrase"]
		else
			m.bsp.obfuscatedEncryptionKey = ""
		endif

        ' check for a forced reboot
        forceReboot$ = LCase(m.stateMachine.newSync.LookupMetadata("client", "forceReboot"))
        if forceReboot$ = "true" then
            m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_RECEIVED, "FORCE REBOOT")
            m.stateMachine.logging.FlushLogFile()
	        a=RebootSystem()
            stop
        endif
        
        ' check for forced log upload
        forceLogUpload$ = LCase(m.stateMachine.newSync.LookupMetadata("client", "forceLogUpload"))
        if forceLogUpload$ = "true" then
            m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_RECEIVED, "FORCE LOG UPLOAD")
            m.stateMachine.logging.CutoverLogFile(true)
			m.bsp.LogActivePresentation()
        endif

		' Determine if device is in BrightWall group, then clear brightWall settings in registry if they are currently set but device is no longer part of BrightWall group
		if m.bsp.registrySettings.brightWallName$ <> "" and m.bsp.registrySettings.brightWallScreenNumber$ <> "" then

			clearBrightWallSettings = false
		
			serverMetadata = m.stateMachine.newSync.GetMetadata("server")
			if type(serverMetadata) = "roAssociativeArray" then
				if not serverMetadata.DoesExist("brightWall") or serverMetadata["brightWall"] = "" then
					clearBrightWallSettings = true
				endif
			endif

			if clearBrightWallSettings then
				m.bsp.registrySection.Write("brightWallName", "")
				m.bsp.registrySection.Write("brightWallScreenNumber", "")
				m.bsp.registrySection.Flush()
				m.bsp.registrySettings.brightWallName$ = ""
				m.bsp.registrySettings.brightWallScreenNumber$ = ""
			endif
		endif
				
	    if m.stateMachine.newSync.EqualTo(m.stateMachine.currentSync) then
            m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_RECEIVED, "NO")
            m.bsp.diagnostics.PrintDebug("### Server has given us a spec that matches current-sync. Nothing more to do.")
            m.stateMachine.AddDeviceDownloadItem("SyncSpecUnchanged", "", "")
		    m.stateMachine.newSync = invalid
		    m.stateMachine.numRetries% = 0
		    m.stateMachine.currentTimeBetweenNetConnects% = m.stateMachine.timeBetweenNetConnects%

			' if necessary, upload the list of current files to the server
			if m.stateMachine.FileListPendingUpload then
				m.stateMachine.BuildFileDownloadList(m.stateMachine.currentSync)
				m.stateMachine.UploadDeviceDownloadProgressFileList()
				m.stateMachine.FileListPendingUpload = false
			endif

            return m.stateMachine.stWaitForTimeout
	    endif
        badReadySync = CreateObject("roSyncSpec")
	    if badReadySync.ReadFromFile("bad-sync.xml") then
		    if m.stateMachine.newSync.EqualTo(badReadySync) then
                m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_RECEIVED, "BAD SYNC")
                m.bsp.diagnostics.PrintDebug("### Server has given us a spec that matches bad-sync. Nothing more to do.")
			    badReadySync = invalid
			    m.stateMachine.newSync = invalid
			    m.stateMachine.numRetries% = 0
			    m.stateMachine.currentTimeBetweenNetConnects% = m.stateMachine.timeBetweenNetConnects%
				return m.stateMachine.stWaitForTimeout
		    endif
	    endif

        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_RECEIVED, "YES")

		m.stateMachine.assetCollection = m.stateMachine.newSync.GetAssets("download")
		m.stateMachine.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.assetPool, m.stateMachine.assetCollection)

		m.stateMachine.BuildFileDownloadList(m.stateMachine.newSync)
        m.stateMachine.UploadDeviceDownloadProgressFileList()
		m.stateMachine.FileListPendingUpload = false
        m.stateMachine.AddDeviceDownloadItem("SyncSpecDownloadStarted", "", "")
                                                
        m.stateMachine.contentDownloaded# = 0

' Update the pool sizes based on the newly downloaded sync spec
		m.bsp.SetPoolSizes( m.stateMachine.newSync )

' Only update values that the application web server can change if not currently in an override state.
		if type(m.bsp.overrideBSNTimer) = "roTimer" then
			overrideModeActive = true
		else
			overrideModeActive = false
		endif
		m.stateMachine.SetBASParameters(m.stateMachine.newSync, true, true, not overrideModeActive, not overrideModeActive)

' Retrieve network connection priorities from the sync spec
		networkConnectionPriorityWired$ = m.stateMachine.newSync.LookupMetadata("client", "networkConnectionPriorityWired")
		if networkConnectionPriorityWired$ <> "" then
			networkConnectionPriorityWired% = int(val(networkConnectionPriorityWired$))
			nc = CreateObject("roNetworkConfiguration", 0)
			if type(nc) = "roNetworkConfiguration" then
				nc.SetRoutingMetric(networkConnectionPriorityWired%)
				nc.Apply()
				nc = invalid
			endif
		endif

		networkConnectionPriorityWireless$ = m.stateMachine.newSync.LookupMetadata("client", "networkConnectionPriorityWireless")
		if networkConnectionPriorityWireless$ <> "" then
			networkConnectionPriorityWireless% = int(val(networkConnectionPriorityWireless$))
			nc = CreateObject("roNetworkConfiguration", 1)
			if type(nc) = "roNetworkConfiguration" then
				nc.SetRoutingMetric(networkConnectionPriorityWireless%)
				nc.Apply()
				nc = invalid
			endif
		endif

' Retrieve data xfers enabled information from the sync spec and write to registry if it's changed
		m.bsp.contentXfersEnabledWired = GetDataTransferEnabled(m.statemachine.newSync, "contentXfersEnabledWired")
		m.bsp.registrySettings.contentXfersEnabledWired$ = m.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "contentXfersEnabledWired"), m.bsp.registrySettings.contentXfersEnabledWired$, "cwr")

		m.bsp.textFeedsXfersEnabledWired = GetDataTransferEnabled(m.statemachine.newSync, "textFeedsXfersEnabledWired")
		m.bsp.registrySettings.textFeedsXfersEnabledWired$ = m.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "textFeedsXfersEnabledWired"), m.bsp.registrySettings.textFeedsXfersEnabledWired$, "twr")

		m.bsp.healthXfersEnabledWired = GetDataTransferEnabled(m.statemachine.newSync, "healthXfersEnabledWired")
		m.bsp.registrySettings.healthXfersEnabledWired$ = m.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "healthXfersEnabledWired"), m.bsp.registrySettings.healthXfersEnabledWired$, "hwr")

		m.bsp.mediaFeedsXfersEnabledWired = GetDataTransferEnabled(m.statemachine.newSync, "mediaFeedsXfersEnabledWired")
		m.bsp.registrySettings.mediaFeedsXfersEnabledWired$ = m.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "mediaFeedsXfersEnabledWired"), m.bsp.registrySettings.mediaFeedsXfersEnabledWired$, "mwr")

		m.bsp.logUploadsXfersEnabledWired = GetDataTransferEnabled(m.statemachine.newSync, "logUploadsXfersEnabledWired")
		m.bsp.registrySettings.logUploadsXfersEnabledWired$ = m.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "logUploadsXfersEnabledWired"), m.bsp.registrySettings.logUploadsXfersEnabledWired$, "lwr")
    
		m.bsp.contentXfersEnabledWireless = GetDataTransferEnabled(m.statemachine.newSync, "contentXfersEnabledWireless")
		m.bsp.registrySettings.contentXfersEnabledWireless$ = m.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "contentXfersEnabledWireless"), m.bsp.registrySettings.contentXfersEnabledWireless$, "cwf")

		m.bsp.textFeedsXfersEnabledWireless = GetDataTransferEnabled(m.statemachine.newSync, "textFeedsXfersEnabledWireless")
		m.bsp.registrySettings.textFeedsXfersEnabledWireless$ = m.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "textFeedsXfersEnabledWireless"), m.bsp.registrySettings.textFeedsXfersEnabledWireless$, "twf")

		m.bsp.healthXfersEnabledWireless = GetDataTransferEnabled(m.statemachine.newSync, "healthXfersEnabledWireless")
		m.bsp.registrySettings.healthXfersEnabledWireless$ = m.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "healthXfersEnabledWireless"), m.bsp.registrySettings.healthXfersEnabledWireless$, "hwf")

		m.bsp.mediaFeedsXfersEnabledWireless = GetDataTransferEnabled(m.statemachine.newSync, "mediaFeedsXfersEnabledWireless")
		m.bsp.registrySettings.mediaFeedsXfersEnabledWireless$ = m.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "mediaFeedsXfersEnabledWireless"), m.bsp.registrySettings.mediaFeedsXfersEnabledWireless$, "mwf")

		m.bsp.logUploadsXfersEnabledWireless = GetDataTransferEnabled(m.statemachine.newSync, "logUploadsXfersEnabledWireless")
		m.bsp.registrySettings.logUploadsXfersEnabledWireless$ = m.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "logUploadsXfersEnabledWireless"), m.bsp.registrySettings.logUploadsXfersEnabledWireless$, "lwf")
    
' Retrieve logging information from the sync spec
        playbackLoggingEnabled = false
        b$ = LCase(m.statemachine.newSync.LookupMetadata("client", "playbackLoggingEnabled"))
        if b$ = "yes" then playbackLoggingEnabled = true

        eventLoggingEnabled = false
        b$ = LCase(m.statemachine.newSync.LookupMetadata("client", "eventLoggingEnabled"))
        if b$ = "yes" then eventLoggingEnabled = true

        diagnosticLoggingEnabled = false
        b$ = LCase(m.statemachine.newSync.LookupMetadata("client", "diagnosticLoggingEnabled"))
        if b$ = "yes" then diagnosticLoggingEnabled = true

        stateLoggingEnabled = false
        b$ = LCase(m.statemachine.newSync.LookupMetadata("client", "stateLoggingEnabled"))
        if b$ = "yes" then stateLoggingEnabled = true

        variableLoggingEnabled = false
        b$ = LCase(m.statemachine.newSync.LookupMetadata("client", "variableLoggingEnabled"))
        if b$ = "yes" then variableLoggingEnabled = true

        uploadLogFilesAtBoot = false
        b$ = LCase(m.statemachine.newSync.LookupMetadata("client", "uploadLogFilesAtBoot"))
        if b$ = "yes" then uploadLogFilesAtBoot = true

        uploadLogFilesAtSpecificTime = false
        b$ = LCase(m.statemachine.newSync.LookupMetadata("client", "uploadLogFilesAtSpecificTime"))
        if b$ = "yes" then uploadLogFilesAtSpecificTime = true

        uploadLogFilesTime% = 0
        uploadLogFilesTime$ = m.statemachine.newSync.LookupMetadata("client", "uploadLogFilesTime")
        if uploadLogFilesTime$ <> "" then uploadLogFilesTime% = int(val(uploadLogFilesTime$))
        
        m.stateMachine.logging.ReinitializeLogging(playbackLoggingEnabled, eventLoggingEnabled, stateLoggingEnabled, diagnosticLoggingEnabled, variableLoggingEnabled, uploadLogFilesAtBoot, uploadLogFilesAtSpecificTime, uploadLogFilesTime%)

' Retrieve unit name, description from sync spec and write to registry if they have changed

		' parameters are not set in SFN case, so don't change them here
		if m.statemachine.newSync.GetName() <> "Simple Networking" then
			unitNameFromRegistry$ = m.bsp.registrySettings.unitName$
			unitNamingMethodFromRegistry$ = m.bsp.registrySettings.unitNamingMethod$
			unitDescriptionFromRegistry$ = m.bsp.registrySettings.unitDescription$

			unitName$ = m.stateMachine.newSync.LookupMetadata("client", "unitName")
			unitNamingMethod$ = m.stateMachine.newSync.LookupMetadata("client", "unitNamingMethod")
			unitDescription$ = m.stateMachine.newSync.LookupMetadata("client", "unitDescription")

			if unitName$ <> unitNameFromRegistry$ then
				m.bsp.WriteRegistrySetting("un", unitName$)
				m.bsp.registrySettings.unitName$ = unitName$
			endif
		
			if unitNamingMethod$ <> unitNamingMethodFromRegistry$ then
				m.bsp.WriteRegistrySetting("unm", unitNamingMethod$)
				m.bsp.registrySettings.unitNamingMethod$ = unitNamingMethod$
			endif
		
			if unitDescription$ <> unitDescriptionFromRegistry$ then
				m.bsp.WriteRegistrySetting("ud", unitDescription$)
				m.bsp.registrySettings.unitDescription$ = unitDescription$
			endif

			proxyFromSyncSpec$ = m.stateMachine.newSync.LookupMetadata("client", "proxy")
			if proxyFromSyncSpec$ <> "" then
				useProxy$ = "yes"
			else
				useProxy$ = "no"
			endif
			useProxy$ = m.UpdateRegistrySetting(useProxy$, m.bsp.registrySettings.useProxy, "up")

			proxySpec$ = m.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "proxy"), m.bsp.registrySettings.proxy$, "ps")
			nc = CreateObject("roNetworkConfiguration", 0)
			if type(nc) = "roNetworkConfiguration" then
				ok = nc.SetProxy(proxySpec$)
				nc.Apply()
			endif

			nc = CreateObject("roNetworkConfiguration", 1)
			if type(nc) = "roNetworkConfiguration" then
				ok = nc.SetProxy(proxySpec$)
				nc.Apply()
			endif

			nc = invalid

			networkHosts$ = m.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "networkHosts"), m.bsp.registrySettings.networkHosts$, "bph")
			networkHosts = ParseJSON(networkHosts$)

			bypassProxyHosts = []
			for each networkHost in networkHosts
				if networkHost.HostName <> "" then
					bypassProxyHosts.push(networkHost.HostName)
				endif
			next

			' bypass proxy servers
			if m.bsp.bypassProxySupported then
				nc = CreateObject("roNetworkConfiguration", 0)
				if type(nc) = "roNetworkConfiguration" then
					ok = nc.SetProxyBypass(bypassProxyHosts)
					nc.Apply()
				endif

				nc = CreateObject("roNetworkConfiguration", 1)
				if type(nc) = "roNetworkConfiguration" then
					ok = nc.SetProxyBypass(bypassProxyHosts)
					nc.Apply()
				endif

				nc = invalid
			endif

		endif

' Retrieve latest network configuration information from sync spec
        useWireless$ = m.stateMachine.newSync.LookupMetadata("client", "useWireless")
        if not m.stateMachine.modelSupportsWifi then useWireless$ = "no"
        
        if useWireless$ = "yes" then
			m.stateMachine.useWireless = true
            ssid$ = m.stateMachine.newSync.LookupMetadata("client", "ssid")
            passphrase$ = m.stateMachine.newSync.LookupMetadata("client", "passphrase")
        else
			m.stateMachine.useWireless = false
		endif

        timeServer$ = m.stateMachine.newSync.LookupMetadata("client", "timeServer")

' if useDHCP is not set, don't touch the networking configuration (likely that the BrightSign is using simple networking)
        useDHCP = m.stateMachine.newSync.LookupMetadata("client", "useDHCP")
		if useDHCP <> "" then

			wiredNetworkingParameters = {}
			wiredNetworkingParameters.networkConfigurationIndex% = 0
			wiredNetworkingParameters.networkConnectionPriority$ = m.stateMachine.newSync.LookupMetadata("client", "networkConnectionPriorityWired")

			if m.stateMachine.useWireless then

				wirelessNetworkingParameters = {}
				wirelessNetworkingParameters.networkConfigurationIndex% = 1
				wirelessNetworkingParameters.networkConnectionPriority$ = m.stateMachine.newSync.LookupMetadata("client", "networkConnectionPriorityWireless")

				if useDHCP <> "" then
					wirelessNetworkingParameters.useDHCP$ = m.stateMachine.newSync.LookupMetadata("client", "useDHCP")
					if useDHCP = "no" then
						wirelessNetworkingParameters.staticIPAddress$ = m.stateMachine.newSync.LookupMetadata("client", "staticIPAddress")
						wirelessNetworkingParameters.subnetMask$ = m.stateMachine.newSync.LookupMetadata("client", "subnetMask")
						wirelessNetworkingParameters.gateway$ = m.stateMachine.newSync.LookupMetadata("client", "gateway")
						wirelessNetworkingParameters.dns1$ = m.stateMachine.newSync.LookupMetadata("client", "dns1")
						wirelessNetworkingParameters.dns2$ = m.stateMachine.newSync.LookupMetadata("client", "dns2")
						wirelessNetworkingParameters.dns3$ = m.stateMachine.newSync.LookupMetadata("client", "dns3")
					endif
					wirelessNetworkingParameters.useWireless = true
					wirelessNetworkingParameters.ssid$ = ssid$
					wirelessNetworkingParameters.passphrase$ = passphrase$
					wirelessNetworkingParameters.timeServer$ = timeServer$

					m.ConfigureNetwork(wirelessNetworkingParameters, m.bsp.registrySettings.wirelessNetworkingParameters, "")
				endif

				wiredNetworkingParameters.useDHCP$ = m.stateMachine.newSync.LookupMetadata("client", "useDHCP_2")
				if wiredNetworkingParameters.useDHCP$ = "no" then
					wiredNetworkingParameters.staticIPAddress$ = m.stateMachine.newSync.LookupMetadata("client", "staticIPAddress_2")
					wiredNetworkingParameters.subnetMask$ = m.stateMachine.newSync.LookupMetadata("client", "subnetMask_2")
					wiredNetworkingParameters.gateway$ = m.stateMachine.newSync.LookupMetadata("client", "gateway_2")
					wiredNetworkingParameters.dns1$ = m.stateMachine.newSync.LookupMetadata("client", "dns1_2")
					wiredNetworkingParameters.dns2$ = m.stateMachine.newSync.LookupMetadata("client", "dns2_2")
					wiredNetworkingParameters.dns3$ = m.stateMachine.newSync.LookupMetadata("client", "dns3_2")
				endif
				wiredNetworkingParameters.useWireless = false
				wiredNetworkingParameters.timeServer$ = timeServer$

				m.ConfigureNetwork(wiredNetworkingParameters, m.bsp.registrySettings.wiredNetworkingParameters, "2")

			else

				wiredNetworkingParameters.useDHCP$ = m.stateMachine.newSync.LookupMetadata("client", "useDHCP")
				if wiredNetworkingParameters.useDHCP$ = "no" then
					wiredNetworkingParameters.staticIPAddress$ = m.stateMachine.newSync.LookupMetadata("client", "staticIPAddress")
					wiredNetworkingParameters.subnetMask$ = m.stateMachine.newSync.LookupMetadata("client", "subnetMask")
					wiredNetworkingParameters.gateway$ = m.stateMachine.newSync.LookupMetadata("client", "gateway")
					wiredNetworkingParameters.dns1$ = m.stateMachine.newSync.LookupMetadata("client", "dns1")
					wiredNetworkingParameters.dns2$ = m.stateMachine.newSync.LookupMetadata("client", "dns2")
					wiredNetworkingParameters.dns3$ = m.stateMachine.newSync.LookupMetadata("client", "dns3")
				endif
				wiredNetworkingParameters.useWireless = false
				wiredNetworkingParameters.timeServer$ = timeServer$

				m.ConfigureNetwork(wiredNetworkingParameters, m.bsp.registrySettings.wiredNetworkingParameters, "")

				' if a device is setup to not use wireless, ensure that wireless is not used
				if m.stateMachine.modelSupportsWifi then
					nc = CreateObject("roNetworkConfiguration", 1)
					if type(nc) = "roNetworkConfiguration" then
						nc.SetDHCP()
						nc.SetWiFiESSID("")
						nc.SetObfuscatedWifiPassphrase("")
						nc.Apply()
					endif
				endif

			endif

		endif

		useWirelessFromRegistry$ = m.bsp.registrySettings.useWireless$
		ssidFromRegistry$ = m.bsp.registrySettings.ssid$
		passphraseFromRegistry$ = m.bsp.registrySettings.passphrase$
                    
        if useWirelessFromRegistry$ <> useWireless$ then 
			m.bsp.WriteRegistrySetting("wifi", useWireless$)
			m.bsp.registrySettings.useWireless$ = useWireless$
		endif
					
        if m.stateMachine.useWireless then
            if ssidFromRegistry$ <> ssid$ then 
				m.bsp.WriteRegistrySetting("ss", ssid$)
				m.bsp.registrySettings.ssid$ = ssid$
			endif
						
            if passphraseFromRegistry$ <> passphrase$ then
				m.bsp.WriteRegistrySetting("pp", passphrase$)
				m.bsp.registrySettings.passphrase$ = passphrase$
			endif
        endif
                    
        timeServerFromRegistry$ = m.bsp.registrySettings.timeServer$
        if timeServerFromRegistry$ <> timeServer$ then 
			m.bsp.WriteRegistrySetting("ts", timeServer$)
			m.bsp.registrySettings.timeServer$ = timeServer$
		endif

' Retrieve persistent beacon data, and write to registry and update beacons if changed
		beacon1Data = m.stateMachine.newSync.LookupMetadata("client", "beacon1")
		beacon2Data = m.stateMachine.newSync.LookupMetadata("client", "beacon2")

		beaconsChanged = false
		beacon1FromRegistry$ = m.bsp.registrySettings.beacon1
		if beacon1Data <> beacon1FromRegistry$ then
			m.bsp.WriteRegistrySetting("beacon1", beacon1Data)
			m.bsp.registrySettings.beacon1 = beacon1Data
			beaconsChanged = true
		endif
		beacon2FromRegistry$ = m.bsp.registrySettings.beacon2
		if beacon2Data <> beacon2FromRegistry$ then
			m.bsp.WriteRegistrySetting("beacon2", beacon2Data)
			m.bsp.registrySettings.beacon2 = beacon2Data
			beaconsChanged = true
		endif

		if beaconsChanged then
			' Update beacons with changed data and restart
			m.bsp.btManager.UpdatePersistentBeacons()
		endif

' Retrieve latest net connect spec information from sync spec
        timeBetweenHeartbeats$ = m.stateMachine.newSync.LookupMetadata("client", "timeBetweenHeartbeats")
		if timeBetweenHeartbeats$ <> "" then
			m.stateMachine.timeBetweenHeartbeats% = int(val(timeBetweenHeartbeats$))
		endif

        timeBetweenNetConnects$ = m.stateMachine.newSync.LookupMetadata("client", "timeBetweenNetConnects")
        
        if timeBetweenNetConnects$ <> "" then
        
			' check for timeBetweenNetConnects override
			tbnco$ = m.bsp.registrySettings.tbnco$
			if tbnco$ <> "" then
				timeBetweenNetConnects$	= tbnco$
			endif    

            ' if the timeBetweenNetConnects has changed, restart the timer
            newTimeBetweenNetConnects% = val(timeBetweenNetConnects$)
            if newTimeBetweenNetConnects% <> m.stateMachine.timeBetweenNetConnects% then
                m.stateMachine.timeBetweenNetConnects% = newTimeBetweenNetConnects%
                m.bsp.diagnostics.PrintDebug("### Time between net connects has changed to: " + timeBetweenNetConnects$)
            else
                m.bsp.diagnostics.PrintDebug("### Time between net connects = " + timeBetweenNetConnects$)
            endif
            
        endif
        
		' clear any existing timers associated with rate limitings / content download window
        if type(m.stateMachine.contentDownloadWindowStartTimer) = "roTimer" then
			m.stateMachine.contentDownloadWindowStartTimer.Stop()
		endif
        if type(m.stateMachine.contentDownloadWindowEndTimer) = "roTimer" then
			m.stateMachine.contentDownloadWindowEndTimer.Stop()
		endif

		' clear any existing timers associated with heartbeats window
        if type(m.stateMachine.heartbeatsWindowStartTimer) = "roTimer" then
			m.stateMachine.heartbeatsWindowStartTimer.Stop()
		endif
        if type(m.stateMachine.heartbeatsWindowEndTimer) = "roTimer" then
			m.stateMachine.heartbeatsWindowEndTimer.Stop()
		endif

		if m.stateMachine.useWireless then
			rateLimitModeOutsideWindowWired$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitModeOutsideWindow_2")
			rateLimitRateOutsideWindowWired$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitRateOutsideWindow_2")
			rateLimitModeInWindowWired$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitModeInWindow_2")
			rateLimitRateInWindowWired$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitRateInWindow_2")
		else
			rateLimitModeOutsideWindowWired$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitModeOutsideWindow")
			rateLimitRateOutsideWindowWired$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitRateOutsideWindow")
			rateLimitModeInWindowWired$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitModeInWindow")
			rateLimitRateInWindowWired$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitRateInWindow")
		endif
		' needs to also work in the SFN case where the values don't exist in the sync spec (same for contentDownloadsRestricted)
		' update rate limiting values
		SetRateLimitValues(false, m.stateMachine.wiredRateLimits, rateLimitModeOutsideWindowWired$, rateLimitRateOutsideWindowWired$, rateLimitModeInWindowWired$, rateLimitRateInWindowWired$)

		if m.stateMachine.useWireless then
			rateLimitModeOutsideWindowWireless$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitModeOutsideWindow")
			rateLimitRateOutsideWindowWireless$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitRateOutsideWindow")
			rateLimitModeInWindowWireless$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitModeInWindow")
			rateLimitRateInWindowWireless$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitRateInWindow")
		else
			rateLimitModeOutsideWindowWireless$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitModeOutsideWindow_2")
			rateLimitRateOutsideWindowWireless$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitRateOutsideWindow_2")
			rateLimitModeInWindowWireless$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitModeInWindow_2")
			rateLimitRateInWindowWireless$ = m.stateMachine.newSync.LookupMetadata("client", "rateLimitRateInWindow_2")
		endif
		' needs to also work in the SFN case where the values don't exist in the sync spec (same for contentDownloadsRestricted)
		' update rate limiting values
		SetRateLimitValues(false, m.stateMachine.wirelessRateLimits, rateLimitModeOutsideWindowWireless$, rateLimitRateOutsideWindowWireless$, rateLimitModeInWindowWireless$, rateLimitRateInWindowWireless$)

        currentTime = m.stateMachine.systemTime.GetLocalDateTime()

		heartbeatsRestricted = m.stateMachine.newSync.LookupMetadata("client", "heartbeatsRestricted")
        if heartbeatsRestricted = "yes" then
			m.stateMachine.heartbeatsRestricted = true
			heartbeatsRangeStart = m.stateMachine.newSync.LookupMetadata("client", "heartbeatsRangeStart")
			heartbeatsRangeLength = m.stateMachine.newSync.LookupMetadata("client", "heartbeatsRangeLength")
	        m.stateMachine.heartbeatsRangeStart% = val(heartbeatsRangeStart)
		    m.stateMachine.heartbeatsRangeLength% = val(heartbeatsRangeLength)

			startOfRange% = m.stateMachine.heartbeatsRangeStart%
			endOfRange% = startOfRange% + m.stateMachine.heartbeatsRangeLength%
            
			notInHeartbeatWindow = OutOfDownloadWindow(currentTime, startOfRange%, endOfRange%)

			if notInHeartbeatWindow then
				m.stateMachine.RestartHeartbeatsWindowStartTimer(currentTime, startOfRange%)
			else
				m.stateMachine.RestartHeartbeatsWindowEndTimer(currentTime, endOfRange%)
			endif
		else
			m.bsp.diagnostics.PrintDebug("### Heartbeats are unrestricted")
			m.stateMachine.heartbeatsRestricted = false
		endif

        contentDownloadsRestricted = m.stateMachine.newSync.LookupMetadata("client", "contentDownloadsRestricted")
        if contentDownloadsRestricted = "yes" then
            m.stateMachine.contentDownloadsRestricted = true
            contentDownloadRangeStart = m.stateMachine.newSync.LookupMetadata("client", "contentDownloadRangeStart")
            m.stateMachine.contentDownloadRangeStart% = val(contentDownloadRangeStart)
            contentDownloadRangeLength = m.stateMachine.newSync.LookupMetadata("client", "contentDownloadRangeLength")
            m.stateMachine.contentDownloadRangeLength% = val(contentDownloadRangeLength)
            m.bsp.diagnostics.PrintDebug("### Content downloads are restricted to the time from " + contentDownloadRangeStart + " for " + contentDownloadRangeLength + " minutes.")
        else
            m.bsp.diagnostics.PrintDebug("### Content downloads are unrestricted")
            m.stateMachine.contentDownloadsRestricted = false
        endif

' Only proceed with sync list download if the current time is within the range of allowed times for content downloads
        if m.stateMachine.contentDownloadsRestricted then
            startOfRange% = m.stateMachine.contentDownloadRangeStart%
            endOfRange% = startOfRange% + m.stateMachine.contentDownloadRangeLength%
            
			notInDownloadWindow = OutOfDownloadWindow(currentTime, startOfRange%, endOfRange%)

			if notInDownloadWindow then
				m.bsp.diagnostics.PrintDebug("### Not in window to download content")
	            m.stateMachine.AddDeviceDownloadItem("SyncSpecUnchanged", "", "")
			    m.stateMachine.numRetries% = 0
			    m.stateMachine.currentTimeBetweenNetConnects% = m.stateMachine.timeBetweenNetConnects%

				' if necessary, upload the list of current files to the server
				if m.stateMachine.FileListPendingUpload then
					m.stateMachine.BuildFileDownloadList(m.stateMachine.currentSync)
					m.stateMachine.UploadDeviceDownloadProgressFileList()
					m.stateMachine.FileListPendingUpload = false
				endif

	            m.stateMachine.newSync = invalid

				' set timer to go off when download window starts and program rate limit appropriately
				m.stateMachine.contentDownloadWindowStartTimer = CreateObject("roTimer")

				hour% = startOfRange% / 60
				minute% = startOfRange% - (hour% * 60)
				timeoutTime = CopyDateTime(currentTime)
				timeoutTime.SetHour(hour%)
				timeoutTime.SetMinute(minute%)
				timeoutTime.SetSecond(0)
				timeoutTime.SetMillisecond(0)
				GetNextTimeout(m.stateMachine.systemTime, timeoutTime)
				m.stateMachine.contentDownloadWindowStartTimer.SetDateTime(timeoutTime)
				m.stateMachine.contentDownloadWindowStartTimer.SetPort(m.stateMachine.msgPort)
				m.stateMachine.contentDownloadWindowStartTimer.Start()

		        m.bsp.diagnostics.PrintDebug("SyncSpecXferEvent: set timer to start of content download window" + timeoutTime.GetString())

				SetDownloadRateLimit(m.bsp.diagnostics, 0, m.stateMachine.wiredRateLimits.rlOutsideWindow%)

		        if m.stateMachine.useWireless then
					SetDownloadRateLimit(m.bsp.diagnostics, 1, m.stateMachine.wirelessRateLimits.rlOutsideWindow%)
				endif

				return m.stateMachine.stWaitForTimeout
			
			else
				' set timer to go off when download window ends and program rate limit appropriately
				m.stateMachine.contentDownloadWindowEndTimer = CreateObject("roTimer")
				currentTime.SetHour(0)
				currentTime.SetMinute(0)
				currentTime.SetSecond(0)
				currentTime.SetMillisecond(0)
				currentTime.AddSeconds(endOfRange% * 60)
				currentTime.Normalize()
				GetNextTimeout(m.stateMachine.systemTime, currentTime)
				m.stateMachine.contentDownloadWindowEndTimer.SetDateTime(currentTime)
				m.stateMachine.contentDownloadWindowEndTimer.SetPort(m.stateMachine.msgPort)
				m.stateMachine.contentDownloadWindowEndTimer.Start()

		        m.bsp.diagnostics.PrintDebug("STNetworkSchedulerEventHandler: set timer to end of content download window - " + currentTime.GetString())

				SetDownloadRateLimit(m.bsp.diagnostics, 0, m.stateMachine.wiredRateLimits.rlInWindow%)

		        if m.stateMachine.useWireless then
					SetDownloadRateLimit(m.bsp.diagnostics, 1, m.stateMachine.wirelessRateLimits.rlInWindow%)
				endif

			endif

        else

			SetDownloadRateLimit(m.bsp.diagnostics, 0, m.stateMachine.wiredRateLimits.rlOutsideWindow%)

	        if m.stateMachine.useWireless then
				SetDownloadRateLimit(m.bsp.diagnostics, 1, m.stateMachine.wirelessRateLimits.rlOutsideWindow%)
			endif

		endif
	
        return m.stateMachine.stDownloadingSyncFiles
	
    else if event.GetResponseCode() = 404 then
    
        m.bsp.diagnostics.PrintDebug("### Server has no sync list for us: 404")
        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_NO_SYNCSPEC_AVAILABLE, "404")
    
    else
		if event.GetResponseCode() <> 503 then
			' retry - server returned something other than a 200, a 404 or a 503
			m.stateMachine.ResetDownloadTimerToDoRetry()

			if event.GetFailureReason() <> "" then
				eventData$ = event.GetFailureReason()
			else
				eventData$ = str(event.GetResponseCode())
			endif

			m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_RETRIEVE_SYNCSPEC_FAILURE, eventData$)
			m.bsp.diagnostics.PrintDebug("### Failed to download sync list: " + eventData$)
			m.stateMachine.AddDeviceErrorItem("deviceError", "", "Failed to download sync list: " + event.GetFailureReason(), str(event.GetResponseCode()))
		else
			print "received 503"
		endif
	endif

	return m.stateMachine.stWaitForTimeout
	
End Function


Function NetworkingIsActive()

	return m.networkingActive

End Function


' update parameters that can be set by BrightSign application server
' either update them when no override is active, or when the override expires
Sub SetBASParameters(syncSpec As Object, setURLSFromSyncSpec As Boolean, setRegistryFromSyncSpec As Boolean, setGlobalsFromRegistry As Boolean, updateRunTime As Boolean)

	globalAA = GetGlobalAA()

	registrySection = CreateObject("roRegistrySection", "networking")
	if type(registrySection)<>"roRegistrySection" then print "Error: Unable to create roRegistrySection":stop

	if setURLSFromSyncSpec then
		m.SetRemoteSnapshotUrls(syncSpec)
	endif

	if setRegistryFromSyncSpec then
		registrySection.Write("enableRemoteSnapshot", syncSpec.LookupMetadata("client", "deviceScreenShotsEnabled"))
		registrySection.Write("remoteSnapshotInterval", syncSpec.LookupMetadata("client", "deviceScreenShotsInterval"))
		registrySection.Write("remoteSnapshotMaxImages", syncSpec.LookupMetadata("client", "deviceScreenShotsCountLimit"))
		registrySection.Write("remoteSnapshotJpegQualityLevel", syncSpec.LookupMetadata("client", "deviceScreenShotsQuality"))
		registrySection.Write("remoteSnapshotDisplayPortrait", syncSpec.LookupMetadata("client", "deviceScreenShotsDisplayPortrait"))
	endif

	' remember current settings
	currentEnableRemoteSnapshot = globalAA.enableRemoteSnapshot
	currentRemoteSnapshotInterval = globalAA.remoteSnapshotInterval
	currentRemoteSnapshotMaxImages = globalAA.remoteSnapshotMaxImages
	currentRemoteSnapshotJpegQualityLevel = globalAA.remoteSnapshotJpegQualityLevel
	currentRemoteSnapshotDisplayPortrait = globalAA.remoteSnapshotDisplayPortrait

	if setGlobalsFromRegistry then
		globalAA.enableRemoteSnapshot = GetBoolFromString(registrySection.Read("enableRemoteSnapshot"), false)
		globalAA.remoteSnapshotInterval = int(val(registrySection.Read("remoteSnapshotInterval")))
		globalAA.remoteSnapshotMaxImages = int(val(registrySection.Read("remoteSnapshotMaxImages")))
		globalAA.remoteSnapshotJpegQualityLevel = int(val(registrySection.Read("remoteSnapshotJpegQualityLevel")))
		globalAA.remoteSnapshotDisplayPortrait = GetBoolFromString(registrySection.Read("remoteSnapshotDisplayPortrait"), false)
	endif

	if updateRunTime then
		if currentEnableRemoteSnapshot <> globalAA.enableRemoteSnapshot then
			if globalAA.enableRemoteSnapshot then		' remote snapshots were off; turn them on
				m.bsp.remoteSnapshotTimer = CreateObject("roTimer")
				m.bsp.remoteSnapshotTimer.SetPort(m.bsp.msgPort)
				m.bsp.remoteSnapshotTimer.SetElapsed(m.bsp.globalAA.remoteSnapshotInterval, 0) 
				m.bsp.remoteSnapshotTimer.Start()
			else										' remote snapshots were on; turn them off
				if type(m.remoteSnapshotTimer) = "roTimer" then
					m.remoteSnapshotTimer.Stop()
				endif
			endif
		endif
	endif

End Sub


Sub SetDownloadRateLimit(diagnostics As Object, networkConfigurationIndex% As Integer, rateLimit% As Integer)
	nc = CreateObject("roNetworkConfiguration", networkConfigurationIndex%)
	if type(nc) = "roNetworkConfiguration"
		diagnostics.PrintDebug("SetInboundShaperRate to " + stri(rateLimit%))
		ok = nc.SetInboundShaperRate(rateLimit%)
		if not ok then print "Failure calling SetInboundShaperRate with parameter ";rateLimit%
		ok = nc.Apply()
		if not ok then print "Failure calling roNetworkConfiguration.Apply()"
	endif
End Sub


Function UpdateRegistrySetting(newValue$ As String, existingValue$ As String, registryKey$ As String) As String

	if lcase(newValue$) <> lcase(existingValue$) then
		m.bsp.WriteRegistrySetting(registryKey$, newValue$)
	endif

	return newValue$

End Function


Sub ConfigureNetwork(networkingParameters As Object, registryNetworkingParameters As Object, registryKeySuffix$ As String)

  nc = CreateObject("roNetworkConfiguration", networkingParameters.networkConfigurationIndex%)
  if type(nc) = "roNetworkConfiguration" then

    if networkingParameters.useDHCP$ = "no" then

      nc.SetIP4Address(networkingParameters.staticIPAddress$)
      nc.SetIP4Netmask(networkingParameters.subnetMask$)
      nc.SetIP4Gateway(networkingParameters.gateway$)

	  if m.stateMachine.bsp.setDNSServersSupported then

		dnsServers = []
		if networkingParameters.dns1$ <> "" then
			dnsServers.push(networkingParameters.dns1$)
		endif
		if networkingParameters.dns2$ <> "" then
			dnsServers.push(networkingParameters.dns2$)
		endif
		if networkingParameters.dns3$ <> "" then
			dnsServers.push(networkingParameters.dns3$)
		endif
		if dnsServers.Count() > 0 then
			ok = nc.SetDNSServers([])
			ok = nc.SetDNSServers(dnsServers)
		endif
		
	  else

		if networkingParameters.dns1$ <> "" then nc.AddDNSServer(networkingParameters.dns1$)
		if networkingParameters.dns2$ <> "" then nc.AddDNSServer(networkingParameters.dns2$)
		if networkingParameters.dns3$ <> "" then nc.AddDNSServer(networkingParameters.dns3$)

	  endif

    else

      nc.SetDHCP()

    endif

		nc.SetRoutingMetric(int(val(networkingParameters.networkConnectionPriority$)))

        if networkingParameters.useWireless then
            wpaEnabled = registryNetworkingParameters.enableWPAEnterpriseAuthentication or registryNetworkingParameters.enableWPAEnterpriseAuthentication2
            if not wpaEnabled then
              ' Don't overwrite the eap or peap passphrase, if WPA is in use.
              ' This is necessary because BSN and this autorun doesn't support WPA.
              nc.SetWiFiESSID(networkingParameters.ssid$)
              nc.SetObfuscatedWifiPassphrase(networkingParameters.passphrase$)
            endif
        endif

        timeServer$ = m.stateMachine.newSync.LookupMetadata("client", "timeServer")
        nc.SetTimeServer(networkingParameters.timeServer$)
        success = nc.Apply()
        nc = invalid

        if not success then
            m.bsp.diagnostics.PrintDebug("### roNetworkConfiguration.Apply failure.")
        else
            ' save parameters to the registry
			networkConnectionPriorityFromRegistry$ = registryNetworkingParameters.networkConnectionPriority$
            if networkConnectionPriorityFromRegistry$ <> networkingParameters.networkConnectionPriority$ then
				m.bsp.WriteRegistrySetting("ncp" + registryKeySuffix$, networkingParameters.networkConnectionPriority$)
				registryNetworkingParameters.networkConnectionPriority$ = networkingParameters.networkConnectionPriority$
			endif

            if networkingParameters.useDHCP$ = "no" then
                    
				if registryNetworkingParameters.useDHCP$ <> "no" then
					m.bsp.WriteRegistrySetting("dhcp" + registryKeySuffix$, "no")
					registryNetworkingParameters.useDHCP$ = "no"
                endif
                        
                staticIPAddressFromRegistry$ = registryNetworkingParameters.staticIPAddress$
                subnetMaskFromRegistry$ = registryNetworkingParameters.subnetMask$
                gatewayFromRegistry$ = registryNetworkingParameters.gateway$
'                broadcastFromRegistry$ = registryNetworkingParameters.broadcast$
                dns1FromRegistry$ = registryNetworkingParameters.dns1$
                dns2FromRegistry$ = registryNetworkingParameters.dns2$
                dns3FromRegistry$ = registryNetworkingParameters.dns3$

                if staticIPAddressFromRegistry$ <> networkingParameters.staticIPAddress$ then
					m.bsp.WriteRegistrySetting("sip" + registryKeySuffix$, networkingParameters.staticIPAddress$)
					registryNetworkingParameters.staticIPAddress$ = networkingParameters.staticIPAddress$
				endif
						
                if subnetMaskFromRegistry$ <> networkingParameters.subnetMask$ then 
					m.bsp.WriteRegistrySetting("sm" + registryKeySuffix$, networkingParameters.subnetMask$)
					registryNetworkingParameters.subnetMask$ = networkingParameters.subnetMask$
				endif
						
                if gatewayFromRegistry$ <> networkingParameters.gateway$ then 
					m.bsp.WriteRegistrySetting("gw" + registryKeySuffix$, networkingParameters.gateway$)
					registryNetworkingParameters.gateway$ = networkingParameters.gateway$
				endif
						
                if dns1FromRegistry$ <> networkingParameters.dns1$ then 
					m.bsp.WriteRegistrySetting("d1" + registryKeySuffix$, networkingParameters.dns1$)
					registryNetworkingParameters.dns1$ = networkingParameters.dns1$
				endif
						
                if dns2FromRegistry$ <> networkingParameters.dns2$ then 
					m.bsp.WriteRegistrySetting("d2" + registryKeySuffix$, networkingParameters.dns2$)
					registryNetworkingParameters.dns2$ = networkingParameters.dns2$
				endif
						
                if dns3FromRegistry$ <> networkingParameters.dns3$ then 
					m.bsp.WriteRegistrySetting("d3" + registryKeySuffix$, networkingParameters.dns3$)
					registryNetworkingParameters.dns3$ = networkingParameters.dns3$
				endif
                    
            else
                    
				if registryNetworkingParameters.useDHCP$ <> "yes" then
					m.bsp.WriteRegistrySetting("dhcp" + registryKeySuffix$, "yes")
					registryNetworkingParameters.useDHCP$ = "yes"
				endif
						
            endif

        endif

    else

        m.bsp.diagnostics.PrintDebug("Unable to create roNetworkConfiguration - index = " + stri(networkConfigurationIndex%))
	
	endif

End Sub


Function STDownloadingSyncFilesEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

                nextState = m.StartSyncListDownload()
                                
                if type(nextState) = "roAssociativeArray" then
                    stop ' can't do this - no transitions on entry
'!!!!!!!!!!!!!!!!!! - is this a violation? performing a transition on an entry signal?                
                    stateData.nextState = nextState
		            return "TRANSITION"
                endif

                return "HANDLED"

            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            else if event["EventType"] = "CANCEL_DOWNLOADS" then
            
				m.bsp.diagnostics.PrintDebug("Cancel assetFetcher downloads message received")
				if type(m.stateMachine.assetFetcher) = "roAssetFetcher" then
					m.bsp.diagnostics.PrintDebug("Cancel assetFetcher downloads")
					m.stateMachine.assetFetcher.AsyncCancel()
					m.stateMachine.assetFetcher = invalid
					stateData.nextState = m.stateMachine.stWaitForTimeout
					return "TRANSITION"
				else
					return "HANDLED"
				endif
				
            endif
            
        endif
        
    else if type(event) = "roAssetFetcherProgressEvent" then

		m.bsp.diagnostics.PrintDebug("### File download progress " + event.GetFileName() + str(event.GetCurrentFilePercentage()))

		m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_FILE_DOWNLOAD_PROGRESS, event.GetFileName() + chr(9) + str(event.GetCurrentFilePercentage()))

		fileIndex% = event.GetFileIndex()
		fileItem = m.stateMachine.newSync.GetFile("download", fileIndex%)

		if type(fileItem) = "roAssociativeArray" then
			m.stateMachine.AddDeviceDownloadProgressItem(fileItem, str(event.GetCurrentFilePercentage()), "ok")
		endif

        return "HANDLED"

    else if type(event) = "roAssetFetcherEvent" then

		if event.GetUserData() = "BSN" then	    

	        nextState = m.HandleAssetFetcherEvent(event)
	        
            if type(nextState) = "roAssociativeArray" then
                stateData.nextState = nextState
	            return "TRANSITION"
            endif

            return "HANDLED"
            
	    endif

    endif
            
    stateData.nextState = m.superState
    return "SUPER"
    
End Function


Function StartSyncListDownload() As Object

    m.bsp.diagnostics.PrintDebug("### Start sync list download")
    m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_DOWNLOAD_START, "")
    m.stateMachine.AddEventItem("StartSyncListDownload", m.stateMachine.newSync.GetName(), "")

	m.bsp.assetPool.ReserveMegabytes(50)

	m.stateMachine.assetFetcher = CreateObject("roAssetFetcher", m.bsp.assetPool)
	m.stateMachine.assetFetcher.SetUserData("BSN")
    m.stateMachine.assetFetcher.SetPort(m.stateMachine.msgPort)
    if m.stateMachine.setUserAndPassword then m.stateMachine.assetFetcher.SetUserAndPassword(m.stateMachine.user$, m.stateMachine.password$)
	m.stateMachine.assetFetcher.EnableUnsafeAuthentication(m.stateMachine.enableUnsafeAuthentication)
    m.stateMachine.assetFetcher.SetMinimumTransferRate(100,60)
    m.stateMachine.assetFetcher.SetHeaders(m.stateMachine.newSync.GetMetadata("server"))
	m.stateMachine.assetFetcher.AddHeader("User-Agent", m.bsp.userAgent$)
    m.stateMachine.assetFetcher.AddHeader("DeviceID", m.stateMachine.deviceUniqueID$)
    m.stateMachine.assetFetcher.AddHeader("DeviceModel", m.stateMachine.deviceModel$)
	m.stateMachine.assetFetcher.AddHeader("DeviceFamily", m.stateMachine.deviceFamily$)
    m.stateMachine.assetFetcher.SetFileProgressIntervalSeconds(15)

	binding% = GetBinding(m.stateMachine.bsp.contentXfersEnabledWired, m.stateMachine.bsp.contentXfersEnabledWireless)
    m.bsp.diagnostics.PrintDebug("### Binding for assetFetcher is " + stri(binding%))
	ok = m.stateMachine.assetFetcher.BindToInterface(binding%)
	if not ok then stop

' clear file download failure count                
	m.stateMachine.fileDownloadFailureCount% = 0
				
' this error implies that the current sync list is corrupt - go back to sync list in registry and reboot - no need to retry. do this by deleting autorun.brs and rebooting
    if (not m.bsp.assetPool.ProtectAssets("BNM-new", m.stateMachine.newSync)) or (not m.bsp.assetPool.ProtectAssets("current", m.stateMachine.currentSync)) then ' don't allow download to delete current files
		m.stateMachine.LogProtectFilesFailure()				
    endif
    
    if m.stateMachine.proxy_mode then
        m.stateMachine.assetFetcher.AddHeader("Roku-Cache-Request", "Yes")
    endif
    
    if m.stateMachine.syncType$ = "download" then
        if m.stateMachine.downloadOnlyIfCached then m.stateMachine.assetFetcher.AddHeader("Cache-Control", "only-if-cached")
        if not m.stateMachine.assetFetcher.AsyncDownload(m.stateMachine.newSync) then
            m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_IMMEDIATE_FAILURE, m.stateMachine.assetFetcher.GetFailureReason())
            m.bsp.diagnostics.PrintTimestamp()
            m.bsp.diagnostics.PrintDebug("### AsyncDownload failed: " + m.stateMachine.assetFetcher.GetFailureReason())
            m.stateMachine.AddDeviceErrorItem("deviceError", m.stateMachine.newSync.GetName(), "AsyncDownloadFailure: " + m.stateMachine.assetFetcher.GetFailureReason(), "")
            m.stateMachine.ResetDownloadTimerToDoRetry()
            m.stateMachine.newSync = invalid
			return m.stateMachine.stWaitForTimeout
        endif
    else
        m.stateMachine.assetFetcher.AsyncSuggestCache(m.stateMachine.newSync)
    endif
    
    return 0

End Function


Function HandleAssetFetcherEvent(event As Object) As Object

    m.bsp.diagnostics.PrintTimestamp()
    m.bsp.diagnostics.PrintDebug("### assetFetcher_event")

	if (event.GetEvent() = m.stateMachine.POOL_EVENT_FILE_DOWNLOADED) then
        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_FILE_DOWNLOAD_COMPLETE, event.GetName())
        m.bsp.diagnostics.PrintDebug("### File downloaded " + event.GetName())
        
        ' see if the user should be charged for this download
        if m.stateMachine.chargeableFiles.DoesExist(event.GetName()) then
            filePath$ = m.stateMachine.assetPoolFiles.GetPoolFilePath(event.GetName())            
            file = CreateObject("roReadFile", filePath$)
            if type(file) = "roReadFile" then
                file.SeekToEnd()

				totalContentDownloaded# = m.stateMachine.contentDownloaded#
		        totalContentDownloaded# = totalContentDownloaded# + file.CurrentPosition()
				m.stateMachine.contentDownloaded# = totalContentDownloaded#

                m.bsp.diagnostics.PrintDebug("### File size " + str(file.CurrentPosition()))
				m.bsp.diagnostics.PrintDebug("### Content downloaded = " + str(m.stateMachine.contentDownloaded#))
            endif
            file = invalid
        endif

	else if (event.GetEvent() = m.stateMachine.POOL_EVENT_FILE_FAILED) then
        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_FILE_DOWNLOAD_FAILURE, event.GetName() + chr(9) + event.GetFailureReason())
        m.bsp.diagnostics.PrintDebug("### File failed " + event.GetName() + ": " + event.GetFailureReason())
        m.stateMachine.AddDeviceErrorItem("FileDownloadFailure", event.GetName(), event.GetFailureReason(), str(event.GetResponseCode()))
        
        ' log this error to the download progress handler
        fileIndex% = event.GetFileIndex()
        fileItem = m.stateMachine.newSync.GetFile("download", fileIndex%)
        if type(fileItem) = "roAssociativeArray" then
            m.stateMachine.AddDeviceDownloadProgressItem(fileItem, "-1", event.GetFailureReason())
        endif

		m.stateMachine.fileDownloadFailureCount% = m.stateMachine.fileDownloadFailureCount% + 1
		if m.stateMachine.fileDownloadFailureCount% >= m.stateMachine.maxFileDownloadFailures% then
            m.bsp.diagnostics.PrintDebug("### " + stri(m.stateMachine.maxFileDownloadFailures%) + " file download failures - set timer for retry.")
            m.stateMachine.assetFetcher.AsyncCancel()
		    m.stateMachine.ResetDownloadTimerToDoRetry()
			m.stateMachine.assetFetcher = invalid
	        return m.stateMachine.stWaitForTimeout
		endif
       
	else if (event.GetEvent() = m.stateMachine.POOL_EVENT_ALL_FAILED) then
	    if m.stateMachine.syncType$ = "download" then
		    m.stateMachine.ResetDownloadTimerToDoRetry()
            m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_FAILURE, event.GetFailureReason())		
            m.bsp.diagnostics.PrintDebug("### Sync failed: " + event.GetFailureReason())
            m.stateMachine.AddDeviceErrorItem("POOL_EVENT_ALL_FAILED", "", event.GetFailureReason(), str(event.GetResponseCode()))

            ' capture total content downloaded
            m.bsp.diagnostics.PrintDebug("### Total content downloaded = " + str(m.stateMachine.contentDownloaded#))
            ok = m.stateMachine.UploadTrafficDownload(m.stateMachine.contentDownloaded#)
            if ok then
				m.stateMachine.contentDownloaded# = 0
			endif
        else
            m.bsp.diagnostics.PrintDebug("### Proxy mode sync complete")
        endif
		m.stateMachine.newSync = invalid
		m.stateMachine.assetFetcher = invalid
    
        return m.stateMachine.stWaitForTimeout
    
	else if (event.GetEvent() = m.stateMachine.POOL_EVENT_ALL_DOWNLOADED) then

        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_DOWNLOAD_COMPLETE, "")		
        m.bsp.diagnostics.PrintDebug("### All files downloaded")
        m.stateMachine.AddDeviceDownloadItem("All files downloaded", "", "")

' send up the list of files downloaded
		m.stateMachine.BuildFileDownloadList(m.stateMachine.newSync)
        m.stateMachine.UploadDeviceDownloadProgressFileList()
		m.stateMachine.FileListPendingUpload = false

' capture total content downloaded
        m.bsp.diagnostics.PrintDebug("### Total content downloaded = " + str(m.stateMachine.contentDownloaded#))
        ok = m.stateMachine.UploadTrafficDownload(m.stateMachine.contentDownloaded#)
        if ok then
			m.stateMachine.contentDownloaded# = 0
		endif
        
' Log the end of sync list download
        m.stateMachine.AddEventItem("EndSyncListDownload", m.stateMachine.newSync.GetName(), str(event.GetResponseCode()))

' Clear retry count and reset timeout period
		m.stateMachine.numRetries% = 0
	    m.stateMachine.currentTimeBetweenNetConnects% = m.stateMachine.timeBetweenNetConnects%
    
' diagnostic web server
		dwsParams = GetDWSParams(m.stateMachine.newSync, m.bsp.registrySettings)

		dwsRebootRequired = false
		nc = CreateObject("roNetworkConfiguration", 0)
		if type(nc) = "roNetworkConfiguration"
			dwsAA = CreateObject("roAssociativeArray")
			if dwsParams.dwsEnabled$ = "yes" then
				dwsAA["port"] = "80"
				dwsAA["password"] = dwsParams.dwsPassword$
			else if dwsParams.dwsEnabled$ = "no" then
				dwsAA["port"] = 0
			endif
			dwsRebootRequired = nc.SetupDWS(dwsAA)
			nc = invalid
		endif
                
		oldSyncSpecScriptsOnly  = m.stateMachine.currentSync.FilterFiles("download", { group: "script" } )
		newSyncSpecScriptsOnly  = m.stateMachine.newSync.FilterFiles("download", { group: "script" } )
	
		rebootRequired = false

		if not oldSyncSpecScriptsOnly.FilesEqualTo(newSyncSpecScriptsOnly) then

			realizer = CreateObject("roAssetRealizer", m.bsp.assetPool, "/")
			globalAA = GetGlobalAA()
			globalAA.bsp.msgPort.DeferWatchdog(120)
			event = realizer.Realize(newSyncSpecScriptsOnly)
			realizer = invalid

			if event.GetEvent() <> m.stateMachine.EVENT_REALIZE_SUCCESS then
		        m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_REALIZE_FAILURE, stri(event.GetEvent()) + chr(9) + event.GetName() + chr(9) + event.GetFailureReason())
				m.bsp.diagnostics.PrintDebug("### Realize failed " + stri(event.GetEvent()) + chr(9) + event.GetName() + chr(9) + event.GetFailureReason() )
				m.stateMachine.AddDeviceErrorItem("RealizeFailure", event.GetName(), event.GetFailureReason(), str(event.GetEvent()))
			
				m.stateMachine.newSync = invalid
				m.stateMachine.assetFetcher = invalid
    
				return m.stateMachine.stWaitForTimeout
			endif

			' reboot if successful
			rebootRequired = true

		endif

' Save to current-sync.xml then do cleanup
	    if not m.stateMachine.newSync.WriteToFile("current-sync.xml") then stop
        timezone = m.stateMachine.newSync.LookupMetadata("client", "timezone")
        if timezone <> "" then
            m.stateMachine.systemTime.SetTimeZone(timezone)
        endif

        m.bsp.diagnostics.PrintTimestamp()
        m.bsp.diagnostics.PrintDebug("### DOWNLOAD COMPLETE")
        
        DeleteFile("bad-sync.xml")

		if rebootRequired then
			m.bsp.diagnostics.PrintDebug("### new script or upgrade found - reboot")
            m.stateMachine.AddEventItem("DownloadComplete - new script or upgrade file found", m.stateMachine.newSync.GetName(), "")
            m.stateMachine.RebootAfterEventsSent()
		endif

		if dwsRebootRequired then
            m.bsp.diagnostics.PrintDebug("### DWS parameter change - reboot")
            m.stateMachine.AddEventItem("DownloadComplete - DWS parameter change", m.stateMachine.newSync.GetName(), "")
            m.stateMachine.RebootAfterEventsSent()
		endif

		m.stateMachine.assetCollection = m.stateMachine.newSync.GetAssets("download")
		m.stateMachine.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.assetPool, m.stateMachine.assetCollection)
        if type(m.stateMachine.assetPoolFiles) <> "roAssetPoolFiles" then stop
        
		globalAA = GetGlobalAA()
		globalAA.autoscheduleFilePath$ = GetPoolFilePath(m.stateMachine.assetPoolFiles, "autoschedule.xml")
		globalAA.resourcesFilePath$ = GetPoolFilePath(m.stateMachine.assetPoolFiles, "resources.txt")
		globalAA.boseProductsFilePath$ = GetPoolFilePath(m.stateMachine.assetPoolFiles, "BoseProducts.xml")
		if globalAA.autoscheduleFilePath$ = "" then stop

		m.stateMachine.newSync = invalid
		m.stateMachine.assetFetcher = invalid

        ' reset m.currentSync (could the script just do m.currentSync = m.newSync earlier?)
        
        m.stateMachine.currentSync = CreateObject("roSyncSpec")
        if type(m.stateMachine.currentSync) <> "roSyncSpec" then stop
        if not m.stateMachine.currentSync.ReadFromFile("current-sync.xml") then stop

        m.bsp.diagnostics.PrintTimestamp()
        m.bsp.diagnostics.PrintDebug("### return from HandleAssetFetcherEvent")

		debugOn = false
		if m.stateMachine.currentSync.LookupMetadata("client", "enableSerialDebugging") = "True" then
			debugOn = true
		endif
		m.bsp.diagnostics.UpdateDebugOn(debugOn)

		systemLogDebugOn = false
		if m.stateMachine.currentSync.LookupMetadata("client", "enableSystemLogDebugging") = "True" then
			systemLogDebugOn = true
		endif
		m.bsp.diagnostics.UpdateSystemLogDebugOn(systemLogDebugOn)

		m.bsp.contentEncrypted = false
		if m.bsp.contentEncryptionSupported then
			deviceCustomization = CreateObject("roDeviceCustomization")
			deviceCustomization.StoreObfuscatedEncryptionKey("AesCtrHmac", m.bsp.obfuscatedEncryptionKey)
			if m.bsp.obfuscatedEncryptionKey <> "" then
				m.bsp.contentEncrypted = true
			endif
		endif
		
		m.bsp.SetPerFileEncryptionStatus(m.stateMachine.currentSync)

' send internal message to prepare for restart
        prepareForRestartEvent = CreateObject("roAssociativeArray")
        prepareForRestartEvent["EventType"] = "PREPARE_FOR_RESTART"
        m.stateMachine.msgPort.PostMessage(prepareForRestartEvent)

' send internal message indicating that new content is available
        contentUpdatedEvent = CreateObject("roAssociativeArray")
        contentUpdatedEvent["EventType"] = "CONTENT_UPDATED"
        m.stateMachine.msgPort.PostMessage(contentUpdatedEvent)

        return m.stateMachine.stWaitForTimeout
            
    endif
	
End Function


Sub LogProtectFilesFailure()
	m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, "AssetPool Protect Failure")
	m.stateMachine.logging.FlushLogFile()
	DeleteFile("autorun.brs")
	m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + "AssetPool Protect Failure")
	m.stateMachine.AddDeviceErrorItem("deviceError", m.stateMachine.currentSync.GetName(), "ProtectFilesFailure: " + "AssetPool Protect Failure", "")

	globalAA = GetGlobalAA()
    globalAA.bsp.msgPort.DeferWatchdog(15)

	msg = wait(10000, 0)   ' wait for either a timeout (10 seconds) or a message indicating that the post was complete
	a=RebootSystem()
End Sub

'endregion

'region BP State Machine
' *************************************************
'
' BP State Machine
'
' *************************************************
Function newBPStateMachine(bsp As Object, inputPortIdentity$ As String, buttonPanelIndex% As Integer, buttonNumber% As Integer) As Object

	BPStateMachine = CreateObject("roAssociativeArray")
	
	BPStateMachine.bsp = bsp
	BPStateMachine.msgPort = bsp.msgPort
	BPStateMachine.inputPortIdentity$ = inputPortIdentity$
	BPStateMachine.buttonPanelIndex% = buttonPanelIndex%
	BPStateMachine.buttonNumber% = buttonNumber%
	BPStateMachine.timer = invalid
	
	BPStateMachine.configuration$ = "press"
	BPStateMachine.initialHoldoff% = -1
	BPStateMachine.repeatInterval% = -1	
	
	BPStateMachine.ConfigureButton = ConfigureButton
	BPStateMachine.EventHandler = BPEventHandler
		
	BPStateMachine.state$ = "ButtonUp"

	return BPStateMachine
	
End Function


Sub BPEventHandler(event As Object)

	if m.state$ = "ButtonUp" then

		if type(event) = "roControlDown" and stri(event.GetSourceIdentity()) = m.inputPortIdentity$ and m.buttonNumber% = event.GetInt() then
    		
			m.bsp.diagnostics.PrintDebug("BP control down" + str(event.GetInt()))

			bpControlDown = CreateObject("roAssociativeArray")
			bpControlDown["EventType"] = "BPControlDown"
			bpControlDown["ButtonPanelIndex"] = StripLeadingSpaces(str(m.buttonPanelIndex%))
			bpControlDown["ButtonNumber"] = StripLeadingSpaces(str(event.GetInt()))
			m.msgPort.PostMessage(bpControlDown)

			if m.configuration$ = "pressContinuous" then
				m.timer = CreateObject("roTimer")
				m.timer.SetPort(m.msgPort)
				m.timer.SetElapsed(0, m.initialHoldoff%)
				m.timer.Start()
			endif
			
            m.state$ = "ButtonDown"
	
		endif
		
    else 
    
		if type(event) = "roControlUp" and stri(event.GetSourceIdentity()) = m.inputPortIdentity$ and m.buttonNumber% = event.GetInt() then
    
			m.bsp.diagnostics.PrintDebug("BP control up" + str(event.GetInt()))

			' if continuous, stop and destroy the timer
			if type(m.timer) = "roTimer" then
				m.timer.Stop()
				m.timer = invalid
			endif

            m.state$ = "ButtonUp"
			    
		' else check for repeat timeout
		else if type(event) = "roTimerEvent" and type(m.timer) = "roTimer" then
            if stri(event.GetSourceIdentity()) = stri(m.timer.GetIdentity()) then
            
				m.bsp.diagnostics.PrintDebug("BP REPEAT control down" + str(m.buttonNumber%))

				bpControlDown = CreateObject("roAssociativeArray")
				bpControlDown["EventType"] = "BPControlDown"
				bpControlDown["ButtonPanelIndex"] = StripLeadingSpaces(str(m.buttonPanelIndex%))
				bpControlDown["ButtonNumber"] = StripLeadingSpaces(str(m.buttonNumber%))
				m.msgPort.PostMessage(bpControlDown)

				m.timer.SetElapsed(0, m.repeatInterval%)
				m.timer.Start()
			
            endif
		endif
	endif
	
End Sub


Sub ConfigureButton(bpSpec As Object)

	bpConfiguration$ = bpSpec.configuration$
	
' no change necessary if the old and new configurations are the same and are a simple press	
	if bpConfiguration$ = "press" and m.configuration$ = "press" then return

' if the old configuration was continuous and the new configuration is a simple press, stop the timer and destroy the timer object	
	if bpConfiguration$ = "press" and m.configuration$ = "pressContinuous" then
		if type(m.timer) = "roTimer" then
			m.timer.Stop()
			m.timer = invalid
		endif
		m.configuration$ = "press"
		return
	endif
	
' capture the repeat rates if the new configuration is continuous	
	if bpConfiguration$ = "pressContinuous" then
		m.initialHoldoff% = int(val(bpSpec.initialHoldoff$))
		m.repeatInterval% = int(val(bpSpec.repeatInterval$))		
	endif	
	
' if both the old and new configurations are continuous, restart the timer (if it is active)
	if bpConfiguration$ = "pressContinuous" and m.configuration$ = "pressContinuous" then
		if type(m.timer) = "roTimer" then
			m.timer.Stop()
			m.timer.SetElapsed(0, m.initialHoldoff%)
			m.timer.Start()
		endif
	endif
	
' if the old configuration was simple press and the new configuration is continuous, then capture the new values
' but don't start a timer (repeating won't start if the button was down at the time the state is entered).
	m.configuration$ = bpConfiguration$
	
End Sub

'endregion


'region GPIO State Machine
' *************************************************
'
' GPIO State Machine
'
' *************************************************
Function newGPIOStateMachine(bsp As Object, controlPort As Object, inputPortIdentity$ As String, buttonNumber% As Integer) As Object

	GPIOStateMachine = CreateObject("roAssociativeArray")
	
	GPIOStateMachine.bsp = bsp
	GPIOStateMachine.msgPort = bsp.msgPort
	GPIOStateMachine.inputPortIdentity$ = inputPortIdentity$
	GPIOStateMachine.buttonNumber% = buttonNumber%
	GPIOStateMachine.timer = invalid
	
	GPIOStateMachine.configuration$ = "press"
	GPIOStateMachine.initialHoldoff% = -1
	GPIOStateMachine.repeatInterval% = -1	
	
	GPIOStateMachine.ConfigureButton = ConfigureButton
	GPIOStateMachine.EventHandler = GPIOEventHandler
		
	if IsControlPort(controlPort) then 	
		if controlPort.IsInputActive(buttonNumber%) then
			GPIOStateMachine.state$ = "ButtonDown"
		else
			GPIOStateMachine.state$ = "ButtonUp"
		endif
	endif

	return GPIOStateMachine
	
End Function


Sub GPIOEventHandler(event As Object)

	if m.state$ = "ButtonUp" then

		if type(event) = "roControlDown" and stri(event.GetSourceIdentity()) = m.inputPortIdentity$ and m.buttonNumber% = event.GetInt() then
    		
			m.bsp.diagnostics.PrintDebug("GPIO control down" + str(event.GetInt()))

			gpioControlDown = CreateObject("roAssociativeArray")
			gpioControlDown["EventType"] = "GPIOControlDown"
			gpioControlDown["ButtonNumber"] = StripLeadingSpaces(str(event.GetInt()))
			m.msgPort.PostMessage(gpioControlDown)

			if m.configuration$ = "pressContinuous" then
				m.timer = CreateObject("roTimer")
				m.timer.SetPort(m.msgPort)
				m.timer.SetElapsed(0, m.initialHoldoff%)
				m.timer.Start()
			endif
			
            m.state$ = "ButtonDown"
	
		endif
		
    else 
    
		if type(event) = "roControlUp" and stri(event.GetSourceIdentity()) = m.inputPortIdentity$ and m.buttonNumber% = event.GetInt() then
    
			m.bsp.diagnostics.PrintDebug("GPIO control up" + str(event.GetInt()))

			' if continuous, stop and destroy the timer
			if type(m.timer) = "roTimer" then
				m.timer.Stop()
				m.timer = invalid
			endif

			gpioControlUp = CreateObject("roAssociativeArray")
			gpioControlUp["EventType"] = "GPIOControlUp"
			gpioControlUp["ButtonNumber"] = StripLeadingSpaces(str(event.GetInt()))
			m.msgPort.PostMessage(gpioControlUp)

            m.state$ = "ButtonUp"
			    
		' else check for repeat timeout
		else if type(event) = "roTimerEvent" and type(m.timer) = "roTimer" then
            if stri(event.GetSourceIdentity()) = stri(m.timer.GetIdentity()) then
            
				m.bsp.diagnostics.PrintDebug("GPIO REPEAT control down" + str(m.buttonNumber%))

				gpioControlDown = CreateObject("roAssociativeArray")
				gpioControlDown["EventType"] = "GPIOControlDown"
				gpioControlDown["ButtonNumber"] = StripLeadingSpaces(str(m.buttonNumber%))
				m.msgPort.PostMessage(gpioControlDown)

				m.timer.SetElapsed(0, m.repeatInterval%)
				m.timer.Start()
			
            endif
		endif
	endif
	
End Sub

'endregion


'region EventLoop
' *************************************************
'
' Event Loop and associated processing
'
' *************************************************
Sub EventLoop()

	SQLITE_COMPLETE = 100

    while true
        
        msg = wait(0, m.msgPort)
        
		m.diagnostics.PrintTimestamp()
		m.diagnostics.PrintDebug("msg received - type=" + type(msg))

	    if type(msg) = "roControlDown" and stri(msg.GetSourceIdentity()) = stri(m.svcPort.GetIdentity()) then
            if msg.GetInt()=12 then
                stop
            endif
        endif

		eventHandled = false
		for each scriptPlugin in m.scriptPlugins
'			ERR_NORMAL_END = &hFC
			if scriptPlugin.plugin = invalid then
				m.diagnostics.PrintDebug("Plugin for " + scriptPlugin.name$ + " is invalid.")
				m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_SCRIPT_PLUGIN_FAILURE, scriptPlugin.name$)
			else
				eventHandled = scriptPlugin.plugin.ProcessEvent(msg)
				if eventHandled then
					exit for
				endif
			endif
'			retVal = Eval("eventHandled = scriptPlugin.plugin.ProcessEvent(msg)")
'			if retVal <> &hFC then
'				' log the failure
'				m.diagnostics.PrintDebug("Failure executing Eval to execute script plugin file event handler: return value = " + stri(retVal))
'				m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_SCRIPT_PLUGIN_FAILURE, stri(retVal) + chr(9) + "EventHandler")
'			endif
		next

		' don't propagate the event if it was handled by a plugin
		if not eventHandled then

			if type(msg) = "roSqliteEvent" then
				if msg.GetSqlResult() <> SQLITE_COMPLETE then
					m.diagnostics.PrintDebug("roSqliteEvent.GetSqlResult() <> SQLITE_COMPLETE")
					if type(msg.GetSqlResult()) = "roInt" then
						m.diagnostics.PrintDebug("roSqliteEvent.GetSqlResult() = " + stri(roSqliteEvent.GetSqlResult()))
					endif
				endif
			endif

			if type(msg) = "roHttpEvent" then
        
				userdata = msg.GetUserData()
				if type(userdata) = "roAssociativeArray" and type(userdata.HandleEvent) = "roFunction" then
					userData.HandleEvent(userData, msg)
				endif

			else
		                                     
				m.playerHSM.Dispatch(msg)
			
				for buttonPanelIndex% = 0 to 3
					for i% = 0 to 10
						if type(m.bpSM[buttonPanelIndex%, i%]) = "roAssociativeArray" then
							m.bpSM[buttonPanelIndex%, i%].EventHandler(msg)
						endif
					next
				next

				for i% = 0 to 7
					if type(m.gpioSM[i%]) = "roAssociativeArray" then
						m.gpioSM[i%].EventHandler(msg)
					endif
				next

				if type(m.sign) = "roAssociativeArray" then

					numZones% = m.sign.zonesHSM.Count()
					for i% = 0 to numZones% - 1
						m.dispatchingZone = m.sign.zonesHSM[i%]
						m.dispatchingZone.Dispatch(msg)
					next

				endif

				if m.networkingActive then
					m.networkingHSM.Dispatch(msg)
				endif

			endif

		endif

    end while
    
End Sub


Function ExecuteSwitchPresentationCommand(presentationName$ As String) As Boolean

	' retrieve target presentation
	presentation = m.presentations.Lookup(presentationName$)

	if type(presentation) = "roAssociativeArray" then

		' check for existence of target presentation - if it's not present, don't try to switch to it
		autoplayFileName$ = "autoplay-" + presentation.presentationName$ + ".xml"
		xmlFileName$ = m.assetPoolFiles.GetPoolFilePath(autoplayFileName$)
		if xmlFileName$ = "" then
			m.diagnostics.PrintDebug("switchPresentation: target presentation not found - " + presentationName$)
			return false
		endif

		' send internal message to prepare for restart
		prepareForRestartEvent = CreateObject("roAssociativeArray")
		prepareForRestartEvent["EventType"] = "PREPARE_FOR_RESTART"
		m.msgPort.PostMessage(prepareForRestartEvent)

		' send switch presentation internal message
		switchPresentationEvent = CreateObject("roAssociativeArray")
		switchPresentationEvent["EventType"] = "SWITCH_PRESENTATION"
		switchPresentationEvent["Presentation"] = presentation.presentationName$
		m.msgPort.PostMessage(switchPresentationEvent)
	
		return true

	endif

	return false

End Function


Sub ExecuteMediaStateCommands(zoneHSM As Object, cmds As Object)

	if type(cmds) = "roArray" then

        for each cmd in cmds

			if cmd.name$ = "switchPresentation" then
				m.diagnostics.PrintDebug("switchPresentation: not supported by media state commands")
				return
			endif

            m.ExecuteCmd(zoneHSM, cmd.name$, cmd.parameters)

        next

    endif

End Sub


Function ExecuteTransitionCommands(zoneHSM As Object, transition As Object) As Boolean

    transitionCmds = transition.transitionCmds
    if type(transitionCmds) = "roArray" then
    
        for each transitionCmd in transitionCmds
        
            command$ = transitionCmd.name$
            
            if command$ = "synchronize" and not m.sign.enableEnhancedSynchronization then

                ' if the next command is synchronize, get the file to preload
                nextState$ = transition.targetMediaState$
		        zoneHSM.preloadState = zoneHSM.stateTable[nextState$]
                                       
            else if command$ = "switchPresentation" then

				presentationName$ = transitionCmd.parameters["presentationName"].GetCurrentParameterValue()

				return m.ExecuteSwitchPresentationCommand(presentationName$)

			else if command$ = "internalSynchronize" then

				if type(transition.internalSynchronizeEventsMaster) = "roAssociativeArray" then

					activeState = zoneHSM.activeState
					if type(activeState) = "roAssociativeArray" then
						activeState.internalSynchronizeEventsMaster = transition.internalSynchronizeEventsMaster
					endif

				endif

			endif

            m.ExecuteCmd(zoneHSM, transitionCmd.name$, transitionCmd.parameters)
            
        next
        
    endif

	return false

End Function


Function GetNonPrintableKeyboardCode(keyboardInput% As Integer) As String

    keyboardInput$ = LCase(StripLeadingSpaces(stri(keyboardInput%)))
    if m.nonPrintableKeyboardKeys.DoesExist(keyboardInput$) then
        return m.nonPrintableKeyboardKeys.Lookup(keyboardInput$)
    endif
    
    return ""
    
End Function


Sub InitializeNonPrintableKeyboardCodeList()
' Space				<sp>	32
' Left arrow        <la>    32848
' Right arrow       <ra>    32847
' Up arrow          <ua>    32850
' Down arrow        <da>    32849
' Return            <rn>    10
' Enter             <en>    13
' Escape            <es>    27
' Page Up           <pu>    32843
' Page Down         <pd>    32846
' F1                <f1>    32826
' F2                <f2>    32827
' F3                <f3>    32828
' F4                <f4>    32829
' F5                <f5>    32830
' F6                <f6>    32831
' F7                <f7>    32832
' F8                <f8>    32833
' F9                <f9>    32834
' F10               <f10>   32835
' F11               <f11>   32836
' F12               <f12>   32837
' F13 (Print Screen)<ps>    32838
' F14 (Scroll Lock) <sl>    32839
' F15 (Pause Break) <pb>    32840
' Backspace         <bs>    8
' Tab               <tb>    9
' Insert            <in>    32841
' Delete            <de>    127
' Home              <ho>    32842
' End               <ed>    32845
' Capslock          <cl>    32825
' Mute				<mu>	32895
' Volume down		<vd>	32897
' Volume up			<vu>	32896
' Next track		<nt>	786613
' Previous track	<pt>	786614
' Play/Pause		<pp>	786637
' Stop music		<sm>	786615
' Stop browsing		<sm>	786982
' Power				<pwr>	65665
' Back				<bk>	786980
' Forward			<fw>	786981
' Refresh			<rf>	786983


    m.nonPrintableKeyboardKeys = CreateObject("roAssociativeArray")
    m.nonPrintableKeyboardKeys.AddReplace("8","<bs>")
    m.nonPrintableKeyboardKeys.AddReplace("9","<tb>")
    m.nonPrintableKeyboardKeys.AddReplace("10","<rn>")
    m.nonPrintableKeyboardKeys.AddReplace("13","<en>")
    m.nonPrintableKeyboardKeys.AddReplace("27","<es>")
    m.nonPrintableKeyboardKeys.AddReplace("32","<sp>")
    m.nonPrintableKeyboardKeys.AddReplace("127","<de>")
    m.nonPrintableKeyboardKeys.AddReplace("32848","<la>")
    m.nonPrintableKeyboardKeys.AddReplace("32847","<ra>")
    m.nonPrintableKeyboardKeys.AddReplace("32850","<ua>")
    m.nonPrintableKeyboardKeys.AddReplace("32849","<da>")
    m.nonPrintableKeyboardKeys.AddReplace("32843","<pu>")
    m.nonPrintableKeyboardKeys.AddReplace("32846","<pd>")
    m.nonPrintableKeyboardKeys.AddReplace("32826","<f1>")
    m.nonPrintableKeyboardKeys.AddReplace("32827","<f2>")
    m.nonPrintableKeyboardKeys.AddReplace("32828","<f3>")
    m.nonPrintableKeyboardKeys.AddReplace("32829","<f4>")
    m.nonPrintableKeyboardKeys.AddReplace("32830","<f5>")
    m.nonPrintableKeyboardKeys.AddReplace("32831","<f6>")
    m.nonPrintableKeyboardKeys.AddReplace("32832","<f7>")
    m.nonPrintableKeyboardKeys.AddReplace("32833","<f8>")
    m.nonPrintableKeyboardKeys.AddReplace("32834","<f9>")
    m.nonPrintableKeyboardKeys.AddReplace("32835","<f10>")
    m.nonPrintableKeyboardKeys.AddReplace("32836","<f11>")
    m.nonPrintableKeyboardKeys.AddReplace("32837","<f12>")
    m.nonPrintableKeyboardKeys.AddReplace("32838","<ps>")
    m.nonPrintableKeyboardKeys.AddReplace("32839","<sl>")
    m.nonPrintableKeyboardKeys.AddReplace("32840","<pb>")
    m.nonPrintableKeyboardKeys.AddReplace("32841","<in>")
    m.nonPrintableKeyboardKeys.AddReplace("32842","<ho>")
    m.nonPrintableKeyboardKeys.AddReplace("32845","<ed>")
    m.nonPrintableKeyboardKeys.AddReplace("32825","<cl>")
    m.nonPrintableKeyboardKeys.AddReplace("32895","<mu>")
    m.nonPrintableKeyboardKeys.AddReplace("32897","<vd>")
    m.nonPrintableKeyboardKeys.AddReplace("32896","<vu>")
    m.nonPrintableKeyboardKeys.AddReplace("786613","<nt>")
    m.nonPrintableKeyboardKeys.AddReplace("786614","<pt>")
    m.nonPrintableKeyboardKeys.AddReplace("786637","<pp>")
    m.nonPrintableKeyboardKeys.AddReplace("786615","<sm>")
    m.nonPrintableKeyboardKeys.AddReplace("786982","<sb>")
    m.nonPrintableKeyboardKeys.AddReplace("65665","<pwr>")
    m.nonPrintableKeyboardKeys.AddReplace("786980","<bk>")
    m.nonPrintableKeyboardKeys.AddReplace("786981","<fw>")
    m.nonPrintableKeyboardKeys.AddReplace("786983","<rf>")
    
End Sub


Function ConvertToRemoteCommand(remoteCommand% As Integer) As String

	Dim remoteCommands[19]
	remoteCommands[0]="WEST"
	remoteCommands[1]="EAST"
	remoteCommands[2]="NORTH"
	remoteCommands[3]="SOUTH"
	remoteCommands[4]="SEL"
	remoteCommands[5]="EXIT"
	remoteCommands[6]="PWR"
	remoteCommands[7]="MENU"
	remoteCommands[8]="SEARCH"
	remoteCommands[9]="PLAY"
	remoteCommands[10]="FF"
	remoteCommands[11]="RW"
	remoteCommands[12]="PAUSE"
	remoteCommands[13]="ADD"
	remoteCommands[14]="SHUFFLE"
	remoteCommands[15]="REPEAT"
	remoteCommands[16]="VOLUP"
	remoteCommands[17]="VOLDWN"
	remoteCommands[18]="BRIGHT"

    if remoteCommand% < 0 or remoteCommand% > 18 return ""
    
    return remoteCommands[remoteCommand%]
    
End Function


Function GetIntegerParameterValue(parameters As Object, parameterName$ as String, defaultValue% As Integer) As Integer

	parameter = parameters[parameterName$]
	parameter$ = parameter.GetCurrentParameterValue()
	parameter% = defaultValue%
	if parameter$ <> "" then
		parameter% = int(val(parameter$))
	endif

	return parameter% 
End Function


' m is bsp
Sub ExecuteCmd(zoneHSM As Object, command$ As String, parameters As Object)

    m.diagnostics.PrintDebug("ExecuteCmd " + command$)

    if command$ = "gpioOnCommand" then
    
		gpioNumberParameter = parameters["gpioNumber"]
		gpioNumber$ = gpioNumberParameter.GetCurrentParameterValue()

        m.diagnostics.PrintDebug("Turn on gpioNumber " + gpioNumber$)
		if IsControlPort(m.controlPort) then 
			m.controlPort.SetOutputState(int(val(gpioNumber$)), 1)
        endif
        
    else if command$ = "gpioOffCommand" then
    
		gpioNumberParameter = parameters["gpioNumber"]
		gpioNumber$ = gpioNumberParameter.GetCurrentParameterValue()
        m.diagnostics.PrintDebug("Turn off gpioNumber " + gpioNumber$)
		if IsControlPort(m.controlPort) then 
			m.controlPort.SetOutputState(int(val(gpioNumber$)), 0)
		endif

    else if command$ = "gpioSetStateCommand" then
    
		gpioStateParameter = parameters["stateValue"]
		gpioState$ = gpioStateParameter.GetCurrentParameterValue()
        m.diagnostics.PrintDebug("Set GPIO's to " + gpioState$)
		if IsControlPort(m.controlPort) then 
			m.controlPort.SetWholeState(int(val(gpioState$)))
		endif

    else if command$ = "mapDigitalOutputVideo" then
    
		if zoneHSM.SendCommandToVideo() then
			zone = m.GetVideoZone(zoneHSM)
			if type(zone) = "roAssociativeArray" then
				m.MapDigitalOutput(zone.videoPlayer, parameters)
			endif
		endif

    else if command$ = "mapDigitalOutputAudio" then
    
        m.MapDigitalOutput(zoneHSM.audioPlayer, parameters)

	else if command$ = "setAllAudioOutputs" then

		m.SetAllAudioOutputs(parameters)

    else if command$ = "setAudioMode" then

		m.SetAudioMode1(parameters)

	else if command$ = "setAudioOutputVideo" then
    
		if zoneHSM.SendCommandToVideo() then
			m.SetAudioOutput(zoneHSM, true, parameters)
		endif
		    
    else if command$ = "setAudioOutputAudio" then

        m.SetAudioOutput(zoneHSM, false, parameters)

    else if command$ = "setAudioModeVideo" then

		if zoneHSM.SendCommandToVideo() then
			zone = m.GetVideoZone(zoneHSM)
			if type(zone) = "roAssociativeArray" then
				m.SetAudioMode(zone.videoPlayer, parameters)
			endif
		endif

    else if command$ = "setAudioModeAudio" then

        m.SetAudioMode(zoneHSM.audioPlayer, parameters)

    else if command$ = "mapStereoOutputVideo" then

		if zoneHSM.SendCommandToVideo() then
			m.MapStereoOutput(zoneHSM, true, parameters)
		endif

    else if command$ = "mapStereoOutputAudio" then

        m.MapStereoOutput(zoneHSM, false, parameters)

    else if command$ = "muteAudioOutputs" then

		m.MuteAudioOutputs(true, parameters)

	else if command$ = "unmuteAudioOutputs" then

		m.MuteAudioOutputs(false, parameters)

	else if command$ = "setConnectorVolume" then

		m.SetConnectorVolume(parameters)

	else if command$ = "incrementConnectorVolume" then

		m.ChangeConnectorVolume(1, parameters)

	else if command$ = "decrementConnectorVolume" then

		m.ChangeConnectorVolume(-1, parameters)

	else if command$ = "setZoneVolume" then

		m.SetZoneVolume(parameters)

	else if command$ = "incrementZoneVolume" then

		m.ChangeZoneVolume(1, parameters)

	else if command$ = "decrementZoneVolume" then

		m.ChangeZoneVolume(-1, parameters)

	else if command$ = "setZoneChannelVolume" then

		m.SetZoneChannelVolume(parameters)

	else if command$ = "incrementZoneChannelVolume" then

		m.ChangeZoneChannelVolume(1, parameters)

	else if command$ = "decrementZoneChannelVolume" then

		m.ChangeZoneChannelVolume(-1, parameters)

	else if command$ = "setSpdifMuteVideo" then

		if zoneHSM.SendCommandToVideo() then
			zone = m.GetVideoZone(zoneHSM)
			if type(zone) = "roAssociativeArray" then
				m.SetSpdifMute(zone.videoPlayer, parameters)
			endif
		endif

    else if command$ = "setSpdifMuteAudio" then

        m.SetSpdifMute(zoneHSM.audioPlayer, parameters)

    else if command$ = "setAnalogMuteVideo" then

		if zoneHSM.SendCommandToVideo() then
			zone = m.GetVideoZone(zoneHSM)
			if type(zone) = "roAssociativeArray" then
				m.SetAnalogMute(zone.videoChannelVolumes, zone.videoPlayer, parameters)
			endif
		endif

    else if command$ = "setAnalogMuteAudio" then

        m.SetAnalogMute(zoneHSM.audioChannelVolumes, zoneHSM.audioPlayer, parameters)

	else if command$ = "setHDMIMute" then
	
		m.SetHDMIMute(parameters)
		
	else if command$ = "setVideoVolumeByConnector" then
	
		if zoneHSM.SendCommandToVideo() then

			outputParameter = parameters["output"]
			volumeParameter = parameters["volume"]

			output$ = outputParameter.GetCurrentParameterValue()
			volume$ = volumeParameter.GetCurrentParameterValue()

			m.diagnostics.PrintDebug("Set video volume on output " + output$ + " to " + volume$)
			m.SetVideoVolumeByConnector(zoneHSM, output$, volume$)

		endif

    else if command$ = "incrementVideoVolumeByConnector" then

		if zoneHSM.SendCommandToVideo() then

			outputParameter = parameters["output"]
			volumeDeltaParameter = parameters["volumeDelta"]

			output$ = outputParameter.GetCurrentParameterValue()
			volumeDelta$ = volumeDeltaParameter.GetCurrentParameterValue()
		
			m.diagnostics.PrintDebug("Increment video volume on output " + output$ + " by " + volumeDelta$)

			m.IncrementVideoVolumeByConnector(zoneHSM, output$, volumeDelta$)
		
		endif

    else if command$ = "decrementVideoVolumeByConnector" then

		if zoneHSM.SendCommandToVideo() then

			outputParameter = parameters["output"]
			volumeDeltaParameter = parameters["volumeDelta"]

			output$ = outputParameter.GetCurrentParameterValue()
			volumeDelta$ = volumeDeltaParameter.GetCurrentParameterValue()
		
			m.diagnostics.PrintDebug("Decrement video volume on output " + output$ + " by " + volumeDelta$)

			m.DecrementVideoVolumeByConnector(zoneHSM, output$, volumeDelta$)

		endif

    else if command$ = "setVideoVolume" then
    
		if zoneHSM.SendCommandToVideo() then

			volumeParameter = parameters["volume"]
			volume$ = volumeParameter.GetCurrentParameterValue()

			m.diagnostics.PrintDebug("Set video volume to " + volume$)
			m.SetVideoVolume(zoneHSM, volume$)

		endif

    else if command$ = "incrementVideoVolume" then

		if zoneHSM.SendCommandToVideo() then

			volumeDeltaParameter = parameters["volumeDelta"]
			volumeDelta$ = volumeDeltaParameter.GetCurrentParameterValue()
		
			m.diagnostics.PrintDebug("Increment video volume by " + volumeDelta$)
			m.IncrementVideoVolume(zoneHSM, volumeDelta$)

		endif

    else if command$ = "decrementVideoVolume" then
    
		if zoneHSM.SendCommandToVideo() then

			volumeDeltaParameter = parameters["volumeDelta"]
			volumeDelta$ = volumeDeltaParameter.GetCurrentParameterValue()
		
			m.diagnostics.PrintDebug("Decrement video volume by " + volumeDelta$)
			m.DecrementVideoVolume(zoneHSM, volumeDelta$)

		endif

	else if command$ = "setAudioVolumeByConnector" then
	
		outputParameter = parameters["output"]
		volumeParameter = parameters["volume"]

		output$ = outputParameter.GetCurrentParameterValue()
		volume$ = volumeParameter.GetCurrentParameterValue()

        m.diagnostics.PrintDebug("Set audio volume on analog " + output$ + " to " + volume$)
		m.SetAudioVolumeByConnector(zoneHSM, output$, volume$)

    else if command$ = "incrementAudioVolumeByConnector" then

		outputParameter = parameters["output"]
		output$ = outputParameter.GetCurrentParameterValue()
		volumeDeltaParameter = parameters["volumeDelta"]
		volumeDelta$ = volumeDeltaParameter.GetCurrentParameterValue()

		m.diagnostics.PrintDebug("Increment audio volume on output " + output$ + " by " + volumeDelta$)
		m.IncrementAudioVolumeByConnector(zoneHSM, output$, volumeDelta$)
		
    else if command$ = "decrementAudioVolumeByConnector" then

		outputParameter = parameters["output"]
		output$ = outputParameter.GetCurrentParameterValue()
		volumeDeltaParameter = parameters["volumeDelta"]
		volumeDelta$ = volumeDeltaParameter.GetCurrentParameterValue()

		m.diagnostics.PrintDebug("Decrement audio volume on output " + output$ + " by " + volumeDelta$)
		m.DecrementAudioVolumeByConnector(zoneHSM, output$, volumeDelta$)
		
    else if command$ = "setAudioVolume" then
    
		volumeParameter = parameters["volume"]
		volume$ = volumeParameter.GetCurrentParameterValue()

        m.diagnostics.PrintDebug("Set audio volume to " + volume$)
        m.SetAudioVolume(zoneHSM, volume$)

    else if command$ = "incrementAudioVolume" then
    
		if IsAudioPlayer(zoneHSM.audioPlayer) then
			volumeDeltaParameter = parameters["volumeDelta"]
			volumeDelta$ = volumeDeltaParameter.GetCurrentParameterValue()
			m.diagnostics.PrintDebug("Increment audio volume by " + volumeDelta$)
			m.IncrementAudioVolume(zoneHSM, volumeDelta$, zoneHSM.audioPlayerAudioSettings.maxVolume%)
		endif
		
    else if command$ = "decrementAudioVolume" then
    
		if IsAudioPlayer(zoneHSM.audioPlayer) then
			volumeDeltaParameter = parameters["volumeDelta"]
			volumeDelta$ = volumeDeltaParameter.GetCurrentParameterValue()
			m.diagnostics.PrintDebug("Decrement audio volume by " + volumeDelta$)
			m.DecrementAudioVolume(zoneHSM, volumeDelta$, zoneHSM.audioPlayerAudioSettings.minVolume%)
		endif
		
    else if command$ = "setVideoChannelVolumes" then
    	
		if zoneHSM.SendCommandToVideo() then

			channelMaskParameter = parameters["channel"]
			channelMask$ = channelMaskParameter.GetCurrentParameterValue()

			volumeParameter = parameters["volume"]
			volume$ = volumeParameter.GetCurrentParameterValue()

			m.diagnostics.PrintDebug("Set video channel volume: channel = " + channelMask$ + ", volume = " + volume$)
			m.SetVideoChannnelVolume(zoneHSM, channelMask$, volume$)
        
		endif

    else if command$ = "incrementVideoChannelVolumes" then
    
		if zoneHSM.SendCommandToVideo() then

			channelMaskParameter = parameters["channel"]
			channelMask$ = channelMaskParameter.GetCurrentParameterValue()

			volumeDeltaParameter = parameters["volumeDelta"]
			volumeDelta$ = volumeDeltaParameter.GetCurrentParameterValue()

			m.diagnostics.PrintDebug("Increment video channel volumes: channel = " + channelMask$ + ", volume delta = " + volumeDelta$)
			m.IncrementVideoChannnelVolumes(zoneHSM, channelMask$, volumeDelta$)
        
		endif

    else if command$ = "decrementVideoChannelVolumes" then
            
		if zoneHSM.SendCommandToVideo() then

			channelMaskParameter = parameters["channel"]
			channelMask$ = channelMaskParameter.GetCurrentParameterValue()

			volumeDeltaParameter = parameters["volumeDelta"]
			volumeDelta$ = volumeDeltaParameter.GetCurrentParameterValue()

			m.diagnostics.PrintDebug("Decrement video channel volumes: channel = " + channelMask$ + ", volume delta = " + volumeDelta$)
			m.DecrementVideoChannnelVolumes(zoneHSM, channelMask$, volumeDelta$)
      
	  endif
	    
    else if command$ = "setAudioChannelVolumes" then
    
		channelMaskParameter = parameters["channel"]
		channelMask$ = channelMaskParameter.GetCurrentParameterValue()

		volumeParameter = parameters["volume"]
		volume$ = volumeParameter.GetCurrentParameterValue()

        m.diagnostics.PrintDebug("Set audio channel volume: channel = " + channelMask$ + ", volume = " + volume$)
        m.SetAudioChannnelVolume(zoneHSM, channelMask$, volume$)
        
    else if command$ = "incrementAudioChannelVolumes" then
    
		channelMaskParameter = parameters["channel"]
		channelMask$ = channelMaskParameter.GetCurrentParameterValue()

		volumeDeltaParameter = parameters["volumeDelta"]
		volumeDelta$ = volumeDeltaParameter.GetCurrentParameterValue()

        m.diagnostics.PrintDebug("Increment audio channel volumes: channel = " + channelMask$ + ", volume delta = " + volumeDelta$)
        m.IncrementAudioChannelVolumes(zoneHSM, channelMask$, volumeDelta$)
        
    else if command$ = "decrementAudioChannelVolumes" then
            
		channelMaskParameter = parameters["channel"]
		channelMask$ = channelMaskParameter.GetCurrentParameterValue()

		volumeDeltaParameter = parameters["volumeDelta"]
		volumeDelta$ = volumeDeltaParameter.GetCurrentParameterValue()

        m.diagnostics.PrintDebug("Decrement audio channel volumes: channel = " + channelMask$ + ", volume delta = " + volumeDelta$)
        m.DecrementAudioChannelVolumes(zoneHSM, channelMask$, volumeDelta$)
        
    else if command$ = "sendSerialStringCommand" then
    
		portParameter = parameters["port"]
		port$ = portParameter.GetCurrentParameterValue()

		serialStringParameter = parameters["serialString"]
		serialString$ = serialStringParameter.GetCurrentParameterValue()

        m.diagnostics.PrintDebug("sendSerialStringCommand " + serialString$ + " to port " + port$)
        
        if type(m.serial) = "roAssociativeArray" then
            serial = m.serial[port$]
            if type(serial) = "roSerialPort" or type(serial) = "roUsbTap" then
                serial.SendLine(serialString$)
			endif
        endif
        
    else if command$ = "sendSerialBlockCommand" then
    
		portParameter = parameters["port"]
		port$ = portParameter.GetCurrentParameterValue()

		serialStringParameter = parameters["serialString"]
		serialString$ = serialStringParameter.GetCurrentParameterValue()

        m.diagnostics.PrintDebug("sendSerialBlockCommand " + serialString$ + " to port " + port$)
        
        if type(m.serial) = "roAssociativeArray" then
            serial = m.serial[port$]
            if type(serial) = "roSerialPort" or type(serial) = "roUsbTap" then
                serial.SendBlock(serialString$)
            endif
        endif
        
    else if command$ = "sendSerialByteCommand" then

		portParameter = parameters["port"]
		port$ = portParameter.GetCurrentParameterValue()

		byteValueParameter = parameters["byteValue"]
		byteValue$ = byteValueParameter.GetCurrentParameterValue()
    
        m.diagnostics.PrintDebug("sendSerialByteCommand " + byteValue$ + " to port " + port$)
        
        if type(m.serial) = "roAssociativeArray" then
            serial = m.serial[port$]
            if type(serial) = "roSerialPort" or type(serial) = "roUsbTap" then
                serial.SendByte(int(val(byteValue$)))
            endif
        endif
        
    else if command$ = "sendSerialBytesCommand" then

		portParameter = parameters["port"]
		port$ = portParameter.GetCurrentParameterValue()

		byteValueParameter = parameters["byteValues"]
		byteValues$ = byteValueParameter.GetCurrentParameterValue()
    
        m.diagnostics.PrintDebug("sendSerialBytesCommand " + byteValues$ + " to port " + port$)
        
        if type(m.serial) = "roAssociativeArray" then
            serial = m.serial[port$]
            if type(serial) = "roSerialPort" or type(serial) = "roUsbTap" then
                byteString$ = StripLeadingSpaces(byteValues$)
                if len(byteString$) > 0 then
                    commaPosition = -1
    	            while commaPosition <> 0	
    		            commaPosition = instr(1, byteString$, ",")
			            if commaPosition = 0 then
				            serial.SendByte(val(byteString$))					
			            else 
				            serial.SendByte(val(left(byteString$, commaPosition - 1)))
			            endif
			            byteString$ = mid(byteString$, commaPosition+1)
    	            end while
                endif
            endif
        endif

    else if command$ = "sendUDPCommand" then

		udpStringParameter = parameters["udpString"]
		udpString$ = udpStringParameter.GetCurrentParameterValue()

        m.diagnostics.PrintDebug("Send UDP command " + udpString$)
        m.udpSender.Send(udpString$)
    
	else if command$ = "sendUDPBytesCommand" then

		byteValueParameter = parameters["byteValues"]
		byteValues$ = byteValueParameter.GetCurrentParameterValue()
    
        m.diagnostics.PrintDebug("sendUDPBytesCommand " + byteValues$)
        
		ba = CreateObject("roByteArray")
        byteString$ = StripLeadingSpaces(byteValues$)
        if len(byteString$) > 0 then
            commaPosition = -1
    	    while commaPosition <> 0	
    		    commaPosition = instr(1, byteString$, ",")
				ba.push(val(byteString$))
			    byteString$ = mid(byteString$, commaPosition+1)
    	    end while
        endif

		m.diagnostics.PrintDebug("Send UDP command " + ba.ToHexString())
		m.udpSender.Send(ba)

    else if command$ = "sendProntoIRRemote" then

		irRemoteOutParameter = parameters["irRemoteOut"]
		irRemoteOut$ = irRemoteOutParameter.GetCurrentParameterValue()
        
	    m.diagnostics.PrintDebug("Send Pronto IR Remote " + irRemoteOut$)

        if type(m.remote) <> "roIRRemote" then
            m.remote = CreateObject("roIRRemote")
            m.remote.SetPort(m.msgPort)
        endif
        
        if type(m.remote) = "roIRRemote" then
            m.remote.Send("PHC", irRemoteOut$)
        endif

    else if command$ = "sendIRRemote" then
    
		irRemoteOutParameter = parameters["irRemoteOut"]
		irRemoteOut$ = irRemoteOutParameter.GetCurrentParameterValue()
        
        if instr(1, irRemoteOut$, "b-") = 1 then
			irRemoteOut$ = mid(irRemoteOut$, 3)
			m.diagnostics.PrintDebug("Send Bose IR Remote " + irRemoteOut$)
			protocol$ = "Bose Sounddock"
        else
	        m.diagnostics.PrintDebug("Send IR Remote " + irRemoteOut$)
			protocol$ = "NEC"
        endif
        
        if type(m.remote) <> "roIRRemote" then
            m.remote = CreateObject("roIRRemote")
            m.remote.SetPort(m.msgPort)
        endif
        
        if type(m.remote) = "roIRRemote" then
            irRemoteOut% = int(val(irRemoteOut$))
            m.remote.Send(protocol$, irRemoteOut%)
        endif

    else if command$ = "sendBLC400Output" then

		controllerIndexParameter = parameters["controllerIndex"]
		controllerIndex$ = controllerIndexParameter.GetCurrentParameterValue()
        controllerIndex% = int(val(controllerIndex$))

		if type(m.blcs[controllerIndex%]) = "roControlPort" then

			CHANNEL_CMD_INTENSITY% = &h1000
			CHANNEL_CMD_BLINK%     = &h1100
			CHANNEL_CMD_BREATHE%   = &h1200
			CHANNEL_CMD_STROBE%    = &h1300
			CHANNEL_CMD_MARQUEE%   = &h1400

			' blink mode enumeration
			BLINK_SPEED_SLOW%   = &h20
			BLINK_SPEED_MEDIUM% = &h21
			BLINK_SPEED_FAST%   = &h22

			' marquee sub commands
			MARQUEE_EXECUTE%    = &h30
			MARQUEE_ON_TIME%    = &h31
			MARQUEE_OFF_TIME%   = &h32
			MARQUEE_FADE_OUT%   = &h33
			MARQUEE_PLAYBACK%   = &h34
			MARQUEE_TRANSITION% = &h35
			MARQUEE_INTENSITY%  = &h36

			' marquee playback mode enumeration
			MARQUEE_PLAYBACK_LOOP%   = &h40
			MARQUEE_PLAYBACK_BOUNCE% = &h41
			MARQUEE_PLAYBACK_ONCE%   = &h42
			MARQUEE_PLAYBACK_RANDOM% = &h43

			' marquee transition mode enumeration
			MARQUEE_TRANSITION_OFF%     = &h50
			MARQUEE_TRANSITION_FULL%    = &h51
			MARQUEE_TRANSITION_OVERLAP% = &h52

			controlCmd = CreateObject("roArray", 4, false)

			effectParameter = parameters["effect"]
			effect$ = effectParameter.GetCurrentParameterValue()

			channelsParameter = parameters["channels"]
			channels$ = channelsParameter.GetCurrentParameterValue()
			channels% = int(val(channels$))

			time% = GetIntegerParameterValue(parameters, "time", 0)

			intensity% = GetIntegerParameterValue(parameters, "intensity", 100)

			blinkRateParameter = parameters["blinkRate"]
			blinkRate$ = blinkRateParameter.GetCurrentParameterValue()

			minimumIntensity% = GetIntegerParameterValue(parameters, "minimumIntensity", 0)
			maximumIntensity% = GetIntegerParameterValue(parameters, "maximumIntensity", 100)

			controlCmd[0] = channels%

			if effect$ = "intensity" then

				controlCmd[0] = CHANNEL_CMD_INTENSITY% or channels%
				controlCmd[1] = time%                ' time in seconds for transition (zero for instantaneous)
				controlCmd[2] = intensity%           ' target intensity
				controlCmd[3] = 0					 ' unused

		        m.diagnostics.PrintDebug("sendBLC400Output - intensity: time = " + stri(time%) + " intensity = " + stri(intensity%))

			else if effect$ = "blink" then

				if blinkRate$ = "fast" then
					blinkRate% = BLINK_SPEED_FAST%
				else if blinkRate$ = "medium" then
					blinkRate% = BLINK_SPEED_MEDIUM%
				else
					blinkRate% = BLINK_SPEED_SLOW%
				endif

				controlCmd[ 0 ] = CHANNEL_CMD_BLINK% or channels%
				controlCmd[ 1 ] = blinkRate%		   ' blink mode
				controlCmd[ 2 ] = 100                  ' intensity (0 = use current value)
				controlCmd[ 3 ] = 0                    ' unused

		        m.diagnostics.PrintDebug("sendBLC400Output - blink: blinkRate = " + blinkRate$)

			else if effect$ = "breathe" then

				controlCmd[ 0 ] = CHANNEL_CMD_BREATHE% or channels%
				controlCmd[ 1 ] = time%                   ' time in seconds for change (zero for instantaneous)
				controlCmd[ 2 ] = minimumIntensity%       ' min intensity (or rather starting intensity)
				controlCmd[ 3 ] = maximumIntensity%       ' max intensity

		        m.diagnostics.PrintDebug("sendBLC400Output - breathe: time = " + stri(time%) + " minimumIntensity = " + stri(minimumIntensity%) + " maximumIntensity = " + stri(maximumIntensity%))

			else if effect$ = "strobe" then

				controlCmd[ 0 ] = CHANNEL_CMD_STROBE% or channels%
				controlCmd[ 1 ] = time%                ' time in milliseconds for strobe
				controlCmd[ 2 ] = intensity%           ' intensity (0 = use current value)
				controlCmd[ 3 ] = 0                    ' unused

		        m.diagnostics.PrintDebug("sendBLC400Output - strobe: time = " + stri(time%) + " intensity = " + stri(intensity%))

			else if effect$ = "marquee" then

				lightOnTime% = GetIntegerParameterValue(parameters, "lightOnTime", 0)
				lightOffTime% = GetIntegerParameterValue(parameters, "lightOffTime", 0)

				transitionModeParameter = parameters["transitionMode"]
				transitionMode$ = transitionModeParameter.GetCurrentParameterValue()

				playbackModeParameter = parameters["playbackMode"]
				playbackMode$ = playbackModeParameter.GetCurrentParameterValue()

				if playbackMode$ = "loop" then
					playbackMode% = MARQUEE_PLAYBACK_LOOP%
				else if playbackMode$ = "backAndForth" then
					playbackMode% = MARQUEE_PLAYBACK_BOUNCE%
				else if playbackMode$ = "playOnce" then
					playbackMode% = MARQUEE_PLAYBACK_ONCE%
				else
					playbackMode% = MARQUEE_PLAYBACK_RANDOM%
				endif

		        m.diagnostics.PrintDebug("sendBLC400Output - marquee: mode = " + playbackMode$)

				transitionMode% = MARQUEE_TRANSITION_OFF%

				if transitionMode$ = "hard" then
					fadeOut% = 0
				else
					fadeOut% = 1

					if transitionMode$ = "smoothFull" then
						transitionMode% = MARQUEE_TRANSITION_FULL%
					else if transitionMode$ = "smoothOverlap"
						transitionMode% = MARQUEE_TRANSITION_OVERLAP%
					endif

				endif

				controlCmd[ 0 ] = CHANNEL_CMD_MARQUEE%
				controlCmd[ 1 ] = MARQUEE_PLAYBACK%			' changing playback mode
				controlCmd[ 2 ] = playbackMode%				' playback mode
				controlCmd[ 3 ] = 0							' unused

				m.blcs[controllerIndex%].SetOutputValues(controlCmd)

				controlCmd[ 0 ] = CHANNEL_CMD_MARQUEE%
				controlCmd[ 1 ] = MARQUEE_FADE_OUT%			' fadeOut
				controlCmd[ 2 ] = fadeOut%					' hard or soft
				controlCmd[ 3 ] = 0							' unused

				m.blcs[controllerIndex%].SetOutputValues(controlCmd)

				controlCmd[ 0 ] = CHANNEL_CMD_MARQUEE%
				controlCmd[ 1 ] = MARQUEE_TRANSITION%			
				controlCmd[ 2 ] = transitionMode%					
				controlCmd[ 3 ] = 0							' unused

				m.blcs[controllerIndex%].SetOutputValues(controlCmd)

				controlCmd[ 0 ] = CHANNEL_CMD_MARQUEE%
				controlCmd[ 1 ] = MARQUEE_ON_TIME%			' on time
				controlCmd[ 2 ] = lightOnTime%				' msec
				controlCmd[ 3 ] = 0							' unused

				m.blcs[controllerIndex%].SetOutputValues(controlCmd)

				controlCmd[ 0 ] = CHANNEL_CMD_MARQUEE%
				controlCmd[ 1 ] = MARQUEE_OFF_TIME%			' off time
				controlCmd[ 2 ] = lightOffTime%				' msec
				controlCmd[ 3 ] = 0							' unused

				m.blcs[controllerIndex%].SetOutputValues(controlCmd)

				controlCmd[ 0 ] = CHANNEL_CMD_MARQUEE% or channels%
				controlCmd[ 1 ] = MARQUEE_EXECUTE%         ' marquee sub command
				controlCmd[ 2 ] = 0                        ' unused
				controlCmd[ 3 ] = 0                        ' unused

			endif

			m.blcs[controllerIndex%].SetOutputValues(controlCmd)

		endif

	else if command$ = "sendBPOutput" then
    
		buttonPanelIndexParameter = parameters["buttonPanelIndex"]
		buttonPanelIndex$ = buttonPanelIndexParameter.GetCurrentParameterValue()
        buttonPanelIndex% = int(val(buttonPanelIndex$))

		buttonNumberParameter = parameters["buttonNumber"]
		buttonNumber$ = buttonNumberParameter.GetCurrentParameterValue()

		actionParameter = parameters["action"]
		action$ = actionParameter.GetCurrentParameterValue()

        if type(m.bpOutput[buttonPanelIndex%]) = "roControlPort" then
        
            m.diagnostics.PrintDebug("Apply action " + action$ + " to BP button " + buttonNumber$)

            buttonNumber% = int(val(buttonNumber$))

            if buttonNumber% = -1 then
				for i% = 0 to 10
					if action$ = "on" then
						m.bpOutput[buttonPanelIndex%].SetOutputState(i%, 1)            
					else if action$ = "off" then
						m.bpOutput[buttonPanelIndex%].SetOutputState(i%, 0)            
					else if action$ = "fastBlink" then
						m.bpOutput[buttonPanelIndex%].SetOutputValue(i%, &h038e38c)
					else if action$ = "mediumBlink" then
						m.bpOutput[buttonPanelIndex%].SetOutputValue(i%, &h03f03e0)
					else if action$ = "slowBlink" then
						m.bpOutput[buttonPanelIndex%].SetOutputValue(i%, &h03ff800)
					endif
				next
            else
				if action$ = "on" then
					m.bpOutput[buttonPanelIndex%].SetOutputState(buttonNumber%, 1)            
				else if action$ = "off" then
					m.bpOutput[buttonPanelIndex%].SetOutputState(buttonNumber%, 0)            
				else if action$ = "fastBlink" then
					m.bpOutput[buttonPanelIndex%].SetOutputValue(buttonNumber%, &h038e38c)
				else if action$ = "mediumBlink" then
					m.bpOutput[buttonPanelIndex%].SetOutputValue(buttonNumber%, &h03f03e0)
				else if action$ = "slowBlink" then
					m.bpOutput[buttonPanelIndex%].SetOutputValue(buttonNumber%, &h03ff800)
				endif
            endif
            
        endif        

    else if command$ = "synchronize" then

		synchronizeKeywordParameter = parameters["synchronizeKeyword"]
		synchronizeKeyword$ = synchronizeKeywordParameter.GetCurrentParameterValue()

		if m.sign.enableEnhancedSynchronization then

			m.diagnostics.PrintDebug("Send synchronize command " + synchronizeKeyword$ + " using SyncManager.")

			syncManagerEvent = m.SyncManager.Synchronize(synchronizeKeyword$, 300)
			m.diagnostics.PrintDebug("@@@ Created syncManagerEvent with sync keyword: " + synchronizeKeyword$)
			zoneHSM.syncInfo = CreateObject("roAssociativeArray")
			zoneHSM.syncInfo.SyncDomain = syncManagerEvent.GetDomain()
			zoneHSM.syncInfo.SyncId = syncManagerEvent.GetId()
			zoneHSM.syncInfo.SyncIsoTimestamp = syncManagerEvent.GetIsoTimestamp()

		else

			m.diagnostics.PrintDebug("Send synchronize command " + synchronizeKeyword$)
    
			m.udpSender.Send("pre-" + synchronizeKeyword$)
	    
			preloadRequired = true
			if type(zoneHSM.preloadState) = "roAssociativeArray" then
				if zoneHSM.preloadedStateName$ = zoneHSM.preloadState.name$ then
					preloadRequired = false
				endif
			endif                                    

			' currently only support preload / synchronizing with images and videos
			if preloadRequired then
				zoneHSM.preloadState.PreloadItem()
			endif
	    
			sleep(300)
	    
			' m.udpSender.Send("ply-" + synchronizeKeyword$)
	    
			if type(m.udpReceiver) = "roDatagramReceiver" then
				udpReceiverExists = true
				m.udpReceiver = 0
			else
				udpReceiverExists = false
			endif
	    
			m.WaitForSyncResponse(synchronizeKeyword$)
	    
			if udpReceiverExists then
				m.udpReceiver = CreateObject("roDatagramReceiver", m.udpReceivePort)
				m.udpReceiver.SetPort(m.msgPort)
			endif

		endif

	else if command$ = "sendZoneMessage" then
	
        m.diagnostics.PrintDebug("Execute sendZoneMessage command")

		zoneMessageParameter = parameters["zoneMessage"]
		sendZoneMessageParameter$ = zoneMessageParameter.GetCurrentParameterValue()

		' send ZoneMessage message
		zoneMessageCmd = CreateObject("roAssociativeArray")
		zoneMessageCmd["EventType"] = "SEND_ZONE_MESSAGE"
		zoneMessageCmd["EventParameter"] = sendZoneMessageParameter$
		m.msgPort.PostMessage(zoneMessageCmd)

	else if command$ = "sendPluginMessage" then

        m.diagnostics.PrintDebug("Execute sendPluginMessage command")

		pluginName = parameters["pluginName"]
		pluginMessageParameter = parameters["message"]

		pluginName$ = pluginName.GetCurrentParameterValue()
		sendPluginMessageParameter$ = pluginMessageParameter.GetCurrentParameterValue()

		' send ZoneMessage message
		pluginMessageCmd = CreateObject("roAssociativeArray")
		pluginMessageCmd["EventType"] = "SEND_PLUGIN_MESSAGE"
		pluginMessageCmd["PluginName"] = pluginName$
		pluginMessageCmd["PluginMessage"] = sendPluginMessageParameter$
		m.msgPort.PostMessage(pluginMessageCmd)

	else if command$ = "resizeZone" then

		m.diagnostics.PrintDebug("Execute resizeZone command")

		zoneId$ = parameters["zone"].GetCurrentParameterValue()
		x% = int(val(parameters["x"].GetCurrentParameterValue()))
		y% = int(val(parameters["y"].GetCurrentParameterValue()))
		width% = int(val(parameters["width"].GetCurrentParameterValue()))
		height% = int(val(parameters["height"].GetCurrentParameterValue()))

		zone = m.GetZone(zoneId$)

		if type(zone) = "roAssociativeArray" then

			r = CreateObject("roRectangle", x%, y%, width%, height%)

			if type(zone.videoPlayer) = "roVideoPlayer" then
				zone.videoPlayer.SetRectangle(r)
			endif

			if type(zone.mjpegvideoPlayer) = "roVideoPlayer" then
				zone.mjpegVideoPlayer.SetRectangle(r)
			endif

			if type(zone.imagePlayer) = "roImageWidget" then
				zone.imagePlayer.SetRectangle(r)
			endif

			if type(zone.displayedHtmlWidget) = "roHtmlWidget" then
				zone.displayedHtmlWidget.SetRectangle(r)
			endif

			if type(zone.canvasWidget) = "roCanvasWidget" then
				zone.canvasWidget.SetRectangle(r)
			endif
			
			if type(zone.widget) = "roClockWidget" or type(zone.widget) = "roTextWidget" then
				ok = zone.widget.SetRectangle(r)
			endif

		endif

	else if command$ = "hideZone" then

		m.diagnostics.PrintDebug("Execute hideZone command")

		zoneId$ = parameters["zone"].GetCurrentParameterValue()
		zoneHSM = m.GetZone(zoneId$)

		if type(zoneHSM) = "roAssociativeArray" then
			
			if not zoneHSM.isVisible return

			if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
				
				if m.showHideVideoZoneSupported then
					if type(zoneHSM.videoPlayer) = "roVideoPlayer" then zoneHSM.videoPlayer.Hide()
				endif
				if type(zoneHSM.imagePlayer) <> "Invalid" then zoneHSM.imagePlayer.Hide()
				if type(zoneHSM.canvasWidget) <> "Invalid" then zoneHSM.canvasWidget.Hide()
				if type(zoneHSM.loadingHtmlWidget) <> "Invalid" then zoneHSM.loadingHtmlWidget.Hide()
				if type(zoneHSM.displayedHtmlWidget) <> "Invalid" then zoneHSM.displayedHtmlWidget.Hide()

			else if zoneHSM.type$ = "VideoOnly" then

				if m.showHideVideoZoneSupported then
					if type(zoneHSM.videoPlayer) = "roVideoPlayer" then zoneHSM.videoPlayer.Hide()
				endif

			else if zoneHSM.type$ = "Clock" then

				zoneHSM.widget.Hide()
			
			else if zoneHSM.type$ = "Ticker" then

				zoneHSM.widget.Hide()			

			else if zoneHSM.type$ = "BackgroundImage" then

			endif

			zoneHSM.isVisible = false

		endif

	else if command$ = "showZone" then

		m.diagnostics.PrintDebug("Execute showZone command")

		zoneId$ = parameters["zone"].GetCurrentParameterValue()
		zoneHSM = m.GetZone(zoneId$)

		if type(zoneHSM) = "roAssociativeArray" then
			
			if zoneHSM.isVisible return

			if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then

				if m.showHideVideoZoneSupported then
					if type(zoneHSM.videoPlayer) = "roVideoPlayer" then zoneHSM.videoPlayer.Show()
				endif
				if type(zoneHSM.imagePlayer) <> "Invalid" and not zoneHSM.imageHidden then zoneHSM.imagePlayer.Show()
				if type(zoneHSM.canvasWidget) <> "Invalid" and not zoneHSM.canvasHidden then zoneHSM.canvasWidget.Show()
				if type(zoneHSM.loadingHtmlWidget) <> "Invalid" and not zoneHSM.htmlHidden then zoneHSM.loadingHtmlWidget.Show()
				if type(zoneHSM.displayedHtmlWidget) <> "Invalid" and not zoneHSM.htmlHidden then zoneHSM.displayedHtmlWidget.Show()

			else if zoneHSM.type$ = "VideoOnly" then

				if m.showHideVideoZoneSupported then
					if type(zoneHSM.videoPlayer) = "roVideoPlayer" then zoneHSM.videoPlayer.Show()
				endif

			else if zoneHSM.type$ = "Clock" then

				zoneHSM.widget.Show()
			
			else if zoneHSM.type$ = "Ticker" then

				zoneHSM.widget.Show()			

			else if zoneHSM.type$ = "BackgroundImage" then

			endif

			zoneHSM.isVisible = true

		endif

    else if command$ = "internalSynchronize" then

        m.diagnostics.PrintDebug("Execute internalSynchronize command")

		internalSyncParameter = parameters["synchronizeKeyword"]
		internalSyncParameter$ = internalSyncParameter.GetCurrentParameterValue()

' send InternalSyncPreload message
		internalSyncPreload = CreateObject("roAssociativeArray")
		internalSyncPreload["EventType"] = "INTERNAL_SYNC_PRELOAD"
		internalSyncPreload["EventParameter"] = internalSyncParameter$
		m.msgPort.PostMessage(internalSyncPreload)

' send InternalSyncMasterPreload message
		internalSyncMasterPreload = CreateObject("roAssociativeArray")
		internalSyncMasterPreload["EventType"] = "INTERNAL_SYNC_MASTER_PRELOAD"
		internalSyncMasterPreload["EventParameter"] = internalSyncParameter$
		m.msgPort.PostMessage(internalSyncMasterPreload)

' current state is zoneHSM.activeState
		activeState = zoneHSM.activeState
		if type(activeState) = "roAssociativeArray" then
			if type(activeState.internalSynchronizeEventsMaster) = "roAssociativeArray" then
				if type(activeState.internalSynchronizeEventsMaster[internalSyncParameter$]) = "roAssociativeArray" then
					transition = activeState.internalSynchronizeEventsMaster[internalSyncParameter$]
					nextState$ = transition.targetMediaState$
					if nextState$ <> "" then
						zoneHSM.preloadState = zoneHSM.stateTable[nextState$]
						zoneHSM.preloadState.PreloadItem()
					endif
				endif

			endif
		endif

	else if command$ = "reboot" then
		
		m.diagnostics.PrintDebug("Reboot")
        RebootSystem()

	else if command$ = "cecDisplayOn" then
	
        m.diagnostics.PrintDebug("Display On")
        m.CecDisplayOn()

	else if command$ = "cecDisplayOff" then
	
        m.diagnostics.PrintDebug("Display Off")
        m.CecDisplayOff()

	else if command$ = "cecSetSourceBrightSign" then
	
        m.diagnostics.PrintDebug("CecSetSourceBrightSign")
        m.CecSetSourceBrightSign()

	else if command$ = "cecSendString" then
	
		cecCommandParameter = parameters["cecAsciiString"]
		cecCommand$ = cecCommandParameter.GetCurrentParameterValue()

		cecSubstituteSourceAddressParameter = parameters["cecSubstituteSourceAddress"]
		if type(cecSubstituteSourceAddressParameter) = "roAssociativeArray" then
			cecSubstituteSourceAddress$ = cecSubstituteSourceAddressParameter.GetCurrentParameterValue()
			' cecSubstituteSourceAddress$ is "true" or "false"
		else
			' old presentation where user did not set this value - for compatibility set this value to true
			cecSubstituteSourceAddress$ = "true"
		endif

		m.diagnostics.PrintDebug("cecSendString: " + cecCommand$ + ", cecSubstituteSourceAddress: " + cecSubstituteSourceAddress$)
		m.SendCecCommand(cecCommand$, cecSubstituteSourceAddress$)
		
	else if command$ = "cecPhilipsSetVolume" then

		volumeParameter = parameters["volume"]
		volume$ = volumeParameter.GetCurrentParameterValue()

        m.diagnostics.PrintDebug("Set cec Philips volume to " + volume$)
        volume% = int(val(volume$))
		m.CecPhilipsSetVolume(volume%)
		
    else if command$ = "pauseVideoCommand" then
    
        m.diagnostics.PrintDebug("Pause video")
        m.PauseVideo(zoneHSM)

    else if command$ = "resumeVideoCommand" then
    
        m.diagnostics.PrintDebug("Resume video")
        m.ResumeVideo(zoneHSM)

    else if command$ = "enablePowerSaveMode" then
    
        m.diagnostics.PrintDebug("Enable Power Save Mode")
        m.SetPowerSaveMode(true)

    else if command$ = "disablePowerSaveMode" then
    
        m.diagnostics.PrintDebug("Disable Power Save Mode")
        m.SetPowerSaveMode(false)

    else if command$ = "pause" then
    
		pauseTimeParameter = parameters["pauseTime"]
		pauseTime$ = pauseTimeParameter.GetCurrentParameterValue()

        m.diagnostics.PrintDebug("Pause for " + pauseTime$ + " milliseconds")

        pauseTime% = int(val(pauseTime$))
        sleep(pauseTime%)

	else if command$ = "setVariable" then

		variableNameParameter = parameters["variableName"]
		variableValueParameter = parameters["variableValue"]

		variableName$ = variableNameParameter.GetVariableName()
		variableValue$ = variableValueParameter.GetCurrentParameterValue()

		userVariable = m.GetUserVariable(variableName$)
		if type(userVariable) = "roAssociativeArray" then
			userVariable.SetCurrentValue(variableValue$, true)

			userVariablesChanged = {}
			userVariablesChanged["EventType"] = "USER_VARIABLES_UPDATED"
			m.msgPort.PostMessage(userVariablesChanged)

			' Notify controlling devices to refresh
			m.SendUDPNotification("refresh")

		else
      m.diagnostics.PrintDebug("User variable " + variableName$ + " not found.")
      m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND, variableName$)
		endif

	else if command$ = "incrementVariable" then

		m.ChangeUserVariableValue(parameters, 1)

	else if command$ = "decrementVariable" then

		m.ChangeUserVariableValue(parameters, -1)

	else if command$ = "resetVariable" then

		m.ResetVariable(parameters)

	else if command$ = "resetVariables" then

		m.ResetVariables()

	else if command$ = "configureAudioResources" then

        m.diagnostics.PrintDebug("Configure Audio Resources")
        zoneHSM.ConfigureAudioResources()

	else if command$ = "updateDataFeed" then

		m.UpdateDataFeed(parameters)

	else if command$ = "beaconStart" then

		nameParameter = parameters["beaconName"]
		name$ = nameParameter.GetCurrentParameterValue()
		m.btManager.StartBeacon(name$)

	else if command$ = "beaconStop" then

		nameParameter = parameters["beaconName"]
		name$ = nameParameter.GetCurrentParameterValue()
		m.btManager.StopBeacon(name$)

	endif
	
End Sub


Sub ResetVariable(parameters As Object)

	variableNameParameter = parameters["variableName"]
	variableName$ = variableNameParameter.GetVariableName()

	userVariable = m.GetUserVariable(variableName$)
	if type(userVariable) = "roAssociativeArray" then
		userVariable.Reset(true)
	else
	    m.diagnostics.PrintDebug("User variable " + variableName$ + " not found.")
		m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND, variableName$)
	endif

End Sub


Sub ChangeUserVariableValue(parameters As Object, delta% As Integer)

	variableNameParameter = parameters["variableName"]
	variableName$ = variableNameParameter.GetVariableName()

	userVariable = m.GetUserVariable(variableName$)
	if type(userVariable) = "roAssociativeArray" then
		currentValue% = val(userVariable.GetCurrentValue())
		currentValue% = currentValue% + delta%
		userVariable.SetCurrentValue(StripLeadingSpaces(stri(currentValue%)), true)
	else
		m.diagnostics.PrintDebug("User variable " + variableName$ + " not found.")
		m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND, variableName$)
	endif

End Sub


Sub ChangeRFChannel(zone As Object, channelDelta% As Integer)

	startingChannel% = zone.currentChannelIndex%

' loop unnecessary if we don't support ignoreChannel
	while true

		zone.currentChannelIndex% = zone.currentChannelIndex% + channelDelta%

		if zone.currentChannelIndex% < 0 then
			zone.currentChannelIndex% = m.scannedChannels.Count() - 1
		endif

		if zone.currentChannelIndex% >= m.scannedChannels.Count() then
			zone.currentChannelIndex% = 0
		endif

		return

'		if not m.scannedChannels[zone.currentChannelIndex%].ignoreChannel return

		if zone.currentChannelIndex% = startingChannel% return

	end while

End Sub


Function STTopEventHandler(event As Object, stateData As Object) As Object
	
    stateData.nextState = invalid
    return "IGNORED"
    
End Function


Function GetPoolFilePath(assetPoolFiles As Object, fileName$ As String) As String

    if type(assetPoolFiles) = "roAssetPoolFiles" then
        return assetPoolFiles.GetPoolFilePath(fileName$)
    else
        return fileName$
    endif

End Function

'endregion

'region Logging
REM *******************************************************
REM *******************************************************
REM ***************                    ********************
REM *************** LOGGING OBJECT     ********************
REM ***************                    ********************
REM *******************************************************
REM *******************************************************

REM
REM construct a new logging BrightScript object
REM
Function newLogging() As Object

    logging = CreateObject("roAssociativeArray")
    
    logging.bsp = m
    logging.msgPort = m.msgPort
    logging.systemTime = m.systemTime
    logging.diagnostics = m.diagnostics
    
    logging.SetSystemInfo = SetSystemInfo

    logging.CreateLogFile = CreateLogFile
    logging.MoveExpiredCurrentLog = MoveExpiredCurrentLog
    logging.MoveCurrentLog = MoveCurrentLog
    logging.InitializeLogging = InitializeLogging
    logging.ReinitializeLogging = ReinitializeLogging
    logging.InitializeCutoverTimer = InitializeCutoverTimer
    logging.WritePlaybackLogEntry = WritePlaybackLogEntry
    logging.WriteEventLogEntry = WriteEventLogEntry
	logging.WriteStateLogEntry = WriteStateLogEntry
    logging.WriteDiagnosticLogEntry = WriteDiagnosticLogEntry
    logging.WriteVariableLogEntry = WriteVariableLogEntry
    logging.PushLogFile = PushLogFile
    logging.CutoverLogFile = CutoverLogFile
    logging.HandleTimerEvent = HandleLoggingTimerEvent
    logging.PushLogFilesOnBoot = PushLogFilesOnBoot
    logging.OpenOrCreateCurrentLog = OpenOrCreateCurrentLog
    logging.DeleteExpiredFiles = DeleteExpiredFiles
    logging.DeleteOlderFiles = DeleteOlderFiles
    logging.DeleteLogFiles = DeleteLogFiles
    logging.DeleteAllLogFiles = DeleteAllLogFiles
	logging.GetLogFiles = GetLogFiles
	logging.CopyAllLogFiles = CopyAllLogFiles
	logging.CopyLogFiles = CopyLogFiles
    logging.FlushLogFile = FlushLogFile
	logging.UpdateLogCounter = UpdateLogCounter
	logging.AddVariableDataToLog = AddVariableDataToLog
    logging.logFile = invalid
    
    logging.uploadLogFolder = "logs"
    logging.uploadLogArchiveFolder = "archivedLogs"
    logging.uploadLogFailedFolder = "failedLogs"
    logging.logFileUpload = invalid
    
    logging.playbackLoggingEnabled = false
    logging.eventLoggingEnabled = false
    logging.diagnosticLoggingEnabled = false
    logging.stateLoggingEnabled = false
    logging.variableLoggingEnabled = false
    logging.uploadLogFilesAtBoot = false
    logging.uploadLogFilesAtSpecificTime = false
    logging.uploadLogFilesTime% = 0
	logging.uploadLogFilesInterval% = invalid
    
	logging.useDate = logging.systemTime.IsValid()

    return logging
    
End Function


Function UpdateLogCounter(logCounter$ As String, maxValue% As Integer, numDigits% As Integer, writeToRegistry As Boolean) As String

	logCounter% = val(logCounter$)
	logCounter% = logCounter% + 1
	if logCounter% > maxValue% then
		logCounter% = 0
	endif
	logCounter$ = StripLeadingSpaces(stri(logCounter%))

	while len(logCounter$) < numDigits%
		logCounter$ = "0" + logCounter$
	end while

	if writeToRegistry then
		m.bsp.WriteRegistrySetting("lc", logCounter$)
		m.bsp.registrySettings.logCounter$ = logCounter$
	else
		WriteAsciiFile("logCounter.txt", logCounter$)
	endif

	return logCounter$

End Function


Function CreateLogFile() As Object

    if not m.useDate then
		
		' don't use date for file name, use log counter
		logCounter$ = ReadAsciiFile("logCounter.txt")

		if logCounter$ = "" then
			logCounter$ = "000000"
		endif

		localFileName$ = "BrightSignLog." + m.deviceUniqueID$ + "-" + logCounter$ + ".log"

		fileNameLogCounter$ = logCounter$ 
		logCounter$ = m.UpdateLogCounter(logCounter$, 999999, 6, false)

	else

		' use date for file name

		logCounter$ = m.bsp.registrySettings.logCounter$

		dtLocal = m.systemTime.GetLocalDateTime()
		year$ = Right(stri(dtLocal.GetYear()), 2)
		month$ = StripLeadingSpaces(stri(dtLocal.GetMonth()))
		if len(month$) = 1 then
			month$ = "0" + month$
		endif
		day$ = StripLeadingSpaces(stri(dtLocal.GetDay()))
		if len(day$) = 1 then
			day$ = "0" + day$
		endif
		dateString$ = year$ + month$ + day$
    
		logDate$ = m.bsp.registrySettings.logDate$
    
		if logDate$ = "" or logCounter$ = "" then
			logCounter$ = "000"
		else if logDate$ <> dateString$ then
			logCounter$ = "000"
		endif
		logDate$ = dateString$
    
		localFileName$ = "BrightSign" + "Log." + m.deviceUniqueID$ + "-" + dateString$ + logCounter$ + ".log"

		m.bsp.WriteRegistrySetting("ld", logDate$)
		m.bsp.registrySettings.logDate$ = logDate$
    
		logCounter$ = m.UpdateLogCounter(logCounter$, 999, 3, true)

	endif

    fileName$ = "currentLog/" + localFileName$
    logFile = CreateObject("roCreateFile", fileName$)
    m.diagnostics.PrintDebug("Create new log file " + localFileName$)
    
    t$ = chr(9)
    
    ' version
    header$ = "BrightSignLogVersion"+t$+"4"
    logFile.SendLine(header$)
    
    ' serial number
    header$ = "SerialNumber"+t$+m.deviceUniqueID$
    logFile.SendLine(header$)
    
	' log counter
    if not m.useDate then
		counterInHeader$ = "LogCounter" + t$ + fileNameLogCounter$
		logFile.SendLine(counterInHeader$)
	endif

    ' group id
    if type(m.networking) = "roAssociativeArray" then
		if type(m.networking.currentSync) = "roSyncSpec" then
			header$ = "Account"+t$+m.networking.currentSync.LookupMetadata("server", "account")
			logFile.SendLine(header$)
			header$ = "Group"+t$+m.networking.currentSync.LookupMetadata("server", "group")
			logFile.SendLine(header$)
		endif
    endif
    
    ' timezone
    header$ = "Timezone"+t$+m.systemTime.GetTimeZone()
    logFile.SendLine(header$)

    ' timestamp of log creation
    header$ = "LogCreationTime"+t$+m.systemTime.GetLocalDateTime().GetString()
    logFile.SendLine(header$)
    
    ' ip address
    nc = CreateObject("roNetworkConfiguration", 0)
    if type(nc) = "roNetworkConfiguration" then
        currentConfig = nc.GetCurrentConfig()
        nc = invalid
        ipAddress$ = currentConfig.ip4_address
        header$ = "IPAddress"+t$+ipAddress$
        logFile.SendLine(header$)
    endif
    
    ' fw version
    header$ = "FWVersion"+t$+m.firmwareVersion$
    logFile.SendLine(header$)
    
    ' script version
    header$ = "ScriptVersion"+t$+m.autorunVersion$
    logFile.SendLine(header$)

    ' custom script version
    header$ = "CustomScriptVersion"+t$+m.customAutorunVersion$
    logFile.SendLine(header$)

    ' model
    header$ = "Model"+t$+m.deviceModel$
    logFile.SendLine(header$)

    logFile.AsyncFlush()
    
    return logFile
    
End Function


Sub MoveExpiredCurrentLog()

    dtLocal = m.systemTime.GetLocalDateTime()
    currentDate$ = StripLeadingSpaces(stri(dtLocal.GetDay()))
    if len(currentDate$) = 1 then
        currentDate$ = "0" + currentDate$
    endif

    listOfPendingLogFiles = MatchFiles("/currentLog", "*")
        
    for each file in listOfPendingLogFiles
    
        logFileDate$ = left(right(file, 9), 2)
    
        if logFileDate$ <> currentDate$ then
            sourceFilePath$ = "currentLog/" + file
            destinationFilePath$ = "logs/" + file
            CopyFile(sourceFilePath$, destinationFilePath$)
            DeleteFile(sourceFilePath$)
        endif
        
    next

End Sub


Sub MoveCurrentLog()

    listOfPendingLogFiles = MatchFiles("/currentLog", "*")
    for each file in listOfPendingLogFiles
        sourceFilePath$ = "currentLog/" + file

		m.AddVariableDataToLog(sourceFilePath$)
		
        destinationFilePath$ = "logs/" + file
        CopyFile(sourceFilePath$, destinationFilePath$)
        DeleteFile(sourceFilePath$)
    next
    
End Sub


Sub AddVariableDataToLog(filePath$ As String)

	if m.variableLoggingEnabled then

		openedDB = false
		if not m.bsp.variablesDBExists then
			m.bsp.ReadVariablesDB("")
			openedDB = true
		endif

		if m.bsp.variablesDBExists then

			logFile = CreateObject("roAppendFile", filePath$)
			if type(logFile) <> "roAppendFile" return

		    timestamp$ = m.systemTime.GetLocalDateTime().GetString()
  
			sectionNames = m.bsp.GetDBSectionNames()
			for each sectionName in sectionNames
				m.WriteVariableLogEntry(logFile, timestamp$, "Section", sectionName, "", "")
				sectionId% = m.bsp.GetDBSectionId(sectionName)
				if sectionId% > 0 then
					categoryNames = m.bsp.GetDBCategoryNames(sectionName)
					for each categoryName in categoryNames
						m.WriteVariableLogEntry(logFile, timestamp$, "Category", categoryName, "", "")
						userVariablesList = m.bsp.GetUserVariablesGivenCategory(sectionName, false, categoryName, false)
						for each userVariable in userVariablesList
							m.WriteVariableLogEntry(logFile, timestamp$, "Variable", userVariable.name$, userVariable.currentValue$, userVariable.defaultValue$)
						next
					next
				endif
			next

			logFile.Flush()

			if openedDB then
				m.bsp.userVariablesDB = invalid
			endif

		endif

	endif

End Sub


Sub WriteVariableLogEntry(logFile As Object, timeStamp$ As String, entityType$ As String, name$ As String, currentValue$ As String, defaultValue$ As String)

    if not m.variableLoggingEnabled then return

    if type(logFile) <> "roAppendFile" then return

    t$ = chr(9) 
    logFile.SendLine("L=u"+t$+"T="+timestamp$+t$+"E="+entityType$+t$+"N="+name$+t$+"C="+currentValue$+t$+"D="+defaultValue$)

End Sub


Sub InitializeLogging(playbackLoggingEnabled As Boolean, eventLoggingEnabled As Boolean, stateLoggingEnabled As Boolean, diagnosticLoggingEnabled As Boolean, variableLoggingEnabled As Boolean, uploadLogFilesAtBoot As Boolean, uploadLogFilesAtSpecificTime As Boolean, uploadLogFilesTime% As Integer)

    m.loggingEnabled = playbackLoggingEnabled or eventLoggingEnabled or stateLoggingEnabled or diagnosticLoggingEnabled or variableLoggingEnabled

	if m.loggingEnabled then
	    CreateDirectory("logs")
		CreateDirectory("currentLog")
		CreateDirectory("archivedLogs")
		CreateDirectory("failedLogs")
	endif
	     
    m.DeleteExpiredFiles()
    
    m.playbackLoggingEnabled = playbackLoggingEnabled
    m.eventLoggingEnabled = eventLoggingEnabled
    m.stateLoggingEnabled = stateLoggingEnabled
    m.diagnosticLoggingEnabled = diagnosticLoggingEnabled
    m.variableLoggingEnabled = variableLoggingEnabled
    m.uploadLogFilesAtBoot = uploadLogFilesAtBoot
    m.uploadLogFilesAtSpecificTime = uploadLogFilesAtSpecificTime
    m.uploadLogFilesTime% = uploadLogFilesTime%

    m.uploadLogsEnabled = uploadLogFilesAtBoot or uploadLogFilesAtSpecificTime  

    if m.uploadLogFilesAtBoot then
        m.PushLogFilesOnBoot()
    endif
    
    m.MoveExpiredCurrentLog()

    if m.loggingEnabled then m.OpenOrCreateCurrentLog()
    
    m.InitializeCutoverTimer()
    
End Sub


Sub ReinitializeLogging(playbackLoggingEnabled As Boolean, eventLoggingEnabled As Boolean, stateLoggingEnabled As Boolean, diagnosticLoggingEnabled As Boolean, variableLoggingEnabled As Boolean, uploadLogFilesAtBoot As Boolean, uploadLogFilesAtSpecificTime As Boolean, uploadLogFilesTime% As Integer)

    if playbackLoggingEnabled = m.playbackLoggingEnabled and eventLoggingEnabled = m.eventLoggingEnabled and stateLoggingEnabled = m.stateLoggingEnabled and diagnosticLoggingEnabled = m.diagnosticLoggingEnabled and variableLoggingEnabled = m.variableLoggingEnabled and uploadLogFilesAtBoot = m.uploadLogFilesAtBoot and uploadLogFilesAtSpecificTime = m.uploadLogFilesAtSpecificTime and uploadLogFilesTime% = m.uploadLogFilesTime% then return
    
    m.loggingEnabled = playbackLoggingEnabled or eventLoggingEnabled or stateLoggingEnabled or diagnosticLoggingEnabled or variableLoggingEnabled

	if m.loggingEnabled then
	    CreateDirectory("logs")
		CreateDirectory("currentLog")
		CreateDirectory("archivedLogs")
		CreateDirectory("failedLogs")
	endif
	     
    if type(m.cutoverTimer) = "roTimer" then
        m.cutoverTimer.Stop()
        m.cutoverTimer = invalid
    endif

    m.playbackLoggingEnabled = playbackLoggingEnabled
    m.eventLoggingEnabled = eventLoggingEnabled
    m.stateLoggingEnabled = stateLoggingEnabled
    m.diagnosticLoggingEnabled = diagnosticLoggingEnabled
    m.variableLoggingEnabled = variableLoggingEnabled
    m.uploadLogFilesAtBoot = uploadLogFilesAtBoot
    m.uploadLogFilesAtSpecificTime = uploadLogFilesAtSpecificTime
    m.uploadLogFilesTime% = uploadLogFilesTime%

    m.uploadLogsEnabled = uploadLogFilesAtBoot or uploadLogFilesAtSpecificTime  

    if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" and m.loggingEnabled then
        m.OpenOrCreateCurrentLog()
    endif
            
    m.InitializeCutoverTimer()

End Sub


Sub InitializeCutoverTimer()

	if type(m.cutoverTimer) = "roTimer" then
		m.cutoverTimer.stop()
	endif

	m.cutoverTimer = CreateObject("roTimer")
	m.cutoverTimer.SetPort(m.msgPort)

	if type(m.uploadLogFilesInterval%) = "Integer" or type(m.uploadLogFilesInterval%) = "roInt" then
		
		m.cutoverTimer.SetElapsed(m.uploadLogFilesInterval% * 60, 0)

	else

		if m.uploadLogFilesAtSpecificTime then
			hour% = m.uploadLogFilesTime% / 60
			minute% = m.uploadLogFilesTime% - (hour% * 60)
		else
			hour% = 0
			minute% = 0
		endif
    
		m.cutoverTimer.SetDate(-1, -1, -1)
		m.cutoverTimer.SetTime(hour%, minute%, 0)
    
	endif

	m.cutoverTimer.Start()    
    
End Sub


Function CopyAllLogFiles(storagePath$ As String) As Boolean

    if type(m.logFile) = "roCreateFile" or type(m.logFile) = "roAppendFile" then
	    m.logFile.Flush()
	endif

	ok = m.CopyLogFiles(storagePath$, "currentLog")
	if not ok return ok
	
	ok = m.CopyLogFiles(storagePath$, "logs")
	if not ok return ok
	
	ok = m.CopyLogFiles(storagePath$, "failedLogs")
	if not ok return ok

	ok = m.CopyLogFiles(storagePath$, "archivedLogs")
	return ok

End Function


Function CopyLogFiles(storagePath$ As String, folderName$ As String)

    listOfLogFiles = MatchFiles("/" + folderName$, "*")
        
    for each file in listOfLogFiles
        sourceFilePath$ = "/" + folderName$ + "/" + file
		destinationFilePath$ = storagePath$ + file
		ok = CopyFile(sourceFilePath$, destinationFilePath$)
		if not ok return ok
    next

	return true

End Function


Sub DeleteAllLogFiles()

	' close the current log file before deleting

    if type(m.logFile) = "roCreateFile" or type(m.logFile) = "roAppendFile" then
	    m.logFile.Flush()
		m.logFile = invalid
	endif

	m.DeleteLogFiles("currentLog")
	m.DeleteLogFiles("logs")
	m.DeleteLogFiles("failedLogs")
	m.DeleteLogFiles("archivedLogs")

End Sub


Sub DeleteLogFiles(folderName$ As String)

    listOfLogFiles = MatchFiles("/" + folderName$, "*")
        
    for each file in listOfLogFiles
        fullFilePath$ = "/" + folderName$ + "/" + file
		DeleteFile(fullFilePath$)
    next

End Sub


Sub DeleteExpiredFiles()

	if m.useDate then

		' delete any files that are more than 30 days old
    
		dtExpired = m.systemTime.GetLocalDateTime()
		dtExpired.SubtractSeconds(60 * 60 * 24 * 30)
    
		' look in the following folders
		'   logs
		'   failedLogs
		'   archivedLogs
    
		m.DeleteOlderFiles("logs", dtExpired)
		m.DeleteOlderFiles("failedLogs", dtExpired)
		m.DeleteOlderFiles("archivedLogs", dtExpired)
    
	else

		MAX_FILES_TO_KEEP = 60

		' get a list of all log files
		logFiles = CreateObject("roArray", 1, true)
		m.GetLogFiles("logs", logFiles)
		m.GetLogFiles("failedLogs", logFiles)
		m.GetLogFiles("archivedLogs", logFiles)

		' sort them in ascending order
		sortedIndices = CreateObject("roArray", 1, true)
		SortItems(logFiles, sortedIndices)

		' if the count is > than the number to keep, delete the first n in the list
		while sortedIndices.Count() > MAX_FILES_TO_KEEP

			fullFilePath$ = logFiles[sortedIndices[0]].fullFilePath$
            m.diagnostics.PrintDebug("Delete log file " + fullFilePath$)
            DeleteFile(fullFilePath$)
			
			sortedIndices.shift()

		endwhile

	endif

End Sub


' sorted indices is an array that can grow and has no values on entry
' items is an array of associative arrays
Sub SortItems(logFiles As Object, sortedIndices As Object)

    ' initialize array with indices.
    for i% = 0 to logFiles.Count()-1
        sortedIndices[i%] = i%
    next

    numItemsToSort% = logFiles.Count()

    for i% = numItemsToSort% - 1 to 1 step -1
        for j% = 0 to i%-1
	        index0% = sortedIndices[j%]
	        logCounter0% = logFiles[index0%].counter%
            index1% = sortedIndices[j%+1]
            logCounter1% = logFiles[index1%].counter%
            if logCounter0% > logCounter1% then
                k% = sortedIndices[j%]
                sortedIndices[j%] = sortedIndices[j%+1]
                sortedIndices[j%+1] = k%
            endif
        next
    next
    
End Sub


Sub GetLogFiles(folderName$ As String, logFiles As Object)

    listOfLogFiles = MatchFiles("/" + folderName$, "*")

    for each file in listOfLogFiles
		logFile = { }
		logFile.counter% = int(val(left(right(file, 7), 3)))
		logFile.fullFilePath$ = "/" + folderName$ + "/" + file
		logFiles.push(logFile)
	next

End Sub


Sub DeleteOlderFiles(folderName$ As String, dtExpired As Object)

    listOfLogFiles = MatchFiles("/" + folderName$, "*")
        
    for each file in listOfLogFiles
    
        year$ = "20" + left(right(file,13), 2)
        month$ = left(right(file,11), 2)
        day$ = left(right(file, 9), 2)
        dtFile = CreateObject("roDateTime")
        dtFile.SetYear(int(val(year$)))
        dtFile.SetMonth(int(val(month$)))
        dtFile.SetDay(int(val(day$)))
               
        if dtFile < dtExpired then
            fullFilePath$ = "/" + folderName$ + "/" + file
            m.diagnostics.PrintDebug("Delete expired log file " + fullFilePath$)
            DeleteFile(fullFilePath$)
        endif
        
    next

End Sub


Sub FlushLogFile()

    if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return

    m.logFile.Flush()

End Sub


Sub WritePlaybackLogEntry(zoneName$ As String, startTime$ As String, endTime$ As String, itemType$ As String, fileName$ As String)

    if not m.playbackLoggingEnabled then return
    
    if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return

    t$ = chr(9) 
    m.logFile.SendLine("L=p"+t$+"Z="+zoneName$+t$+"S="+startTime$+t$+"E="+endTime$+t$+"I="+itemType$+t$+"N="+fileName$)
    m.logFile.AsyncFlush()

End Sub


Sub WriteStateLogEntry(stateMachine As Object, stateName$ As String, stateType$ As String)

    if not m.stateLoggingEnabled then return

    if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return

    timestamp$ = m.systemTime.GetLocalDateTime().GetString()

    t$ = chr(9) 

	if type(stateMachine.lastStateName$) = "roString" then
		lastStateName$ = stateMachine.lastStateName$
		lastEventType$ = stateMachine.lastEventType$
		lastEventData$ = stateMachine.lastEventData$
	else
		lastStateName$ = ""
		lastEventType$ = ""
		lastEventData$ = ""
	endif

    m.logFile.SendLine("L=s"+t$+"S="+stateName$+t$+"T="+timestamp$+t$+"Y="+stateType$+t$+"LS="+lastStateName$+t$+"LE="+lastEventType$+t$+"LD="+lastEventData$)
    m.logFile.AsyncFlush()

End Sub


Sub WriteEventLogEntry(stateMachine As Object, stateName$ As String, eventType$ As String, eventData$ As String, eventActedOn$ As String)

    if not (m.eventLoggingEnabled or m.stateLoggingEnabled) then return

    if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return

    timestamp$ = m.systemTime.GetLocalDateTime().GetString()

	if eventActedOn$ = "1" then
		stateMachine.lastStateName$ = stateName$
		stateMachine.lastEventType$ = eventType$
		stateMachine.lastEventData$ = eventData$
	endif

	if m.eventLoggingEnabled then
		t$ = chr(9) 
		m.logFile.SendLine("L=e"+t$+"S="+stateName$+t$+"T="+timestamp$+t$+"E="+eventType$+t$+"D="+eventData$+t$+"A="+eventActedOn$)
		m.logFile.AsyncFlush()
	endif

End Sub


Sub WriteDiagnosticLogEntry(eventId$ As String, eventData$ As String)

    if not m.diagnosticLoggingEnabled then return

    if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return

    timestamp$ = m.systemTime.GetLocalDateTime().GetString()
    
    t$ = chr(9) 
    m.logFile.SendLine("L=d"+t$+"T="+timestamp$+t$+"I="+eventId$+t$+"D="+eventData$)
    m.logFile.AsyncFlush()
    
End Sub


Sub PushLogFile(forceUpload As Boolean)

	if type(m.networking) <> "roAssociativeArray" then return

    if not m.uploadLogsEnabled and not forceUpload then return
    
' files that failed to upload in the past were moved to a different folder. move them back to the appropriate folder so that the script can attempt to upload them again
    listOfFailedLogFiles = MatchFiles("/" + m.uploadLogFailedFolder, "*.log")
    for each file in listOfFailedLogFiles
        target$ = m.uploadLogFolder + "/" + file
        fullFilePath$ = m.uploadLogFailedFolder + "/" + file
        ok = MoveFile(fullFilePath$, target$)
    next

    m.networking.UploadLogFiles()
    
End Sub


Sub PushLogFilesOnBoot()

    m.MoveCurrentLog()
    m.PushLogFile(false)

End Sub


Sub HandleLoggingTimerEvent()

    m.CutoverLogFile(false)

    m.cutoverTimer.Start()

End Sub


Sub CutoverLogFile(forceUpload As Boolean)

    if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return

    m.logFile.Flush()
    m.MoveCurrentLog()
    m.logFile = m.CreateLogFile()

	if forceUpload or m.uploadLogFilesAtSpecificTime then
		m.PushLogFile(forceUpload)
	endif
    
    m.DeleteExpiredFiles()

End Sub


Sub OpenOrCreateCurrentLog()

' if there is an existing log file for today, just append to it. otherwise, create a new one to use

    listOfPendingLogFiles = MatchFiles("/currentLog", "*")
    
    for each file in listOfPendingLogFiles
        fileName$ = "currentLog/" + file
        m.logFile = CreateObject("roAppendFile", fileName$)
        if type(m.logFile) = "roAppendFile" then
            m.diagnostics.PrintDebug("Use existing log file " + file)
            return
        endif
    next

    m.logFile = m.CreateLogFile()
    
End Sub

'endregion

'region Hierarchical State Machine
' *************************************************
'
' Hierarchical State Machine Implementation
'
' *************************************************
Function newHSM() As Object

    HSM = CreateObject("roAssociativeArray")
    
    HSM.Initialize = HSMInitialize
	HSM.Constructor = HSMConstructor
    HSM.Dispatch = HSMDispatch
    HSM.IsIn = HSMIsIn
    
    HSM.InitialPseudostateHandler = invalid
	HSM.ConstructorHandler = invalid

    HSM.newHState = newHState
    HSM.topState = invalid
    HSM.activeState = invalid
    
    return HSM
    
End Function


Sub HSMConstructor()

	if type(m.ConstructorHandler) = invalid then stop

	m.ConstructorHandler()

End Sub


Sub HSMInitialize()

' there is definitely some confusion here about the usage of both activeState and m.activeState

    stateData = CreateObject("roAssociativeArray")
    
' empty event used to get super states
    emptyEvent = CreateObject("roAssociativeArray")
    emptyEvent["EventType"] = "EMPTY_SIGNAL"
    
' entry event 
    entryEvent = CreateObject("roAssociativeArray")
    entryEvent["EventType"] = "ENTRY_SIGNAL"

' init event
    initEvent = CreateObject("roAssociativeArray")
    initEvent["EventType"] = "INIT_SIGNAL"  

' execute initial transition     
	m.activeState = m.InitialPseudoStateHandler()
	
' if there is no activeState, the playlist is empty
	if type(m.activeState) <> "roAssociativeArray" return
	
	activeState = m.activeState
	    
' start at the top state
    if type(m.topState) <> "roAssociativeArray" then stop
    sourceState = m.topState
    
    while true
    
        entryStates = CreateObject("roArray", 4, true)
        entryStateIndex% = 0
        
        entryStates[0] = activeState                                            ' target of the initial transition
                
        status$ = m.activeState.HStateEventHandler(emptyEvent, stateData)       ' send an empty event to get the super state
        activeState = stateData.nextState
        m.activeState = stateData.nextState

        while (activeState.id$ <> sourceState.id$)                              ' walk up the tree until the current source state is hit
            entryStateIndex% = entryStateIndex% + 1
            entryStates[entryStateIndex%] = activeState
            status$ = m.activeState.HStateEventHandler(emptyEvent, stateData)
            activeState = stateData.nextState
            m.activeState = stateData.nextState
        end while
        
'        activeState = entryStates[0]                                           ' restore the target of the initial transition
        
        while (entryStateIndex% >= 0)                                           ' retrace the entry path in reverse (desired) order
            entryState = entryStates[entryStateIndex%]
            status$ = entryState.HStateEventHandler(entryEvent, stateData)
            entryStateIndex% = entryStateIndex% - 1
        end while

        sourceState = entryStates[0]                                            ' new source state is the current state
        
        status$ = sourceState.HStateEventHandler(initEvent, stateData)
        if status$ <> "TRANSITION" then
            m.activeState = sourceState
            return
        endif

        activeState = stateData.nextState        
        m.activeState = stateData.nextState
        
    end while

End Sub


Sub HSMDispatch(event As Object)

' if there is no activeState, the playlist is empty
	if type(m.activeState) <> "roAssociativeArray" return

    stateData = CreateObject("roAssociativeArray")
    
' empty event used to get super states
    emptyEvent = CreateObject("roAssociativeArray")
    emptyEvent["EventType"] = "EMPTY_SIGNAL"
    
' entry event 
    entryEvent = CreateObject("roAssociativeArray")
    entryEvent["EventType"] = "ENTRY_SIGNAL"

' exit event 
    exitEvent = CreateObject("roAssociativeArray")
    exitEvent["EventType"] = "EXIT_SIGNAL"

' init event
    initEvent = CreateObject("roAssociativeArray")
    initEvent["EventType"] = "INIT_SIGNAL"  
     
    t = m.activeState                                                       ' save the current state
    
    status$ = "SUPER"
    while (status$ = "SUPER")                                               ' process the event hierarchically
        s = m.activeState
        status$ = s.HStateEventHandler(event, stateData)
        m.activeState = stateData.nextState
'if type(m.activeState) = "roAssociativeArray" then
'    print "m.activeState set to " + m.activeState.id$ + "0"        
'else
'    print "m.activeState set to invalid 0"
'endif
    end while
    
    if (status$ = "TRANSITION")
        path = CreateObject("roArray", 4, true)
        
        path[0] = m.activeState                                             ' save the target of the transition
        path[1] = t                                                         ' save the current state
        
        while (t.id$ <> s.id$)                                              ' exit from the current state to the transition s
            status$ = t.HStateEventHandler(exitEvent, stateData)
            if status$ = "HANDLED" then
                status$ = t.HStateEventHandler(emptyEvent, stateData)
            endif
            t = stateData.nextState
        end while
        
        t = path[0]                                                         ' target of the transition
        
        ' s is the source of the transition
        
        if (s.id$ = t.id$) then                                             ' check source == target (transition to self)
            status$ = s.HStateEventHandler(exitEvent, stateData)            ' exit the source
            ip = 0
        else
            status$ = t.HStateEventHandler(emptyEvent, stateData)           ' superstate of target
            t = stateData.nextState
            if (s.id$ = t.id$) then                                         ' check source == target->super
                ip = 0                                                      ' enter the target
            else
                status$ = s.HStateEventHandler(emptyEvent, stateData)       ' superstate of source
                if (stateData.nextState.id$ = t.id$) then                   ' check source->super == target->super
                    status$ = s.HStateEventHandler(exitEvent, stateData)    ' exit the source
                    ip = 0                                                  ' enter the target
                else
                    if (stateData.nextState.id$ = path[0].id$) then         ' check source->super == target
                        status$ = s.HStateEventHandler(exitEvent, stateData)     ' exit the source
                    else                                                    ' check rest of source == target->super->super and store the entry path along the way
                        iq = 0                                              ' indicate LCA not found
                        ip = 1                                              ' enter target and its superstate
                        path[1] = t                                         ' save the superstate of the target
                        t = stateData.nextState                             ' save source->super
                                                                            ' get target->super->super
                        status$ = path[1].HStateEventHandler(emptyEvent, stateData)
                        while (status$ = "SUPER")
                             ip = ip + 1
                             path[ip] = stateData.nextState                 ' store the entry path
                             if (stateData.nextState.id$ = s.id$) then      ' is it the source?
                                iq = 1                                      ' indicate that LCA found
                                ip = ip - 1                                 ' do not enter the source
                                status$ = "HANDLED"                         ' terminate the loop
                             else                                           ' it is not the source; keep going up
                                status$ = stateData.nextState.HStateEventHandler(emptyEvent, stateData)
                             endif
                        end while
                    
                        if (iq = 0) then                                    ' LCA not found yet
                            status$ = s.HStateEventHandler(exitEvent, stateData) ' exit the source
                            
                                                                            ' check the rest of source->super == target->super->super...
                            iq = ip
                            status = "IGNORED"                              ' indicate LCA not found
                            while (iq >= 0)
                                if (t.id$ = path[iq].id$) then              ' is this the LCA?
                                    status = "HANDLED"                      ' indicate LCA found
                                    ip = iq - 1                             ' do not enter LCA
                                    iq = -1                                 ' terminate the loop
                                else
                                    iq = iq -1                              ' try lower superstate of target
                                endif
                            end while
                            
                            if (status <> "HANDLED") then                   ' LCA not found yet?
                            
                                                                            ' check each source->super->... for each target->super...
                                status = "IGNORED"                          ' keep looping
                                while (status <> "HANDLED")
                                    status$ = t.HStateEventHandler(exitEvent, stateData)
                                    if (status$ = "HANDLED") then
                                        status$ = t.HStateEventHandler(emptyEvent, stateData)
                                    endif
                                    t = stateData.nextState                 ' set to super of t
                                    iq = ip
                                    while (iq > 0)
                                        if (t.id$ = path[iq].id$) then      ' is this the LCA?
                                            ip = iq - 1                     ' do not enter LCA
                                            iq = -1                         ' break inner
                                            status = "HANDLED"              ' break outer
                                        else
                                            iq = iq - 1
                                        endif
                                    end while
                                end while
                            endif
                        endif
                    endif
                endif
            endif
        endif
        
        ' retrace the entry path in reverse (desired) order...
        while (ip >= 0)
            status$ = path[ip].HStateEventHandler(entryEvent, stateData)    ' enter path[ip]
            ip = ip - 1
        end while
        
        t = path[0]                                                         ' stick the target into register */
        m.activeState = t                                                   ' update the current state */
'print "m.activeState set to " + m.activeState.id$ + "1"        


        ' drill into the target hierarchy...
        status$ = t.HStateEventHandler(initEvent, stateData)
        m.activeState = stateData.nextState
        while (status$ = "TRANSITION")
' stop            
            ip = 0
            path[0] = m.activeState
            status$ = m.activeState.HStateEventHandler(emptyEvent, stateData)            ' find superstate
            m.activeState = stateData.nextState
'print "m.activeState set to " + m.activeState.id$ + "2"        
            while (m.activeState.id$ <> t.id$)
                ip = ip + 1
                path[ip] = m.activeState
                status$ = m.activeState.HStateEventHandler(emptyEvent, stateData)        ' find superstate
                m.activeState = stateData.nextState
'print "m.activeState set to " + m.activeState.id$ + "3"        
            end while
            m.activeState = path[0]
'print "m.activeState set to " + m.activeState.id$ + "4"        
            
            while (ip >= 0)
                status$ = path[ip].HStateEventHandler(entryEvent, stateData)
                ip = ip - 1
            end while
            
            t = path[0]
            
            status$ = t.HStateEventHandler(initEvent, stateData)

        end while
        
    endif
    
    m.activeState = t   ' set the new state or restore the current state
'print "m.activeState set to " + m.activeState.id$ + "5"        
    
End Sub


Function HSMIsIn() As Boolean

    return false

End Function


Function newHState(bsp As Object, id$ As String) As Object

    HState = CreateObject("roAssociativeArray")
    
    HState.HStateEventHandler = invalid         ' filled in by HState instance
    
    HState.stateMachine = m
    HState.bsp = bsp
    
    HState.superState = invalid                 ' filled in by HState instance
    HState.id$ = id$
    
    return HState
    
End Function

'endregion

'region Diagnostics
REM *******************************************************
REM *******************************************************
REM ***************                    ********************
REM *************** DIAGNOSTICS OBJECT ********************
REM ***************                    ********************
REM *******************************************************
REM *******************************************************
REM
REM construct a new diagnostics BrightScript object
REM
Function newDiagnostics(sysFlags As Object) As Object

    diagnostics = CreateObject("roAssociativeArray")
    
    diagnostics.debug = sysFlags.debugOn
    diagnostics.autorunVersion$ = "unknown"
    diagnostics.customAutorunVersion$ = "unknown"
    diagnostics.firmwareVersion$ = "unknown"
    diagnostics.systemTime = CreateObject("roSystemTime")
    
    diagnostics.systemLogDebug = sysFlags.systemLogDebugOn
    if diagnostics.systemLogDebug then
		diagnostics.systemLog = CreateObject("roSystemLog")
	endif
	
    diagnostics.UpdateDebugOn = UpdateDebugOn
    diagnostics.UpdateSystemLogDebugOn = UpdateSystemLogDebugOn
    diagnostics.PrintDebug = PrintDebug
    diagnostics.PrintTimestamp = PrintTimestamp
    diagnostics.SetSystemInfo = SetSystemInfo

    return diagnostics

End Function


Sub UpdateDebugOn(debugOn As Boolean)

    m.debug = debugOn

End Sub


Sub UpdateSystemLogDebugOn(systemLogDebug As Boolean)

    m.systemLogDebug = systemLogDebug
    
    if systemLogDebug and type(m.systemLog) <> "roSystemLog" then
		m.systemLog = CreateObject("roSystemLog")
    endif

End Sub


Sub PrintDebug(debugStr$ As String)

    if type(m) <> "roAssociativeArray" then stop
    
    if m.debug then 

        print debugStr$
        
    endif

    if m.systemLogDebug then
		m.systemLog.SendLine(debugStr$)
	endif
	
    return

End Sub


Sub PrintTimestamp()

    eventDateTime = m.systemTime.GetLocalDateTime()
    if m.debug then print eventDateTime.GetString()

    if m.systemLogDebug then
		m.systemLog.SendLine(eventDateTime.GetString())
	endif

    return

End Sub


Sub SetSystemInfo(sysInfo As Object, diagnosticCodes As Object)

    m.autorunVersion$ = sysInfo.autorunVersion$
    m.customAutorunVersion$ = sysInfo.customAutorunVersion$
    m.firmwareVersion$ = sysInfo.deviceFWVersion$
    m.deviceUniqueID$ = sysInfo.deviceUniqueID$
    m.deviceModel$ = sysInfo.deviceModel$
    m.deviceFamily$ = sysInfo.deviceFamily$
    m.modelSupportsWifi = sysInfo.modelSupportsWifi
    
    m.enableLogDeletion = sysInfo.enableLogDeletion
    
    m.diagnosticCodes = diagnosticCodes
    
    return

End Sub


Function GetColor(colorAttrs As Object) As Integer

    alpha$ = colorAttrs["a"]
    alpha% = val(alpha$)
    red$ = colorAttrs["r"]
    red% = val(red$)
    green$ = colorAttrs["g"]
    green% = val(green$)
    blue$ = colorAttrs["b"]
    blue% = val(blue$)
    
    color_spec% = (alpha%*256*256*256) + (red%*256*256) + (green%*256) + blue%
    return color_spec%

End Function


Function GetHexColor(colorAttrs As Object) As String

	ba = CreateObject("roByteArray")

	ba[0] = val(colorAttrs["a"])
	alpha$ = ba.ToHexString()

    ba[0] = val(colorAttrs["r"])
	red$ = ba.ToHexString()
	    
    ba[0] = val(colorAttrs["g"])
	green$ = ba.ToHexString()
	    
    ba[0] = val(colorAttrs["b"])
	blue$ = ba.ToHexString()
	    
	return alpha$ + red$ + green$ + blue$

End Function


Function ByteArraysMatch(baInput As Object, baSpec As Object) As Boolean

	if baSpec.Count() > baInput.Count() return false
	
	numBytesToMatch% = baSpec.Count()
	numBytesInInput% = baInput.Count()
	startByteInInput% = numBytesInInput% - numBytesToMatch%
	
	for i% = 0 to baSpec.Count() - 1
		if baInput[startByteInInput% + i%] <> baSpec[i%] return false
	next
		
	return true
	
End Function


Function StripLeadingSpaces(inputString$ As String) As String

    while true
        if left(inputString$, 1)<>" " then return inputString$
        inputString$ = right(inputString$, len(inputString$)-1)
    endwhile

    return inputString$

End Function


Function CopyDateTime(dateTimeIn As Object) As Object

    dateTimeOut = CreateObject("roDateTime")
    dateTimeOut.SetYear(dateTimeIn.GetYear())
    dateTimeOut.SetMonth(dateTimeIn.GetMonth())
    dateTimeOut.SetDay(dateTimeIn.GetDay())
    dateTimeOut.SetHour(dateTimeIn.GetHour())
    dateTimeOut.SetMinute(dateTimeIn.GetMinute())
    dateTimeOut.SetSecond(dateTimeIn.GetSecond())
    dateTimeOut.SetMillisecond(dateTimeIn.GetMillisecond())
    
    return dateTimeOut
    
End Function


REM *******************************************************
REM *******************************************************
REM ***************                    ********************
REM *************** DIAGNOSTIC CODES   ********************
REM ***************                    ********************
REM *******************************************************
REM *******************************************************

Function newDiagnosticCodes() As Object

    diagnosticCodes = CreateObject("roAssociativeArray")
    
    diagnosticCodes.EVENT_STARTUP                               = "1000"
    diagnosticCodes.EVENT_SYNCSPEC_RECEIVED                     = "1001"
    diagnosticCodes.EVENT_DOWNLOAD_START                        = "1002"
    diagnosticCodes.EVENT_FILE_DOWNLOAD_START                   = "1003"
    diagnosticCodes.EVENT_FILE_DOWNLOAD_COMPLETE                = "1004"
    diagnosticCodes.EVENT_DOWNLOAD_COMPLETE                     = "1005"
    diagnosticCodes.EVENT_READ_SYNCSPEC_FAILURE                 = "1006"
    diagnosticCodes.EVENT_RETRIEVE_SYNCSPEC_FAILURE             = "1007"
    diagnosticCodes.EVENT_NO_SYNCSPEC_AVAILABLE                 = "1008"
    diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_IMMEDIATE_FAILURE   = "1009"
    diagnosticCodes.EVENT_FILE_DOWNLOAD_FAILURE                 = "1010"
    diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_FAILURE             = "1011"
    diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE              = "1012"
    diagnosticCodes.EVENT_LOGFILE_UPLOAD_FAILURE                = "1013"
    diagnosticCodes.EVENT_SYNC_ALREADY_ACTIVE                   = "1014"
    diagnosticCodes.EVENT_CHECK_CONTENT                         = "1015"
    diagnosticCodes.EVENT_FILE_DOWNLOAD_PROGRESS                = "1016"
    diagnosticCodes.EVENT_FIRMWARE_DOWNLOAD                     = "1017"
    diagnosticCodes.EVENT_SCRIPT_DOWNLOAD                       = "1018"
	diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND				= "1021"
	diagnosticCodes.EVENT_MEDIA_COUNTER_VARIABLE_NOT_FOUND		= "1022"
	diagnosticCodes.EVENT_START_PRESENTATION					= "1023"
	diagnosticCodes.EVENT_GPS_LOCATION							= "1024"
	diagnosticCodes.EVENT_GPS_NOT_LOCKED						= "1025"
    diagnosticCodes.EVENT_RETRIEVE_USER_VARIABLE_FEED           = "1026"
    diagnosticCodes.EVENT_RETRIEVE_LIVE_TEXT_FEED				= "1027"
    diagnosticCodes.EVENT_USER_VARIABLE_FEED_DOWNLOAD_FAILURE   = "1028"
    diagnosticCodes.EVENT_LIVE_TEXT_FEED_DOWNLOAD_FAILURE		= "1029"
	diagnosticCodes.EVENT_UNASSIGNED_LOCAL_PLAYLIST				= "1030"
	diagnosticCodes.EVENT_UNASSIGNED_LOCAL_PLAYLIST_NO_NAVIGATION = "1031"
	diagnosticCodes.EVENT_REALIZE_FAILURE						= "1032"
    diagnosticCodes.EVENT_LIVE_TEXT_PLUGIN_FAILURE				= "1033"
    diagnosticCodes.EVENT_INVALID_DATE_TIME_SPEC				= "1034"
    diagnosticCodes.EVENT_HTML5_LOAD_ERROR						= "1035"
    diagnosticCodes.EVENT_USB_UPDATE_SECURITY_ERROR				= "1036"
    diagnosticCodes.EVENT_TUNE_FAILURE							= "1037"
    diagnosticCodes.EVENT_SCAN_START							= "1038"
    diagnosticCodes.EVENT_CHANNEL_FOUND							= "1039"
    diagnosticCodes.EVENT_SCAN_COMPLETE							= "1040"
    diagnosticCodes.EVENT_SCRIPT_PLUGIN_FAILURE					= "1041"
    diagnosticCodes.EVENT_DISK_ERROR							= "1042"
    diagnosticCodes.EVENT_LIVE_MRSS_PLUGIN_FAILURE				= "1043"
	diagnosticCodes.EVENT_EMPTY_MEDIA_PLAYLIST					= "1044"
    diagnosticCodes.EVENT_CUSTOM_USER_AGENT_FAILURE				= "1045"
    diagnosticCodes.EVENT_BLC400_STATUS							= "1100"
    diagnosticCodes.EVENT_CONTINUE_LIVE_DATA_FEED_CONTENT_DOWNLOAD		= "1200"
	diagnosticCodes.EVENT_RESTART_LIVE_DATA_FEED_CONTENT_DOWNLOAD		= "1201"
	diagnosticCodes.EVENT_START_LIVE_DATA_FEED_CONTENT_DOWNLOAD			= "1202"
    diagnosticCodes.EVENT_ASSETPOOL_UNPROTECT_FAILURE					= "1203"
    diagnosticCodes.EVENT_PLAYBACK_FAILURE								= "1204"
    diagnosticCodes.EVENT_START_MRSS_FEED_CONTENT_DOWNLOAD				= "1205"
    diagnosticCodes.EVENT_UNABLE_TO_CREATE_ASSET_POOL					= "1206"
    diagnosticCodes.EVENT_DELETE_USER_VARIABLES_DB				= "1207"
    diagnosticCodes.EVENT_SCREENSHOT_ERROR						= "1208"
    diagnosticCodes.EVENT_SCREENSHOT_UPLOAD_ERROR				= "1209"
	diagnosticCodes.EVENT_SCREENSHOT_UPLOADED_AND_QUEUED		= "1210"
	diagnosticCodes.EVENT_SCREENSHOT_QUEUE_ERROR				= "1211"
	diagnosticCodes.EVENT_SET_BSN_OVERRIDE						= "1212"
	diagnosticCodes.EVENT_CANCEL_BSN_OVERRIDE					= "1213"
	diagnosticCodes.EVENT_BSN_OVERRIDE_EXPIRED					= "1214"
	diagnosticCodes.EVENT_SET_SNAPSHOT_CONFIGURATION			= "1215"
	diagnosticCodes.EVENT_STREAM_END							= "1216"
	diagnosticCodes.EVENT_SET_VIDEO_MODE						= "1217"
	diagnosticCodes.EVENT_SNAPSHOT_PUT_TO_SERVER_ERROR			= "1218"
    diagnosticCodes.EVENT_CHECK_LIVE_TEXT_FEED_HEAD				= "1219"
	diagnosticCodes.EVENT_SCREENSHOT_UPLOADED					= "1220"
    diagnosticCodes.EVENT_BEACON_START							= "1300"
    diagnosticCodes.EVENT_BEACON_START_FAILED					= "1301"
    diagnosticCodes.EVENT_BEACON_START_LIMIT_EXCEEDED			= "1302"
    diagnosticCodes.EVENT_BTLE_START_FAILED						= "1303"
    diagnosticCodes.EVENT_CONTROL_PORT_DISCONNECTED						= "1304"

    return diagnosticCodes
    
End Function

'endregion

'region GPS Functions
REM ==================================================
REM           GPS Functions
REM ==================================================
' Parse the NMEA GPRMC format and return the data in an object - http://www.gpsinformation.org/dale/nmea.htm
' The returned object contains the following fields
' .valid - boolean - is the sentence is correctly formed, has the correct checksum and has the correct GPRMC header
' .fixTime - contains the string from the sentence that is the time the sample was taken - no processing is done on this
' .fixActive - boolean - does the latitude and longitude contain real data
' .latitude - float - signed degrees of the latitude
' .longitude - float - signed degrees of the longitude
Sub ParseGPSdataGPRMCformat(NMEAsentence as string) as object
	gpsData = {}

	starLoc = instr(1, NMEAsentence, "*")
	if starLoc = 0 then
		gpsData.valid = false
	else if starLoc = len(NMEAsentence) - 2 then
		CalcChecksum = CalcChecksum (mid(NMEAsentence, 2, len(NMEAsentence)-4))
		ba=CreateObject("roByteArray")
		ba.fromhexstring(mid(NMEAsentence, len(NMEAsentence)-1, 2))
		CalcChecksum = ba[0]
		if (CalcChecksum <> CalcChecksum) then
			gpsData.valid = false
		else
			' Strip off the beginning $ sign and the * + checksum
			strippedSentence = mid(NMEAsentence, 2, len(NMEAsentence)-4)

			' Get the identifier
			field = getNextGPSfield(strippedSentence, 1)
			gpsData.type = field.fieldString
			
			' Make sure this is the right data format
			if (gpsData.type <> "GPRMC") then
				gpsData.valid = false
			else
				gpsData.valid = true

				' Get the fix time 
				field = getNextGPSfield(strippedSentence, field.nextFieldStart)
				gpsData.fixTime = field.fieldString

				' Get the status of the fix: A=Active, V=Void time, convert to fixActive = true for A, false for V
				field = getNextGPSfield(strippedSentence, field.nextFieldStart)
				if (field.fieldString <> "A") then
					gpsData.fixActive = false
					gpsData.latitude = 0
					gpsData.longitude = 0
				else
					gpsData.fixActive = true

					' Get the Latitude
					field = getNextGPSfield(strippedSentence, field.nextFieldStart)
					latDegrees = val(left(field.fieldString,2))
					latMinutes = val(mid(field.fieldString,3))
					latDegrees = latDegrees + (latMinutes/60)

					' Get the Latitude Direction
					field = getNextGPSfield(strippedSentence, field.nextFieldStart)
				
					' Adjust the sign of the angle based on the direction
					gpsData.latitude = ConvertNSEWtoQuadrant(field.fieldString, latDegrees)

					' Get the Longitude
					field = getNextGPSfield(strippedSentence, field.nextFieldStart)
					longDegrees = val(left(field.fieldString,3))
					longMinutes = val(mid(field.fieldString,4))
					longDegrees = longDegrees + (longMinutes/60)

					' Get the Longitude Direction
					field = getNextGPSfield(strippedSentence, field.nextFieldStart)
				
					' Adjust the sign of the angle based on the direction
					gpsData.longitude = ConvertNSEWtoQuadrant(field.fieldString, longDegrees)
				end if
			end if
		end if
	end if

	return gpsData
end Sub

' Parse and return the next NMEA field from the sentence
' returns an object with two members:
' .fieldString - contains the contents of the field, if nothing is in the field - returns ""
' .nextFieldStart - indicates the location in the string where the next field should start
Sub getNextGPSfield (NMEAsentence as string, startingIndex as integer) as object
	gpsField = {}
	' Look for the next field seperator as a comma (this is the case except for the checksum which is a *)
	fieldEndLoc = instr(startingIndex, NMEAsentence, ",")
	if fieldEndLoc <> 0 then
		if fieldEndLoc > startingIndex then
			gpsField.fieldString = mid(NMEAsentence, startingIndex, fieldEndLoc-startingIndex)
		else
			gpsField.fieldString = ""
		end if
		gpsField.nextFieldStart = fieldEndLoc + 1
	else
		stringLen = len(NMEAsentence)
		if (stringLen >= startingIndex) then
			gpsField.fieldString = mid(NMEAsentence, startingIndex, stringLen-startingIndex+1)
		else
			gpsField.fieldString = ""
		end if
		gpsField.nextFieldStart = stringLen + 1
	end if

	return (gpsField)
end Sub
		

' Calculate the great circle distance of two gps points - points must be in radians
Sub CalcGPSDistance(lat1 as float, lon1 as float, lat2 as float, lon2 as float)  as float

	radiusOfEarthInFeet# = 3963.1 * 5280.0
		
	' Convert coodinate 1 to Cartesian coordinates
	x1# = radiusOfEarthInFeet# * cos(lon1) * sin(lat1)
	y1# = radiusOfEarthInFeet# * sin(lon1) * sin(lat1)
	z1# = radiusOfEarthInFeet#  * sin(lat1)

	' Convert coodinate 2 to Cartesian coordinates
	x2# = radiusOfEarthInFeet# * cos(lon2) * sin(lat2)
	y2# = radiusOfEarthInFeet# * sin(lon2) * sin(lat2)
	z2# = radiusOfEarthInFeet#  * sin(lat2)

	' Calc the distance based on Euclidean distance
	distance = sqr((x1#-x2#)*(x1#-x2#) + (y1#-y2#)*(y1#-y2#) + (z1#-z2#)*(z1#-z2#))

	return (distance)
end sub


' Calculate the checksum based on the NMEA stardard - http://www.gpsinformation.org/dale/nmea.htm
' the checksum is an XOR of all characters between the $ and * in the sentence
Sub CalcChecksum (theString as string) as integer
	checksum = 0

	theStringLen = len (theString)
	if (theStringLen >= 2) then 
		a = asc(mid(theString,1,1))
		b = asc(mid(thestring,2,1))
		' XOR the two first two characters in the string
		checksum = &HFF AND ((a OR b) AND (NOT(a AND b)))
	else if (theStringLen = 1) then
		' If only one character is in the string, it is the checksum
		checksum = asc(mid(theString,1,1))
	end if
	if (theStringLen >= 3) then 
		for i= 3 to theStringLen
			a = checksum
			b = asc(mid(thestring,i,1))
			' XOR the current checksum with the next character
			checksum =  &HFF AND ((a OR b) AND (NOT(a AND b)))
		next
	end if

	return (checksum)
end sub


Sub ConvertDecimalDegtoRad(deg as float) as float
    pi = 3.14159265358979
	radians = deg * (pi/180)

	return (radians)
end Sub

Sub ConvertNSEWtoQuadrant(direction as string, angle as float) as float
	if (direction = "W") or (direction = "w") or (direction = "S") or (direction = "s") then
		angle = angle * -1
	end if

	return (angle)
end sub

'endregion

'region SIGNCHANNEL / MEDIARSS helpers
REM *******************************************************
REM *******************************************************
REM ***************                         ***************
REM *************** SIGNCHANNEL / MEDIARSS  ***************
REM ***************                         ***************
REM *******************************************************
REM *******************************************************

Function isImage( item As Object ) As Boolean
	REM Default is the item is an image
	rv = TRUE

	if item.type = "video/mpeg" OR item.type = "video/mp4" OR item.type = "video/quicktime" OR item.type = "video/x-matroska" OR item.medium = "video" or item.type = "audio/mpeg" or item.medium = "audio" or item.type = "text/html" or item.type = "application/widget" or item.medium = "document" then
		rv = false
	endif

   return rv
End Function


Function isAudio( item As Object ) As Boolean
	REM Default is the item is an image
	rv = false

	if item.type = "audio/mpeg" or item.medium = "audio" then
		rv = true
	endif

   return rv
End Function

Function isHtml( item As Object ) As Boolean
	REM Default is the item is an image
	rv = false
	
	if item.type = "text/html" or item.type = "application/widget" or item.medium = "document" then
		rv = true
	endif

   return rv
End Function

REM ================================================================
REM          helper_GetDuration
REM ================================================================
REM
REM Get duration attribute of a media:content sub element 
REM of RSS Item element.  Duration is number of seconds for
REM image to be displayed on screen.
REM
REM If no duration found sets default of 15.
REM If duration < 5 seconds sets minimum of 5
REM

Function helper_GetDuration( contentElement As Object ) As Integer

   duration = contentElement.GetAttributes()["duration"]

   if duration = Invalid then
       return 15
   end if

   duration = Val(duration)

'   if duration < 5 then
'      duration = 5
'   end if

   return duration

End Function


Function helper_GetFileSize( contentElement As Object ) As Integer

   size = contentElement.GetAttributes()["fileSize"]

   if size = Invalid then
       return 0
   end if

   fileSize = Val(size)

   return fileSize

End Function


Function helper_GetProbeData( contentElement As Object ) As String

   probe = contentElement.GetAttributes()["probe"]

   if probe = Invalid then
       return ""
   end if

   return probe

End Function

'endregion


'region TripleUSB
' TripleUSB state handler function
'
' information that needs to be available to this function includes
'		serial port object for Triple USB
'		serial port object for selected Bose product
'		product name for selected Bose product
'		noise threshold
'		Quiet transition
'		Loud transition
'
' Entry
'		Make GetAmbientNoise call
' Serial Line Event
'		Serial line event - Noise response
'		Parse noise value
'		Compare to noise threshold
'		SetVolume on Bose product
'		If noise value <= noise threshold, execute Quiet transition
'		Else if noise value > noise threshold, execute Loud transition
'
Function STTripleUSBEventHandler(event As Object, stateData As Object) As Object

    stateData.nextState = invalid
    
    if type(event) = "roAssociativeArray" then      ' internal message event

        if IsString(event["EventType"]) then
        
            if event["EventType"] = "ENTRY_SIGNAL" then
            
                m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")

				m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)

				m.tripleUSBSerialPort = m.bsp.serial[m.tripleUSBPort$]
				m.boseProductPort = m.bsp.serial[m.boseProductPort$]
				
				m.tripleUSBSerialPort.SendLine("v?")

				' state logging
				m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "tripleUSB")

				return "HANDLED"
				
            else if event["EventType"] = "EXIT_SIGNAL" then

                m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
            
			endif
			
		endif
		
    else if type(event) = "roStreamLineEvent" then
    
	    m.bsp.diagnostics.PrintDebug("Noise response serial event " + event.GetString())
        
        noiseResponse$ = event.GetString()
        
		m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", noiseResponse$, "1")

        if len(noiseResponse$) = 5 then
            'Get noise value
			noiseValue% = int(val(mid(noiseResponse$, 3)))
        
            'send out volume command
            if m.SendVolumeCommand(noiseValue%) = "HANDLED" then
                return "HANDLED"
            endif
                       
			if noiseValue% <= m.noiseThreshold% then
				return m.ExecuteTransition(m.quietUserEvent, stateData, "")
			else
				return m.ExecuteTransition(m.loudUserEvent, stateData, "")
			endif
			
        else
        
			m.tripleUSBSerialPort.SendLine("v?")
        
        endif

		return "HANDLED"
    
	endif

    stateData.nextState = m.superState
    return "SUPER"

End Function


Function SendVolumeCommand(noiseValue% As Integer) As Object

	if m.boseProductName$ = "" then
		return ""
	endif
	
    'lookup noise value in our volume table for the product and send vol command
    m.bsp.diagnostics.PrintDebug("SendVolumeCommand: enter") 
    m.bsp.diagnostics.PrintDebug("noiseval " + str(noiseValue%) ) 
                               
    'perform linear interpolation
    volume% = 0

    'protect against no volume table in the BoseProducts xml file
    if type(m.volumeTable) <> "roAssociativeArray" then
        m.bsp.diagnostics.PrintDebug("No Bose Product Volume Table found!" ) 
        return "HANDLED"
    endif

    if noiseValue% >= m.volumeTable.xval3% then
        volume% = m.volumeTable.yval3%
    else if noiseValue% >= m.volumeTable.xval2% then
        if m.volumeTable.xval3% - m.volumeTable.xval2% = 0 then
            m.bsp.diagnostics.PrintDebug("Divide by Zero will occur, check volume table info" )
            return "HANDLED"
        endif                                   
        volume% = (noiseValue% - m.volumeTable.xval2%)*(m.volumeTable.yval3% - m.volumeTable.yval2%)/(m.volumeTable.xval3% - m.volumeTable.xval2%) + m.volumeTable.yval2% 
    else if noiseValue% >= m.volumeTable.xval1% then
        if m.volumeTable.xval2% - m.volumeTable.xval1% = 0 then
            m.bsp.diagnostics.PrintDebug("Divide by Zero will occur, check volume table info" )
            return "HANDLED"
        endif
        volume% = (noiseValue% - m.volumeTable.xval1%)*(m.volumeTable.yval2% - m.volumeTable.yval1%)/(m.volumeTable.xval2% - m.volumeTable.xval1%) + m.volumeTable.yval1% 
    else
        volume% = m.volumeTable.yval1%            
    endif
  
    m.bsp.diagnostics.PrintDebug("volume " + str(volume%) ) 

    ' send volume command to Bose product
    if m.boseProductName$ = "Chihuahua" then
        m.bsp.diagnostics.PrintDebug("Send Volume set command to Chihuahua")
        'ask Ted about this, its giving me a string of 3 digits back with first one as a space?  
        m.boseProductPort.SendLine("vo " + mid(str(volume%),2))
    else if m.boseProductName$ = "Hershey" then
        m.bsp.diagnostics.PrintDebug("Send Volume set command to Hershey")
        'have to subract our volume from 100 since its an attenuation
        volume% = 100 - volume% 
        m.boseProductPort.SendLine("VO CB " + mid(str(volume%),2))
    else if m.boseProductName$ = "Onyx" then
        m.bsp.diagnostics.PrintDebug("Send Volume set command to Onyx")
        'ask Ted about this, its giving me a string of 3 digits back with first one as a space?  
        m.boseProductPort.SendLine("vo " + mid(str(volume%),2))
    else if m.boseProductName$ = "Cinnamon" then
        m.bsp.diagnostics.PrintDebug("Send Volume set command to Cinnamon")
        m.boseProductPort.SendLine("vo " + mid(str(volume%),2))
    else if m.boseProductName$ = "Whippit" then
        m.bsp.diagnostics.PrintDebug("Send Volume set command to Whippit")
        m.boseProductPort.SendLine("vo " + mid(str(volume%),2))
    else if m.boseProductName$ = "Reframe" then
        m.bsp.diagnostics.PrintDebug("Send Volume set command to Reframe")
        m.boseProductPort.SendLine("VS" + mid(str(volume%),2))
    else if m.boseProductName$ = "Max" then
        m.bsp.diagnostics.PrintDebug("Send Volume set command to Max")
        m.boseProductPort.SendLine("SP 102," + mid(str(volume%),2))
    else
        m.bsp.diagnostics.PrintDebug("Unknown Bose Product: " + m.boseProductName$)
        return "HANDLED"
    endif

    return ""

End Function
'endregion
'region Miscellaneous functions

Function ReadSyncSpec() As Object

	currentSync = CreateObject("roSyncSpec")
	if type(currentSync) = "roSyncSpec" then
		if not currentSync.ReadFromFile("current-sync.xml") then
			if not currentSync.ReadFromFile("local-sync.xml") then
				if not currentSync.ReadFromFile("localToBSN-sync.xml") then
					if not currentSync.ReadFromFile("localSetupToStandalone-sync.xml") then
						currentSync = invalid
					endif
				endif
			endif
		endif
	endif

	return currentSync

End Function


Function FileExists(filePath$ As String) As Boolean

	file = CreateObject("roReadFile", filePath$)
	if not type(file) = "roReadFile"
		return false
	endif

	file = invalid
	return true

End Function

'endregion


'region MRSSDataFeed methods

' check for existence of the feed file associated with this feed
' if it exists, setup asset collection / assetPoolFiles objects for the feed (independent of whether or not the assets are actually on the card)
Sub ReadMRSSContent()

	feedFileName$ = "feed_cache/" + m.name$ + ".xml"

	m.isMRSSFeed = m.FeedIsMRSS( feedFileName$ )
	if not m.isMRSSFeed and m.parser$ = "" return

    m.bsp.diagnostics.PrintDebug("Read existing content for feed " + m.name$ + ".")

	file = CreateObject("roReadFile", feedFileName$)
	if type(file) <> "roReadFile" return
	file = invalid

	' parse the feed, building an asset collection and a list of file items
	m.assetCollection = CreateObject("roAssetCollection")

	m.ParseMRSSFeed(feedFileName$)

	for each item in m.feed.items
		' Do not download content of type 'text/html' - this is accessed directly
		if item.type = invalid or item.type <> "text/html" then
			asset = CreateObject("roAssociativeArray")
			asset.link = item.url
' SignChannel sizes appear to be inaccurate
'			if item.size > 0 then
'				asset.size = item.size
'		endif
			asset.name = item.url
		if IsNonEmptyString(item.guid) then
				asset.change_hint = item.guid
			else if IsString(item.url) then
				asset.change_hint = item.url
			endif
			m.assetCollection.AddAsset(asset)
		endif
	next

	if not m.bsp.feedPool.ProtectAssets("display-" + m.name$, m.assetCollection) then
		m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, m.bsp.feedPool.GetFailureReason())
		m.bsp.logging.FlushLogFile()
		m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + m.bsp.feedPool.GetFailureReason())
		stop
	endif

	m.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.feedPool, m.assetCollection)

End Sub


Function FeedIsMRSS( fileName$ As String )

	xml = ReadAsciiFile( fileName$ )
	if len(xml) = 0 return false

	feedXML = CreateObject("roXMLElement")
	if not feedXML.Parse(xml) return false

	if feedXML.HasAttribute("xmlns:media") then
		attrs = feedXML.GetAttributes()
		if attrs["xmlns:media"] = "http://search.yahoo.com/mrss/" return true
	endif

	return false

End Function


Sub DownloadMRSSContent()

	m.bsp.diagnostics.PrintDebug("DownloadMRSSContent")

	if type(m.assetFetcher) = "roAssetFetcher" then
		return
	endif

	fileNameOnCard$ = "feed_cache/" + m.name$ + ".xml"

	' write the mrss feed to the card
	CopyFile(m.rssFileName$, fileNameOnCard$)

    m.bsp.diagnostics.PrintDebug("Download new content for feed " + m.name$ + ".")
	m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_START_MRSS_FEED_CONTENT_DOWNLOAD, m.name$)

	' parse the feed, building an asset collection and a list of file items
	m.assetCollection = CreateObject("roAssetCollection")

	m.ParseMRSSFeed(m.rssFileName$)

	m.feedContentFilesToDownload = {}

	for each item in m.feed.items
		' Do not download content of type 'text/html' - this is accessed directly
		if item.type = invalid or item.type <> "text/html" then
			asset = CreateObject("roAssociativeArray")
			asset.link = item.url
			if item.size > 0 then
				asset.size = item.size
			endif
			asset.name = item.url

			if IsNonEmptyString(item.guid) then
				asset.change_hint = item.guid
			else if IsString(item.url) then
				asset.change_hint = item.url
			endif

			m.assetCollection.AddAsset(asset)
		endif

		' track feed content downloads
	    fileToDownload = {}
	    fileToDownload.name = item.title
	    fileToDownload.size = item.size
	    fileToDownload.hash = item.guid
        fileToDownload.currentFilePercentage$ = ""
        fileToDownload.status$ = ""

		if type(asset) = "roAssociativeArray" and type(asset.link) = "roString" then
			if type(m.assetPoolFiles) = "roAssetPoolFiles" then
				filePath = m.assetPoolFiles.GetPoolFilePath(asset.link)
				if filePath <> "" then
					fileToDownload.currentFilePercentage$ = "100"
					fileToDownload.status$ = "ok"
				endif
			endif
		endif

    if IsString(fileToDownload.hash) then
  		m.feedContentFilesToDownload.AddReplace(fileToDownload.hash, fileToDownload)
	  endif

	next

	if type(m.bsp.networkingHSM) = "roAssociativeArray" then
		m.bsp.networkingHSM.UploadDeviceDownloadProgressFileList()
		m.bsp.networkingHSM.FileListPendingUpload = false
	endif

	if not m.bsp.feedPool.ProtectAssets("download-" + m.name$, m.assetCollection) then
		m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, m.bsp.feedPool.GetFailureReason())
		m.bsp.logging.FlushLogFile()
		m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + m.bsp.feedPool.GetFailureReason())
		stop
	endif

	m.bsp.feedPool.ReserveMegabytes(50)

	m.assetFetcher = CreateObject("roAssetFetcher", m.bsp.feedPool)
	m.assetFetcher.SetPort(m.bsp.msgPort)
	m.assetFetcher.AddHeader("User-Agent", m.bsp.userAgent$)
	m.assetFetcher.SetMinimumTransferRate(1000,60)
	m.assetFetcher.SetFileProgressIntervalSeconds(5)
	m.assetFetcher.SetUserData(m.name$)

	binding% = GetBinding(m.bsp.mediaFeedsXfersEnabledWired, m.bsp.mediaFeedsXfersEnabledWireless)
    m.bsp.diagnostics.PrintDebug("### Binding for assetFetcher is " + stri(binding%))
	ok = m.assetFetcher.BindToInterface(binding%)
	if not ok then stop

	if not m.assetFetcher.AsyncDownload(m.assetCollection) then
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_IMMEDIATE_FAILURE, m.assetFetcher.GetFailureReason())
        m.bsp.diagnostics.PrintTimestamp()
        m.bsp.diagnostics.PrintDebug("### AsyncDownload failed: " + m.assetFetcher.GetFailureReason())
		m.assetFetcher = invalid
    endif

	' tell the states to switch over to the new spec immediately
	m.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.feedPool, m.assetCollection)

	mrssSpecUpdatedEvent = CreateObject("roAssociativeArray")
	mrssSpecUpdatedEvent["EventType"] = "MRSS_SPEC_UPDATED"
	mrssSpecUpdatedEvent["LiveDataFeed"] = m
	m.bsp.msgPort.PostMessage(mrssSpecUpdatedEvent)

End Sub


Function newMRSSFeed( liveDataFeed As Object ) As Object

	feed = {}
	feed.liveDataFeed = liveDataFeed
	feed.ttlSeconds = -1

	feed.PopulateFeedItems			= mrssFeed_PopulateFeedItems
	feed.ParseFeedByPlugin			= mrssFeed_ParseFeedByPlugin
	feed.SetTTLMinutes				= mrssFeed_SetTTLMinutes
	feed.SetTTLSeconds				= mrssFeed_SetTTLSeconds
	feed.ContentExists				= mrssFeed_ContentExists
	feed.AllContentExists			= mrssFeed_AllContentExists

	return feed

End Function


Sub ParseMRSSFeed( filePath$ As String )

	m.feed = newMRSSFeed( m )
	
	if m.parser$ <> "" then
		m.feed.ParseFeedByPlugin( filePath$ )
	else
		m.feed.PopulateFeedItems( filePath$ )
	endif

	' if the ttl specified in the feed < the update interval, set the update interval to the specified ttl
	if m.feed.ttlSeconds > 0 and m.feed.ttlSeconds < m.updateInterval% then
		m.updateInterval% = m.feed.ttlSeconds
	endif
	if m.feed.displayOn <> invalid then
		m.displayOn = m.feed.displayOn
	endif

End Sub


Function mrssFeed_ContentExists( assetPoolFiles As Object ) As Boolean

	for each item in m.items
		file$ = item.url
		filePath$ = GetPoolFilePath(assetPoolFiles, file$)
		if filePath$ <> ""  return true
	next

	return false

End Function


Function mrssFeed_AllContentExists( assetPoolFiles As Object ) As Boolean

	for each item in m.items
		' skip checkng for HTML items - bug 24245
		if item.type = invalid or item.type <> "text/html" then
			file$ = item.url
			filePath$ = GetPoolFilePath(assetPoolFiles, file$)
			if filePath$ = ""  return false
		endif
	next

	return true

End Function

Sub mrssFeed_PopulateFeedItems( filePath$ As String )

	m.items = []

	xml = ReadAsciiFile( filePath$ )
	if len(xml) = 0 return

' check for encryption
	if getGlobalAA().bsp.contentEncrypted then
		isEncrypted = true
	else
		isEncrypted = false
	endif

	mrssFeedXML = CreateObject("roXMLElement")
	if not mrssFeedXML.Parse(xml) stop

	for each elt in mrssFeedXML.GetBody().Peek().GetBody()
		name = elt.GetName()

		if name = "ttl" then
			m.SetTTLMinutes( elt.GetBody() )
		else if name = "frameuserinfo:playtime" Then
			m.playtime = Val(elt.GetBody())
		else if name = "title" then
			m.title = elt.GetBody()
		else if name = "item" then
			item = newMRSSItem(elt)
			if (item <> invalid) then 
				item.isEncrypted = isEncrypted
				m.items.Push( item )
			end if	
		end if
	next

End Sub


Sub mrssFeed_SetTTLMinutes( ttl As String )

	if ttl = invalid or Val(ttl) <= 0 then
		m.ttlSeconds = -1
	else if Val(ttl) < 2 then
		m.ttlSeconds = 120
	else
		m.ttlSeconds = Val(ttl) * 60
	end if

	' the ttl is the lower of the ttl specified in the feed and the update rate of the live data
	if type( m.liveDataFeed ) = "roAssociativeArray" and type( m.liveDataFeed.updateInterval% ) = "roInt" and m.liveDataFeed.updateInterval% < m.ttlSeconds then	
		m.ttlSeconds = m.liveDataFeed.updateInterval%
	endif

End Sub

Sub mrssFeed_SetTTLSeconds( ttlSeconds As String )

	if ttlSeconds <> invalid then
		secs = Val(ttlSeconds)
		if secs < 30 then
			m.ttlSeconds = 30
		else
			m.ttlSeconds = secs
		endif

		' the ttl is the lower of the ttl specified in the feed and the update rate of the live data
		if type( m.liveDataFeed ) = "roAssociativeArray" and type( m.liveDataFeed.updateInterval% ) = "roInt" and m.liveDataFeed.updateInterval% < m.ttlSeconds then	
			m.ttlSeconds = m.liveDataFeed.updateInterval%
		endif
	endif

End Sub

Function newMRSSItem( xml as Object ) As Object

	item = { durationSeconds: 60, url:"no_url", category:"no_category", thumbnail:"no_thumbnail", title:"no_title", displayStart:0, medium:"no_medium", size:0, isEncrypted: GetGlobalAA().bsp.contentEncrypted }

	contentPresent = false
	for each elt in xml.GetBody()
		name = elt.GetName()
		if name = "guid" then
			item.guid = elt.GetBody()
		else if name = "title" then
			item.title = elt.GetBody()
		else if name = "description" then
			item.description = elt.GetBody()
		else if name = "media:content" then
			item.url= elt.GetAttributes()["url"]
			item.type = elt.GetAttributes()["type"]
			item.duration = helper_GetDuration( elt )
			item.size = helper_GetFileSize(elt)
			item.medium = elt.GetAttributes()["medium"]
			contentPresent = true
			item.probeData = helper_GetProbeData(elt)
		else if name = "media:thumbnail" then
			item.thumbnail = elt.GetAttributes()["url"]
		else if name = "category" then
			item.category = elt.GetBody()
		else if name = "media:group" then
			for each eltmg in elt.GetBody()
				name = eltmg.GetName()
				if name = "media:content" then
					item.url= eltmg.GetAttributes()["url"]
					item.type = eltmg.GetAttributes()["type"]
					item.duration = helper_GetDuration( eltmg )
					item.size = helper_GetFileSize(eltmg)
					item.medium = eltmg.GetAttributes()["medium"]
					contentPresent = true
					item.probeData = helper_GetProbeData(eltmg)
				else if name = "media:thumbnail" then
					item.thumbnail = eltmg.GetAttributes()["url"]
				end if        
			next

		' custom fields
		' ignore the following elements in a feed: link, ?
		else if name <> "link" then
			if type(item.mrssCustomFields) <> "roAssociativeArray" then
				item.mrssCustomFields = {}
			endif

			if elt.GetBody() = invalid then
				value = ""
			else
				value = elt.GetBody()
			endif

			item.mrssCustomFields.AddReplace(name, value)
		end if
	
	next

   ' make item.guid the hash of the guid
	if IsString(item.guid) then
		hashGen = CreateObject("roHashGenerator", "SHA1")
		item.guid = hashGen.hash(item.guid).ToHexString()
		hashGen = invalid
	endif

	if (contentPresent) then 
		return item
	else 
		return invalid
	end if

End Function

Sub mrssFeed_ParseFeedByPlugin( filePath$ As String )

	items = CreateObject("roArray", 1, true)
	metadata = CreateObject("roAssociativeArray")

	m.items = []

	ERR_NORMAL_END = &hFC
	retVal = Eval(m.liveDataFeed.parser$ + "(filePath$, items, metadata)")
	if retVal <> ERR_NORMAL_END then
		' log the failure
		bsp = m.liveDataFeed.bsp
		bsp.diagnostics.PrintDebug("Failure invoking Eval to parse live MRSS data feed: return value = " + stri(retVal) + ", parser is " + m.liveDataFeed.parser$)
		bsp.logging.WriteDiagnosticLogEntry(bsp.diagnosticCodes.EVENT_LIVE_MRSS_PLUGIN_FAILURE, stri(retVal) + chr(9) + m.liveDataFeed.parser$)
	else
		' Use the item array to build the item array for the feed, making sure it has
		'  all required elements
		for each item in items
			' Skip any item that does not have a url
			if item.url <> invalid and IsString(item.url) then
				mrssItem = {}
				mrssItem.url = item.url
				if IsString(item.title) then
					mrssItem.title = item.title
				else
					mrssItem.title = ""
				endif
				if IsString(item.description) then
					mrssItem.description = item.description
				else
					mrssItem.description = ""
				endif
				if IsString(item.description) then
					mrssItem.duration = Val(item.duration)
				else
					mrssItem.duration = 15
				endif
				if IsString(item.type) then
					mrssItem.type = item.type
				else
					mrssItem.type = ""
				endif
				if IsString(item.medium) then
					mrssItem.medium = item.medium
				else
					mrssItem.medium = "no_medium"
				endif
				if IsString(item.size) then
					mrssItem.size = Val(item.size)
				else
					mrssItem.size = 0
				endif
				if IsString(item.probeData) then
					mrssItem.probeData = item.probeData
				else
					mrssItem.probeData = ""
				endif
				
			   ' make mrssItem.guid the hash of the guid from the feed
				if IsString(item.guid) then
					guid$ = item.guid
					hashGen = CreateObject("roHashGenerator", "SHA1")
					mrssItem.guid = hashGen.hash(guid$).ToHexString()
					hashGen = invalid
				endif
				
				' if plugin specified custom fields, just copy them in
				if type(item.mrssCustomFields) = "roAssociativeArray" then
					mrssItem.mrssCustomFields = item.mrssCustomFields
				endif
				
				m.items.Push(mrssItem)
			endif
		next

		'Populate feed metadata
		'If the parser gives us an update interval in seconds, use that, otherwise look for interval in minutes (like standard MRSS)
		if metadata.ttlSeconds <> invalid and IsString(metadata.ttlSeconds) then
			m.SetTTLSeconds(metadata.ttlSeconds)
		else if metadata.ttl <> invalid and IsString(metadata.ttl) then
			m.SetTTLMinutes(metadata.ttl)
		endif
		if metadata.title <> invalid and IsString(metadata.title) then
			m.title = metadata.title
		endif
		if metadata.playtime <> invalid and IsString(metadata.playtime) then
			m.playtime = metadata.playtime
		endif
		if metadata.displayOn <> invalid and IsString(metadata.displayOn) then
			m.displayOn = metadata.displayOn
		endif
	endif

End Sub

'endregion

'region LiveDataFeed methods


Function ParseSimpleRSSFeed(filePath$ As String) As Boolean

	success = true

	m.articles = CreateObject("roArray", 1, true)
	m.articleTitles = CreateObject("roArray", 1, true)
	m.articlesByTitle = CreateObject("roAssociativeArray")
	m.articleHashTypes = CreateObject("roArray", 1, true)
	m.articleHashes = CreateObject("roArray", 1, true)

	if m.parser$ <> "" then
		userVariables = m.bsp.currentUserVariables
		ERR_NORMAL_END = &hFC

		' try plugin using new interface first
		feedItems = []
		retVal = Eval(m.parser$ + "(filePath$, feedItems, m.bsp)")
		if retVal = ERR_NORMAL_END then
			' success using updated interface. extract data and convert to structures used by other parts of autorun
			index% = 0
			for each feedItem in feedItems
				m.articleTitles[index%] = feedItem.key
				m.articles[index%] = feedItem.url
				m.articlesByTitle.AddReplace(feedItem.key, feedItem.url)
				m.articleHashTypes[index%] = feedItem.hashType
				m.articleHashes[index%] = feedItem.hash
				index% = index% + 1
			next
		else
			' failure using new interface: try old interface
			retVal = Eval(m.parser$ + "(filePath$, m.articles, m.articlesByTitle, userVariables)")
			if retVal <> ERR_NORMAL_END then
				' log the failure
				m.bsp.diagnostics.PrintDebug("Failure invoking Eval to parse live text data feed: return value = " + stri(retVal) + ", parser is " + m.parser$)
				m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_LIVE_TEXT_PLUGIN_FAILURE, stri(retVal) + chr(9) + m.parser$)
				success = false
			endif
		endif		
		
	else
		if type(m.isJSON) = "roBoolean" and m.isJSON then
			jsonString=ReadAsciiFile(filePath$)
			json = ParseJSON(jsonString)
			numItemsAdded% = 0

			for each jsonItem in json
			    if type(m.isTwitterFeed) = "roBoolean" and m.isTwitterFeed then
					text$ = jsonItem.full_text
				else
					text$ = jsonItem.text
				endif
				m.articles.Push(text$)
				m.articleTitles.Push(text$)
				m.articlesByTitle.AddReplace(text$, text$)

				numItemsAdded% = numItemsAdded% + 1

				if m.restrictNumberOfItems and (numItemsAdded% >= m.numberOfItemsToDisplay%) then
					exit for
				endif

			next
			
			if numItemsAdded% = 0 then
				success = false
			endif
		else
			parser = CreateObject("roRssParser")
			success = parser.ParseFile(filePath$)
			if success then
				article = parser.GetNextArticle()
				while type(article) = "roRssArticle"
					title = article.GetTitle()
					description = article.GetDescription()
					m.articles.Push(description)
					m.articleTitles.Push(title)
					m.articlesByTitle.AddReplace(title, description)
					article = parser.GetNextArticle()
				endwhile
			endif
		endif
	endif
	return success
End Function


Function ContentDataFeedsIdentical(articlesFeed1 As Object, articlesFeed2 As Object, compareArticleTitles As Boolean, articleTitlesFeed1 As Object, articleTitlesFeed2 As Object) As Boolean

	if articlesFeed1.Count() = articlesFeed2.Count() then

		for i% = 0 to articlesFeed1.Count() - 1
			if articlesFeed1[i%] <> articlesFeed2[i%] then
				return false
			else if compareArticleTitles and articleTitlesFeed1[i%] <> articleTitlesFeed2[i%] then
				return false
			endif
		next
			
		return true

	endif

	return false

End Function


Sub ReadFeedContent()

	if m.usage$ = "content" then
		m.ReadLiveFeedContent()
	else if m.usage$ = "mrss" or m.usage$ = "mrsswith4k" then
		m.ReadMRSSContent()
	endif

End Sub


Sub ReadLiveFeedContent()

	filePath$ = "feed_cache/" + m.name$ + ".xml"

	ok=true
	if m.isDynamicPlaylist or m.isLiveMediaFeed then
		m.ParseMRSSFeed(filePath$)
		m.ConvertMRSSFormatToContent()
	else
		ok = m.ParseSimpleRSSFeed(filePath$)
	endif

	if ok then
	
		m.itemUrls = m.articles
		m.fileUrls = m.articles
		if type(m.articleMediaTypes) = "roArray" then
			m.fileTypes = m.articleMediaTypes
		endif

		m.fileKeys = CreateObject("roArray", m.articles.Count(), true)
		if m.articleTitles.Count() > 0 then
			m.fileKeys = m.articleTitles
		else if type(m.articlesByTitle) = "roAssociativeArray" then
			' the following algorithm has poor performance- improve in the future by building and using a dictionary
			for each key in m.articlesByTitle
				' find the corresponding url by linearly searching through m.articles
				index% = 0
				url = m.articlesByTitle[key]				
				for each articleUrl in m.articles
					if articleUrl = url then
						m.fileKeys[index%] = key
					endif
					index% = index% + 1
				next
			next
		endif

		' build data structures so that script can check if all content exists on the card
		m.assetCollection = CreateObject("roAssetCollection")

		index% = 0
		for each url in m.fileUrls
			asset = CreateObject("roAssociativeArray")
			asset.link = url
			asset.name = url

			if type(m.assets) = "roArray" then
				asset.hash = m.assets[index%].hash
			endif

			m.assetCollection.AddAsset(asset)

			index% = index% + 1
		next

		' verify that all specified files are actually on the card
		m.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.feedPool, m.assetCollection)
		for each url in m.fileUrls
			filePath$ = m.assetPoolFiles.GetPoolFilePath(url)
			if filePath$ = "" then
				m.assetPoolFiles = invalid
				m.itemUrls = invalid
				m.fileKeys = invalid
				m.fileUrls = invalid
				return
			endif
		next

		' protect these assets
		if not m.bsp.feedPool.ProtectAssets("current-" + m.name$, m.assetCollection) then
			m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, m.bsp.feedPool.GetFailureReason())
			m.bsp.logging.FlushLogFile()
			m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + m.bsp.feedPool.GetFailureReason())
			stop
		endif

		' post message indicating load complete
		contentDataFeedLoaded = CreateObject("roAssociativeArray")
		contentDataFeedLoaded["EventType"] = "CONTENT_DATA_FEED_LOADED"
		contentDataFeedLoaded["Name"] = m.name$
		m.bsp.msgPort.PostMessage(contentDataFeedLoaded)

	endif

End Sub


Sub DownloadLiveFeedContent()

	m.bsp.diagnostics.PrintDebug("DownloadLiveFeedContent")

	if type(m.parser$) = "roString" and m.parser$<>"" then
		compareArticleTitles = false
	else
		compareArticleTitles = true
	endif

	if type(m.assetFetcher) = "roAssetFetcher" then

		' fetch active, see if feed has changed
		if ContentDataFeedsIdentical(m.articlesDownloading, m.articles, compareArticleTitles, m.articleTitlesDownloading, m.articleTitles) then
	        m.bsp.diagnostics.PrintDebug("### live data feed asset fetch active and there are no changes to the feed spec so we'll let it continue")
			m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_CONTINUE_LIVE_DATA_FEED_CONTENT_DOWNLOAD, "")
			return
		endif

		' feed has changed, cancel download and start new download
        m.bsp.diagnostics.PrintDebug("### asset fetch active but feed has changed - cancel current download")
		m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_RESTART_LIVE_DATA_FEED_CONTENT_DOWNLOAD, "")
        m.assetFetcher.AsyncCancel()
		m.assetFetcher = invalid

	endif

' it is only necessary to download content if ...
'	there is no current content OR
'	current keys are different from keys specified in new feed OR
'	current content is different from content specified in new feed

	if type(m.assetPoolFiles) = "roAssetPoolFiles" and type(m.fileUrls) = "roArray" and m.fileUrls.Count() > 0 then

		' there is current content; check to see if it matches the new feed

		' compare to existing keys and Urls
		if ContentDataFeedsIdentical(m.itemUrls, m.articles, compareArticleTitles, m.fileKeys, m.articleTitles) then
			m.bsp.diagnostics.PrintDebug("No change in content feed " + m.name$ + ". No need to download content.")

			' post message indicating no need to download content
			contentDataFeedUnchanged = CreateObject("roAssociativeArray")
			contentDataFeedUnchanged["EventType"] = "CONTENT_DATA_FEED_UNCHANGED"
			contentDataFeedUnchanged["Name"] = m.name$
			m.bsp.msgPort.PostMessage(contentDataFeedUnchanged)

			return
		endif

	endif

	' write the rss feed to the card
	CopyFile(m.rssFileName$, "feed_cache/" + m.name$ + ".xml")

    m.bsp.diagnostics.PrintDebug("Download new content for feed " + m.name$ + ".")
	m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_START_LIVE_DATA_FEED_CONTENT_DOWNLOAD, m.name$)

	' parse the feed, building an asset collection and a list of file items
	m.assetCollection = CreateObject("roAssetCollection")

	index% = 0

	for each url in m.articles
		asset = CreateObject("roAssociativeArray")
		asset.link = url
		asset.name = url
' Code commented out until fix for bug 17733 / 29442 is fully implemented for usageType = "content"
'	    asset.change_hint = url

		if type(m.articleHashTypes) = "roArray" and m.articleHashTypes.Count() > index% and type(m.articleHashTypes[index%]) = "roString" and type(m.articleHashes) = "roArray" and m.articleHashes.Count() > index% and type(m.articleHashes[index%]) = "roString" then
			asset.hash = m.articleHashTypes[index%] + ":" + m.articleHashes[index%]
		endif

		m.assetCollection.AddAsset(asset)

' Code commented out until fix for bug 17733 / 29442 is fully implemented for usageType = "content": the issue is that there is no m.feed / m.feed.items for this type of feed
'		' track feed content downloads
'	    fileToDownload = {}
'	    fileToDownload.name = url
'	    fileToDownload.size = 0
'	    fileToDownload.hash = url
'       fileToDownload.currentFilePercentage$ = ""
'        fileToDownload.status$ = ""

'		if type(asset) = "roAssociativeArray" and type(asset.link) = "roString" then
'			if type(m.assetPoolFiles) = "roAssetPoolFiles" then
'				filePath = m.assetPoolFiles.GetPoolFilePath(asset.link)
'				if filePath <> "" then
'					fileToDownload.currentFilePercentage$ = "100"
'					fileToDownload.status$ = "ok"
'				endif
'			endif
'		endif

'		m.feedContentFilesToDownload.AddReplace(fileToDownload.hash, fileToDownload)

		index% = index% + 1
	next

	if not m.bsp.feedPool.ProtectAssets("new-" + m.name$, m.assetCollection) then
		m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, m.bsp.feedPool.GetFailureReason())
		m.bsp.logging.FlushLogFile()
		m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + m.bsp.feedPool.GetFailureReason())
		stop
	endif

	m.bsp.feedPool.ReserveMegabytes(50)

	m.assetFetcher = CreateObject("roAssetFetcher", m.bsp.feedPool)
	m.assetFetcher.SetPort(m.bsp.msgPort)
	m.assetFetcher.AddHeader("User-Agent", m.bsp.userAgent$)
	m.assetFetcher.SetMinimumTransferRate(1000,60)
	m.assetFetcher.SetFileProgressIntervalSeconds(5)
	m.assetFetcher.SetUserData(m.name$)

	binding% = GetBinding(m.bsp.mediaFeedsXfersEnabledWired, m.bsp.mediaFeedsXfersEnabledWireless)
    m.bsp.diagnostics.PrintDebug("### Binding for assetFetcher is " + stri(binding%))
	ok = m.assetFetcher.BindToInterface(binding%)
	if not ok then stop

	m.articlesDownloading = m.articles
	m.articleTitlesDownloading = m.articleTitles

	if not m.assetFetcher.AsyncDownload(m.assetCollection) then
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_IMMEDIATE_FAILURE, m.assetFetcher.GetFailureReason())
        m.bsp.diagnostics.PrintTimestamp()
        m.bsp.diagnostics.PrintDebug("### AsyncDownload failed: " + m.assetFetcher.GetFailureReason())
		m.assetFetcher = invalid
    endif

End Sub


Sub RestartLiveDataFeedDownloadTimer(timespan% As Integer)

	if timespan% > 0 then

		' set a timer to update live data feed
		if type(m.timer) = "roTimer" then
			m.timer.Stop()
		else
			m.timer = CreateObject("roTimer")
			m.timer.SetPort(m.bsp.msgPort)
		endif

		m.timer.SetElapsed(timespan%, 0)
		m.timer.Start()

		m.bsp.liveDataFeedsByTimer.AddReplace(stri(m.timer.GetIdentity()), m)

	endif

End Sub


Sub HandleLiveDataFeedContentDownloadAssetFetcherProgressEvent(event)

    m.bsp.diagnostics.PrintDebug("### HandleLiveDataFeedContentDownloadAssetFetcherProgressEvent")
    m.bsp.diagnostics.PrintDebug("### File download progress " + event.GetFileName() + str(event.GetCurrentFilePercentage()))

    m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_FILE_DOWNLOAD_PROGRESS, event.GetFileName() + chr(9) + str(event.GetCurrentFilePercentage()))

    fileIndex% = event.GetFileIndex()
    assetList = m.assetCollection.GetAssetList()
    asset = assetList[fileIndex%]
    hash = asset.change_hint

' Hash is invalid until fix for bug 17733 / 29442 is fully implemented for usageType = "content": the issue is that there is no m.feed / m.feed.items for this type of feed
	if type(hash) <> "Invalid" then
	    fileItem = m.feedContentFilesToDownload.Lookup(hash)
		if type(m.bsp.networkingHSM) = "roAssociativeArray" then
			m.bsp.networkingHSM.AddDeviceDownloadProgressItem(fileItem, str(event.GetCurrentFilePercentage()), "ok")
		endif
	endif

print "----------------------------- HandleLiveDataFeedContentDownloadAssetFetcherProgressEvent: ";event.GetCurrentFilePercentage()

End Sub


Sub HandleLiveDataFeedContentDownloadAssetFetcherEvent(event)

    POOL_EVENT_FILE_DOWNLOADED = 1
    POOL_EVENT_FILE_FAILED = -1
    POOL_EVENT_ALL_DOWNLOADED = 2
    POOL_EVENT_ALL_FAILED = -2

    m.bsp.diagnostics.PrintTimestamp()
    m.bsp.diagnostics.PrintDebug("### LiveDataFeedContentDownloadAssetFetcherEvent")

	if (event.GetEvent() = POOL_EVENT_FILE_DOWNLOADED) then
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_FILE_DOWNLOAD_COMPLETE, event.GetName())
        m.bsp.diagnostics.PrintDebug("### File downloaded " + event.GetName())

		' track download traffic for dynamic playlists
		if m.isDynamicPlaylist and type(m.bsp.networkingHSM) = "roAssociativeArray" and type(m.assetPoolFiles) = "roAssetPoolFiles" then
			fileName$ = event.GetName()
			filePath$ = m.assetPoolFiles.GetPoolFilePath(fileName$)
			if filePath$ <> "" then
				checkFile = CreateObject("roReadFile", filePath$)
				if (checkFile <> invalid) then
					checkFile.SeekToEnd()
					size = checkFile.CurrentPosition()
					checkFile = invalid
					m.bsp.networkingHSM.UploadMRSSTrafficDownload(size)
				endif
			endif
		endif

	else if (event.GetEvent() = POOL_EVENT_FILE_FAILED) then
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_FILE_DOWNLOAD_FAILURE, event.GetName() + chr(9) + event.GetFailureReason())
        m.bsp.diagnostics.PrintDebug("### File failed " + event.GetName() + ": " + event.GetFailureReason())

        ' log this error to the download progress handler
        fileIndex% = event.GetFileIndex()
	    assetList = m.assetCollection.GetAssetList()
		asset = assetList[fileIndex%]

		if IsString(asset.change_hint) then

  		hash = asset.change_hint
	    fileItem = m.feedContentFilesToDownload.Lookup(hash)

      if type(fileItem) = "roAssociativeArray" and type(m.bsp.networkingHSM) = "roAssociativeArray" then
        m.bsp.networkingHSM.AddDeviceDownloadProgressItem(fileItem, "-1", event.GetFailureReason())
      endif

    endif
        
		' count number of failure and cancel if there are too many??

	else if (event.GetEvent() = POOL_EVENT_ALL_FAILED) then
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_FAILURE, event.GetFailureReason())		
        m.bsp.diagnostics.PrintDebug("### Download failed: " + event.GetFailureReason())

		m.assetFetcher = invalid

		m.lastDownloadedFailed = true

		m.bsp.RemoveFailedFeedFromQueue()

		m.RestartLiveDataFeedDownloadTimer(30)

	else if (event.GetEvent() = POOL_EVENT_ALL_DOWNLOADED) then

        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_DOWNLOAD_COMPLETE, "")		
        m.bsp.diagnostics.PrintDebug("### All files downloaded")

' send up the list of files downloaded
        m.feedContentFilesToDownload = {}

' m.feed is invalid for feeds used for Media Lists (usageType$ = "content'): progress updates not supported yet
		if type(m.feed) = "roAssociativeArray" and type(m.feed.items) = "roArray" then
			for each item in m.feed.items
				fileToDownload = {}
				fileToDownload.name = item.title
				fileToDownload.size = item.size
				fileToDownload.hash = item.guid
				fileToDownload.currentFilePercentage$ = "100"
				fileToDownload.status$ = "ok"

        if IsString(fileToDownload.hash) then
				  m.feedContentFilesToDownload.AddReplace(fileToDownload.hash, fileToDownload)
				endif

			next
		endif

		if type(m.bsp.networkingHSM) = "roAssociativeArray" then
		    m.bsp.networkingHSM.UploadDeviceDownloadProgressFileList()
			m.bsp.networkingHSM.FileListPendingUpload = false
		endif

		m.assetFetcher = invalid

		if m.usage$ = "content" then

			' unprotect old assets, keep protection on new (now current) assets
			if not m.bsp.feedPool.ProtectAssets("current-" + m.name$, m.assetCollection) then
				m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, m.bsp.feedPool.GetFailureReason())
				m.bsp.logging.FlushLogFile()
				m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + m.bsp.feedPool.GetFailureReason())
				stop
			endif
			
			if not m.bsp.feedPool.UnprotectAssets("new-" + m.name$) then
				m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, m.bsp.feedPool.GetFailureReason())
				m.bsp.diagnostics.PrintDebug("### UnprotectFiles failed: " + m.bsp.feedPool.GetFailureReason())
			endif

			' get asset pool
			m.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.feedPool, m.assetCollection)

			' copy result of previous parse
			m.itemUrls = []
			m.fileKeys = []
			m.fileUrls = []
			m.fileTypes = CreateObject("roArray", 1, true)

			' for MediaList states
			titlesByUrl = {}
			for each title in m.articlesByTitle
				url = m.articlesByTitle.Lookup(title)
				titlesByUrl.AddReplace(url, title)
			next

			index% = 0
			for each itemUrl in m.articles
				m.itemUrls.push(itemUrl)

				if type(m.articleMediaTypes) = "roArray" and m.articleMediaTypes.Count() > index% then
					m.fileTypes[index%] = m.articleMediaTypes[index%]
				endif

				' get corresponding title
				title = titlesByUrl.Lookup(itemUrl)
				m.fileKeys.push(title)
				m.fileUrls.push(itemUrl)

				index% = index% + 1
			next

			' post message indicating load complete
			contentDataFeedLoaded = CreateObject("roAssociativeArray")
			contentDataFeedLoaded["EventType"] = "CONTENT_DATA_FEED_LOADED"
			contentDataFeedLoaded["Name"] = m.name$
			m.bsp.msgPort.PostMessage(contentDataFeedLoaded)
			
		else

			' unprotect old assets, keep protection on new (now current) assets
			if not m.bsp.feedPool.ProtectAssets("display-" + m.name$, m.assetCollection) then
				m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, m.bsp.feedPool.GetFailureReason())
				m.bsp.logging.FlushLogFile()
				m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + m.bsp.feedPool.GetFailureReason())
				stop
			endif

			if not m.bsp.feedPool.UnprotectAssets("download-" + m.name$) then
				m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, m.bsp.feedPool.GetFailureReason())
				m.bsp.diagnostics.PrintDebug("### UnprotectFiles failed: " + m.bsp.feedPool.GetFailureReason())
			endif

			' post message indicating load complete
			mrssDataFeedLoaded = CreateObject("roAssociativeArray")
			mrssDataFeedLoaded["EventType"] = "MRSS_DATA_FEED_LOADED"
			mrssDataFeedLoaded["Name"] = m.name$
			m.bsp.msgPort.PostMessage(mrssDataFeedLoaded)
			
		endif

	endif

End Sub


Sub ConvertMRSSFormatToContent()

	' convert to format required for content feed
	m.articles = CreateObject("roArray", 1, true)
	m.articleTitles = CreateObject("roArray", 1, true)
	m.articlesByTitle = CreateObject("roAssociativeArray")
	m.articleMediaTypes = CreateObject("roArray", 1, true)

	for each item in m.feed.items
		m.articles.push(item.url)
		m.articleTitles.push(item.title)
		m.articlesByTitle.AddReplace(item.title, item.url)
		m.articleMediaTypes.push(item.medium)
	next

End Sub


Function IsFeatureSupported(featureName$ As String, fwVersion$ As String, featureMinRevs As Object) As Boolean

	featureExists = featureMinRevs.DoesExist(featureName$)
	if featureExists then
		featureMinFWRev = featureMinRevs[featureName$]
		featureMinFWRevVSFWVersion% = CompareFirmwareVersions(featureMinFWRev, fwVersion$)
		if featureMinFWRevVSFWVersion% <= 0 return true
	endif

	return false

End Function


Function CompareFirmwareVersions(a$ As String, b$ As String) As Integer

    start_a% = 0
    start_b% = 0

    while true

		if start_a% >= len(a$) then
			if start_b% >= len(b$) then
				return 0
			else
				return -1
			endif
		else if start_b% >= len(b$) then
			return 1
		endif

		aChar$ = mid(a$, start_a% + 1, 1)
		a_digit = IsDigit(aChar$)

		bChar$ = mid(b$, start_b% + 1, 1)
		b_digit = IsDigit(bChar$)

		if a_digit and b_digit then

			' Now we need to find the end of each of the sequences of digits.
			aa = {}
			aa.index = start_a%
			a_number% = ReadDigits(a$, aa)
			start_a% = aa.index

			bb = {}
			bb.index = start_b%
			b_number% = ReadDigits(b$, bb)
			start_b% = bb.index

			if a_number% < b_number% then
				return -1
			else if a_number% > b_number% then
				return 1
			endif
		else if a_digit then
            ' The first string has a digit but the second one has a
            ' non-digit so it must be greater.
			return 1
        else if b_digit then
            return -1
		else
			aChar$ = mid(a$, start_a% + 1, 1)
			bChar$ = mid(b$, start_b% + 1, 1)

			if asc(aChar$) < asc(bChar$) then
				return -1
			else if asc(aChar$) > asc(bChar$)
				return 1
			endif

			' Otherwise we've dealt with this character
			start_a% = start_a% + 1
			start_b% = start_b% + 1
		endif
	endwhile

End Function


Function IsDigit(a$ As String) As Boolean

	if asc(a$) >= 48 and asc(a$) <= 57 return true
	return false

End Function


Function ReadDigits(s$ As String, aa As Object) as Integer

	value% = 0

	index% = aa.index

	sChar$ = mid(s$, index% + 1, 1)

	while index% < len(s$) and IsDigit(sChar$)
	
		new_value% = value% * 10 + asc(sChar$) - asc("0")
		index% = index% + 1

		value% = new_value%

		if len(s$) > index% then
			sChar$ = mid(s$, index% + 1, 1)
		endif

	endwhile

	aa.index = index%
	return value%

End Function

'endregion

'region Bluetooth/Beacons

Function newBtManager() as Object
	' m is BSP
	btm = { bsp: m, btActive: false, beaconsSupported: false }
	btm.newBeacon = newBeacon
	btm.ParsePresentationBeacons = ParsePresentationBeacons
	btm.ResetPresentationBeacons = ResetPresentationBeacons
	btm.UpdatePersistentBeacons = UpdatePersistentBeacons
	btm.AddPersistentBeacon = AddPersistentBeacon
	btm.StartBeacon = StartBeacon
	btm.StopBeacon = StopBeacon
	btm.SetBtAdvertising = SetBtAdvertising
	btm.StartBtleClient = StartBtleClient
	btm.StopBtleClient = StopBtleClient
	btm.SetBtleStatus = SetBtleStatus
	btm.HandleEvent = btManager_HandleEvent

	btm.persistentBeacons = { }
	btm.presentationBeacons = { }

	btm.btleSupported = false
	btm.btleClientManager = invalid
	btm.btleClientServiceData = invalid
	btm.btleStatus% = 0

	if m.beaconsSupported then
		btm.beaconsSupported = true
		btm.btManager = CreateObject("roBtManager")
		btm.btManager.SetPort(m.msgPort)
		btm.UpdatePersistentBeacons()
	endif
	if m.btleSupported then
		btm.btleSupported = true
	endif
	return btm
End Function

Function btManager_HandleEvent(event as Object)
	btEventType = event.GetEvent()
	if btEventType = "add-adapter" then
		m.bsp.diagnostics.PrintDebug("-- Bluetooth adapter added")
		m.btActive = true
		m.SetBtAdvertising()
	else if btEventType = "remove-adapter" then
		m.bsp.diagnostics.PrintDebug("-- Bluetooth adapter removed")
		m.btActive = false
	end if
End Function

Function newBeacon(beaconXml as Object, persistent as Boolean) as Object
	beacon = invalid
	if m.beaconsSupported then
		beaconName$ = beaconXml.name.GetText()
		beaconType$ = beaconXml.type.GetText()
		beaconData = invalid

		if beaconType$ = "IBeacon" then
			beaconId = beaconXml.beaconId.GetText()
			' BeaconId must be specified
			if IsString(beaconId) and Len(beaconId) > 0 then
				beaconId = LCase(beaconId)
				major% = Val(beaconXml.data1.GetText())
				minor% = Val(beaconXml.data2.GetText())
				txp% = Val(beaconXml.txlevel.GetText())
				beaconData = { mode: "beacon", beacon_uuid: beaconId, beacon_major: major%, beacon_minor: minor%, beacon_level: txp%, persistent: persistent }
			endif
		else if beaconType$ = "EddystoneUrl" then
			url = beaconXml.beaconId.GetText()
			if IsString(url) then
				txp% = Val(beaconXml.txlevel.GetText())
				beaconData = { mode: "eddystone-url", url: url, tx_power: txp%, persistent: persistent }
			endif
		else if beaconType$ = "EddystoneUid" then
			txp% = Val(beaconXml.txlevel.GetText())
			beaconData = GetEddystoneUidBeaconData(beaconXml.beaconId.GetText(), beaconXml.data1.GetText(), txp%, persistent)
		endif

		if beaconData <> invalid then
			beacon = { name: beaconName$, type: beaconType$, data: beaconData, activate: true, isActive: false }
			if not persistent and beacon.autostart <> invalid then
				autostart = beaconXml.autostart.GetText()
				if not IsString(autostart) or LCase(autostart.Left(1)) <> "t" then
					beacon.activate = false
				endif
			endif
		endif
	endif
	return beacon
End Function

Function ParsePresentationBeacons(beaconsXml as Object) as Boolean
	beacons = { }
	if m.beaconsSupported and type(beaconsXml) = "roXMLList" and beaconsXml.Count() >= 1 then
		for each beaconXML in beaconsXml
			beacon = m.newBeacon(beaconXML, false)
			if beacon <> invalid then
				beacons.AddReplace(beacon.name, beacon)
			endif
		next
	endif
	m.presentationBeacons = beacons
	return not m.presentationBeacons.IsEmpty()
End Function

Sub ResetPresentationBeacons()
	if m.beaconsSupported and not m.presentationBeacons.IsEmpty() then
		m.presentationBeacons = { }
		m.SetBtAdvertising()
	endif
End Sub

Function GetEddystoneUidBeaconData(namespace as String, instance as String, txLevel% as Integer, persistent as Boolean) as Object
	beaconData = invalid

	if Len(namespace) > 0 and Len(instance) > 0 then
		if LCase(namespace.Left(2)) = "0x" then
			namespace = namespace.mid(2)
		endif
		ns = CreateObject("roByteArray")
		ns.FromHexString(namespace)
		' namespace string must have exactly ten bytes
		pad = 10 - ns.Count()
		for i=1 to pad
			ns.Unshift(0)
		next

		if LCase(instance.Left(2)) = "0x" then
			instance = instance.mid(2)
		endif
		in = CreateObject("roByteArray")
		in.FromHexString(instance)
		' instance string must have exactly six bytes
		pad = 6 - in.Count()
		for i=1 to pad
			in.Unshift(0)
		next

		beaconData = { mode: "eddystone-uid", namespace: ns, instance: in, tx_power: txLevel%, persistent: persistent }
	endif
	return beaconData
End Function

Sub UpdatePersistentBeacons()
	wasEmpty = m.persistentBeacons.IsEmpty()
	m.persistentBeacons = { }
	m.AddPersistentBeacon(m.bsp.registrySettings.beacon1)
	m.AddPersistentBeacon(m.bsp.registrySettings.beacon2)
	if not (m.persistentBeacons.IsEmpty() and wasEmpty) then
		' Restart beacons to handle any persistent beacon changes
		m.SetBtAdvertising()
	endif
End Sub

Function AddPersistentBeacon(beaconJson as String) as Boolean
	success = false
	if IsString(beaconJson) and Len(beaconJson) > 0 then
		beaconInputData = ParseJson(beaconJson)
		if type(beaconInputData) = "roAssociativeArray" then
			beaconName$ = beaconInputData.Name
			beaconType% = beaconInputData.Type
			beaconType$ = ""
			beaconData = invalid

			if beaconType% = 0 then
				beaconId = LCase(beaconInputData.BeaconId)
				if Len(beaconId) > 0 then
					beaconType$ = "IBeacon"
					major% = Val(beaconInputData.Data1)
					minor% = Val(beaconInputData.Data2)
					txp% = beaconInputData.TxLevel
					beaconData = { mode: "beacon", beacon_uuid: beaconId, beacon_major: major%, beacon_minor: minor%, beacon_level: txp%, persistent: true }
				endif
			else if beaconType% = 1 then
				url = beaconInputData.BeaconId
				if Len(url) > 0 then
					beaconType$ = "EddystoneUrl"
					txp% = beaconInputData.TxLevel
					beaconData = { mode: "eddystone-url", url: url, tx_power: txp%, persistent: true }
				endif
			else if beaconType% = 2 then
				beaconType$ = "EddystoneUid"
				beaconData = GetEddystoneUidBeaconData(beaconInputData.BeaconId, beaconInputData.Data1, beaconInputData.TxLevel, true)
			endif

			if beaconData <> invalid then
				beacon = { name: beaconName$, type: beaconType$, data: beaconData, activate: true, isActive: false }
				m.persistentBeacons.AddReplace(beacon.name, beacon)
			endif
		endif
	endif
	return success
End Function

Function StartBeacon(name as String) as Boolean
	success = false
	beacon = m.presentationBeacons[name]
	if beacon <> invalid then
		beacon.activate = true
		success = m.SetBtAdvertising()
	endif
	return success
End Function

Function StopBeacon(name as String) as Boolean
	success = false
	beacon = m.presentationBeacons[name]
	if beacon <> invalid then
		beacon.activate = false
		success = m.SetBtAdvertising()
	endif
	return success
End Function

Function StartBtleClient(clientParams as Object, appId as String, txPower as Integer) as Boolean
	success = false
	if m.btleSupported then
		if m.btManager.GetAdapterList().Count() > 0 then
			m.bsp.diagnostics.PrintDebug("---- Starting BTLE Client manager")
			m.btleClientManager = CreateObject("roBtClientManager")
			m.btleClientManager.SetPort(m.bsp.msgPort)
			success = m.btleClientManager.Start(clientParams)
			if not success then
				m.bsp.diagnostics.PrintDebug("-- Start BTLE Client manager failed: "+m.btleClientManager.GetFailureReason())
				m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BTLE_START_FAILED, "Start BTLE Client manager failed: "+m.btleClientManager.GetFailureReason())
			else
				' remember service ID
				m.btleServiceId = clientParams.service_uuid
				' set up service data array
				m.btleClientServiceData = CreateObject("roByteArray")
				m.btleClientServiceData.FromHexString(appId)
				if m.btleClientServiceData.Count() < 4 then
					while m.btleClientServiceData.Count() < 4
						m.btleClientServiceData.Unshift(0)
					end while
				else if m.btleClientServiceData.Count() > 4 then
					while m.btleClientServiceData.Count() > 4
						m.btleClientServiceData.Pop()
					end while
				endif
				m.btleClientServiceData.push(txPower)
				m.btleClientServiceData.push(0)
				' restart advertising to set connectable flag for all beacons
				success = m.SetBtAdvertising()
			endif
		else
			m.bsp.diagnostics.PrintDebug("-- Start BTLE Client manager failed - there is no bluetooth adapter")
			m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BTLE_START_FAILED, "Start BTLE Client manager failed - there is no bluetooth adapter")
		endif
	endif
	return success
End Function

Function StopBtleClient(clientParams as Object) as Boolean
	success = false
	if m.btleClientManager then
		m.bsp.diagnostics.PrintDebug("---- Stopping BTLE Client manager")
		m.btleClientManager.Stop()
		m.btleClientManager = invalid
		m.btleClientServiceData = invalid
		m.btleServiceId = invalid
		' restart advertising to reset connectable flag for all beacons
		success = m.SetBtAdvertising()
	endif
	return success
End Function

Sub SetBtleStatus(status% as Integer)
	newStatus% = status% and 255
	if newStatus% <> m.btleStatus% then
		m.btleStatus% = newStatus%
		m.SetBtAdvertising()
	endif
End Sub

Function SetBtAdvertising()
	success = false
	if m.beaconsSupported then
		' We must have active bluetooth hardware
		if m.btManager.GetAdapterList().Count() > 0 then
			' If a btClientManager is active, we need to set 'connectable' flag in all beacons
			isConnectable = false
			sd = invalid
			if m.btleClientManager <> invalid and m.btleServiceId <> invalid then
				isConnectable = true
				sd = {}
				sd.uuid = m.BtleServiceId
				sd.data = m.btleClientServiceData
				sd.data.SetEntry(5, m.btleStatus%)
			endif
			' Get array of beacon data that should be active now
			beaconDataArray = [ ]
			for each beaconName in m.persistentBeacons
				beacon = m.persistentBeacons[beaconName]
				data = beacon.data
				data.connectable = isConnectable
				if sd <> invalid then
					sdlist = [sd]
					data.service_data = sdlist
				end if
				beaconDataArray.Push(data)
			next
			for each beaconName in m.presentationBeacons
				beacon = m.presentationBeacons[beaconName]
				if beacon.activate then 
					if beaconDataArray.Count() < 5 then
						data = beacon.data
						data.connectable = isConnectable
						if sd <> invalid then
							sdlist = [sd]
							data.service_data = sdlist
						end if
						beaconDataArray.Push(data)
					else
						' There is a limit of 5 beacons
						msg$ = "-- SetBtAdvertising - attempted to set more than 5 beacons - some beacons will not be started"
						m.bsp.diagnostics.PrintDebug(msg$)
						m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BEACON_START_LIMIT_EXCEEDED, msg$)
						exit for
					endif
				endif
			next
			' If we have the BTLE client manager running, and there are no beacons defined, just advertise the client service
			if beaconDataArray.Count() = 0 and sd <> invalid then
				sdlist = [sd]
				data = { mode: "custom", connectable: true, service_data: sdlist }
				beaconDataArray.Push(data)
			endif
			for each beaconName in m.persistentBeacons
				m.persistentBeacons[beaconName].isActive = false
			next
			for each beaconName in m.presentationBeacons
				m.presentationBeacons[beaconName].isActive = false
			next
			if beaconDataArray.Count() > 0 then
				beaconMsgSpec$ = stri(beaconDataArray.Count())+" beacons"
				if isConnectable then
					beaconMsgSpec$ = beaconMsgSpec$ + " (connectable, status =" + stri(m.btleStatus%) + ")"
				endif
				success = m.btManager.StartAdvertising(beaconDataArray)
				if success then
					m.bsp.diagnostics.PrintDebug("-- Set Bluetooth Advertising for"+beaconMsgSpec$)
					m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BEACON_START, "Set Bluetooth Advertising for"+beaconMsgSpec$)
					for each beaconName in m.persistentBeacons
						m.persistentBeacons[beaconName].isActive = true
					next
					for each beaconName in m.presentationBeacons
						beacon = m.presentationBeacons[beaconName]
						if beacon.activate then
							' TODO - we shouldn't set active flag for beacons over the limit of 5
							beacon.isActive = true
						endif
					next
				else
					beaconMsgSpec$ = beaconMsgSpec$ + ", reason: "+m.btManager.GetFailureReason()
					m.bsp.diagnostics.PrintDebug("-- Set Bluetooth Advertising failed for"+beaconMsgSpec$)
					m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BEACON_START_FAILED, "Set Bluetooth Advertising failed for"+beaconMsgSpec$)
				endif
			else
				success = m.btManager.StopAdvertising()
				m.bsp.diagnostics.PrintDebug("-- Stop all Bluetooth Advertising")
				m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BEACON_START, "Stop all Bluetooth Advertising")
			endif
		else if not (m.persistentBeacons.IsEmpty() and m.presentationBeacons.IsEmpty()) then
			m.bsp.diagnostics.PrintDebug("-- Set Bluetooth Advertising failed - there is no bluetooth adapter")
			m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BEACON_START_FAILED, "Set Bluetooth Advertising failed - there is no bluetooth adapter")
		endif
	endif
	return success
End Function
