content: Library "autoplugins.brs"
  
  'region Main
  Sub Main()
    
    autorunVersion$ = "10.0.49" 'Bacon in development
    customAutorunVersion$ = "10.0.0"
    
    debugParams = EnableDebugging()
    
    sysFlags = { }
    sysFlags.debugOn = debugParams.serialDebugOn
    sysFlags.systemLogDebugOn = debugParams.systemLogDebugOn
    
    modelObject = CreateObject("roDeviceInfo")
    sysInfo = { }
    sysInfo.autorunVersion$ = autorunVersion$
    sysInfo.customAutorunVersion$ = customAutorunVersion$
    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
        end if
      end if
    end if
    
    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
        end if
      end if
    end if
    
    nc = invalid
    
    videoMode = GetVideoMode()
    if type(videoMode) = "roVideoMode" then
      sysInfo.numberOfVideoPlanes = videoMode.getDecoderModes().count()
      ' HACK, due to current implementation of getDecoderModes()
      if sysInfo.deviceModel$ = "XD1034" or sysInfo.deviceModel$ = "XD1033" or sysInfo.deviceModel$ = "XD233" or sysInfo.deviceModel$ = "XD234" then
        sysInfo.numberOfVideoPlanes = 2
      endif
  
      edid = videoMode.GetEdidIdentity(true)
      UpdateEdidValues(edid, sysInfo)
      edid = invalid
      videoMode = invalid
    endif
  
    ' determine if the storage device is writable
    du = CreateObject("roStorageInfo", "./")
    if du.IsReadOnly() then
      sysInfo.storageIsWriteProtected = true
    else
      sysInfo.storageIsWriteProtected = false
    end if
    
    systemTime = CreateObject("roSystemTime")
    
    InitRemoteSnapshots(sysInfo)
    
    ' check to see whether or not the current firmware meets the minimum compatibility requirements
    versionNumber% = modelObject.GetVersionNumber()
    
    'TEDTODO - update for bacon
    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$ = "tiger" then
      minVersionNumber% = 327952
      minVersion$ = "5.1.16"
    else if sysInfo.deviceFamily$ = "lynx" then
      minVersionNumber% = 327952
      minVersion$ = "5.1.16"
      ' TODO Bacon'
    else ' no supported devices should hit this else
      minVersionNumber% = 393761
      minVersion$ = "6.2.33"
    end if
    
    if versionNumber% < minVersionNumber% then
      videoMode = CreateObject("roVideoMode")
      if type(videoMode) = "roVideoMode" then
        resX = videoMode.GetResX()
        resY = videoMode.GetResY()
        videoMode = invalid
        r = CreateObject("roRectangle", 0, resY / 2 - resY / 64, resX, resY / 32)
        twParams = { }
        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()
      end if
      
      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, "")
      end if
      
      sleep(120000)
      RebootSystem()
    end if
    
    diagnosticCodes = newDiagnosticCodes()
    
    RunBsp(sysFlags, sysInfo, diagnosticCodes, systemTime)
    
  end sub
  
  
  Function GetVideoMode() as object
    return CreateObject("roVideoMode")
  end function
  
  
  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.remoteSnapshotOrientation = registrySection.Read("remoteSnapshotOrientation")
      ' since remoteSnapshot orientation was not supported when using remoteSnapshotDisplayPortrait, use the default value in this case
      if not IsNonEmptyString(globalAA.remoteSnapshotOrientation) then
        globalAA.remoteSnapshotOrientation = "Landscape"
      end if
    end if
    
    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
          end if
        next
        n = newn
        
      end while
      
    end if
    
  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$ = ""
    end if
    
  end sub
  
  
  Sub DisplayErrorScreen(msg1$ as string, msg2$ as string, msToWaitBeforeRebooting as integer)
    
    videoMode = CreateObject("roVideoMode")
    if type(videoMode) = "roVideoMode" then
      resX = videoMode.GetResX()
      resY = videoMode.GetResY()
      videoMode = invalid
      
      r = CreateObject("roRectangle", 0, 0, resX, resY)
      twParams = { }
      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()
    end if
    
    globalAA = GetGlobalAA()
    
    if type(globalAA.bsp) = "roAssociativeArray" then
      
      if type(globalAA.bsp.msgPort) = "roMessagePort" then
        globalAA.bsp.msgPort.SetWatchdogTimeout(0)
      end if
      
      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$)
      end if
      
    end if
    
    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
  
  
  Function EnableDebugging() as object
    
    debugParams = { }
    
    debugParams.serialDebugOn = false
    debugParams.systemLogDebugOn = false
    
    syncSpec = CreateObject("roSyncSpec")
    
    if syncSpec.ReadFromFile("current-sync.json") or syncSpec.ReadFromFile("local-sync.json") or syncSpec.ReadFromFile("localToBSN-sync.json") then
      if syncSpecValueTrue(syncSpec.LookupMetadata("client", "enableSerialDebugging")) then
        debugParams.serialDebugOn = true
      end if
      if syncSpecValueTrue(syncSpec.LookupMetadata("client", "enableSystemLogDebugging")) then
        debugParams.systemLogDebugOn = true
      end if
    end if
    
    syncSpec = invalid
    
    return debugParams
    
  end function
  
  
  Sub WriteRegistrySetting(key$ as string, value$ as string)
    
    m.registrySection.Write(key$, value$)
    
  end sub
  
  
  ' BACONTODO - update registry settings as part of migrating devices?
  Function GetRegistrySettingValue(newRegistryKey$ as string, oldRegistryKey$ as string) as string
    
    value$ = m.registrySection.Read(newRegistryKey$)
    if value$ = "" then
      value$ = m.registrySection.Read(oldRegistryKey$)
    end if
    return value$
    
  end function
  
  
  Sub ReadCachedRegistrySettings()
    
    m.registrySettings = { }
  
    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"
    end if
  
    m.registrySettings.setupSplashScreenEnabled = m.registrySection.Read("susse")
    
    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.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$ = ""
    end if
    
    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.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")
    end if
    
    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.remoteDwsEnabled$ = m.registrySection.Read("rdwse")
    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"
    end if
    
    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")
  
    ' BCN-3097 open datagramSocket for autorun and bootstrap to communicate
    'Read DatagramMessagePort
    m.registrySettings.daUdpSocketPort$ = m.registrySection.Read("daUdpSocketPort")
    m.registrySettings.wsUdpSocketPort$ = m.registrySection.Read("wsUdpSocketPort")
    
    m.registrySettings.setupType = m.registrySection.Read("sut")
  
  end sub
  
  
  Sub RunBsp(sysFlags as object, sysInfo as object, diagnosticCodes as object, systemTime as object)
    
    globalAA = GetGlobalAA()
    
    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 = ""
      end if
    end if
    
    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 = GetVideoMode()
    if type(BSP.videoMode) = "roVideoMode" then
      BSP.videoMode.SetPort(msgPort)
    endif
  
    ' BCN-3097 open datagramSocket for autorun and bootstrap to communicate
    ' create object for bootstrap udp messages
    BSP.dgSocket = CreateObject("roDatagramSocket")
    if BSP.registrySettings.daUdpSocketPort$ = "" then
      BSP.registrySettings.daUdpSocketPort% = 8888
    else
      BSP.registrySettings.daUdpSocketPort% = int(val(BSP.registrySettings.daUdpSocketPort$))
    end if
    BSP.dgSocket.BindToLocalPort(BSP.registrySettings.daUdpSocketPort%)
    BSP.dgSocket.SetUserData("bootstrap")
    BSP.dgSocket.SetPort(msgPort)
  
    if BSP.registrySettings.wsUdpSocketPort$ = "" then
      BSP.registrySettings.wsUdpSocketPort% = 9999
    else
      BSP.registrySettings.wsUdpSocketPort% = int(val(BSP.registrySettings.wsUdpSocketPort$))
    end if
    
    ' 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)
    end if
    
    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)
    end if
    
    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)
    end if
    
    ' 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  
    end if
    
    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
    end if
    
    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
    end if
    
    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
    end if
    
    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
    end if
    
    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 = { }
          lwsCredentials.AddReplace(lwsUserName$, lwsPassword$)
        else
          lwsCredentials = invalid
        end if
        
      end if
      
      BSP.GetDeviceConfigurationJsonAA = { HandleEvent: GetDeviceConfigurationJson, mVar: BSP }
      BSP.GetDeviceStatusJsonAA = { HandleEvent: GetDeviceStatusJson, mVar: BSP }
      
      BSP.GetSnapshotConfigurationJsonAA = { HandleEvent: GetSnapshotConfigurationJson, mVar: BSP }
      BSP.PostSnapshotConfigurationJsonAA = { HandleEvent: PostSnapshotConfigurationJson, mVar: BSP }
      BSP.GetSnapshotHistoryJsonAA = { HandleEvent: GetSnapshotHistoryJson, mVar: BSP }
      BSP.GetSnapshotJsonAA = { HandleEvent: GetSnapshotJson, mVar: BSP }
      
      BSP.PostFileJsonAA = { HandleEvent: PostFileJson, mVar: BSP }
      BSP.PostSyncSpecJsonAA = { HandleEvent: PostSyncSpecJson, mVar: BSP }
      BSP.PostPrepareForTransferJsonAA = { HandleEvent: PostPrepareForTransferJson, mVar: BSP }
      BSP.PostCardSizeLimitsJsonAA = { HandleEvent: PostCardSizeLimitsJson, mVar: BSP }
      BSP.GetCardSizeLimitsJsonAA = { HandleEvent: GetCardSizeLimitsJson, mVar: BSP }
      
      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.GetIDInfoPageAA = { HandleEvent: GetIDInfoPage, 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.AddGetFromEvent({ url_path: "/v2/device/configuration", user_data: BSP.GetDeviceConfigurationJsonAA, passwords: lwsCredentials })
      BSP.localServer.AddGetFromEvent({ url_path: "/v2/device/status", user_data: BSP.GetDeviceStatusJsonAA })
      
      BSP.localServer.AddGetFromEvent({ url_path: "/v2/snapshot/configuration", user_data: BSP.GetSnapshotConfigurationJsonAA, passwords: lwsCredentials })
      BSP.localServer.AddPostToFormData({ url_path: "/v2/snapshot/configuration", user_data: BSP.PostSnapshotConfigurationJsonAA, passwords: lwsCredentials })
      BSP.localServer.AddGetFromEvent({ url_path: "/v2/snapshot/history", user_data: BSP.GetSnapshotHistoryJsonAA })
      BSP.localServer.AddGetFromEvent({ url_path: "/v2/snapshot", user_data: BSP.GetSnapshotJsonAA })
      
      BSP.localServer.AddPostToFormData({ url_path: "/v2/storage/configuration", user_data: BSP.PostCardSizeLimitsJsonAA, passwords: lwsCredentials })
      BSP.localServer.AddGetFromEvent({ url_path: "/v2/storage/configuration", user_data: BSP.GetCardSizeLimitsJsonAA, passwords: lwsCredentials })
      
      BSP.localServer.AddPostToFile({ url_path: "/v2/publish", destination_directory: GetDefaultDrive(), user_data: BSP.PostPrepareForTransferJsonAA, passwords: lwsCredentials })
      BSP.localServer.AddPostToFile({ url_path: "/v2/publish/file", destination_directory: GetDefaultDrive(), user_data: BSP.PostFileJsonAA, passwords: lwsCredentials })
      BSP.localServer.AddPostToFile({ url_path: "/v2/publish/sync", destination_directory: GetDefaultDrive(), user_data: BSP.PostSyncSpecJsonAA, passwords: lwsCredentials })
      
      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: "/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 })
      
      BSP.GetSynchronizerFilesToTransferAA = { HandleEvent: GetSynchronizerFilesToTransfer, mVar: BSP }
      BSP.localServer.AddPostToFile({ url_path: "/GetSynchronizerFilesToTransfer", destination_directory: GetDefaultDrive(), user_data: BSP.GetSynchronizerFilesToTransferAA })
      
      BSP.SynchronizerFilePostedAA = { HandleEvent: SynchronizerFilePosted, mVar: BSP }
      BSP.localServer.AddPostToFile({ url_path: "/SynchronizerUploadFile", destination_directory: GetDefaultDrive(), user_data: BSP.SynchronizerFilePostedAA, passwords: lwsCredentials })
      
      BSP.RestartScriptAA = { HandleEvent: RestartScript, mVar: BSP }
      BSP.localServer.AddGetFromEvent({ url_path: "/RestartScript", user_data: BSP.RestartScriptAA })
      
      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"
      end if
      
      service = { }
      
      service.AddReplace("name", "BRIGHTSIGN-LWS-SERVICE")
      service.AddReplace("type", "_http._tcp")
      service.AddReplace("port", 8080)
      service.AddReplace("_functionality", BSP.lwsConfig$)
      service.AddReplace("_serialnumber", sysInfo.deviceUniqueID$)
      service.AddReplace("_unitname", unitName$)
      service.AddReplace("_unitnamingmethod", unitNamingMethod$)
      service.AddReplace("_unitdescription", unitDescription$)
      BSP.advert = CreateObject("roNetworkAdvertisement", service)
      if BSP.advert = invalid then
        stop
      end if
      
    else
      
      BSP.lwsConfig$ = "none"
      
    end if
  
    ' 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")
    
    localSyncSpecFileName = "local-sync.json"
    localSetupToStandaloneSyncSpecFileName = GetLocalSetupToStandaloneSyncSpecFileName()
    
    if localCurrentSync.ReadFromFile(localSyncSpecFileName) or localCurrentSync.ReadFromFile(localSetupToStandaloneSyncSpecFileName) 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$)
        end if
      end if
      
      BSP.GetSupportedFeatures()
      
      if metadata.DoesExist("obfuscatedPassphrase") then
        obfuscatedPassphrase$ = localCurrentSync.LookupMetadata("client", "obfuscatedPassphrase")
        deviceCustomization = CreateObject("roDeviceCustomization")
        deviceCustomization.StoreObfuscatedEncryptionKey("AesCtrHmac", obfuscatedPassphrase$)
        BSP.contentEncrypted = true
      end if
      
    end if
    
    ' networking is considered active if current-sync.json is present.
    networkedCurrentSyncValid = false
    networkedCurrentSync = CreateObject("roSyncSpec")
    
    ok = networkedCurrentSync.ReadFromFile("current-sync.json") or networkedCurrentSync.ReadFromFile("localToBSN-sync.json")
    
    if ok 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 deviceCustomization.IsEncryptionKeyPresent("AesCtrHmac") then
        BSP.contentEncrypted = true
      end if
      
      BSP.SetPerFileEncryptionStatus(networkedCurrentSync)
      
      BSP.networkingHSM.Initialize()
      
    else
      
      BSP.networkingHSM = invalid
      
      BSP.networkingActive = false
      
    end if
    
    ' determine and set file paths for global files
    globalAA.autoscheduleFilePath$ = GetAutoscheduleFilePath(BSP)
    if globalAA.autoscheduleFilePath$ = "" then stop
    
    globalAA.boseProductsFilePath$ = GetPoolFilePath(BSP.assetPoolFiles, "PartnerProducts.json")
  
    'move define of this function to apply to all setup types
    BSP.GetDataTransferEnabledFromRegistry = GetDataTransferEnabledFromRegistry
    
    ' initialize logging parameters
    playbackLoggingEnabled = false
    eventLoggingEnabled = false
    diagnosticLoggingEnabled = false
    stateLoggingEnabled = false
    variableLoggingEnabled = false
    uploadLogFilesAtBoot = false
    uploadLogFilesAtSpecificTime = false
    uploadLogFilesTime% = 0
    
    if networkedCurrentSyncValid then
      if syncSpecValueTrue(networkedCurrentSync.LookupMetadata("client", "playbackLoggingEnabled")) then playbackLoggingEnabled = true
      if syncSpecValueTrue(networkedCurrentSync.LookupMetadata("client", "eventLoggingEnabled")) then eventLoggingEnabled = true
      if syncSpecValueTrue(networkedCurrentSync.LookupMetadata("client", "diagnosticLoggingEnabled")) then diagnosticLoggingEnabled = true
      if syncSpecValueTrue(networkedCurrentSync.LookupMetadata("client", "stateLoggingEnabled")) then stateLoggingEnabled = true
      if syncSpecValueTrue(networkedCurrentSync.LookupMetadata("client", "variableLoggingEnabled")) then variableLoggingEnabled = true
      if syncSpecValueTrue(networkedCurrentSync.LookupMetadata("client", "uploadLogFilesAtBoot")) then uploadLogFilesAtBoot = true
      if syncSpecValueTrue(networkedCurrentSync.LookupMetadata("client", "uploadLogFilesAtSpecificTime")) 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 syncSpecValueTrue(localCurrentSync.LookupMetadata("client", "playbackLoggingEnabled")) then
        playbackLoggingEnabled = true
      else if syncSpecValueFalse(localCurrentSync.LookupMetadata("client", "playbackLoggingEnabled")) then
        playbackLoggingEnabled = false
      else if BSP.registrySettings.playbackLoggingEnabled = "yes" then
        playbackLoggingEnabled = true
      end if
      
      if syncSpecValueTrue(localCurrentSync.LookupMetadata("client", "eventLoggingEnabled")) then
        eventLoggingEnabled = true
      else if syncSpecValueFalse(localCurrentSync.LookupMetadata("client", "eventLoggingEnabled")) then
        eventLoggingEnabled = false
      else if BSP.registrySettings.eventLoggingEnabled = "yes" then
        eventLoggingEnabled = true
      end if
      
      if syncSpecValueTrue(localCurrentSync.LookupMetadata("client", "diagnosticLoggingEnabled")) then
        diagnosticLoggingEnabled = true
      else if syncSpecValueFalse(localCurrentSync.LookupMetadata("client", "diagnosticLoggingEnabled")) then
        diagnosticLoggingEnabled = false
      else if BSP.registrySettings.diagnosticLoggingEnabled = "yes" then
        diagnosticLoggingEnabled = true
      end if
      
      if syncSpecValueTrue(localCurrentSync.LookupMetadata("client", "stateLoggingEnabled")) then
        stateLoggingEnabled = true
      else if syncSpecValueFalse(localCurrentSync.LookupMetadata("client", "stateLoggingEnabled")) then
        stateLoggingEnabled = false
      else if BSP.registrySettings.stateLoggingEnabled = "yes" then
        stateLoggingEnabled = true
      end if
      
      if syncSpecValueTrue(localCurrentSync.LookupMetadata("client", "variableLoggingEnabled")) then
        variableLoggingEnabled = true
      else if syncSpecValueFalse(localCurrentSync.LookupMetadata("client", "variableLoggingEnabled")) then
        variableLoggingEnabled = false
      else if BSP.registrySettings.variableLoggingEnabled = "yes" then
        variableLoggingEnabled = true
      end if
      
      uploadLogFilesTime$ = ""
      
      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$
    end if
    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()
    end if
    
    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()
    end if
    
    ' 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")
    end if
    
    ' limit pool sizes
    BSP.limitStorageSpace = false
    BSP.SetPoolSizes(activeSyncSpec)
    
    ' Read and parse BoseProducts.json
    BSP.boseProductSpecs = ReadBoseProductsFile()
    
    ' 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()
    
    ' NOTE - since minimum FW is required for bacon, all features that were supported in BA Classic are supported in this version of FW
    featureMinRevsFilePath$ = GetPoolFilePath(m.assetPoolFiles, "featureMinRevs.json")
    m.featureMinRevs = ParseFeatureMinRevs(featureMinRevsFilePath$)
    
    m.setSyncDomainSupported = true
    m.forceResolutionSupported = true
    m.showHideVideoZoneSupported = true
    m.bypassProxySupported = true
    m.contentEncryptionSupported = true
    m.htmlSetTransformSupported = true
    m.httpWidgetGetUserAgentSupported = true
    m.mosaicModeSupported = true
    m.beaconsSupported = true
    m.btleSupported = true
    m.fullResolutionGraphicsSupported = true
    
  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)
      end if
    next
    
  end sub
  
  
  Function ParseFeatureMinRevs(path$ as string)
    
    featureMinRevs = { }
    
    featureMinRevs$ = ReadAsciiFile(path$)
    
    if len(featureMinRevs$) > 0 then
      
      publishedFeatureMinRevs = ParseJson(featureMinRevs$)
      
      ' verify that this is a valid FeatureMinRevs Json file
      if type(publishedFeatureMinRevs.FeatureMinRevs) <> "roAssociativeArray" then print "Invalid FeatureMinRevs JSON file - name not FeatureMinRevs" : stop
      if not IsString(publishedFeatureMinRevs.FeatureMinRevs.version) then print "Invalid FeatureMinRevs JSON file - version not found" : stop
      
      for each featureAA in publishedFeatureMinRevs.FeatureMinRevs.features
        featureName$ = featureAA.name
        minRev$ = featureAA.minFWRev
        featureMinRevs.AddReplace(featureName$, minRev$)
      next
      
    end if
    
    return featureMinRevs
    
  end function
  
  
  Sub SetPoolSizes(syncSpec as object) as object
  
    limitStorageSpace = syncSpecValueTrue(syncSpec.LookupMetadata("client", "limitStorageSpace"))
    if limitStorageSpace then
      spaceLimitedByAbsoluteSize = syncSpecValueTrue(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")
    end if
    
    if limitStorageSpace then
      
      if not spaceLimitedByAbsoluteSize 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
        end if
        
        ' size for html indexed db
        htmlIndexedDBSizeLimitPercentage% = 0
        maximumHTMLIndexedDBPoolSizeMB% = 0
        if htmlIndexedDBSizeLimitPercentage <> "" then
          htmlIndexedDBSizeLimitPercentage% = int(val(htmlIndexedDBSizeLimitPercentage))
          maximumHTMLIndexedDBPoolSizeMB% = htmlIndexedDBSizeLimitPercentage% * totalCardSizeMB% / 100
        end if
        
      else
        
        maximumPublishedDataPoolSizeMB% = int(val(publishedDataSizeLimitMB))
        maximumDynamicDataPoolSizeMB% = int(val(dynamicDataSizeLimitMB))
        maximumHTMLDataPoolSizeMB% = int(val(htmlDataSizeLimitMB))
        
        maximumHTMLLocalStoragePoolSizeMB% = 0
        if htmlLocalStorageSizeLimitMB <> "" then
          maximumHTMLLocalStoragePoolSizeMB% = int(val(htmlLocalStorageSizeLimitMB))
        end if
        
        maximumHTMLIndexedDBPoolSizeMB% = 0
        if htmlIndexedDBSizeLimitMB <> "" then
          maximumHTMLIndexedDBPoolSizeMB% = int(val(htmlIndexedDBSizeLimitMB))
        end if
        
      end if
      
      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
        end if
        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
        end if
      end if
      
      if maximumHTMLLocalStoragePoolSizeMB% > 0 then
        
        ' max int is  2147483647 => <2048 MB
        if maximumHTMLLocalStoragePoolSizeMB% >= 2048 then
          maximumHTMLLocalStoragePoolSizeMB% = 2047
        end if
        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
        end if
      end if
      
      if maximumHTMLIndexedDBPoolSizeMB% > 0 then
        
        ' max int is  2147483647 => <2048 MB
        if maximumHTMLIndexedDBPoolSizeMB% >= 2048 then
          maximumHTMLIndexedDBPoolSizeMB% = 2047
        end if
        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
        end if
      end if
      
    else
      ' clear prior settings
      ok = m.assetPool.SetMaximumPoolSizeMegabytes( - 1)
      ' if not ok ??
      ok = m.feedPool.SetMaximumPoolSizeMegabytes( - 1)
      ' if not ok ??
      
    end if
    
  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
      end if
      return dataTransferEnabled
      
    end function
    
    
    Function GetDataTransferEnabledFromRegistry(registryKey$ as string) as boolean
      
      if m.registrySettings.Lookup(registryKey$) = "False" then
        registryValue = false
      else
        registryValue = true
      end if
      
      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
        end if
      end if
      
      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
      end if
      BSP.registrySection = registrySection
      
      BSP.systemTime = systemTime
      
      BSP.diagnostics = newDiagnostics(sysFlags)
      
      BSP.Restart = Restart
      BSP.StartPlayback = StartPlayback
      
      BSP.newLogging = newLogging
      BSP.logging = BSP.newLogging()
      BSP.LogActivePresentation = LogActivePresentation
      
      BSP.SetTouchRegions = SetTouchRegions
      BSP.InitializeTouchScreen = InitializeTouchScreen
      BSP.AddRectangularTouchRegion = AddRectangularTouchRegion
      
      BSP.ExecuteMediaStateCommands = ExecuteMediaStateCommands
      BSP.ExecuteTransitionCommands = ExecuteTransitionCommands
      
      BSP.ExecuteCmd = ExecuteCmd
      BSP.ExecuteSendWssCommand = ExecuteSendWssCommand
      BSP.ExecuteSwitchPresentationCommand = ExecuteSwitchPresentationCommand
      BSP.MatchWssEvent = MatchWssEvent
      BSP.ExecuteGpioOnCommand = ExecuteGpioOnCommand
      BSP.ExecuteGpioOffCommand = ExecuteGpioOffCommand
      BSP.ExecuteGpioSetStateCommand = ExecuteGpioSetStateCommand
      
      BSP.ExecuteSerialSendStringCommand = ExecuteSerialSendStringCommand
      BSP.ExecuteSendSerialBlockCommand = ExecuteSendSerialBlockCommand
      BSP.ExecuteSendSerialByteCommand = ExecuteSendSerialByteCommand
      BSP.ExecuteSendSerialBytesCommand = ExecuteSendSerialBytesCommand
      BSP.ExecuteSendUDPCommand = ExecuteSendUDPCommand
      BSP.ExecuteSendUDPBytesCommand = ExecuteSendUDPBytesCommand
      BSP.ExecuteSendProntoIRRemote = ExecuteSendProntoIRRemote
      BSP.ExecuteSendIRRemoteCommand = ExecuteSendIRRemoteCommand
      BSP.ExecuteSendBLC400OutputCommand = ExecuteSendBLC400OutputCommand
      BSP.ExecuteSendBPOutputCommand = ExecuteSendBPOutputCommand
      BSP.ExecuteSynchronizeCommand = ExecuteSynchronizeCommand
      BSP.ExecuteSendZoneMessageCommand = ExecuteSendZoneMessageCommand
      BSP.ExecuteSendPluginMessageCommand = ExecuteSendPluginMessageCommand
      BSP.ExecuteResizeZoneCommand = ExecuteResizeZoneCommand
      BSP.ExecuteHideZoneCommand = ExecuteHideZoneCommand
      BSP.ExecuteShowZoneCommand = ExecuteShowZoneCommand
      BSP.ExecutePauseZonePlaybackCommand = ExecutePauseZonePlaybackCommand
      BSP.ExecuteResumeZonePlaybackCommand = ExecuteResumeZonePlaybackCommand
      BSP.ExecuteInternalSynchronizeCommand = ExecuteInternalSynchronizeCommand
      BSP.ExecuteCecSendStringCommand = ExecuteCecSendStringCommand
      BSP.ExecuteCecPhilipsSetVolumeCommand = ExecuteCecPhilipsSetVolumeCommand
      BSP.ExecutePauseCommand = ExecutePauseCommand
      BSP.ExecuteSetVariableCommand = ExecuteSetVariableCommand
      BSP.ExecuteBeaconStartCommand = ExecuteBeaconStartCommand
      BSP.ExecuteBeaconStopCommand = ExecuteBeaconStopCommand
      
      BSP.ExecuteSetAllAudioOutputsCommand = ExecuteSetAllAudioOutputsCommand
  
      BSP.EventLoop = EventLoop
      
      BSP.SetAudioMode = SetAudioMode
      BSP.UnmuteAllAudio = UnmuteAllAudio
      BSP.UnmuteAudioConnector = UnmuteAudioConnector
      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.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.CecSetSourceToBrightSign = CecSetSourceToBrightSign
      BSP.CecPhilipsSetVolume = CecPhilipsSetVolume
      BSP.SendCecCommand = SendCecCommand
      
      BSP.WaitForSyncResponse = WaitForSyncResponse
      
      BSP.GetAutoschedule = GetAutoschedule
      
      BSP.GetNonPrintableKeyboardCode = GetNonPrintableKeyboardCode
      BSP.InitializeNonPrintableKeyboardCodeList = InitializeNonPrintableKeyboardCodeList
      
      BSP.ConfigureIRRemote = ConfigureIRRemote
  
      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.FreeSpaceOnDrive = FreeSpaceOnDriveJson
      
      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.UpdateEdidUserVariables = UpdateEdidUserVariables
      
      BSP.GetAttachedFiles = GetAttachedFiles
      
      BSP.PostponeRestart = PostponeRestart
      BSP.ProcessMediaEndEvent = ProcessMediaEndEvent
      
      BSP.SetPoolSizes = SetPoolSizes
      
      BSP.InitiateRemoteSnapshotTimer = InitiateRemoteSnapshotTimer
      BSP.RemoveRemoteSnapshotTimer = RemoveRemoteSnapshotTimer
      
      BSP.NetworkingIsActive = NetworkingIsActive
      
      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
      
      BSP.GetRuntimeUsbConnector = GetRuntimeUsbConnector
      BSP.GetSpecifiedConnector = GetSpecifiedConnector
  
      ' json functions
      BSP.jsonAutoschedule = jsonAutoschedule
      
      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
        end if
        
      else if mVar.deviceWebPageFilePath$ <> ""
        
        webPageContents$ = ReadAsciiFile(mVar.deviceWebPageFilePath$)
        e.SetResponseBodyString(webPageContents$)
        if not e.SendResponse(200) then
          stop
        end if
        
      else
        
        e.SetResponseBodyString("")
        if not e.SendResponse(404) then
          stop
        end if
        
      end if
      
    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 if
      
    end sub
    
    
    Function GetCategoryFromSection(sectionName$ as string, categoryName$ as string) as integer
      
      sectionId% = m.GetDBSectionId(sectionName$)
      if sectionId% < 0 then
        return -1
      end if
      
      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$)
        end if
        
        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
        end if
        
      end if
      
      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
          end if
        next
      end if
      
      e.AddResponseHeader("Location", e.GetRequestHeader("Referer"))
      if not e.SendResponse(302) then stop
      
      if userVariablesUpdated then
        userVariablesChanged = { }
        userVariablesChanged["EventType"] = "USER_VARIABLES_UPDATED"
        mVar.msgPort.PostMessage(userVariablesChanged)
        
        ' Notify controlling devices to refresh
        mVar.SendUDPNotification("refresh")
      end if
      
    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")
      end if
      
      elem = root.AddElement("snapshotsAvailable")
      globalAA = GetGlobalAA()
      if globalAA.listOfSnapshotFiles.Count() > 0 then
        elem.SetBody("yes")
      else
        elem.SetBody("no")
      end if
      
    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
                  end if
                  
                  if not udpEvents.DoesExist(udpLabel$) then
                    udpEvents.AddReplace(udpLabel$, newUDPItem(udpLabel$, udpEventName))
                  end if
                  
                end if
                
                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
                        end if
                        if export then
                          udpLabel$ = playFileKey
                          if entry.DoesExist("label$") then
                            udpLabel$ = entry["label$"]
                          end if
                          if not udpEvents.DoesExist(udpLabel$) then
                            udpEvents.AddReplace(udpLabel$, newUDPItem(udpLabel$, playFileKey))
                          end if
                        end if
                      next
                    end if
                  end if
                end if
                
              next
              
            end if
            
          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
        
      end if
      
    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$)
      end if
      
      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)
          end if
        end if
      next
      
      if mVar.sign.alphabetizeVariableNames then
        BubbleSortUserVariables(filteredUserVariables, "name$")
      else
        BubbleSortUserVariables(filteredUserVariables, "position%")
      end if
      
      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
            end if
          next
          n = newn
          
        end while
        
      end if
      
    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 = { }
      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 = { }
      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
      end if
      
      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$)
            
            ' snapshotDisplayPortrait changed to snapshotOrientation for BCN-6961
            ' TO DO: change the request sender to expect snapshotOrientation instead of snapshotDisplayPortrait
            elem = root.AddElement("snapshotOrientation")
            elem.AddAttribute("label", "Snapshot Orientation")
            if IsNonEmptyString(mVar.globalAA.remoteSnapshotOrientation) then
              elem.SetBody(mVar.globalAA.remoteSnapshotOrientation)
            else
              elem.SetBody("Landscape")
            end if
          end if
        end if
      end if
      
      '    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
      
      e.AddResponseHeader("Content-type", "text/html; charset=utf-8")
      
      deviceIdWebPageFilePath$ = "sys:/web-client/Default_DeviceWebPage/_deviceIdWebPage.html"
      ret = e.SetResponseBodyFile(deviceIdWebPageFilePath$)
      if not ret then
        print e.GetFailureReason()
        stop
      end if
      
      ret2 = e.SendResponse(200)
      if not ret2 then
        print e.GetFailureReason()
        stop
      end if
      
    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)
          end if
          
        end if
        
      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)
      end if
      if globalAA.remoteSnapshotInterval <> invalid then
        root.AddAttribute("Interval", StripLeadingSpaces(stri(globalAA.remoteSnapshotInterval)))
      end if
      if globalAA.remoteSnapshotMaxImages <> invalid then
        root.AddAttribute("MaxImages", StripLeadingSpaces(stri(globalAA.remoteSnapshotMaxImages)))
      end if
      if globalAA.remoteSnapshotJpegQualityLevel <> invalid then
        root.AddAttribute("Quality", StripLeadingSpaces(stri(globalAA.remoteSnapshotJpegQualityLevel)))
      end if
      if mVar.videoMode <> invalid then
        root.AddAttribute("ResX", StripLeadingSpaces(stri(mVar.videoMode.GetOutputResX())))
        root.AddAttribute("ResY", StripLeadingSpaces(stri(mVar.videoMode.GetOutputResY())))
      end if
      
      if IsNonEmptyString(globalAA.remoteSnapshotOrientation) then
        ' DisplayPortrait changed to Orientation for BCN-6961
        ' TO DO: change the request sender to expect Orientation instead of DisplayPortrait
        root.AddAttribute("Orientation", globalAA.remoteSnapshotOrientation)
      else
        root.AddAttribute("Orientation", "Landscape")
      end if
      ' 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")
      end if
      
      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
        end if
      end if
      
      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
      end if
      
      globalAA = GetGlobalAA()
      
      ' remember current settings
      currentEnableRemoteSnapshot = globalAA.enableRemoteSnapshot
      currentRemoteSnapshotInterval = globalAA.remoteSnapshotInterval
      currentRemoteSnapshotMaxImages = globalAA.remoteSnapshotMaxImages
      currentRemoteSnapshotJpegQualityLevel = globalAA.remoteSnapshotJpegQualityLevel
      currentRemoteSnapshotOrientation = globalAA.remoteSnapshotOrientation
      
      ' set global values from form data
      args = e.GetFormData()
      if args.enableRemoteSnapshot <> invalid then
        globalAA.enableRemoteSnapshot = GetBoolFromString(args.enableRemoteSnapshot, false)
      end if
      if args.remoteSnapshotInterval <> invalid then
        globalAA.remoteSnapshotInterval = int(val(args.remoteSnapshotInterval))
      end if
      if args.remoteSnapshotMaxImages <> invalid then
        globalAA.remoteSnapshotMaxImages = int(val(args.remoteSnapshotMaxImages))
      end if
      if args.remoteSnapshotJpegQualityLevel <> invalid then
        globalAA.remoteSnapshotJpegQualityLevel = int(val(args.remoteSnapshotJpegQualityLevel))
      end if
      ' TO DO: change the request sender to send remoteSnapshotOrientation instead of remoteSnapshotDisplayPortrait
      if IsNonEmptyString(args.remoteSnapshotOrientation) then
        globalAA.remoteSnapshotOrientation = args.remoteSnapshotOrientation
      end if
      
      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("remoteSnapshotOrientation", globalAA.remoteSnapshotOrientation)
      
    end if
    
    if globalAA.enableRemoteSnapshot then
      
      DeleteExcessSnapshots(globalAA)
      
      if not currentEnableRemoteSnapshot or globalAA.remoteSnapshotInterval <> currentRemoteSnapshotInterval then
        mVar.InitiateRemoteSnapshotTimer()
      end if
      
    else if currentEnableRemoteSnapshot then
      
      mVar.RemoveRemoteSnapshotTimer()
      
    end if
    
    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
        end if
      next
    end if
    
    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.")
    end if
    
    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")
    end if
    
    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.")
    end if
    
    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)
    end if
    
    e.AddResponseHeader("Content-type", "text/plain")
    e.SetResponseBodyString("OK")
    e.SendResponse(200)
    
  end function
  
  'endregion
  
  'region Sync
  
  Function FreeSpaceOnDriveJson() as object
  
    proposedPublishFiles$ = ReadAsciiFile("filesToPublish.json")
    ' files that need to be copied by BrightAuthor
    actualPublishFiles = { }
    ' files that can be deleted to make room for more content
    deletionCandidates = { }
    
    if len(proposedPublishFiles$) > 0 then
      
      currentPoolFiles = GetContentFiles("/pool/")
      for each file in currentPoolFiles
        deletionCandidates.AddReplace(file, currentPoolFiles.Lookup(file))
      next
      
      proposedPublishFiles = ParseJson(proposedPublishFiles$)
  
      ' determine total space required
      totalSpaceRequired! = 0
      for each file in proposedPublishFiles.file
        poolFileName$ = file.poolFileName
        o = deletionCandidates.Lookup(poolFileName$)
        if not IsString(o) then ' file is not already on the card
          fileItem = { }
          fileItem.AddReplace("poolFileName", file.poolFileName)
          fileItem.AddReplace("fileName", file.fileName)
          fileItem.AddReplace("filePath", file.filePath)
          fileItem.AddReplace("hash", file.hash)
          fileItem.AddReplace("size", file.size)
          
          actualPublishFiles.AddReplace(poolFileName$, fileItem) ' files that need to be copied to the card
          fileSize% = fileItem.size
          totalSpaceRequired! = totalSpaceRequired! + fileSize%
        end if
      next
  
      proposedPublishFiles = 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% / 100.0) * totalCardSize
        end if
        
        ' 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 currentPoolFiles
          relativePath$ = currentPoolFiles.Lookup(file)
          fullPath$ = relativePath$ + file
          ' get size of file
          size% = GetFileSize(fullPath$)
          totalSizeOfPoolAfterCopy! = totalSizeOfPoolAfterCopy! + size%
        next
      
      end if
    
      deleteUnneededFiles = false
      if totalFreeSpace! < totalSpaceRequired! then
        deleteUnneededFiles = true
      end if
      if m.limitStorageSpace then
        if totalSizeOfPoolAfterCopy! > budgetedMaximumPoolSize then
          deleteUnneededFiles = true
        end if
      end if
    
      if deleteUnneededFiles then
        
        ' parse local-sync.json - remove its files from deletionCandidates
        localSync$ = ReadAsciiFile("local-sync.json")
        if len(localSync$) > 0 then
          localSync = ParseJson(localSync$)
          for each file in localSync.files.download
            hashValue$ = file.hash.hex
            hashMethod$ = file.hash.method
            fileName$ = hashMethod$ + "-" + hashValue$
            fileExisted = deletionCandidates.Delete(fileName$)
          next
        end if
  
        ' remove 'new' files from deletionCandidates
        proposedPublishFiles = ParseJson(proposedPublishFiles$)
        for each file in proposedPublishFiles.file
          poolFileName$ = file.poolFileName
          fileExisted = deletionCandidates.Delete(poolFileName$)
        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%
          end if
          
          deletionCandidates.Delete(fileToDelete)
          DeleteFile(pathOnCard$)
  
          continueDeleting = false
          
          if m.limitStorageSpace then
            
            if totalSizeOfPoolAfterCopy! > budgetedMaximumPoolSize then
              continueDeleting = true
            end if
            
          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
            end if
            
          end if
          
          if not continueDeleting then
            return actualPublishFiles
          end if
        
        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 -1
        
      end if
      
    end if
  
    return actualPublishFiles
  
  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
    end if
    
    return size
    
  end function
  
  
  Function ConvertToInt(str as string) as integer
    
    if str = "" then
      return 0
    end if
    
    return int(val(str))
    
  end function
  
  Sub SyncSpecPosted(userData as object, e as object)
    
    EVENT_REALIZE_SUCCESS = 101
    
    mVar = userData.mVar
    
    '    print "respond to SyncSpecPosted request"
    
    newSyncFileName$ = "new-sync.json"
    
    if ReadAsciiFile("local-sync.json") <> "" then
      oldSyncFileName$ = "local-sync.json"
    end if
    
    autoScheduleFileName$ = "autoschedule.json"
    
    MoveFile(e.GetRequestBodyFile(), newSyncFileName$)
    e.SetResponseBodyString("RECEIVED")
    e.SendResponse(200)
    
    oldSync = CreateObject("roSyncSpec")
    ok = oldSync.ReadFromFile(oldSyncFileName$)
    if not ok then stop
    
    newSync = CreateObject("roSyncSpec")
    ok = newSync.ReadFromFile(newSyncFileName$)
    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
      end if
      
      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(newSyncFileName$)
        newSync = invalid
        return
      else
        rebootRequired = true
      end if
      
    end if
    
    jsonSyncSpec$ = newSync.WriteToString({ format : "json" })
    ok = WriteAsciiFile("local-sync.json", jsonSyncSpec$)
    if not ok then stop
  
    ' cause fsync
    CreateObject("roReadFile", "local-sync.json")
    
    if rebootRequired then
      mVar.diagnostics.PrintDebug("### new script or upgrade found - reboot")
      RebootSystem()
    end if
    
    globalAA = GetGlobalAA()
    globalAA.autoscheduleFilePath$ = GetPoolFilePath(mVar.assetPoolFiles, autoScheduleFileName$)
    globalAA.boseProductsFilePath$ = GetPoolFilePath(mVar.assetPoolFiles, "PartnerProducts.json")
    if globalAA.autoscheduleFilePath$ = "" then stop
    
    metadata = newSync.GetMetadata("client")
    if metadata.DoesExist("obfuscatedPassphrase") then
      obfuscatedPassphrase$ = newSync.LookupMetadata("client", "obfuscatedPassphrase")
      deviceCustomization = CreateObject("roDeviceCustomization")
      deviceCustomization.StoreObfuscatedEncryptionKey("AesCtrHmac", obfuscatedPassphrase$)
      mVar.contentEncrypted = true
    end if
    
    mVar.SetPoolSizes(newSync)
    
    DeleteFile(newSyncFileName$)
    newSync = invalid
    
    ' send internal message to prepare for restart
    prepareForRestartEvent = { }
    prepareForRestartEvent["EventType"] = "PREPARE_FOR_RESTART"
    mVar.msgPort.PostMessage(prepareForRestartEvent)
    
    ' send internal message indicating that new content is available
    contentUpdatedEvent = { }
    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)
    end if
    
  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
    end if
    
  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)
        end if
      next
    else
      buttonNumber% = int(val(buttonNumber$))
      if type(m.bpSM[buttonPanelIndex%, buttonNumber%]) = "roAssociativeArray" then
        m.bpSM[buttonPanelIndex%, buttonNumber%].ConfigureButton(bpSpec)
      end if
    end if
    
  end sub
  
  
  Function NewGlobalVariables() as object
    
    globalVariables = { }
    globalVariables.language$ = "eng"
    
    return globalVariables
    
  end function
  
  
  Function jsonParseScriptPlugin(scriptPluginSpec as object) as object
    
    scriptPlugin = { }
    scriptPlugin.name$ = scriptPluginSpec.name
    scriptPlugin.plugin = invalid
    
    return scriptPlugin
    
  end function
  
  
  Function jsonParseVideoModePlugin(videoModePluginSpec As Object) As Object
  
  	videoModePlugin = {}
  	videoModePlugin.name$ = videoModePluginSpec.name
  	videoModePlugin.functionName$ = videoModePluginSpec.functionName
  
  	return videoModePlugin
  
  End Function
  
  
  Function jsonParseParserPlugin(parserPluginSpec as object) as object
    
    parserPlugin = { }
    parserPlugin.name$ = parserPluginSpec.name
    parserPlugin.parseFeedFunction$ = parserPluginSpec.feedParserFunctionName
    parserPlugin.parseUVFunction$ = parserPluginSpec.userVariableParserFunctionName
    parserPlugin.userAgentFunction$ = parserPluginSpec.userAgentParserFunctionName
    
    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
        end if
      next
    end if
    return invalid
    
  end function
  
  
  Function newHTMLSite(bsp as object, htmlSiteDescription as object) as object
    
    htmlSite = { }
    
    htmlSite.name$ = htmlSiteDescription.name$
    htmlSite.isNodeServer = htmlSiteDescription.enableNode
    htmlSite.queryString = newParameterValue(bsp, htmlSiteDescription.queryString)
  
    if htmlSiteDescription.contentIsLocal then
      htmlSite.prefix$ = htmlSiteDescription.prefix$
      htmlSite.filePath$ = htmlSiteDescription.filePath$
    else
      htmlSite.url = newParameterValue(bsp, htmlSiteDescription.url)
    end if
    
    htmlSite.contentIsLocal = htmlSiteDescription.contentIsLocal
    
    return htmlSite
    
  end function
  
  Function newLiveDataFeed(bsp as object, liveDataFeedDescription as object) as object
    
    liveDataFeed = InitializeLiveDataFeedParameters(bsp)
  
    liveDataFeed.id$ = liveDataFeedDescription.id$
    
    liveDataFeed.isLiveBSNDataFeed = liveDataFeedDescription.isLiveBSNDataFeed
    liveDataFeed.isDynamicPlaylist = liveDataFeedDescription.isDynamicPlaylist
    liveDataFeed.isLiveMediaFeed = liveDataFeedDescription.isLiveMediaFeed
    
    liveDataFeed.url = newParameterValue(bsp, liveDataFeedDescription.urlPV)
    
    parserPluginName = liveDataFeedDescription.parserPluginName
    uvParserPluginName = liveDataFeedDescription.uvParserPluginName
    
    ' 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$
        end if
      end if
    end if
    
    liveDataFeed.updateInterval% = liveDataFeedDescription.updateInterval%
    liveDataFeed.useHeadRequest = liveDataFeedDescription.useHeadRequest
    liveDataFeed.usage$ = lcase(liveDataFeedDescription.usage$)
    liveDataFeed.autoGenerateUserVariables = liveDataFeedDescription.autoGenerateUserVariables
    liveDataFeed.userVariableAccess$ = liveDataFeedDescription.userVariableAccess$
    
    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.id$ = 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.id$ = ""
    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.ParseJSONRSS = ParseJSONRSS
    liveDataFeed.ParseRSSWithParserPlugin = ParseRSSWithParserPlugin
    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
    liveDataFeed.ParseCustomContentFormat = ParseCustomContentFormat
    
  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)
          end if
          part2 = ""
          if (len(output) - index) > 0 then
            part2 = mid(output, index + 1, len(output) - index)
          end if
          output = part1 + "-" + part2
        end if
      end while
    next
    return output
  end function
  
  
  Sub Restart(presentationName$ as string)
    
    globalAA = GetGlobalAA()
    
    m.restartPendingMediaEnd = false
    m.dontChangePresentationUntilMediaEndEventReceived = false
    
    m.currentUserVariables = { }
    m.liveDataFeeds = { }
    m.presentations = { }
    m.htmlSites = { }
    m.beacons = { }
    m.scriptPlugins = []
    m.videoModePlugins = []
    m.parserPlugins = []
    m.additionalPublishedFiles = []
    
    m.bypassProxyHosts = []
    
    m.IsSyncMaster = false
    m.IsSyncSlave = false
    
    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
      
      autoscheduleFileContents$ = ReadAsciiFile(globalAA.autoscheduleFilePath$)
      
      if autoscheduleFileContents$ = "" then
        stop
      end if
      
      schedule = m.GetAutoschedule(globalAA.autoscheduleFilePath$)
      
      if type(schedule.activeScheduledEvent) = "roAssociativeArray" then
        autoplayPath$ = schedule.autoplayPoolFile$
      else
        autoplayPath$ = ""
      end if
      
      m.schedule = schedule
      
      if (autoplayPath$ <> "") then
        presentationName$ = schedule.activeScheduledEvent.presentationName$
      end if
      
    else
      
      autoplayFileName$ = GetAutoplayFileName(presentationName$)
      autoplayPath$ = m.assetPoolFiles.GetPoolFilePath(autoplayFileName$)
      m.activePresentation$ = presentationName$
      
    end if
    
    if (autoplayPath$ <> "") then
      
      brightAuthor = GetAutoplay(autoplayPath$)
      
      m.diagnostics.PrintTimestamp()
      m.diagnostics.PrintDebug("### create sign object from autoplay")
      
      version% = GetAutoplayVersion(brightAuthor)
      
      sign = newSign(brightAuthor, m.globalVariables, m, m.msgPort, m.controlPort, version%)
      
      m.LogActivePresentation()
      
    else
      sign = invalid
      if m.registrySettings.setupSplashScreenEnabled = "True" then
        videoMode = CreateObject("roVideoMode")
        if type(videoMode) = "roVideoMode" then
          m.deviceSetupSplashScreen = SetDeviceSetupSplashScreen("lfn", m.msgPort)
        end if
      else
        ' 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 = GetVideoMode()
        if type(videoMode) = "roVideoMode" then
          videoMode.SetBackgroundColor(color_spec%)
          videoMode = invalid
        endif
      end if
    end if
    
    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
        end if
        if type(zoneHSM.videoPlayer) = "roVideoPlayer" then
          zoneHSM.videoPlayer.Stop()
          zoneHSM.videoPlayer = invalid
        end if
      next
    end if
    
    zoneHSM = invalid
    m.dispatchingZone = invalid
    m.sign = invalid
    RunGarbageCollector()
    
    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")
        end if
        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 }
          end if
        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 }
          end if
        else
          audioRouting = { mode : "dynamic" }
        end if
        
        ok = audioConfiguration.ConfigureAudio(audioRouting)
        if not ok then
          m.diagnostics.PrintDebug("Configure audio failure: " + audioConfiguration$)
        end if
      end if
  
      ' initialize remote
      m.ConfigureIRRemote()
  
    end if
    
    ' 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
      end if
    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
      end if
      for i% = 0 to 10
        if (configuration% and (2 ^ i%)) <> 0 then
          forceUsed = true
        else
          forceUsed = false
        end if
        '			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
        end if
      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
          end if
          '			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%
          end if
          
          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
          end if
          if not (m.bpInputUsed[buttonPanelIndex%, i%] or m.bpOutputUsed[buttonPanelIndex%, i%] or forceUsed) then
            ledFlag% = ledFlag% + loopFlag%
          end if
          
          loopFlag% = loopFlag% * 2
        next
        
        ' the 2 here signifies the mask position for LED disabling
        m.bpOutputSetup[buttonPanelIndex%].SetOutputValue(2, ledFlag%)
      end if
      
    next
    
    ' reset connector volumes
    m.analogVolume% = 100
    m.hdmiVolume% = 100
    m.spdifVolume% = 100
  
    ' UsbCleanup
    m.usbVolumeA% = 100
    m.usbVolumeB% = 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 = { }
        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 })
      
    end if
    
    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
      end if
    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" then
        stop
      end if
      
      if m.sign.deviceWebPageDisplay$ = "Custom" and m.sign.customDeviceWebPage <> invalid then
        
        ' files in the custom device web page start with either
        ' <presentation name>-customDeviceWebPage-
        ' or
        ' <site name>-customDeviceWebPage-
        downloadFiles = currentSync.GetFileList("download")
        for each downloadFile in downloadFiles
          fileName$ = downloadFile.name
          ' the main web page is
          ' <site name>/<custom device web page file name>
          name$ = m.sign.customDeviceWebPage.name
          indexFileName$ = m.sign.customDeviceWebPage.indexFileName
          indexPoolPath$ = name$ + "/" + indexFileName$
          
          if fileName$ = indexPoolPath$ then
            
            m.deviceWebPageFilePath$ = GetPoolFilePath(m.assetPoolFiles, fileName$)
            
          else
            
            indexOfMarker% = instr(0, fileName$, name$)
            if indexOfMarker% > 0 then
              strippedFileName$ = mid(fileName$, indexOfMarker% + len(name$))
              ' 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$ })
                end if
              end if
            end if
          end if
          
        next
        
      else
        
        m.deviceWebPageFilePath$ = GetPoolFilePath(m.assetPoolFiles, "Default_PresentationWebPage/_deviceWebPage.html")
        
      end if
      
    end if
    
    ' 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 getMediumFromMimeType(mimeType as string) as string
  
    if mimeType = "audio/mpeg" or mimeType = "audio/ogg" or mimeType = "audio/flac" or mimeType = "audio/aac" or mimeType = "audio/mp4" then
      return "audio"
    endif
  
    if mimeType = "video/mpeg" or mimeType = "video/mp4" or mimeType = "video/mpeg" or mimeType = "video/quicktime" or mimeType = "video/x-matroska" then
      return "video"
    endif
  
    return "image"
  
  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$ = ""
    end if
    
    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)
      end if
    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)
      end if
    next
    
  end sub
  'endregion
  
  
  'region Bose Products
  Function ReadBoseProductsFile() as object
  
    'boseProductsFileXML = CreateObject("roXMLElement")
    globalAA = GetGlobalAA()
    boseProductsFileContents$ = ReadAsciiFile(globalAA.boseProductsFilePath$)
    if len(boseProductsFileContents$) = 0 then return 0
    boseProductsAA = ParseJson(boseProductsFileContents$)
    
    ' verify that this is a valid BoseProducts json file
    if not IsString(boseProductsAA.lookup("version")) then print "Invalid BoseProducts Json file - version not found" : stop
    if type(boseProductsAA.product) <> "roArray" then print "Invalid BoseProducts Json file - no product array" : stop
  
    boseProductsList = boseProductsAA.product
    numBoseProducts% = boseProductsList.count()
    
    boseProducts = { }
    
    rebootRequired = false
    for each boseProductJson in boseProductsList
      if boseProductJson.usbInternalHub = invalid then
        boseProductJson.usbInternalHub = ""
      endif
      registryKeyWritten = AddBoseProduct(boseProducts, boseProductJson)
      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()
    end if
    
    return boseProducts
    
  end function
  
  
  Function UpdateUsbRegistryKeys(usbRegistrySection as object, usbToSerialEntry as object) as boolean
    registryKey$ = usbToSerialEntry.registryKey
    registryValue$ = usbToSerialEntry.registryValue
    
    existingRegistryValue$ = usbRegistrySection.Read(registryKey$)
    if registryValue$ <> existingRegistryValue$ then
      usbRegistrySection.Write(registryKey$, registryValue$)
      return true
    end if
    
    return false
    
  end function
  
  
  Function AddBoseProduct(boseProducts as object, boseProductJson as object)
    
    rebootRequired = false
    
    boseProduct = { }
    
    productName$ = boseProductJson.productName
    
    boseProduct.tapProtocol = boseProductJson.tapProtocol
   
   ' revisit when bmap is added
    boseProduct.usbHIDCommunication = true
  '  if IsString(boseProduct.tapProtocol) and lcase(boseProduct.tapProtocol) = "hid" then
  '    boseProduct.usbHIDCommunication = true
  '  else
  '    boseProduct.usbHIDCommunication = false
  '  end if
    
    if boseProductJson.isAudioDevice = invalid then
      boseProduct.isAudioDevice = false
    else
      boseProduct.isAudioDevice = boseProductJson.isAudioDevice
    end if
    
    if boseProductJson.usbNetInterfaceIndex = invalid then
      boseProduct.usbNetInterfaceIndex$ = ""
    else
      boseProduct.usbNetInterfaceIndex$ = boseProductJson.usbNetInterfaceIndex
    end if
    
    boseProduct.usbAudioInterfaceIndex$ = boseProductJson.usbAudioInterfaceIndex
    if IsString(boseProduct.usbAudioInterfaceIndex$) then
      if boseProduct.usbAudioInterfaceIndex$ <> "" then
        boseProduct.usbAudio = true
      end if
    else
      boseProduct.usbAudio = false
      boseProduct.usbAudioInterfaceIndex$ = ""
    end if
    
    boseProduct.usbInternalHub$ = boseProductJson.usbInternalHub
    
    boseProduct.usbTapInterfaceIndex$ = boseProductJson.usbTapInterfaceIndex
    
    boseProduct.tapProtocol = boseProductJson.tapProtocol
    
    if boseProductJson.usbAsyncAudio = invalid then
      boseProduct.usbAsyncAudio = false
    else
      boseProduct.usbAsyncAudio = boseProductJson.usbAsyncAudio
    end if
    
    if type(boseProductJson.usbToSerial) = "roAssociativeArray" or (type(boseProductJson.usbToSerial) = "roArray" and boseProductJson.usbToSerial.Count() > 0) then
      
      usbRegistrySection = CreateObject("roRegistrySection", "usb")
      if type(usbRegistrySection) <> "roRegistrySection" then print "Error: Unable to create roRegistrySection": stop
      
      if type(boseProductJson.usbToSerial) = "roAssociativeArray" then
        
        if UpdateUsbRegistryKeys(usbRegistrySection, boseProductJson.usbToSerial) then
          rebootRequired = true
        end if
        
      else
        
        for each usbToSerialEntry in boseProductJson.usbToSerial
          if UpdateUsbRegistryKeys(usbRegistrySection, usbToSerialEntry) then
            rebootRequired = true
          end if
        next
        
      end if
      
    end if
    
    volumeTable = { }
    
    if type(boseProductJson.volumeTable) = "roAssociativeArray" then
      volumeTable.xval1% = boseProductJson.volumeTable.xval1
      volumeTable.yval1% = boseProductJson.volumeTable.yval1
      volumeTable.xval2% = boseProductJson.volumeTable.xval2
      volumeTable.yval2% = boseProductJson.volumeTable.yval2
      volumeTable.xval3% = boseProductJson.volumeTable.xval3
      volumeTable.yval3% = boseProductJson.volumeTable.yval3
    end if
    
    boseProduct.volumeTable = volumeTable
    
    if type(boseProductJson.transport) = "roAssociativeArray" then
      boseProduct.baudRate% = boseProductJson.transport.baudRate
      boseProduct.dataBits$ = boseProductJson.transport.dataBits
      boseProduct.parity$ = boseProductJson.transport.parity
      boseProduct.stopBits$ = boseProductJson.transport.stopBits
      boseProduct.flowControl$ = boseProductJson.transport.flowControl
      boseProduct.invertSignals = boseProductJson.transport.invertSignals
      boseProduct.sendEOL$ = boseProductJson.transport.sendEOL
      boseProduct.receiveEOL$ = boseProductJson.transport.receiveEOL
    else
      ' BACONTODO - not sure whether or not this is required. test at some point to see if it can be removed.
      boseProduct.baudRate% = 115200
      boseProduct.dataBits$ = "8"
      boseProduct.parity$ = "N"
      boseProduct.stopBits$ = "1"
      boseProduct.flowControl$ = "none"
      boseProduct.invertSignals = true
      boseProduct.sendEOL$ = "CR"
      boseProduct.receiveEOL$ = "CR+LF"
    end if
    
    boseProduct.protocol$ = "ASCII"
    
    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
    end if
    
  end function
  
  
  Function MatchToUniqueParameterName(properties as object, uniqueName$ as string, propertyNameBreadcrumbs as object) as boolean
    
    for each propertyName in properties
      if lcase(propertyName) = "uniquename" then
        if properties[propertyName] = uniqueName$ then
          return true
        else
          return false
        end if
      end if
      propertyNameBreadcrumbs.push(propertyName)
      if properties.DoesExist(propertyName) then
        property = properties.Lookup(propertyName)
        if type(property) = "roAssociativeArray" then
          matchFound = MatchToUniqueParameterName(property, uniqueName$, propertyNameBreadcrumbs)
          if matchFound then
            return true
          end if
        end if
      end if
      
      removedPropertyName = propertyNameBreadcrumbs.pop()
    next
    
    return false
    
  end function
  
  
  Function GetWssCommunicationSpecEventFromWebSocketEvent(wssCommunicationSpec as object, webSocketEvent as object) as object
    
    eventHeader = webSocketEvent.header
    eventResource = eventHeader.resource
    
    for i% = 0 to wssCommunicationSpec.events.Count() - 1
      wssCommunicationSpecEvent = wssCommunicationSpec.events[i%]
      resource = wssCommunicationSpecEvent.header.resource
      if resource = eventResource then
        
        matchedWssCommunicationSpecEvent = MatchFixedWssHeaders(wssCommunicationSpecEvent, webSocketEvent)
        if matchedWssCommunicationSpecEvent <> invalid then
          
          webSocketEventProperties = []
          for each eventBodyPropertyName in webSocketEvent.body
            webSocketEventProperty = { }
            webSocketEventProperty.name = eventBodyPropertyName
            webSocketEventProperty.value = webSocketEvent.body[eventBodyPropertyName]
            webSocketEventProperties.push(webSocketEventProperty)
          next
          
          matchedByBody = MatchFixedWssBodyProperties(wssCommunicationSpecEvent.body, webSocketEventProperties)
          if matchedByBody then
            return matchedWssCommunicationSpecEvent
          end if
          
        end if
        
      end if
      
    next
    
    return invalid
    
  end function
  
  
  Function MatchFixedWssBodyProperties(wssCommunicationSpecEventBodyProperties as object, webSocketEventBodyProperties as object) as boolean
    
    print "MatchFixedWssBodyProperties - entry"
    
    for i% = 0 to webSocketEventBodyProperties.Count() - 1
      
      webSocketEventPropertyAA = webSocketEventBodyProperties[i%]
      eventBodyPropertyName = webSocketEventPropertyAa.name
      print "eventBodyPropertyName: " + eventBodyPropertyName
      
      eventBodyPropertyNameValue = webSocketEventPropertyAa.value
      
      ' if associativeArray, drill down further into hierarchy
      if type(eventBodyPropertyNameValue) = "roAssociativeArray" then
        
        ' call recursively, searching for eventBodyPropertyNameValue
        if wssCommunicationSpecEventBodyProperties.DoesExist(eventBodyPropertyName) then
          wssCommunicationSpecEventBodyProperties = wssCommunicationSpecEventBodyProperties.Lookup(eventBodyPropertyName)
          if wssCommunicationSpecEventBodyProperties <> invalid then
            
            ' create webSocketEventBodyProperties to use for recursive call
            childEventProperties = []
            for each childEventPropertyName in eventBodyPropertyNameValue
              childEventProperty = { }
              childEventProperty.name = childEventPropertyName
              childEventProperty.value = eventBodyPropertyNameValue[childEventPropertyName]
              childEventProperties.push(childEventProperty)
            next
            
            return MatchFixedWssBodyProperties(wssCommunicationSpecEventBodyProperties, childEventProperties)
          end if
        end if
        
      else if type(eventBodyPropertyNameValue) = "roString" then
        
        ' get match in wssCommunicationSpecEventBodyProperties
        if wssCommunicationSpecEventBodyProperties.DoesExist(eventBodyPropertyName) then
          wssCommunicationSpecPropertyValue = wssCommunicationSpecEventBodyProperties.Lookup(eventBodyPropertyName)
          if type(wssCommunicationSpecPropertyValue) <> "roAssociativeArray" then
            if type(wssCommunicationSpecPropertyValue) = "roInt" then
              propertyValue$ = StripLeadingSpaces(stri(wssCommunicationSpecPropertyValue))
              if propertyValue$ <> StripLeadingSpaces(eventBodyPropertyNameValue) then
                return false
              end if
              ' string
            else if wssCommunicationSpecPropertyValue <> eventBodyPropertyNameValue then
              return false
            end if
          end if
        end if
      end if
    next
    
    return true
    
  end function
  
  
  Function MatchFixedWssHeaders(wssCommunicationSpecEvent as object, webSocketEvent as object) as object
    
    ' check webSocketEvent header property against fixed header property values from wssCommunicationSpec
    ' any header property in wssCommunicationSpec that is not an associative array is considered 'fixed'
    for each eventHeaderPropertyName in webSocketEvent.header
      
      ' don't need to check resource - it's already been matched
      if eventHeaderPropertyName <> "resource" then
        
        ' skip header properties from the webSocketEvent that does not have a corresponding entry in the wssCommunicationSpec
        ' includes: device
        if webSocketEvent.header.DoesExist(eventHeaderPropertyName) then
          eventHeaderPropertyValue = webSocketEvent.header.Lookup(eventHeaderPropertyName)
          if type(eventHeaderPropertyValue) = "roString" then
            
            ' get match in wssCommunicationSpecEvent
            if wssCommunicationSpecEvent.header.DoesExist(eventHeaderPropertyName) then
              wssCommunicationSpecPropertyValue = wssCommunicationSpecEvent.header.Lookup(eventHeaderPropertyName)
              if type(wssCommunicationSpecPropertyValue) <> "roAssociativeArray" then
                if wssCommunicationSpecPropertyValue <> eventHeaderPropertyValue then
                  return invalid
                end if
              end if
            end if
            
          end if
        end if
      end if
    next
    
    return wssCommunicationSpecEvent
    
  end function
  
  
  Function GetMatchedParameter(webSocketEvent as object, wssCommunicationSpecEvent as object, wssEventProperties as object, wssEventTransitionEventSpec as object, specifiedEventPropertyName as string) as object
    
    paramAttrs = { }
    paramAttrs.propertyNameBreadcrumbs = []
    
    ' find property to match against, if any
    wssEventProperties.Reset()
    propertyName = wssEventProperties.Next()
    
    ' if there is no parameter and everything to this point matched, then this is a match
    if propertyName = invalid then
      paramAttrs.matchFound = true
      return paramAttrs
    end if
    
    matchedPropertyName$ = ""
    for each propertyName in wssEventProperties
      property = wssEventProperties.Lookup(propertyName)
      if property.MatchParameter and propertyName = specifiedEventPropertyName then
        matchedPropertyName$ = propertyName
        exit for
      end if
    next
    if matchedPropertyName$ = "" then
      paramAttrs.matchFound = false
      return paramAttrs
    end if
    
    properties = wssEventProperties.Lookup(matchedPropertyName$)
    
    propertyName$ = properties.PropertyName
    uniqueName$ = properties.UniqueName
    
    paramAttrs.matchFound = MatchToUniqueParameterName(wssCommunicationSpecEvent, uniqueName$, paramAttrs.propertyNameBreadcrumbs)
    
    if paramAttrs.matchFound then
      propertyValue = webSocketEvent
      for i% = 0 to paramAttrs.propertyNameBreadcrumbs.Count() - 1
        if not propertyValue.DoesExist(paramAttrs.propertyNameBreadcrumbs[i%]) then
          paramAttrs.matchFound = false
          return paramAttrs
        end if
        propertyValue = propertyValue.Lookup(paramAttrs.propertyNameBreadcrumbs[i%])
      next
      
      ' at this point, see if any of the keys in wssEventTransitionEventSpec match propertyValue. if they do, use that value for everything
      ' if they don't, see if properties.ParameterValue is a regular expression
      ' if it is, assign it to parameterValue$ and continue as before'
      for each specifiedPropertyValue in wssEventTransitionEventSpec
        if specifiedPropertyValue = propertyValue then
          paramAttrs.matchFound = true
          paramAttrs.parameterValue$ = propertyValue
          paramAttrs.propertyValue = propertyValue
          paramAttrs.transition = wssEventTransitionEventSpec.Lookup(propertyValue)
          return paramAttrs
        end if
      next
      
      paramAttrs.parameterValue$ = properties.ParameterValue ' this is the specified value. (but isn't currently correct for keyPresses)
      paramAttrs.propertyValue = propertyValue ' this is the value from the actual webSocket event
      paramAttrs.transition = wssEventTransitionEventSpec.Lookup(properties.ParameterValue)
      
    end if
    
    return paramAttrs
    
  end function
  
  
  Function MatchWssEvent(wssCommunicationSpecEvent as object, webSocketEvent as object, wssEventProperties as object, wssEventTransitionEventSpec as object, specifiedEventPropertyName as string) as object
    
    autoplayEvent = { }
    
    ' endpoint for this event
    resource = webSocketEvent.header.resource
    
    paramAttrs = GetMatchedParameter(webSocketEvent, wssCommunicationSpecEvent, wssEventProperties, wssEventTransitionEventSpec, specifiedEventPropertyName)
    
    if paramAttrs.matchFound = false then
      return paramAttrs
    else if paramAttrs.propertyNameBreadcrumbs.Count() = 0 then
      return paramAttrs
    end if
    
    parameterValue$ = paramAttrs.parameterValue$
    propertyValue = paramAttrs.propertyValue
    transition = paramAttrs.transition
    
    ' look for regular expression match if spec includes wildcard
    if instr(1, parameterValue$, "(.*)") > 0 then
      r = CreateObject("roRegEx", parameterValue$, "i")
      if type(r) = "roRegex" then
        matches = r.match(propertyValue)
        if matches.Count() > 0 then
          ''        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "wssEvent", propertyValue, "1")
          
          if transition.assignInputToUserVariable then
            transition.AssignEventInputToUserVariable(m, propertyValue)
          end if
          
          if matches.Count() > 1 and transition.assignWildcardToUserVariable then
            transition.AssignWildcardInputToUserVariable(m, matches[1])
          end if
          
          return paramAttrs
        end if
      end if
    end if
    
    if propertyValue = parameterValue$ then
      return paramAttrs
    end if
    
    paramAttrs.matchFound = false
    return paramAttrs
    
  end function
  
  
  ' wssItem = wssEvent or wssCommand
  ' section = 'header' or 'body'
  Function ParseWss(wssItem as object, mainSection as string, section as string) as object
    
    properties = { }
    
    if wssItem.DoesExist(mainSection) then
      wssItemResponse = wssItem.Lookup(mainSection)
      if wssItemResponse.DoesExist(section) then
        wssItemResponseSection = wssItemResponse.Lookup(section)
        if type(wssItemResponseSection) = "roAssociativeArray" then
          for each propertyName in wssItemResponseSection
            if wssItemResponseSection.DoesExist(propertyName) then
              property = wssItemResponseSection.Lookup(propertyName)
              properties[propertyName] = property
            end if
          next
        end if
      end if
    end if
    return properties
    
  end function
  
  
  Sub ParseWssCommands(wssCommands as object) as object
    
    commands = { }
    
    for each wssCommand in wssCommands
      command = { }
      command.header = ParseWss(wssCommand, "request", "header")
      command.body = ParseWss(wssCommand, "request", "body")
      commands[wssCommand.friendlyId] = command
    next
    
    return commands
    
  end sub
  
  
  Sub ParseWssEvents(wssEvents as object) as object
    events = []
    
    for each wssEvent in wssEvents
      event = { }
      event.header = ParseWss(wssEvent, "response", "header")
      event.body = ParseWss(wssEvent, "response", "body")
      if wssEvent.supportsMultipleTransitions = invalid then
        event.supportsMultipleTransitions = false
      else
        event.supportsMultipleTransitions = wssEvent.supportsMultipleTransitions
      end if
      events.push(event)
    next
    
    return events
    
  end sub
  
  
  Sub ParseBoseWssCommunicationSpec(path$ as string) as object
    
    ' TODO - should this log file always be enabled?
    getGlobalAA().eddieDumpFile = CreateObject("roAppendFile", "eddie.json")
    
    json$ = ReadAsciiFile(path$)
    wss = ParseJson(json$)
    
    wssSpec = { }
    wssSpec.name = wss.Name
    wssSpec.commands = ParseWssCommands(wss.Commands)
    wssSpec.events = ParseWssEvents(wss.Events)
    return wssSpec
    
  end sub
  
  
  'endregion
  
  'region Schedule
  
  ' 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
    end if
    
    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
        
      end if
      
      '       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
        end if
        
      end if
      
      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
          end if
          
        end if
        
        '           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
                
              end if
              
            else ' EveryWeekend
              
              if currentDateTime.GetDayOfWeek() = 0 or currentDateTime.GetDayOfWeek() = 6 then
                
                scheduledEventActiveToday = true
                
              end if
              
            end if
            
          else ' Weekly
            
            bitwiseDaysOfWeek% = scheduledEvent.recurrencePatternDaysOfWeek%
            currentDayOfWeek = currentDateTime.GetDayOfWeek()
            bitDayOfWeek% = 2 ^ currentDayOfWeek
            if (bitwiseDaysOfWeek% and bitDayOfWeek%) <> 0 then
              scheduledEventActiveToday = true
            end if
            
          end if
          
        end if
        
      end if
      
      '           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
          
        end if
        
      end if
      
    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
        
      end if
      
      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
          
        end if
        
      end if
      
      '       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
                
              end if
              
            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
                  
                end if
                
              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
                
              end if
              
            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
                  
                end if
                
              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
                
              end if
              
            end if
            
          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
                
              end if
              
            end if
            
            ' 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
                end if
              end while
              
            end if
            
          end if
          
        end if
        
      end if
      
      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]
    end if
    
    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%
        end if
      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
    
    ' set background screen color
    if type(sign.backgroundScreenColor%) = "roInt" then
      videoMode = GetVideoMode()
      if type(videoMode) = "roVideoMode" then
        m.diagnostics.PrintTimestamp()
        m.diagnostics.PrintDebug("### set background screen color")
        videoMode.SetBackgroundColor(sign.backgroundScreenColor%)
        videoMode = invalid
      endif
    end if
    
    ' 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()
        end if
      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()
        end if
      next
    end if
    
    ' 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)
    end if
    
    for each key in zoneHSM.stateTable
      
      state = zoneHSM.stateTable[key]
      
      if state.type$ = "mediaList" then
        for each cmd in state.transitionNextItemCmds
          m.CreateObjectForTransitionCommand(cmd)
        next
        
        for each cmd in state.transitionPreviousItemCmds
          m.CreateObjectForTransitionCommand(cmd)
        next
      end if
      
      gpioEvents = state.gpioEvents
      for each gpioEventNumber in gpioEvents
        if type(gpioEvents[gpioEventNumber]) = "roAssociativeArray" then
          m.CreateObjectsNeededForTransitionCommands(gpioEvents[gpioEventNumber])
        end if
      next
      
      gpioUpEvents = state.gpioUpEvents
      for each gpioEventNumber in gpioUpEvents
        if type(gpioUpEvents[gpioEventNumber]) = "roAssociativeArray" then
          m.CreateObjectsNeededForTransitionCommands(gpioUpEvents[gpioEventNumber])
        end if
      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])
          end if
        next
      next
      
      if type(state.mstimeoutEvent) = "roAssociativeArray"
        m.CreateObjectsNeededForTransitionCommands(state.mstimeoutEvent)
      end if
      
      if type(state.timeClockEvents) = "roArray" then
        for each timeClockEvent in state.timeClockEvents
          m.CreateObjectsNeededForTransitionCommands(timeClockEvent.transition)
        next
      end if
      
      if type(state.videoEndEvent) = "roAssociativeArray"
        m.CreateObjectsNeededForTransitionCommands(state.videoEndEvent)
      end if
      
      if type(state.audioEndEvent) = "roAssociativeArray"
        m.CreateObjectsNeededForTransitionCommands(state.audioEndEvent)
      end if
      
      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])
            end if
          next
        end if
        
        if type(state.usbStringEvents) = "roAssociativeArray"
          usbEvents = state.usbStringEvents
          for each usbEvent in state.usbEvents
            if type(usbEvents[usbEvent]) = "roAssociativeArray" then
              m.CreateObjectsNeededForTransitionCommands(usbEvents[usbEvent])
            end if
          next
        end if
        
      end if
      
      if type(state.remoteEvents) = "roAssociativeArray" then
        remoteEvents = state.remoteEvents
        for each remoteEvent in state.remoteEvents
          m.CreateObjectsNeededForTransitionCommands(remoteEvents[remoteEvent])
        next
      end if
      
      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)
      end if
      
      serialEvents = state.serialEvents
      for each serialPort in serialEvents
        
        m.CreateSerial(m.bsp, serialPort, false)
        
        protocol$ = ""
  
        if IsUsbPort(serialPort) then
  
          usbSpec = GetGlobalAA().usbConnectorNameToUsbSpec.Lookup(serialPort)
  
          if GetGlobalAA().usbHIDPortConfigurations.DoesExist(serialPort) then
            usbHIDPortConfiguration = GetGlobalAA().usbHIDPortConfigurations[serialPort]
            protocol$ = usbHIDPortConfiguration.protocol$
          end if
  
        else
          port% = int(val(serialPort))
          serialPortConfiguration = m.bsp.sign.serialPortConfigurations[port%]
          protocol$ = serialPortConfiguration.protocol$
        end if
        
        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
            end if
          end if
        else if protocol$ <> "" then
          for each serialEvent in serialEvents[serialPort]
            m.CreateObjectsNeededForTransitionCommands(serialEvents[serialPort][serialEvent])
          next
        end if
      next
      
      if type(state.zoneMessageEvents) = "roAssociativeArray" then
        
        for each zoneMessageEvent in state.zoneMessageEvents
          m.CreateObjectsNeededForTransitionCommands(state.zoneMessageEvents[zoneMessageEvent])
        next
        
      end if
      
      if type(state.pluginMessageEvents) = "roAssociativeArray" then
        
        for each pluginMessageEvent in state.pluginMessageEvents
          m.CreateObjectsNeededForTransitionCommands(state.pluginMessageEvents[pluginMessageEvent])
        next
        
      end if
      
      if type(state.internalSynchronizeEvents) = "roAssociativeArray" then
        
        for each internalSynchronizeEvent in state.internalSynchronizeEvents
          m.CreateObjectsNeededForTransitionCommands(state.internalSynchronizeEvents[internalSynchronizeEvent])
        next
        
      end if
      
      createDatagramReceiver = false
      
      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
          end if
        end if
        
        if createDatagramReceiver then
          m.bsp.udpReceiver = CreateObject("roDatagramReceiver", m.bsp.udpReceivePort)
          ' Set user data to distinguish between presentation udp messages and bootstrap udp messages
          m.bsp.udpReceiver.SetUserData("receiver")
          m.bsp.udpReceiver.SetPort(m.bsp.msgPort)
          m.bsp.existingUdpReceivePort = m.bsp.udpReceivePort
        end if
        
        if type(state.udpEvents) = "roAssociativeArray" then
          udpEvents = state.udpEvents
          for each udpEvent in state.udpEvents
            m.CreateObjectsNeededForTransitionCommands(udpEvents[udpEvent])
          next
        end if
        
        if type(state.synchronizeEvents) = "roAssociativeArray" then
          synchronizeEvents = state.synchronizeEvents
          for each synchronizeEvent in state.synchronizeEvents
            m.CreateObjectsNeededForTransitionCommands(synchronizeEvents[synchronizeEvent])
          next
        end if
        
      end if
      
      if state.type$ = "html5" and state.enableMouseEvents then
        m.bsp.InitializeTouchScreen(zoneHSM)
      end if
      
      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
      end if
      
      if type(state.audioTimeCodeEvents) = "roAssociativeArray" then
        for each eventNum in state.audioTimeCodeEvents
          m.CreateObjectsNeededForTransitionCommands(state.audioTimeCodeEvents[eventNum])
        next
      end if
      
      if type(state.videoTimeCodeEvents) = "roAssociativeArray" then
        for each eventNum in state.videoTimeCodeEvents
          m.CreateObjectsNeededForTransitionCommands(state.videoTimeCodeEvents[eventNum])
        next
      end if
      
      if type(state.cmds) = "roArray" then
        for each cmd in state.cmds
          m.CreateCommunicationObjects(cmd)
        next
      end if
      
      if type(state.exitCmds) = "roArray" then
        for each cmd in state.exitCmds
          m.CreateCommunicationObjects(cmd)
        next
      end if
      
    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")
      end if
      
      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
      end if
    end if
    
  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$ = "serialSendString" or commandName$ = "sendSerialStringCommand" or commandName$ = "sendSerialBlockCommand" or commandName$ = "sendSerialByteCommand" or commandName$ = "sendSerialBytesCommand" then
      port$ = cmd.parameters["port"].GetCurrentParameterValue()
      m.CreateSerial(m.bsp, port$, true)
    end if
    
  end sub
  
  
  Sub CreateObjectsNeededForTransitionCommands(transition as object)
    
    if type(transition.transitionCmds) = "roArray" then
      for each cmd in transition.transitionCmds
        m.CreateObjectForTransitionCommand(cmd)
      next
    end if
    
    if type(transition.conditionalTransitions) = "roArray" then
      for each conditionalTransition in transition.conditionalTransitions
        if type(conditionalTransition.transitionCmds) = "roArray" then
          for each cmd in conditionalTransition.transitionCmds
            m.CreateObjectForTransitionCommand(cmd)
          next
        end if
      next
    end if
    
  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$ = "serialSendString" or commandName$ = "sendSerialStringCommand" or commandName$ = "sendSerialBlockCommand" or commandName$ = "sendSerialByteCommand" or commandName$ = "sendSerialBytesCommand" then
      port$ = cmd.parameters["port"].GetCurrentParameterValue()
      m.CreateSerial(m.bsp, port$, true)
    end if
    
    if commandName$ = "synchronize" then
      m.bsp.IsSyncMaster = true
    end if
    
  end sub
  
  
  Sub ConfigureIRRemote()
  
    irInConfiguration = m.sign.irInConfiguration
    irOutConfiguration = m.sign.irOutConfiguration
  
    irRemoteConfiguration = m.sign.irRemoteControl
  
    irConfig = {}
  
    irConfig.source = irInConfiguration.source
  
    irConfig.encodings = []
    irConfig.encodings[0] = irRemoteConfiguration.encoding
    
    m.irReceiver = CreateObject("roIRReceiver", irConfig)
    if type(m.irReceiver) = "roIRReceiver" then
      m.irReceiver.SetPort(m.msgPort)
    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)
          end if
        end if
      end if
    end if
  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
      end if
    end if
    
    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)
      end if
      
      bsp.existingUdpAddressType$ = bsp.udpAddressType$
      bsp.existingUdpAddress$ = bsp.udpAddress$
      bsp.existingUdpSendPort = bsp.udpSendPort
      
    end if
    
  end sub
  
  
  Sub SendUDPNotification(msg as string)
    
    if m.registrySettings.lwsEnableUpdateNotifications$ <> "yes" then
      return
    end if
    
    if type(m.udpNotifier) <> "roDatagramSender" then
      m.diagnostics.PrintDebug("Creating roDatagramSender for notifications")
      m.udpNotifier = CreateObject("roDatagramSender")
      m.udpNotifier.SetDestination(m.udpNotificationAddress$, m.udpNotificationPort%)
    end if
    
    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 = { }
    end if
    
    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$)
      
    end if
    
  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$)
    end if
    
    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)
    end if
    
  end sub
  
  
  Function IsUsbPort(connectorName$) as boolean
    return GetGlobalAA().usbConnectorNameToUsbSpec.DoesExist(connectorName$)
  end function
  
  
  Function GetRuntimeUsbConnector(connector$ as string) as string
  
    if m.replaceUSB700_1_with_USB_C and lcase(connector$) = "usb700_1" then
      connector$ = "usbTypeC"
    else if m.replaceUSB_C_with_USB700_1 and lcase(connector$) = "usbtypec" then
      connector$ = "usb700_1"
    endif
  
    return connector$
  
  end function
  
  
  Function GetSpecifiedConnector(connector$ as string) as string
  
    if m.replaceUSB700_1_with_USB_C and lcase(connector$) = "usbtypec" then
      connector$ = "usb700_1"
    else if m.replaceUSB_C_with_USB700_1 and lcase(connector$) = "usb700_1" then
      connector$ = "usbTypeC"
    endif
  
    return connector$
  
  end function
  
  
  Function AttemptOpenSerial(port$ as string, outputOnly as boolean) as boolean
  
    port$ = m.GetRuntimeUsbConnector(port$)
  
    if type(m.serial) <> "roAssociativeArray" then
      m.serial = { }
    end if
    
    if type(m.serialOutputOnlySpec) <> "roAssociativeArray" then
      m.serialOutputOnlySpec = { }
    end if
  
    usbConnectorNameToUsbSpec = GetGlobalAA().usbConnectorNameToUsbSpec
    if IsUsbPort(port$) then
  
      usbSpec = usbConnectorNameToUsbSpec.Lookup(port$)
      hidOutputSpec = usbSpec.hidOutputSpec
  
      usbHIDPortConfiguration = GetGlobalAA().usbHIDPortConfigurations[port$]
      
      sendEol$ = usbHIDPortConfiguration.sendEol$
      receiveEol$ = usbHIDPortConfiguration.receiveEol$
      protocol$ = usbHIDPortConfiguration.protocol$
  
      if type(m.serial[port$]) = "roUsbTap" then
        serial = m.serial[port$]
      else
        serial = CreateObject("roUsbTap", hidOutputSpec)
        if type(serial) <> "roUsbTap" then
          m.diagnostics.PrintDebug("Error creating roUsbTap " + hidOutputSpec)
          return false
        end if
        m.serialOutputOnlySpec[port$] = true
  
        ' post Usb connect event
        connectData = {}
        connectData.port = port$
        connectData.hidOutputSpec = hidOutputSpec
        usbConnectEvent = { }
        usbConnectEvent["EventType"] = "USB_CONNECT_EVENT"
        usbConnectEvent["EventData"] = connectData
        m.msgPort.PostMessage(usbConnectEvent)
  
      end if
  
    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
        end if
        
        m.serialOutputOnlySpec[port$] = true
      end if
      
      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)
      end if
        
    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)
      end if
    end if
    
    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$ = "CRLF" then
      eol$ = chr(13) + chr(10)
    else if eolSpec$ = "CR+LF" then
      eol$ = chr(13) + chr(10)
    end if
    
    return eol$
    
  end function
  
  'endregion
  
  'region newSign
  Function newSign(BrightAuthor as object, globalVariables as object, bsp as object, msgPort as object, controlPort as object, version% as integer) as object
    
    autoplay = ParseAutoplay(BrightAuthor, bsp)
    meta = autoplay.meta  
    
    ' FUTURE - check for obsolete models here
      
    Sign = { }
    
    Sign.BuildBoseProductsByConnector = BuildBoseProductsByConnector
    
    Sign.numTouchEvents% = 0
    Sign.numAudioTimeCodeEvents% = 0
    Sign.numVideoTimeCodeEvents% = 0
    
    Sign.name$ = meta.name
    if not IsString(Sign.name$) then print "Invalid autoplay file - meta name not found" : stop
    
  ' videoMode plugins
    meta.videoModePlugins = []
    for each videoModePluginSpec in BrightAuthor.meta.videoModePlugins
      videoModePlugin = jsonParseVideoModePlugin(videoModePluginSpec)
      meta.videoModePlugins.push(videoModePlugin)
      bsp.videoModePlugins.push(videoModePlugin)
    next
  
    ' parse script plugins
    meta.scriptPlugins = []
    for each scriptPluginSpec in BrightAuthor.meta.scriptPlugins
      scriptPlugin = jsonParseScriptPlugin(scriptPluginSpec)
      ' BACON - is it necessary to store the script plugins both in meta and in bsp?
      meta.scriptPlugins.push(scriptPlugin)
      bsp.scriptPlugins.push(scriptPlugin)
    next
    
    videoMode = GetVideoMode()
    if type(videoMode) = "roVideoMode" then
    
      Sign.videoMode$ = meta.videoMode
      if Sign.videoMode$ <> "not applicable" then
        if not IsString(Sign.videoMode$) then print "Invalid autoplay file - meta videoMode not found" : stop
        '   print "Video mode is ";Sign.videoMode$
        
        videoMode$ = sign.videoMode$
        
        forceResolution = meta.forceResolution
        
        setPreferredResolution = false
        if not forceResolution then
          setPreferredResolution = true
          videoMode$ = videoMode$ + ":preferred"
        end if
        
        dolbyVisionEnabled = meta.dolbyVisionEnabled
        if dolbyVisionEnabled then
          videoMode$ = videoMode$ + ":dbv"
        end if
        
        fullResGraphicsEnabled = meta.fullResGraphicsEnabled
        if fullResGraphicsEnabled then
          videoMode$ = videoMode$ + ":fullres"
        end if
        
        tenBitColorEnabled = meta.tenBitColorEnabled
        if tenBitColorEnabled then
          videoMode$ = videoMode$ + ":10bit"
        end if
        
        ' 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$
      
        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 videoMode
            if isString(videoModeFromPlugin) and len(videoModeFromPlugin) > 0 then
              videoMode$ = videoModeFromPlugin
            endif
          endif
        next
      
        ok = videoMode.SetMode(videoMode$)
        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: " + videoMode$ + ", actual videoMode: " + videoMode.GetMode())
          bsp.logging.WriteDiagnosticLogEntry(bsp.diagnosticCodes.EVENT_SET_VIDEO_MODE, "Specified videoMode: " + videoMode$ + ", actual videoMode: " + videoMode.GetMode())
          
        else
          bsp.configuredResX = videoMode.GetResX()
          bsp.configuredResY = videoMode.GetResY()
        end if
  
        ' 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$ = meta.videoConnector
        if not IsString(Sign.videoConnector$) then print "Invalid XML file - meta videoConnector not found" : stop
  
      endif
  
      Sign.isVideoWall = false
      if 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)
      end if
  
      videoMode = invalid
  
    endif
  
  Sign.monitorOrientation = meta.monitorOrientation
  
  Sign.deviceWebPageDisplay$ = meta.deviceWebPageDisplay
  Sign.customDeviceWebPage = meta.customDeviceWebPage
  
  Sign.alphabetizeVariableNames = meta.alphabetizeVariableNames
  
  Sign.htmlEnableJavascriptConsole = meta.htmlEnableJavascriptConsole
  
  Sign.backgroundScreenColor% = meta.backgroundScreenColor
  bsp.dontChangePresentationUntilMediaEndEventReceived = meta.delayScheduleChangeUntilMediaEndEvent
  
  Sign.languageKey$ = meta.languageKey
  globalVariables.language$ = Sign.languageKey$
  
  Sign.irRemoteControl = meta.irRemoteControl
  Sign.irInConfiguration = meta.irInConfiguration
  Sign.irOutConfiguration = meta.irOutConfiguration
  
  Sign.serialPortConfigurations = meta.serialPortConfigurations
  GetGlobalAA().usbHIDPortConfigurations = { }
  GetGlobalAA().usbAudioPortConfigurations = { }
  
  if type(bsp.bpInputPorts[0]) = "roControlPort" then
    bsp.bpInputPortConfigurations[0] = GetBPConfiguration(bsp.bpInputPortHardware[0], meta.bp900AConfigureAutomatically, meta.bp900AConfiguration%, meta.bp200AConfigureAutomatically, meta.bp200AConfiguration%)
  end if
  
  if type(bsp.bpInputPorts[1]) = "roControlPort" then
    bsp.bpInputPortConfigurations[1] = GetBPConfiguration(bsp.bpInputPortHardware[1], meta.bp900BConfigureAutomatically, meta.bp900BConfiguration%, meta.bp200BConfigureAutomatically, meta.bp200BConfiguration%)
  end if
  
  if type(bsp.bpInputPorts[2]) = "roControlPort" then
    bsp.bpInputPortConfigurations[2] = GetBPConfiguration(bsp.bpInputPortHardware[2], meta.bp900CConfigureAutomatically, meta.bp900CConfiguration%, meta.bp200CConfigureAutomatically, meta.bp200CConfiguration%)
  end if
  
  if type(bsp.bpInputPorts[3]) = "roControlPort" then
    bsp.bpInputPortConfigurations[3] = GetBPConfiguration(bsp.bpInputPortHardware[3], meta.bp900DConfigureAutomatically, meta.bp900DConfiguration%, meta.bp200DConfigureAutomatically, meta.bp200DConfiguration%)
  end if
  
  ' serial ports'
  bsp.gpsConfigured = false
  bsp.gpsLocation = { latitude: invalid, longitude: invalid }
  bsp.gpsPort$ = ""
  
  for each serialPortConfigurationSpec in meta.serialPortConfigurations
    
    serialPortConfiguration = { }
    serialPortConfiguration.serialPortSpeed% = serialPortConfigurationSpec.serialPortSpeed%
    serialPortConfiguration.protocol$ = serialPortConfigurationSpec.protocol$
    serialPortConfiguration.sendEol$ = serialPortConfigurationSpec.sendEol$
    serialPortConfiguration.receiveEol$ = serialPortConfigurationSpec.receiveEol$
    serialPortConfiguration.invertSignals = serialPortConfigurationSpec.invertSignals
    
    serialPortConfiguration.serialPortMode$ = serialPortConfigurationSpec.serialPortMode
    
    port% = serialPortConfigurationSpec.port
    
    serialPortConfiguration.gps = serialPortConfigurationSpec.gps
    if serialPortConfigurationSpec.gps then
      bsp.gpsConfigured = true
      bsp.gpsPort$ = stri(port%)
    else
      serialPortConfiguration.gps = false
    end if
    
    Sign.serialPortConfigurations[port%] = serialPortConfiguration
  next
  
  ' parse parser plugins
  parserPluginsContainer = meta.parserPlugins
  for each parserPlugin in parserPluginsContainer
    bsp.parserPlugins.push(parserPlugin)
  next
  
  ' first pass parse of user variables
  
  bsp.variablesDBExists = false
  bsp.ReadVariablesDB(bsp.activePresentation$)
  
  ' BACONTODO - parseAutoplay may need this set earlier'
  bsp.privateDBSectionId% = bsp.GetDBSectionId(bsp.activePresentation$)
  if bsp.privateDBSectionId% < 0 then
    bsp.AddDBSection(bsp.activePresentation$)
    bsp.privateDBSectionId% = bsp.GetDBSectionId(bsp.activePresentation$)
  end if
  
  bsp.privateBrightAuthorCategoryId% = bsp.GetDBCategoryId(bsp.privateDBSectionId%, "BrightAuthor")
  if bsp.privateBrightAuthorCategoryId% < 0 then
    bsp.AddDBCategory(bsp.privateDBSectionId%, "BrightAuthor")
    bsp.privateBrightAuthorCategoryId% = bsp.GetDBCategoryId(bsp.privateDBSectionId%, "BrightAuthor")
  end if
  
  bsp.sharedDBSectionId% = bsp.GetDBSectionId("Shared")
  if bsp.sharedDBSectionId% < 0 then
    bsp.AddDBSection("Shared")
    bsp.sharedDBSectionId% = bsp.GetDBSectionId("Shared")
  end if
  
  bsp.sharedBrightAuthorCategoryId% = bsp.GetDBCategoryId(bsp.sharedDBSectionId%, "BrightAuthor")
  if bsp.sharedBrightAuthorCategoryId% < 0 then
    bsp.AddDBCategory(bsp.sharedDBSectionId%, "BrightAuthor")
    bsp.sharedBrightAuthorCategoryId% = bsp.GetDBCategoryId(bsp.sharedDBSectionId%, "BrightAuthor")
  end if
  
  variablePosition% = 0
  
  userVariables = bsp.currentUserVariables
  
  for each userVariableSpec in meta.userVariableSpecs
    
    name$ = userVariableSpec.name
    defaultValue$ = userVariableSpec.defaultValue
    access$ = userVariableSpec.access
    
    if access$ = "Shared" then
      userVariableSpec.categoryId% = bsp.sharedBrightAuthorCategoryId%
    else
      userVariableSpec.categoryId% = bsp.privateBrightAuthorCategoryId%
    end if
    categoryId% = userVariableSpec.categoryId%
    
    systemVariable$ = userVariableSpec.systemVariable$
    url$ = userVariableSpec.url
    liveDataFeedId$ = userVariableSpec.liveDataFeedId
    
    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$)
      end if
    end if
    
    userVariable.position% = variablePosition%
    variablePosition% = variablePosition% + 1
    
    userVariable.systemVariable$ = systemVariable$
    
    if url$ <> "" then
      userVariable.url$ = url$
    else if liveDataFeedId$ <> "" then
      userVariable.liveDataFeedId$ = CleanName(liveDataFeedId$)
    end if
    
  next
  
  ' parse presentations
  for each presentationIdentifier in meta.presentationIdentifiers
    bsp.presentations.AddReplace(presentationIdentifier.presentationId, presentationIdentifier)
  next
  
  ' BACON - retrieve html sites here and de-dupe them?
  ' BACON - parse presentations?
  
  ' BACON - parse beacons
  
  ' get list of additional files to publish
  for each additionalPublishedFileName in meta.additionalPublishedFiles
    additionalPublishedFile = { }
    additionalPublishedFile.fileName$ = additionalPublishedFileName
    additionalPublishedFile.filePath$ = GetPoolFilePath(bsp.assetPoolFiles, additionalPublishedFileName)
    bsp.additionalPublishedFiles.push(additionalPublishedFile)
  next
  
  gaa = GetGlobalAA()
  
  ' parse boseProduct section - information about Bose products in use in this presentation
  Sign.boseProducts = meta.boseProducts
  Sign.boseProductsByConnector = Sign.BuildBoseProductsByConnector(bsp)
  
  ' Get the USB topology of the device and create a mapping of Bose port names in the presentation to USB device names
  bsp.replaceUSB700_1_with_USB_C = false
  bsp.replaceUSB_C_with_USB700_1 = false
  
  BuildUSBDevicesByConnector(bsp, sign)
  
  if bsp.replaceUSB700_1_with_USB_C then
    if gaa.usbHIDPortConfigurations.DoesExist("usb700_1") then
      tmp = gaa.usbHIDPortConfigurations["usb700_1"]
      gaa.usbHIDPortConfigurations["usbTypeC"] = tmp
      gaa.usbHIDPortConfigurations.Delete("usb700_1")
    end if
  
    if gaa.usbAudioPortConfigurations.DoesExist("usb700_1") then
      tmp = gaa.usbAudioPortConfigurations["usb700_1"]
      gaa.usbAudioPortConfigurations["usbTypeC"] = tmp
      gaa.usbAudioPortConfigurations.Delete("usb700_1")
    end if
  
  else if bsp.replaceUSB_C_with_USB700_1 then
    if gaa.usbHIDPortConfigurations.DoesExist("usbTypeC") then
      tmp = gaa.usbHIDPortConfigurations["usbTypeC"]
      gaa.usbHIDPortConfigurations["usb700_1"] = tmp
      gaa.usbHIDPortConfigurations.Delete("usbTypeC")
    end if
  
    if gaa.usbAudioPortConfigurations.DoesExist("usbTypeC") then
      tmp = gaa.usbAudioPortConfigurations["usbTypeC"]
      gaa.usbAudioPortConfigurations["usb700_1"] = tmp
      gaa.usbAudioPortConfigurations.Delete("usbTypeC")
    end if
  
  end if
  
  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 })
    end if
  
    connector = bsp.GetRuntimeUsbConnector(connector)
  
    if gaa.usbConnectorNameToUsbSpec.DoesExist(connector) then
      
      usbSpec = gaa.usbConnectorNameToUsbSpec[connector]
      ' TEDTODO - is this used anywhere?
      boseProduct.usbSpec = usbSpec
  
      if gaa.usbHIDPortConfigurations.DoesExist(connector) then
        usbHIDPortConfiguration = gaa.usbHIDPortConfigurations.Lookup(connector)
        usbHIDPortConfigurations.AddReplace(connector, usbHIDPortConfiguration)
      end if
      
      if gaa.usbAudioPortConfigurations.DoesExist(connector) then
        usbAudioPortConfiguration = gaa.usbAudioPortConfigurations.Lookup(connector)
        usbAudioPortConfigurations.AddReplace(connector, usbAudioPortConfiguration)
      end if
    end if
    
  next
  
  gaa.usbHIDPortConfigurations = usbHIDPortConfigurations
  gaa.usbAudioPortConfigurations = usbAudioPortConfigurations
  
  ' set default serial port speed, mode
  bsp.serialPortConfigurations = CreateObject("roArray", 8, true)
  for i% = 0 to 7
    bsp.serialPortConfigurations[i%] = meta.serialPortConfigurations[i%]
  next
  
  Sign.udpReceivePort = meta.udpReceivePort
  Sign.udpSendPort = meta.udpSendPort
  Sign.udpAddressType$ = meta.udpAddressType
  
  if Sign.udpAddressType$ = "" then Sign.udpAddressType$ = "IPAddress"
  Sign.udpAddress$ = meta.udpAddress
  
  ' synchronization section
  Sign.enableEnhancedSynchronization = meta.enableEnhancedSynchronization
  Sign.deviceIsSyncMaster = meta.deviceIsSyncMaster
  Sign.ptpDomain$ = meta.ptpDomain$
  
  if (Sign.enableEnhancedSynchronization) then
    if Sign.deviceIsSyncMaster then
      targetSyncMasterInRegistry = "1"
    else
      targetSyncMasterInRegistry = "0"
    end if
    
    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
    end if
    
    ' 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
    end if
    
    if rebootRequired then
      bsp.registrySection.Flush()
      RebootSystem()
    end if
  end if
  
  Sign.flipCoordinates = meta.flipCoordinates
  Sign.touchCursorDisplayMode$ = meta.touchCursorDisplayMode
  
  Sign.gpio0Config = meta.gpio[0]
  Sign.gpio1Config = meta.gpio[1]
  Sign.gpio2Config = meta.gpio[2]
  Sign.gpio3Config = meta.gpio[3]
  Sign.gpio4Config = meta.gpio[4]
  Sign.gpio5Config = meta.gpio[5]
  Sign.gpio6Config = meta.gpio[6]
  Sign.gpio7Config = meta.gpio[7]
  
  if IsControlPort(controlPort) then
    for i% = 0 to 7
      if meta.gpio[i%] = "input" then
        controlPort.EnableInput(i%)
      else
        controlPort.EnableOutput(i%)
      end if
    next
  end if
  
  Sign.audioConfiguration$ = meta.audioConfiguration
  Sign.audioAutoLevel = meta.audioAutoLevel
  
  Sign.audio1MinVolume% = meta.audio1MinVolume
  Sign.audio1MaxVolume% = meta.audio1MaxVolume
  
  Sign.usbTypeAMinVolume% = meta.usbTypeAMinVolume
  Sign.usbTypeAMaxVolume% = meta.usbTypeAMaxVolume
  Sign.usbTypeCMinVolume% = meta.usbTypeCMinVolume
  Sign.usbTypeCMaxVolume% = meta.usbTypeCMaxVolume
  Sign.usb700_1MinVolume% = meta.usb700_1MinVolume
  Sign.usb700_1MaxVolume% = meta.usb700_1MaxVolume
  Sign.usb700_2MinVolume% = meta.usb700_2MinVolume
  Sign.usb700_2MaxVolume% = meta.usb700_2MaxVolume
  Sign.usb700_3MinVolume% = meta.usb700_3MinVolume
  Sign.usb700_3MaxVolume% = meta.usb700_3MaxVolume
  Sign.usb700_4MinVolume% = meta.usb700_4MinVolume
  Sign.usb700_4MaxVolume% = meta.usb700_4MaxVolume
  Sign.usb700_5MinVolume% = meta.usb700_5MinVolume
  Sign.usb700_5MaxVolume% = meta.usb700_5MaxVolume
  Sign.usb700_6MinVolume% = meta.usb700_6MinVolume
  Sign.usb700_6MaxVolume% = meta.usb700_6MaxVolume
  Sign.usb700_7MinVolume% = meta.usb700_7MinVolume
  Sign.usb700_7MaxVolume% = meta.usb700_7MaxVolume
  Sign.usb_1MinVolume% = meta.usb_1MinVolume
  Sign.usb_1MaxVolume% = meta.usb_1MaxVolume
  Sign.usb_2MinVolume% = meta.usb_2MinVolume
  Sign.usb_2MaxVolume% = meta.usb_2MaxVolume
  Sign.usb_3MinVolume% = meta.usb_3MinVolume
  Sign.usb_3MaxVolume% = meta.usb_3MaxVolume
  Sign.usb_4MinVolume% = meta.usb_4MinVolume
  Sign.usb_4MaxVolume% = meta.usb_4MaxVolume
  Sign.usb_5MinVolume% = meta.usb_5MinVolume
  Sign.usb_5MaxVolume% = meta.usb_5MaxVolume
  Sign.usb_6MinVolume% = meta.usb_6MinVolume
  Sign.usb_6MaxVolume% = meta.usb_6MaxVolume
  Sign.hdmiMinVolume% = meta.hdmiMinVolume
  Sign.hdmiMaxVolume% = meta.hdmiMaxVolume
  Sign.spdifMinVolume% = meta.spdifMinVolume
  Sign.spdifMaxVolume% = meta.spdifMaxVolume
  
  ' enable zone support here to ensure that SetGraphicsZOrder works
  EnableZoneSupport(true)
  
  videoMode = GetVideoMode()
  
  if type(videoMode) = "roVideoMode" then
  
  graphicsZOrder = meta.graphicsZOrder
  videoMode.SetGraphicsZOrder(lcase(graphicsZOrder))
  
    ' mosaic mode / decoders
    Sign.isMosaic = meta.isMosaic
    if Sign.isMosaic then
      for each mosaicDecoderSpec in meta.mosaicDecoders
        decoderName = mosaicDecoderSpec.decoderName
        timeSliceMode = mosaicDecoderSpec.timeSliceMode
        zOrder = mosaicDecoderSpec.zOrder
        friendlyName = mosaicDecoderSpec.friendlyName
        enableMosaicDeinterlacer = mosaicDecoderSpec.enableMosaicDeinterlacer
        
        ok = videoMode.SetDecoderMode(decoderName, timeSliceMode, int(val(zOrder)), friendlyName, enableMosaicDeinterlacer)
      next
    else
      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
    end if
  
    videoMode = invalid
  
  endif
  
  ' create sign wide objects from parsed info
  for each liveDataFeedDescription in meta.liveDataFeedDescriptions
    liveDataFeed = newLiveDataFeed(bsp, liveDataFeedDescription)
    bsp.liveDataFeeds.AddReplace(liveDataFeed.id$, liveDataFeed)
  next
  
  if bsp.liveDataFeeds.IsEmpty() then
    if type(bsp.networkingHSM) = "roAssociativeArray" then
      bsp.networkingHSM.UploadDeviceDownloadProgressFileList()
      bsp.networkingHSM.FileListPendingUpload = false
    end if
  end if
  
  if type(userVariables) = "roAssociativeArray" then
    for each userVariableKey in userVariables
      userVariable = userVariables.Lookup(userVariableKey)
      if type(userVariable.liveDataFeedId$) <> "Invalid" and userVariable.liveDataFeedId$ <> "" then
        userVariable.liveDataFeed = bsp.liveDataFeeds.Lookup(userVariable.liveDataFeedId$)
      end if
    next
  end if
  
  ' reset variables if indicated in sign
  if meta.resetVariablesOnPresentationStart then
    bsp.ResetVariables()
  end if
  
  ' assign system variables to user variables
  bsp.AssignSystemVariablesToUserVariables()
  
  for each htmlSiteDescription in meta.htmlSiteDescriptions
    htmlSite = newHTMLSite(bsp, htmlSiteDescription)
    bsp.htmlSites.AddReplace(htmlSite.name$, htmlSite)
  next
  
  ' get zone descriptions, then create zone state machines
  zoneDescriptions = ParseZones(bsp, BrightAuthor, Sign)
  numZones% = zoneDescriptions.Count()
  
  Sign.zonesHSM = CreateObject("roArray", numZones%, true)
  for each zoneDescription in zoneDescriptions
    bsZoneHSM = newZoneHSM(bsp, msgPort, Sign, zoneDescription, globalVariables)
    if (bsZoneHSM.type$ = "VideoOrImages" or bsZoneHSM.type$ = "VideoOnly") or Sign.videoZoneHSM = invalid then
      Sign.videoZoneHSM = bsZoneHSM
    end if
    Sign.zonesHSM.push(bsZoneHSM)
  next
  
  return Sign
  
  end function
  
  
  Function BuildBoseProductsByConnector(bsp as object)
    
    gaa = GetGlobalAA()
    
    boseProductsByConnector = { }
  
    for each boseProductInPresentation in m.boseProducts
      
      boseProduct = { }
      boseProduct.productName$ = boseProductInPresentation.productName
      boseProduct.port$ = boseProductInPresentation.port
      boseProductSpec = bsp.GetBoseProductSpec(boseProduct.productName$)
      
      ' BACONTODO - not sure wssCommunicationSpec is getting set / published
      wssCommunicationSpecName = boseProductInPresentation.wssCommunicationSpec
      if (isString(wssCommunicationSpecName) and wssCommunicationSpecName <> "") then
        wssCommunicationSpecFileName$ = boseProductInPresentation.wssCommunicationSpecFileName
        wssCommunicationPath$ = GetPoolFilePath(bsp.assetPoolFiles, wssCommunicationSpecFileName$)
        boseProduct.wssCommunicationSpec = ParseBoseWssCommunicationSpec(wssCommunicationPath$)
      end if
      
      boseProduct.isAudioDevice = boseProductSpec.isAudioDevice
      boseProduct.usbNetInterfaceIndex$ = boseProductSpec.usbNetInterfaceIndex$
      boseProduct.usbInternalHub$ = boseProductSpec.usbInternalHub$
      
      boseProductsByConnector.AddReplace(boseProduct.port$, boseProduct)
      
      if type(boseProductSpec) = "roAssociativeArray" then
        if boseProductSpec.tapProtocol = "HID" then
          usbHIDPortConfiguration = { }
          usbHIDPortConfiguration.sendEol$ = GetEolFromSpec(boseProductSpec.sendEol$)
          usbHIDPortConfiguration.receiveEol$ = GetEolFromSpec(boseProductSpec.receiveEol$)
          usbHIDPortConfiguration.protocol$ = boseProductSpec.protocol$
          usbHIDPortConfiguration.usbTapInterfaceIndex$ = boseProductSpec.usbTapInterfaceIndex$
          usbHIDPortConfiguration.usbInternalHub$ = boseProductSpec.usbInternalHub$
          gaa.usbHIDPortConfigurations.AddReplace(boseProduct.port$, usbHIDPortConfiguration)
        end if
        
        if boseProductSpec.usbAudioInterfaceIndex$ <> "" then
          usbAudioPortConfiguration = { }
          usbAudioPortConfiguration.usbAudioInterfaceIndex$ = boseProductSpec.usbAudioInterfaceIndex$
          gaa.usbAudioPortConfigurations.AddReplace(boseProduct.port$, usbAudioPortConfiguration)
        end if
      end if
      
    next
    
    return boseProductsByConnector
    
  end function
  
  
  'endregion
  
  '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 = { }
      userVariableChanged["EventType"] = "USER_VARIABLE_CHANGE"
      userVariableChanged["UserVariable"] = m
      m.bsp.msgPort.PostMessage(userVariableChanged)
    end if
    
    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)
    end if
    
    m.currentValue$ = value$
    
    print "**--**"
    print m.name$
    print m.currentValue$
    
    m.bsp.UpdateDBVariable(m.bsp.GetCategoryIdFromAccess(m.access$), m.name$, m.currentValue$)
    
    if postMsg then
      userVariableChanged = { }
      userVariableChanged["EventType"] = "USER_VARIABLE_CHANGE"
      userVariableChanged["UserVariable"] = m
      m.bsp.msgPort.PostMessage(userVariableChanged)
      
      m.bsp.SendUDPNotification("refresh")
      
    end if
    
  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 = { }
    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%
    end if
    
    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
    end if
    
    bindResult = deleteStatement.BindByName(params)
    
    if not bindResult then
      m.diagnostics.PrintDebug("Bind failure")
      stop
    end if
    
    sqlResult = deleteStatement.Run()
    
    if sqlResult <> SQLITE_COMPLETE
      m.diagnostics.PrintDebug("sqlResult <> SQLITE_COMPLETE")
    end if
    
    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
    end if
    
    if type(params) = "roArray" then
      bindResult = insertStatement.BindByOffset(params)
    else
      bindResult = insertStatement.BindByName(params)
    end if
    
    if not bindResult then
      m.diagnostics.PrintDebug("Bind failure")
      stop
    end if
    
    sqlResult = insertStatement.Run()
    
    if sqlResult <> SQLITE_COMPLETE
      m.diagnostics.PrintDebug("sqlResult <> SQLITE_COMPLETE")
    end if
    
    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
    end if
    
    bindResult = true
    if type(params) = "roArray" then
      bindResult = selectStmt.BindByOffset(params)
    else if type(params) = "roAssociativeArray" then
      bindResult = selectStmt.BindByName(params)
    end if
    
    if not bindResult then
      m.diagnostics.PrintDebug("Bind failure")
      stop
    end if
    
    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)
    end if
    
    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 "
    end if
    
    select$ = select$ + "Sections.SectionName='" + sectionName$ + "')"
    
    if sortByPosition then
      select$ = select$ + " ORDER BY Variables2.Position"
    end if
    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$)
    end if
    
  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 "
    end if
    
    select$ = select$ + "Sections.SectionName =:sn_param;"
    
    selectStmt = m.userVariablesDB.CreateStatement(select$)
    
    if type(selectStmt) <> "roSqliteStatement" then
      m.diagnostics.PrintDebug("CreateStatement failure - " + select$)
      stop
    end if
    
    bindResult = selectStmt.BindByName(params)
    
    if not bindResult then
      m.diagnostics.PrintDebug("Bind failure")
      stop
    end if
    
    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"
      end if
      
      userVariable = newUserVariable(selectData.bsp, variableName$, currentValue$, defaultValue$, mediaUrl$, access$)
      userVariable.position% = -1
      selectData.currentUserVariables.AddReplace(variableName$, userVariable)
      
    end if
    
  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
    end if
    
    sqlResult = createStmt.Run()
    
    if sqlResult <> SQLITE_COMPLETE
      m.diagnostics.PrintDebug("sqlResult <> SQLITE_COMPLETE")
    end if
    
    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
    end if
    
    sqlResult = deleteStmt.Run()
    
    if sqlResult <> SQLITE_COMPLETE
      m.diagnostics.PrintDebug("sqlResult <> SQLITE_COMPLETE")
    end if
    
    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
      end if
    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
    end if
    
    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)
    end if
    
    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()
      end if
      
      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$)
              end if
              position% = position% + 1
            next
          next
          
          ' drop old tables
          m.DropSchema1Tables()
          
          ' update version
          m.UpdateDBVersion(m.dbSchemaVersion$)
          
        end if
        
      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
        end if
        
        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()
        
      end if
      
    end if
    
    ' 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
      end if
    next
    
  end sub
  
  
  Function GetUserVariable(variableName$ as string) as object
    
    userVariable = invalid
    
    if m.currentUserVariables.DoesExist(variableName$) then
      userVariable = m.currentUserVariables.Lookup(variableName$)
    end if
    
    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%
      end if
      
      m.DeleteDBVariable(categoryId%, variableName$)
      userVariables.Delete(variableName$)
      
    end if
    
    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 = { }
    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$ = "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)
      end if
    end if
    
  end sub
  
  
  Sub AssignSystemVariablesToUserVariables()
    
    for each variableName in m.currentUserVariables
      userVariable = m.currentUserVariables.Lookup(variableName)
      if type(userVariable) = "roAssociativeArray" then
        m.AssignSystemVariableToUserVariables(userVariable)
      end if
    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%
      end if
    else
      if bp200ConfigureAutomatically then
        return 0
      else
        return bp200Configuration%
      end if
    end if
    
  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
    end if
    
  end function
  
  
  Function GetIntFromString(str$ as string) as integer
    
    if str$ <> "" then
      return int(val(str$))
    else
      return 0
    end if
    
  end function
  
  'endregion
  
  'region ZoneHSM
  
  Function newZoneHSM(bsp as object, msgPort as object, sign as object, zoneDescription as object, globalVariables as object) as object
    
    zoneType$ = zoneDescription.type$
    
    ' create objects and read zone specific parameters
    
    if zoneType$ = "VideoOrImages" then
      
      zoneHSM = newVideoOrImagesZoneHSM(bsp, zoneDescription)
      
    else if zoneType$ = "VideoOnly" then
      
      zoneHSM = newVideoZoneHSM(bsp, zoneDescription)
      
    else if zoneType$ = "Images" then
      
      zoneHSM = newImagesZoneHSM(bsp, zoneDescription)
      
    else if zoneType$ = "AudioOnly" then
      
      zoneHSM = newAudioZoneHSM(bsp, zoneDescription)
      
    else if zoneType$ = "EnhancedAudio" then
      
      zoneHSM = newEnhancedAudioZoneHSM(bsp, zoneDescription)
      
    else if zoneType$ = "Ticker" then
      
      zoneHSM = newTickerZoneHSM(bsp, sign, zoneDescription)
      
    else if zoneType$ = "Clock" then
      
      zoneHSM = newClockZoneHSM(bsp, zoneDescription)
      
    else if zoneType$ = "BackgroundImage" then
      
      zoneHSM = newBackgroundImageZoneHSM(bsp, zoneDescription)
  
    else if zoneType$ = "Control" then
  
      zoneHSM = newControlZoneHSM(bsp, zoneDescription)
  
    end if
    
    zoneHSM.type$ = zoneType$
    
    zoneHSM.CreateObjects = CreateObjects
    zoneHSM.CreateCommunicationObjects = CreateCommunicationObjects
    zoneHSM.CreateObjectsNeededForTransitionCommands = CreateObjectsNeededForTransitionCommands
    zoneHSM.CreateObjectForTransitionCommand = CreateObjectForTransitionCommand
    zoneHSM.CreateSerial = CreateSerial
    zoneHSM.CreateUDPSender = CreateUDPSender
    
    zoneHSM.InitializeZoneCommon = InitializeZoneCommon
    
    zoneHSM.language$ = globalVariables.language$
    
    zoneHSM.playlist = newPlaylist(bsp, zoneHSM, sign, zoneDescription.playlist)
    
    zoneHSM.playbackActive = false
    return zoneHSM
    
  end function
  
  
  Function newPlaylist(bsp as object, zoneHSM as object, sign as object, playlistDescription as object) as object
    
    playlistBS = { }
    
    playlistBS.name$ = playlistDescription.name
    
    ' get states
    
    stateDescriptionList = playlistDescription.stateDescriptions
    if type(stateDescriptionList) <> "roArray" then print "Invalid autoplay file - state list not found" : stop
    
    initialMediaStateName = playlistDescription.initialMediaStateName
    
    if zoneHSM.type$ = "Ticker" then
      
      zoneHSM.rssDataFeedItems = []
      
      for each stateDescription in stateDescriptionList
        tickerItem = newTickerItem(bsp, zoneHSM, stateDescription)
        if tickerItem <> invalid then
          zoneHSM.rssDataFeedItems.push(tickerItem)
        end if
      next
      
    else
      
      zoneHSM.transitionDescriptions = []
      
      zoneHSM.stateTable = { }
      for each stateDescription in stateDescriptionList
        bsState = newState(bsp, zoneHSM, sign, stateDescription, invalid)
      next
      
      initialMediaStateName = playlistDescription.initialMediaStateName
      
      ' find the initial state for the playlist
      for each stateName in zoneHSM.stateTable
        bsState = zoneHSM.stateTable[stateName]
        if bsState.id$ = initialMediaStateName then
          playlistBS.firstState = GetInitialState(zoneHSM, bsState)
          exit for
        end if
      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
            end if
          next
        end if
      next
      
      ' get transitions from top level states
      for each transitionDescription in playlistDescription.transitionDescriptions
        newTransition(bsp, zoneHSM, sign, transitionDescription)
      next
      
      ' get transitions from super states
      for each transitionDescription in zoneHSM.transitionDescriptions
        newTransition(bsp, zoneHSM, sign, transitionDescription)
      next
      
    end if
    
    return playlistBS
    
  end function
  
  
  Function GetInitialState(zoneHSM as object, state as object) as object
    
    if state.type$ <> "superState" then
      return state
    end if
    
    initialStateName$ = state.initialStateName$
    
    for each stateName in zoneHSM.stateTable
      state = zoneHSM.stateTable[stateName]
      if state.id$ = initialStateName$ then
        return GetInitialState(zoneHSM, state)
      end if
    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))
      end if
      inputSpec.push(byteValue)
      byteString$ = mid(byteString$, commaPosition + 1)
    end while
    
    return inputSpec
    
  end function
  
  
  Function GetSimpleEventDataFromUserEvent(userEvent as object) as object
    return userEvent.data.data
  end function
  
  
  Sub newTransition(bsp as object, zoneHSM as object, sign as object, transitionSpec as object)
  
    stateTable = zoneHSM.stateTable
    
    sourceMediaState$ = transitionSpec.sourceMediaState
    
    ' given the sourceMediaState, find the associated bsState
    bsState = stateTable.Lookup(sourceMediaState$)
    
    userEventName$ = transitionSpec.userEvent.name
    userEventData = transitionSpec.userEvent.data
    
    nextMediaState$ = transitionSpec.targetMediaState
    
    transition = { }
    transition.AssignEventInputToUserVariable = AssignEventInputToUserVariable
    transition.AssignWildcardInputToUserVariable = AssignWildcardInputToUserVariable
    
    transition.targetMediaState$ = nextMediaState$
    transition.targetMediaStateIsPreviousState = transitionSpec.targetMediaStateIsPreviousState
    transition.remainOnCurrentStateActions = transitionSpec.remainOnCurrentStateActions
    
    ' 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$
      end if
    end if
    
    transition.assignInputToUserVariable = transitionSpec.assignInputToUserVariable
    if transition.assignInputToUserVariable then
      if IsString(transitionspec.variableToAssignFromInput$) then
        transition.variableToAssignFromInput$ = transitionSpec.variableToAssignFromInput$
        transition.variableToAssignFromInput = bsp.GetUserVariable(transition.variableToAssignFromInput$)
      else
        transition.variableToAssignFromInput = invalid
      end if
    else
      transition.variableToAssign$ = ""
    end if
    
    transition.assignWildcardToUserVariable = transitionSpec.assignWildcardToUserVariable
    if transition.assignWildcardToUserVariable then
      transition.variableToAssignFromWildcard = invalid
      if IsString(transitionspec.variableToAssign$) then
        variableToAssign$ = transitionSpec.variableToAssign$
        if variableToAssign$ <> "" then
          transition.variableToAssignFromWildcard = bsp.GetUserVariable(variableToAssign$)
          if transition.variableToAssignFromWildcard = invalid then
            bsp.diagnostics.PrintDebug("User variable " + variableToAssign$ + " not found.")
          end if
        end if
      end if
    end if
    
    if userEventName$ = "gpioUserEvent" then
      
      transition.buttonPanelIndex% = userEventData.buttonPanelIndex%
      transition.buttonNumber$ = userEventData.buttonNumber$
      transition.buttonDirection$ = userEventData.buttonDirection$
      
      transition.configuration$ = userEventData.configuration$
      
      if transition.configuration$ = "pressContinuous" then
        transition.initialHoldoff$ = userEventData.initialHoldoff$
        transition.repeatInterval$ = userEventData.repeatInterval$
      end if
      
      bsp.ConfigureGPIOInput(transition.buttonNumber$)
      
      if transition.buttonDirection$ = "down" then
        bsState.gpioEvents[transition.buttonNumber$] = transition
      else
        bsState.gpioUpEvents[transition.buttonNumber$] = transition
      end if
      
    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$ = userEventData.configuration$
      transition.buttonPanelIndex% = userEventData.buttonPanelIndex%
      transition.buttonNumber$ = userEventData.buttonNumber$
      
      if transition.configuration$ = "pressContinuous" then
        transition.initialHoldoff$ = userEventData.initialHoldoff$
        transition.repeatInterval$ = userEventData.repeatInterval$
      end if
      
      bsp.ConfigureBPInput(transition.buttonPanelIndex%, transition.buttonNumber$)
      
      bpEvents = bsState.bpEvents
      currentBPEvent = bpEvents[transition.buttonPanelIndex%]
      currentBPEvent.AddReplace(transition.buttonNumber$, transition)
      
    else if userEventName$ = "gpsEvent" then
      stop
      enterRegion$ = userEvent.parameters.enterRegion
      
      transition.radiusInFeet = val(userEvent.parameters.gpsRegion.radiusInFeet)
      transition.latitude = val(userEvent.parameters.gpsRegion.latitude)
      transition.latitudeInRadians = ConvertDecimalDegtoRad(transition.latitude)
      transition.longitude = val(userEvent.parameters.gpsRegion.longitude)
      transition.longitudeInRadians = ConvertDecimalDegtoRad(transition.longitude)
      
      if lcase(enterRegion$) = "true" then
        bsState.gpsEnterRegionEvents.push(transition)
      else
        bsState.gpsExitRegionEvents.push(transition)
      end if
      
    else if userEventName$ = "serial" then
      
      userEventData = transitionSpec.userEvent.data
      
      port$ = StripLeadingSpaces(userEventData.port)
      port$ = m.bsp.GetRuntimeUsbConnector(port$)
  
      serial$ = userEventData.data
  
      if IsUsbPort(port$) then
        usbSpec = GetGlobalAA().usbConnectorNameToUsbSpec.Lookup(port$)
        if GetGlobalAA().usbHIDPortConfigurations.DoesExist(port$) then
          usbHIDPortConfiguration = GetGlobalAA().usbHIDPortConfigurations[port$]
          protocol$ = usbHIDPortConfiguration.protocol$
        else
          protocol$ = ""
        end if
        eventKey$ = port$
      else
        port% = int(val(port$))
        serialPortConfiguration = sign.serialPortConfigurations[port%]
        protocol$ = serialPortConfiguration.protocol$
        eventKey$ = port$
      end if
      
      serialEvents = bsState.serialEvents
      if type(serialEvents[eventKey$]) <> "roAssociativeArray" then
        serialEvents[eventKey$] = { }
      end if
      
      if protocol$ = "Binary" then
        if type(serialEvents[eventKey$].streamInputTransitionSpecs) <> "roArray" then
          serialEvents[eventKey$].streamInputTransitionSpecs = CreateObject("roArray", 1, true)
        end if
        
        streamInputTransitionSpec = { }
        streamInputTransitionSpec.transition = transition
        streamInputTransitionSpec.inputSpec = ConvertToByteArray(serial$)
        streamInputTransitionSpec.asciiSpec = serial$
        serialEvents[eventKey$].streamInputTransitionSpecs.push(streamInputTransitionSpec)
        
      else if protocol$ <> "" then
        serialPortEvents = serialEvents[eventKey$]
        serialPortEvents[serial$] = transition
      end if
      
    else if userEventName$ = "timeout" then
      
      bsState.mstimeoutValue% = userEventData.interval * 1000
      bsState.mstimeoutEvent = transition
      
    else if userEventName$ = "wssEvent" then
      
      if type(bsState.wssEvents) <> "roAssociativeArray" then
        bsState.wssEvents = { }
      end if
      
      port$ = GetRuntimeUsbConnector(userEventData.port)
  
      wssEvents = bsState.wssEvents
      if type(wssEvents[port$]) <> "roAssociativeArray" then
        wssEvents[port$] = { }
      end if
      
      wssEvent = transitionspec.userevent.data.wssEventSpec
      
      eventId$ = wssEvent.Id
      
      wssPortEvents = wssEvents[port$]
      
      if type(wssPortEvents[eventId$]) <> "roAssociativeArray" then
        wssPortEvents[eventId$] = { }
      end if
      
      wssPortEvents[eventId$].properties = wssEvent.properties
      wssPortEvents[eventId$].friendlyId = wssEvent.friendlyId
      
      if type(wssPortEvents[eventId$].wssEventTransitionEventSpecs) <> "roAssociativeArray" then
        wssPortEvents[eventId$].wssEventTransitionEventSpecs = { }
      end if
      
      wssEventTransitionEventSpecs = wssPortEvents[eventId$].wssEventTransitionEventSpecs
      
      ' for each property where matchParameter === true, set up an associative array of mapping from event parameter values to transitions
      for each propertyName in wssEvent.properties
        
        property = wssEvent.properties[propertyName]
        
        if property.matchParameter then
          
          if not wssEventTransitionEventSpecs.DoesExist(propertyName) then
            wssEventTransitionEventSpecs.AddReplace(propertyName, { })
          end if
          wssEventTransitionSpecByPropertyName = wssEventTransitionEventSpecs.Lookup(propertyName)
          
          keyPropertyValue = wssEvent.Properties.Lookup(propertyName).ParameterValue
          
          wssEventTransitionSpecByPropertyName.AddReplace(keyPropertyValue, transition)
          
        end if
        
      next
      
      if wssEventTransitionEventSpecs.IsEmpty() then
        wssEventTransitionSpec = { }
        wssEventTransitionSpec.transition = transition
        wssEventTransitionSpec.wssEvent = wssEvent
        wssPortEvents[eventId$] = wssEventTransitionSpec
      end if
      
    else if userEventName$ = "timeClockEvent" then
      
      timeClockEventTransitionSpec = { }
      timeClockEventTransitionSpec.transition = transition
      
      if userEventData.type = "timeClockDateTime" then
        dateTime$ = userEventData.data.dateTime
        timeClockEventTransitionSpec.timeClockEventDateTime = FixDateTime(dateTime$)
      else if userEventData.type = "timeClockDailyOnce" then
        timeClockEventTransitionSpec.daysOfWeek% = userEventData.data.daysOfWeek
        timeClockEventTransitionSpec.timeClockDaily% = userEventData.data.time
      else if userEventData.type = "timeClockDailyPeriodic" then
        timeClockEventTransitionSpec.timeClockPeriodicInterval% = userEventData.data.interval
        timeClockEventTransitionSpec.timeClockPeriodicStartTime% = userEventData.data.startTime
        timeClockEventTransitionSpec.timeClockPeriodicEndTime% = userEventData.data.endTime
        timeClockEventTransitionSpec.daysOfWeek% = userEventData.data.daysOfWeek
      else if userEventData.type = "timeClockDateTimeByUserVariable" then
        userVariableName$ = userEventData.data.userVariableName
        timeClockEventTransitionSpec.userVariableName$ = userVariableName$
        timeClockEventTransitionSpec.userVariable = bsp.GetUserVariable(userVariableName$)
      end if
      
      if type(bsState.timeClockEvents) <> "roArray" then
        bsState.timeClockEvents = CreateObject("roArray", 1, true)
      end if
      
      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$ = "mediaRSS" then
        
        bsState.signChannelEndEvent = transition
        
      else if bsState.type$ = "mediaList" 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$ = "superState" then
        
        bsState.mediaEndEvent = transition
        
      end if
      
    else if userEventName$ = "mediaListEnd" then
      
      bsState.mediaListEndEvent = transition
      
    else if userEventName$ = "keyboard" then
      
      keyboardChar$ = userEventData.data
      
      if len(keyboardChar$) > 1 then
        keyboardChar$ = Lcase(keyboardChar$)
      end if
      
      if type(bsState.keyboardEvents) <> "roAssociativeArray" then
        bsState.keyboardEvents = { }
      end if
      
      bsState.keyboardEvents[keyboardChar$] = transition
      
    else if userEventName$ = "remote" then
    
      remote$ = ucase(userEventData.data)
      
      if type(bsState.remoteEvents) <> "roAssociativeArray" then
        bsState.remoteEvents = { }
      end if
  
      bsState.remoteEvents[remote$] = transition
      
    else if userEventName$ = "usb" then
      
      usbString$ = userEventData.data
      
      if type(bsState.usbStringEvents) <> "roAssociativeArray" then
        bsState.usbStringEvents = { }
      end if
      
      bsState.usbStringEvents[usbString$] = transition
      
    else if userEventName$ = "udp" then
      
      if type(bsState.udpEvents) <> "roAssociativeArray" then
        bsState.udpEvents = { }
      end if
      
      transition.udpLabel$ = userEventData.label
      transition.udpExport = userEventData.export
      
      bsState.udpEvents[userEventData.data] = transition
      
    else if userEventName$ = "synchronize" then
      
      bsp.IsSyncSlave = true
      
      synchronize$ = userEventData.data
      
      if type(bsState.synchronizeEvents) <> "roAssociativeArray" then
        bsState.synchronizeEvents = { }
      end if
      
      bsState.synchronizeEvents[synchronize$] = transition
      
    else if userEventName$ = "zoneMessage" then
      
      if type(bsState.zoneMessageEvents) <> "roAssociativeArray" then
        bsState.zoneMessageEvents = { }
      end if
      
      bsState.zoneMessageEvents[userEventData.data] = transition
      
    else if userEventName$ = "pluginMessageEvent" then
      
      pluginName$ = userEventData.name
      pluginMessage$ = userEventData.data
      ' unique key is concatenation of plugin name and plugin message
      key$ = pluginName$ + pluginMessage$
      
      if type(bsState.pluginMessageEvents) <> "roAssociativeArray" then
        bsState.pluginMessageEvents = { }
      end if
      
      bsState.pluginMessageEvents[key$] = transition
      
    else if userEventName$ = "internalSynchronize" then
      
      internalSynchronize$ = userEventData.data
      
      if type(bsState.internalSynchronizeEvents) <> "roAssociativeArray" then
        bsState.internalSynchronizeEvents = { }
      end if
      
      bsState.internalSynchronizeEvents[internalSynchronize$] = transition
      
    else if userEventName$ = "rectangularTouchEvent" then
      
      if type(bsState.touchEvents) <> "roAssociativeArray" then
        bsState.touchEvents = { }
      end if
      
      transition.x% = userEventData.x
      transition.y% = userEventData.y
      transition.width% = userEventData.width
      transition.height% = userEventData.height
      
      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%)
      end if
      
      bsState.touchEvents[stri(sign.numTouchEvents%)] = transition
      sign.numTouchEvents% = sign.numTouchEvents% + 1
      
    else if userEventName$ = "audioTimeCodeEvent" then
      
      if type(bsState.audioTimeCodeEvents) <> "roAssociativeArray" then
        bsState.audioTimeCodeEvents = { }
      end if
      
      transition.timeInMS% = userEventData.eventTime
      bsState.audioTimeCodeEvents[stri(sign.numAudioTimeCodeEvents%)] = transition
      
      sign.numAudioTimeCodeEvents% = sign.numAudioTimeCodeEvents% + 1
      
    else if userEventName$ = "videoTimeCodeEvent" then
      
      if type(bsState.videoTimeCodeEvents) <> "roAssociativeArray" then
        bsState.videoTimeCodeEvents = { }
      end if
      
      transition.timeInMS% = userEventData.eventTime
      bsState.videoTimeCodeEvents[stri(sign.numVideoTimeCodeEvents%)] = transition
      
      sign.numVideoTimeCodeEvents% = sign.numVideoTimeCodeEvents% + 1
      
    end if
    
    
    ' get commands and conditional targets
    transition.conditionalTransitions = []
    for each conditionalTransitionSpec in transitionSpec.conditionalTransitions
      newConditionalTransition(bsp, zoneHSM, conditionalTransitionSpec, transition.conditionalTransitions)
    next
    
    transition.transitionCmds = []
    transitionCommands = transitionSpec.commands
    if transitionCommands.Count() > 0 then
      for each command in transitionCommands
        newCmd(bsp, command, transition.transitionCmds)
      next
    end if
    
    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 = { }
        end if
        
        internalSynchronizeMasterTransition = { }
        internalSynchronizeMasterTransition.targetMediaState$ = nextMediaState$
        internalSynchronizeMasterTransition.targetMediaStateIsPreviousState = false
        
        '      transition.internalSynchronizeEventsMaster[transitionCmd.parameters["synchronizeKeyword"].GetCurrentParameterValue()] = internalSynchronizeMasterTransition
        transition.internalSynchronizeEventsMaster[transitionCmd.parameters["message"].GetCurrentParameterValue()] = internalSynchronizeMasterTransition
        
        ' modify this state's transition to not go to the next media state
        transition.targetMediaState$ = ""
        
      end if
    next
    
  end sub
  
  
  Function newConditionalTransition(bsp as object, zoneHSM as object, conditionalTransitionSpec as object, conditionalTransitions as object)
    
    userVariableName$ = conditionalTransitionSpec.variableName
    
    operator$ = conditionalTransitionSpec.compareOperator
    if operator$ = "" then operator$ = "EQ"
    
    targetMediaState$ = conditionalTransitionSpec.targetMediaState
    
    parameterValueSpec = jsonParseParameterValue(conditionalTransitionSpec.compareValue1)
    userVariableValue = newParameterValue(bsp, parameterValueSpec)
    
    parameterValueSpec2 = jsonParseParameterValue(conditionalTransitionSpec.compareValue2)
    userVariableValue2 = newParameterValue(bsp, parameterValueSpec2)
    
    userVariable = bsp.GetUserVariable(userVariableName$)
    if type(userVariable) = "roAssociativeArray" then
      conditionalTransition = { }
      conditionalTransition.userVariable = userVariable
      conditionalTransition.operator$ = operator$
      conditionalTransition.userVariableValue = userVariableValue
      conditionalTransition.userVariableValue2 = userVariableValue2
      
      conditionalTransition.targetMediaState$ = targetMediaState$
      if conditionalTransition.targetMediaState$ <> "" then
        targetState = zoneHSM.stateTable[conditionalTransition.targetMediaState$]
        if targetState.type$ = "superState" and targetState.firstState <> invalid then
          conditionalTransition.targetMediaState$ = targetState.firstState.id$
        end if
      end if
      
      targetAction = GetTargetActionFromEventAction(conditionalTransitionSpec.conditionalAction)
      conditionalTransition.remainOnCurrentStateActions = targetAction.remainOnCurrentStateActions
      conditionalTransition.targetMediaStateIsPreviousState = targetAction.targetMediaStateIsPreviousState
      
      conditionalTransition.transitionCmds = []
      for each command in conditionalTransitionSpec.commands
        newCmd(bsp, command, conditionalTransition.transitionCmds)
      next
      
      conditionalTransitions.push(conditionalTransition)
    else
      bsp.diagnostics.PrintDebug("User variable " + userVariableName$ + " not found.")
    end if
    
  end function
  
  
  Function GetTargetActionFromEventAction(eventAction as string) as object
    
    targetAction = { }
    
    if eventAction = "StopPlayback" then
      targetAction.targetMediaStateIsPreviousState = false
      targetAction.remainOnCurrentStateActions = "stop"
    else if eventAction = "StopPlaybackAndClearScreen" then
      targetAction.targetMediaStateIsPreviousState = false
      targetAction.remainOnCurrentStateActions = "stopclear"
    else if eventAction = "ReturnToPriorState" then
      targetAction.targetMediaStateIsPreviousState = true
      targetAction.remainOnCurrentStateActions = "none"
      '  if transitionSpec.eventAction = "None" then
    else
      targetAction.targetMediaStateIsPreviousState = false
      targetAction.remainOnCurrentStateActions = "none"
    end if
    
    return targetAction
    
  end function
  
  
  Function newTickerItem(bsp as object, zoneHSM as object, stateDescription as object) as object
    
    item = invalid
    
    if type(stateDescription.rssDataFeedPlaylistItem) = "roAssociativeArray" then
      item = newRSSDataFeedPlaylistItem(bsp, stateDescription)
    else if type(stateDescription.twitterItem) = "roAssociativeArray" then
      item = newTwitterPlaylistItem(bsp, stateDescription)
    else if type(stateDescription.textItem) = "roAssociativeArray" then
      item = newTextPlaylistItem(stateDescription)
    else if type(stateDescription.userVariableInTickerItem) = "roAssociativeArray"
      item = newUserVariablePlaylistItem(stateDescription, bsp, zoneHSM)
    end if
    
    return item
    
  end function
  
  
  Function newState(bsp as object, zoneHSM as object, sign as object, stateDescription as object, superState as object) as object
  
    ' get the name
    stateName$ = stateDescription.name
    
    state = zoneHSM.newHState(bsp, stateName$)
  
    state.name$ = stateName$
    
    if type(superState) = "roAssociativeArray" then
      state.superState = superState
    else
      state.superState = zoneHSM.stTop
    end if
    
    ' create data structures for arrays of specific events
    state.gpioEvents = { }
    state.gpioUpEvents = { }
    
    state.bpEvents = CreateObject("roArray", 3, true)
    state.bpEvents[0] = { }
    state.bpEvents[1] = { }
    state.bpEvents[2] = { }
    state.bpEvents[3] = { }
    
    state.serialEvents = { }
    state.gpsEnterRegionEvents = CreateObject("roArray", 1, true)
    state.gpsExitRegionEvents = CreateObject("roArray", 1, true)
    
    ' get the item
    item = { }
    
    if type(stateDescription.imageItem) = "roAssociativeArray" then
      
      newImagePlaylistItem(bsp, stateDescription.imageItem, zoneHSM, state, item)
      state.imageItem = item
      state.type$ = "image"
      zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
      
    else if type(stateDescription.videoItem) = "roAssociativeArray" then
      
      newVideoPlaylistItem(bsp, stateDescription.videoItem, state, item)
      state.videoItem = item
      state.type$ = "video"
      ' TODO - Bacon
      '' any idea why the following was here? copy / paste error?'
      ''    zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
      
    else if type(stateDescription.audioItem) = "roAssociativeArray" then
      
      newAudioPlaylistItem(bsp, stateDescription.audioItem, state, item)
      state.audioItem = item
      state.type$ = "audio"
      
    else if type(stateDescription.liveVideoItem) = "roAssociativeArray" then
      
      newLiveVideoPlaylistItem(stateDescription.liveVideoItem, state)
      state.type$ = "liveVideo"
      
    else if type(stateDescription.eventHandlerItem) = "roAssociativeArray" then
      
      newEventHandlerPlaylistItem(stateDescription.eventHandlerItem, state)
      state.type$ = "eventHandler"
      
    else if type(stateDescription.eventHandler2Item) = "roAssociativeArray" then
      
      newEventHandlerPlaylistItem(stateDescription.eventHandler2Item, state)
      state.type$ = "eventHandler"
      
    else if type(stateDescription.templatePlaylistItem) = "roAssociativeArray" then
      
      newTemplatePlaylistItem(bsp, stateDescription.templatePlaylistItem, state)
      state.type$ = "template"
      
    else if type(stateDescription.mrssDataFeedPlaylistItem) = "roAssociativeArray" then
      
      ' require that the storage is writable
      if bsp.sysInfo.storageIsWriteProtected then DisplayStorageDeviceLockedMessage()
      
      newMRSSPlaylistItem(bsp, zoneHSM, stateDescription.mrssDataFeedPlaylistItem, state)
      
      state.type$ = "mediaRSS"
      
      if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
        zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
      end if
      
    else if type(stateDescription.localPlaylistItem) = "roAssociativeArray" then
      
      newLocalPlaylistItem(bsp, stateDescription.localPlaylistItem, state)
      state.type$ = "mediaRSS"
      
      if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
        zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
      end if
      
    else if type(stateDescription.mediaSuperState) = "roAssociativeArray" then
      
      state.HStateEventHandler = MediaItemEventHandler
      SetMediaItemEventHandlers(state)
      
      state.ExecuteTransition = ExecuteTransition
      state.MatchWssEvent = MatchWssEvent
      state.GetNextStateName = GetNextStateName
      state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
      
    else if type(stateDescription.backgroundImageItem) = "roAssociativeArray" then
      
      newBackgroundImagePlaylistItem(bsp, stateDescription.backgroundImageItem, state, item)
      state.backgroundImageItem = item
      
    else if type(stateDescription.mediaListItem) = "roAssociativeArray" then
      
      newMediaListPlaylistItem(bsp, stateDescription.mediaListItem, zoneHSM, state, item)
      state.type$ = "mediaList"
      
    else if type(stateDescription.interactiveMenuItem) = "roAssociativeArray" then
      
      newInteractiveMenuPlaylistItem(bsp, sign, stateDescription.interactiveMenuItem, state)
      state.type$ = "interactiveMenuItem"
      
      if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
        zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
      end if
      
    else if type(stateDescription.playFileItem) = "roAssociativeArray" then
      
      newPlayFilePlaylistItem(bsp, stateDescription.playFileItem, state)
      state.type$ = "playFile"
      
      if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
        zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
      end if
      
    else if type(stateDescription.videoStreamItem) = "roAssociativeArray" then
      
      newStreamPlaylistItem(bsp, stateDescription.videoStreamItem, state)
      state.mediaType$ = "video"
      state.type$ = "stream"
      
    else if type(stateDescription.audioStreamItem) = "roAssociativeArray" then
  
      newStreamPlaylistItem(bsp, stateDescription.audioStreamItem, state)
      state.mediaType$ = "audio"
      state.type$ = "stream"
      
    else if type(stateDescription.mjpegStreamItem) = "roAssociativeArray" then
      
      newMjpegStreamPlaylistItem(bsp, stateDescription.mjpegStreamItem, state)
      state.type$ = "mjpeg"
      
    else if type(stateDescription.html5Item) = "roAssociativeArray" then
      
      newHtml5PlaylistItem(bsp, stateDescription.html5Item, state)
      state.type$ = "html5"
      
    else if type(stateDescription.superStateItem) = "roAssociativeArray" then
      
      newSuperStateItem(bsp, zoneHSM, sign, stateDescription.superStateItem, state)
      state.type$ = "superState"
      
    end if
    
    ' get any media state commands (commands that are executed when a state is entered)
    state.cmds = CreateObject("roArray", 1, true)
    
    ' new style commands
    cmds = stateDescription.brightSignCmd
    if type(cmds) = "roArray" and cmds.Count() > 0 then
      for each cmd in cmds
        newCmd(bsp, cmd, state.cmds)
      next
    end if
    
    ' get media state exit commands
    state.exitCmds = CreateObject("roArray", 1, true)
    exitCmds = stateDescription.brightSignExitCommands
    if type(exitCmds) = "roArray" and exitCmds.Count() > 0 then
      for each cmd in exitCmds
        newCmd(bsp, cmd, state.exitCmds)
      next
    end if
    
    zoneHSM.stateTable[state.id$] = state
    
    return state
    
  end function
  
  
  Function newTextParameterValue(value$ as string) as object
    
    parameterValue = { }
    parameterValue.GetCurrentParameterValue = GetCurrentParameterValue
    
    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 newParameterValueItemText(value$ as string) as object
    
    parameterValueItem = { }
    parameterValueItem.GetCurrentValue = GetCurrentTextParameterValue
    
    parameterValueItem.type$ = "text"
    parameterValueItem.textValue$ = value$
    
    return parameterValueItem
    
  end function
  
  
  Function newParameterValueItemFromTextConstant(textValue$ as string) as object
    
    parameterValueItem = { }
    parameterValueItem.GetCurrentValue = GetCurrentTextParameterValue
    
    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 ""
    end if
    
  end function
  
  
  ' BACONTODO - looks like this is unused currently, but it might still be used in ba
  Function newParameterValueItemMediaCounterVariable(bsp as object, parameterValueItemMediaCounterVariable as object) as object
    stop
    parameterValueItem = { }
    parameterValueItem.GetCurrentValue = GetCurrentUserVariableParameterValue
    
    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$)
    end if
    
    return parameterValueItem
    
  end function
  
  
  Function GetCurrentParameterValue() as string
    
    value$ = ""
    
    for each parameterValueItem in m.parameterValueItems
      if type(parameterValueItem) = "roAssociativeArray" then
        value$ = value$ + parameterValueItem.GetCurrentValue()
      end if
    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$
        end if
      end if
    end if
    
    return variableName$
    
  end function
  
  
  ' cmdDescription is an aa with a single entry - the key is the command name; the value is the command
  Sub newCmd(bsp as object, cmdDescription as object, cmds as object)
  
    usbConnectorNameToUsbSpec = GetGlobalAA().usbConnectorNameToUsbSpec
    
    for each cmdName in cmdDescription
      
      bsCmd = { }
      bsCmd.name$ = cmdName
      bsCmd.parameters = { }
      
      cmdParametersArray = cmdDescription[cmdName]
      for each cmdParameterAA in cmdParametersArray
        pviSpecs = []
        for each cmdParameterName in cmdParameterAA
  
          cmdParameterValueItemArray = cmdParameterAA[cmdParameterName]
          
          for each cmdParameterValueItem in cmdParameterValueItemArray
            pviSpecs.push(cmdParameterValueItem)
          next
          
          parameterValueSpec = jsonParseParameterValue(pviSpecs)
          parameterValue = newParameterValue(bsp, parameterValueSpec)
                  
          bsCmd.parameters.AddReplace(cmdParameterName, parameterValue)
          
        next
        
      next
      
      cmds.push(bsCmd)
      
    next
    
  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()
      end if
    end if
    
    if clearImage then
      if type(m.imagePlayer) = "roImageWidget" then
        m.imagePlayer.StopDisplay()
      end if
    end if
    
    if hideCanvas then
      if type(m.canvasWidget) = "roCanvasWidget" then
        m.canvasWidget.Hide()
      end if
    end if
    
    if hideHtml then
      if type(m.displayedHtmlWidget) = "roHtmlWidget" then
        '			m.displayedHtmlWidget.Hide()
        m.loadingHtmlWidget = invalid
        m.displayedHtmlWidget = invalid
      end if
    end if
    
    if showImage then
      if type(m.imagePlayer) = "roImageWidget" and m.isVisible then
        m.imagePlayer.Show()
      end if
    end if
    
    if showCanvas then
      if type(m.canvasWidget) = "roCanvasWidget" and m.isVisible then
        m.canvasWidget.Show()
      end if
    end if
    
    if showHtml then
      if type(m.displayedHtmlWidget) = "roHtmlWidget" and m.isVisible then
        m.displayedHtmlWidget.Show()
      end if
    end if
    
  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$)
    end if
    
    m.playbackActive = true
    m.playbackStartTime$ = m.bsp.systemTime.GetLocalDateTime().GetString()
    m.playbackItemType$ = itemType$
    m.playbackFileName$ = fileName$
    
  end sub
  
  
  Sub newMediaPlaylistItem(bsp as object, playlistItemDescription as object, state as object, playlistItemBS as object)
    
    playlistItemBS.fileName$ = playlistItemDescription.fileName
    playlistItemBS.userVariable = bsp.GetUserVariable(playlistItemBS.fileName$)
    if type(bsp.encryptionByFile) = "roAssociativeArray" then
      playlistItemBS.isEncrypted = bsp.encryptionByFile.DoesExist(playlistItemBS.fileName$)
    else
      playlistItemBS.isEncrypted = false
    end if
    
    SetMediaItemEventHandlers(state)
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
    state.PreloadItem = PreloadItem
    
  end sub
  
  
  Sub newImagePlaylistItem(bsp as object, playlistItemDescription as object, zoneHSM as object, state as object, playlistItemBS as object)
    
    newMediaPlaylistItem(bsp, playlistItemDescription, state, playlistItemBS)
    
    playlistItemBS.slideTransition% = GetSlideTransitionValue(playlistItemDescription.transitionEffect.transitionType)
    playlistItemBS.transitionDuration% = playlistItemDescription.transitionEffect.transitionDuration
    
    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")
    end if
    
    return probe
    
  end function
  
  
  Sub newVideoPlaylistItem(bsp as object, playlistItemDescription as object, state as object, playlistItemBS as object)
    
    newMediaPlaylistItem(bsp, playlistItemDescription, state, playlistItemBS)
    
    playlistItemBS.automaticallyLoop = playlistItemDescription.automaticallyLoop
    
    playlistItemBS.probeData = GetProbeData(bsp.assetPoolFiles, playlistItemBS.fileName$)
    playlistItemBS.videoDisplayMode% = 0
    playlistItemBS.volume% = 100
    
    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
    
    newMediaPlaylistItem(bsp, playlistItemDescription, state, playlistItemBS)
    
    playlistItemBS.automaticallyLoop = playlistItemDescription.automaticallyLoop
    
  end sub
  
  
  Sub newSuperStateItem(bsp as object, zoneHSM as object, sign as object, playlistItemDescription as object, state as object)
    
    state.initialStateName$ = playlistItemDescription.initialState
    
    for each subState in playlistItemDescription.states
      stateDescription = jsonParseState(subState)
      newState(bsp, zoneHSM, sign, stateDescription, state)
    next
    
    state.HStateEventHandler = STSuperStateEventHandler
    SetMediaItemEventHandlers(state)
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    state.GetNextStateName = GetNextStateName
    state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
    state.ConfigureBPButtons = ConfigureBPButtons
    
  end sub
  
  
  Sub newEventHandlerPlaylistItem(playlistItemDescription as object, state as object)
    
    state.stopPlayback = playlistItemDescription.stopPlayback
    
    state.HStateEventHandler = STEventHandlerEventHandler
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    SetMediaItemEventHandlers(state)
    
  end sub
  
  
  
  Sub newHtml5PlaylistItem(bsp as object, playlistItemDescription as object, state as object)
    
    state.name$ = playlistItemDescription.name$
    state.htmlSiteName$ = playlistItemDescription.htmlSiteName$
    
    ' get the associated html site
    if bsp.htmlSites.DoesExist(state.htmlSiteName$) then
      htmlSite = bsp.htmlSites.Lookup(state.htmlSiteName$)
      state.isNodeServer = htmlSite.isNodeServer
      state.contentIsLocal = htmlSite.contentIsLocal
      if state.contentIsLocal then
        state.prefix$ = htmlSite.prefix$
        state.filePath$ = htmlSite.filePath$
        state.url = invalid
      else
        state.url = htmlSite.url
      end if
  
      state.queryString = htmlSite.queryString  
    else
      ' what to do here?
      stop
    end if
    
    state.enableBrightSignJavascriptObjects = playlistItemDescription.enableBrightSignJavascriptObjects
    state.enableCrossDomainPolicyChecks = playlistItemDescription.enableCrossDomainPolicyChecks
    state.ignoreHttpsCertificateErrors = playlistItemDescription.ignoreHttpsCertificateErrors
    if type(playlistItemDescription.enableCamera) = "Boolean" then
      state.enableCamera = playlistItemDescription.enableCamera
    else
      state.enableCamera = false
    endif
    state.enableMouseEvents = playlistItemDescription.enableMouseEvents
    state.displayCursor = playlistItemDescription.displayCursor
    state.hwzOn = playlistItemDescription.hwzOn
    state.useUserStylesheet = playlistItemDescription.useUserStylesheet
    if state.useUserStylesheet then
      state.userStylesheet = playlistItemDescription.userStylesheet
    end if
    
    state.customFonts = []
    customFonts = playlistItemDescription.customFonts
    for each customFont in customFonts
      state.customFonts.push(customFont)
    next
    
    state.HStateEventHandler = STHTML5PlayingEventHandler
    state.LoadHtmlWidgetStaticInitialization = LoadHtmlWidgetStaticInitialization
    SetMediaItemEventHandlers(state)
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    state.LaunchTimer = LaunchTimer
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    
  end sub
  
  
  Sub newLiveVideoPlaylistItem(playlistItemDescription as object, state as object)
    
    state.volume% = playlistItemDescription.volume
    state.overscan = playlistItemDescription.overscan
    
    state.HStateEventHandler = STLiveVideoPlayingEventHandler
    SetMediaItemEventHandlers(state)
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    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)
    stop
    state.backgroundImage$ = ParseFileNameXML(playlistItemXML.backgroundImage)
    state.backgroundImageUserVariable = bsp.GetUserVariable(state.backgroundImage$)
    
    state.navigateToLastSelectedOnEntry = false
    if lcase(playlistItemXML.navigateToLastSelectedOnEntry.GetText()) = "true" then
      state.navigateToLastSelectedOnEntry = true
    end if
    
    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
    SetMediaItemEventHandlers(state)
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    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
    stop
    interactiveMenuItem = { }
    
    interactiveMenuItem.index$ = interactiveMenuItemXML.index.GetText()
    scaleScreenElement(bsp, false, interactiveMenuItem, interactiveMenuItemXML)
    
    interactiveMenuItem.selectedImage$ = ParseFileNameXML(interactiveMenuItemXML.selectedImage)
    interactiveMenuItem.unselectedImage$ = ParseFileNameXML(interactiveMenuItemXML.unselectedImage)
    
    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$
      end if
      
      interactiveMenuItem.targetAudioFile$ = ParseFileNameXML(interactiveMenuItemXML.targetAudioFile)
      interactiveMenuitem.targetAudioFileUserVariable = bsp.GetUserVariable(interactiveMenuItem.targetAudioFile$)
      if interactiveMenuItem.targetAudioFile$ <> "" then
        mediaFileName = interactiveMenuItem.targetAudioFile$
      end if
      
      interactiveMenuItem.targetImageFile$ = ParseFileNameXML(interactiveMenuItemXML.targetImageFile)
      if interactiveMenuItem.targetImageFile$ <> "" then
        mediaFileName = interactiveMenuItem.targetImageFile$
      end if
      
      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$))
      else if interactiveMenuItem.targetVideoFile$ <> "" then
        interactiveMenuItem.probeData = GetProbeData(bsp.assetPoolFiles, interactiveMenuItem.targetVideoFile$)
      else if interactiveMenuItem.targetAudioFile$ <> "" then
        interactiveMenuItem.probeData = GetProbeData(bsp.assetPoolFiles, interactiveMenuItem.targetAudioFile$)
      end if
      
      if type(bsp.encryptionByFile) = "roAssociativeArray" and mediaFileName <> "" then
        interactiveMenuItem.isEncrypted = bsp.encryptionByFile.DoesExist(mediaFileName)
      else
        interactiveMenuItem.isEncrypted = false
      end if
      
    else if interactiveMenuItem.targetType$ = "mediaState" then
      interactiveMenuItem.targetMediaState$ = interactiveMenuItemXML.targetMediaState.GetText()
    end if
    
    nextIsPrevious$ = interactiveMenuItemXML.targetIsPreviousState.GetText()
    interactiveMenuItem.targetIsPreviousState = false
    if nextIsPrevious$ <> "" and lcase(nextIsPrevious$) = "yes" then
      interactiveMenuItem.targetIsPreviousState = true
    end if
    
    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
    end if
    
    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$ = "serialSendString" or 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)
      end if
    next
    
    interactiveMenuItem.upNavigationIndex$ = interactiveMenuItemXML.upNavigationMenuItem.GetText()
    if IsString(interactiveMenuItem.upNavigationIndex$) and interactiveMenuItem.upNavigationIndex$ <> "" then
      interactiveMenuItem.upNavigationIndex% = int(val(interactiveMenuItem.upNavigationIndex$))
    else
      interactiveMenuItem.upNavigationIndex% = -1
    end if
    
    interactiveMenuItem.downNavigationIndex$ = interactiveMenuItemXML.downNavigationMenuItem.GetText()
    if IsString(interactiveMenuItem.downNavigationIndex$) and interactiveMenuItem.downNavigationIndex$ <> "" then
      interactiveMenuItem.downNavigationIndex% = int(val(interactiveMenuItem.downNavigationIndex$))
    else
      interactiveMenuItem.downNavigationIndex% = -1
    end if
    
    interactiveMenuItem.leftNavigationIndex$ = interactiveMenuItemXML.leftNavigationMenuItem.GetText()
    if IsString(interactiveMenuItem.leftNavigationIndex$) and interactiveMenuItem.leftNavigationIndex$ <> "" then
      interactiveMenuItem.leftNavigationIndex% = int(val(interactiveMenuItem.leftNavigationIndex$))
    else
      interactiveMenuItem.leftNavigationIndex% = -1
    end if
    
    interactiveMenuItem.rightNavigationIndex$ = interactiveMenuItemXML.rightNavigationMenuItem.GetText()
    if IsString(interactiveMenuItem.rightNavigationIndex$) and interactiveMenuItem.rightNavigationIndex$ <> "" then
      interactiveMenuItem.rightNavigationIndex% = int(val(interactiveMenuItem.rightNavigationIndex$))
    else
      interactiveMenuItem.rightNavigationIndex% = -1
    end if
    
    return interactiveMenuItem
    
  end function
  
  
  Function ParseFileNameXML(fileNameXML as object) as string
    stop
    if fileNameXML.Count() = 1 then
      fileElement = fileNameXML[0]
      fileAttributes = fileElement.GetAttributes()
      fileName$ = fileAttributes.Lookup("name")
    else
      fileName$ = ""
    end if
    
    return fileName$
    
  end function
  
  
  Function ParseBoolAttribute(elementXML as object, attr$ as string) as boolean
    stop
    element = elementXML
    attributes = element.GetAttributes()
    val = attributes.Lookup(attr$)
    if IsString(val) and lcase(val) = "true" then
      return true
    end if
    
    return false
    
  end function
  
  
  Function ParseNavigation(bsp as object, sign as object, navigationXML as object) as object
    stop
    navigation = invalid
    
    if navigationXML.Count() = 1 then
      navigationElement = navigationXML[0]
      userEventsList = navigationElement.userEvent
      if userEventsList.Count() > 0 then
        navigation = { }
        for each userEvent in userEventsList
          ParseUserEvent(bsp, sign, navigation, userEvent)
        next
      end if
    end if
    
    return navigation
    
  end function
  
  
  Sub ParseUserEvent(bsp as object, sign as object, aa as object, userEvent as object)
    stop
    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$)
      end if
      
    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)
      end if
      
    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$)
      end if
      
      serial$ = userEvent.parameters.parameter2.GetText()
      
      if IsUsbPort(port$) then
        usbHIDPortConfiguration = GetGlobalAA().usbHIDPortConfigurations[port$]
        protocol$ = usbHIDPortConfiguration.protocol$
      end if
      
      aa.serialEvent = { }
      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$
      end if
      
      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()
      end if
      
      aa.gpioEvent = { }
      aa.gpioEvent.buttonNumber$ = buttonNumber$
      
      bsp.ConfigureGPIOInput(buttonNumber$)
      
    end if
    
  end sub
  
  
  Sub scaleScreenElement(bsp as object, setDimensions as boolean, item as object, itemDescription as object)
    
    if bsp.configuredResX <> bsp.actualResX or bsp.configuredResY <> bsp.actualResY then
      xOffset = itemDescription.x / bsp.configuredResX
      x% = xOffset * bsp.actualResX
      item.x% = x%
      
      yOffset = itemDescription.Y / bsp.configuredResY
      y% = yOffset * bsp.actualResY
      item.y% = y%
      
      if setDimensions then
        width% = bsp.actualResX / bsp.configuredResX * itemDescription.width
        item.width% = width%
        
        height% = bsp.actualResY / bsp.configuredResY * itemDescription.height
        item.height% = height%
      end if
      
    else
      item.x% = itemDescription.x
      item.y% = itemDescription.y
      if setDimensions then
        item.width% = itemDescription.width
        item.height% = itemDescription.height
      end if
    end if
    
  end sub
  
  
  Sub newBaseTemplateItem(templateItem as object, templateItemDescription as object)
    
    ' scale text item if necessary
    bsp = GetGlobalAA().bsp
    scaleScreenElement(bsp, true, templateItem, templateItemDescription)
    templateItem.layer% = templateItemDescription.layer
    
  end sub
  
  
  Sub ParseTemplateWidgets(item as object, itemDescription as object)
    
    ' text widget items
    item.numberOfLines% = itemDescription.textWidget.numberOfLines
    
    item.rotation$ = "0"
    rotation$ = itemDescription.textWidget.rotation
    if rotation$ = "90" then
      item.rotation$ = "270"
    else if rotation$ = "180" then
      item.rotation$ = "180"
    else if rotation$ = "270" then
      item.rotation$ = "90"
    end if
    
    item.alignment$ = lcase(itemDescription.textWidget.alignment)
    
    ' widget items
    item.foregroundTextColor$ = GetHexColor(itemDescription.widget.foregroundTextColor)
    item.backgroundTextColor$ = GetHexColor(itemDescription.widget.backgroundTextColor)
    item.font$ = itemDescription.widget.font
    if item.font$ = "" then item.font$ = "System"
    item.fontSize% = itemDescription.widget.fontSize
    
    item.backgroundColorSpecified = itemDescription.backgroundColorSpecified
    
  end sub
  
  
  Sub newTextTemplateItem(templateItem as object, templateItemDescription as object)
    
    ' fill in base class members
    newBaseTemplateItem(templateItem, templateItemDescription)
    
    ParseTemplateWidgets(templateItem, templateItemDescription)
    
  end sub
  
  
  Sub newConstantTextTemplateItem(bsp as object, state as object, templateItems as object, templateItemDescription as object)
    
    templateItem = { }
    templateItem.type$ = "constantTextTemplateItem"
    
    newTextTemplateItem(templateItem, templateItemDescription)
    
    templateItem.textString$ = templateItemDescription.text
    
    templateItems.push(templateItem)
    
  end sub
  
  
  Sub newSystemVariableTemplateItem(bsp as object, state as object, templateItems as object, templateItemDescription as object)
    
    templateItem = { }
    templateItem.type$ = "systemVariableTemplateItem"
    
    newTextTemplateItem(templateItem, templateItemDescription)
    
    templateItem.systemVariableType$ = templateItemDescription.systemVariable
    
    templateItems.push(templateItem)
    
  end sub
  
  
  Sub newUserVariableTemplateItem(bsp as object, templateItems as object, templateItemDescription as object)
    
    templateItem = { }
    templateItem.type$ = "userVariableTemplateItem"
    
    newTextTemplateItem(templateItem, templateItemDescription)
    
    name$ = templateItemDescription.name
    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$)
    end if
    
    templateItems.push(templateItem)
    
  end sub
  
  
  Sub newMediaCounterTextTemplateItem(bsp as object, templateItems as object, templateItemDescription as object)
    
    templateItem = { }
    templateItem.type$ = "mediaCounterTemplateItem"
    
    newTextTemplateItem(templateItem, templateItemDescription)
    
    fileName$ = templateItemDescription.fileName
    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$)
    end if
    
    templateItems.push(templateItem)
    
  end sub
  
  
  Sub newImageTemplateItem(bsp as object, templateItems as object, templateItemDescription as object)
    
    templateItem = { }
    templateItem.type$ = "imageTemplateItem"
    
    newBaseTemplateItem(templateItem, templateItemDescription)
    
    templateItem.fileName$ = templateItemDescription.fileName
    
    templateItems.push(templateItem)
    
  end sub
  
  
  Function newLiveTextDataEntryTemplateItem(bsp as object, templateItems as object, templateItemDescription as object)
    
    templateItem = { }
    
    newTextTemplateItem(templateItem, templateItemDescription)
    
    liveDataFeedId$ = CleanName(templateItemDescription.liveDataFeedId)
    templateItem.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
    
    return templateItem
    
  end function
  
  
  Sub newIndexedLiveTextDataItem(bsp as object, templateItems as object, templateItemDescription as object)
    templateItem = newLiveTextDataEntryTemplateItem(bsp, templateItems, templateItemDescription)
    templateItem.type$ = "indexedLiveTextDataItem"
    parameterValueSpec = jsonParseParameterValue(templateItemDescription.indexSpec)
    templateItem.index = newParameterValue(bsp, parameterValueSpec)
    templateItems.push(templateItem)
  end sub
  
  
  Sub newTitledLiveTextDataItem(bsp as object, templateItems as object, templateItemDescription as object)
    templateItem = newLiveTextDataEntryTemplateItem(bsp, templateItems, templateItemDescription)
    templateItem.type$ = "titledLiveTextDataItem"
    '    liveDataFeedDescription.urlPV = jsonParseParameterValue(liveDataFeedJson.url)
    parameterValueSpec = jsonParseParameterValue(templateItemDescription.titleSpec)
    templateItem.title = newParameterValue(bsp, parameterValueSpec)
    templateItems.push(templateItem)
  end sub
  
  
  Function newSimpleRssTemplateItem(bsp as object, templateItems as object, templateItemDescription as object) as object
    
    templateItem = { }
    templateItem.type$ = "simpleRssTemplateItem"
    
    newTextTemplateItem(templateItem, templateItemDescription)
    
    templateItem.id$ = templateItemDescription.id
    templateItem.elementName$ = templateItemDescription.elementName
    
    templateItems.push(templateItem)
    
    return templateItem
    
  end function
  
  
  Function newMediaRssTextTemplateItem(bsp as object, templateItems as object, templateItemDescription as object) as object
    
    templateItem = { }
    templateItem.type$ = "mrssTextTemplateItem"
    
    newTextTemplateItem(templateItem, templateItemDescription)
    
    templateItems.push(templateItem)
    
    return templateItem
    
  end function
  
  
  Function newMediaRssMediaTemplateItem(bsp as object, templateItems as object, templateItemDescription as object) as object
    
    templateItem = { }
    templateItem.type$ = "mrssMediaTemplateItem"
    
    newBaseTemplateItem(templateItem, templateItemDescription)
    
    templateItems.push(templateItem)
    
    return templateItem
    
  end function
  
  
  Sub ParseTemplateBackgroundImage(bsp as object, state as object, templateItemDescription as object)
    
    backgroundImageFile = templateItemDescription.backgroundImageFile
    if type(backgroundImageFile) = "roAssociativeArray" then
      state.backgroundImage$ = backgroundImageFile.fileName
      state.backgroundImageWidth% = backgroundImageFile.width
      state.backgroundImageHeight% = backgroundImageFile.height
    else
      state.backgroundImage$ = ""
    end if
    
    state.backgroundImageUserVariable = bsp.GetUserVariable(state.backgroundImage$)
    
  end sub
  
  
  Sub SetTemplateHandlers(state as object)
    
    state.HStateEventHandler = STTemplatePlayingEventHandler
    SetMediaItemEventHandlers(state)
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    
    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, templateItemDescription as object, state as object)
    
    ParseTemplateBackgroundImage(bsp, state, templateItemDescription)
    if templateItemDescription.mediaRssLiveDataFeedIds.Count() > 0 then
      state.mrssActive = true
      state.mrssLiveDataFeeds = []
      for each mediaRssLiveDataFeedId in templateItemDescription.mediaRssLiveDataFeedIds
        liveDataFeedId$ = CleanName(mediaRssLiveDataFeedId)
        liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
        if type(liveDataFeed) = "roAssociativeArray" then
          state.mrssLiveDataFeeds.push(liveDataFeed)
        end if
      next
    else
      state.mrssActive = false
    end if
    
    ' retrieve template items
    state.templateItems = CreateObject("roArray", 1, true)
    
    for each constantTextTemplateItem in templateItemDescription.constantTextTemplateItems
      newConstantTextTemplateItem(bsp, state, state.templateItems, constantTextTemplateItem)
    next
    
    for each systemVariableTemplateItem in templateItemDescription.systemVariableTemplateItems
      newSystemVariableTemplateItem(bsp, state, state.templateItems, systemVariableTemplateItem)
    next
    
    for each userVariableTemplateItem in templateItemDescription.userVariableTemplateItems
      newUserVariableTemplateItem(bsp, state.templateItems, userVariableTemplateItem)
    next
    
    for each mediaCounterLiveTextItem in templateItemDescription.mediaCounterLiveTextItems
      newMediaCounterTextTemplateItem(bsp, state.templateItems, mediaCounterLiveTextItem)
    next
    
    for each imageTemplateItem in templateItemDescription.imageTemplateItems
      newImageTemplateItem(bsp, state.templateItems, imageTemplateItem)
    next
    
    for each titledLiveTextDataItem in templateItemDescription.titledLiveTextDataItems
      newTitledLiveTextDataItem(bsp, state.templateItems, titledLiveTextDataItem)
    next
    
    for each indexedLiveTextDataItem in templateItemDescription.indexedLiveTextDataItems
      newIndexedLiveTextDataItem(bsp, state.templateItems, indexedLiveTextDataItem)
    next
    
    for each mediaRssTextTemplateItem in templateItemDescription.mediaRssTextTemplateItems
      
      templateItem = newMediaRssTextTemplateItem(bsp, state.templateItems, mediaRssTextTemplateItem)
      
      elementName$ = mediaRssTextTemplateItem.elementName
      if lcase(elementName$) = "title" then
        state.mrssTitleTemplateItem = templateItem
      else if lcase(elementName$) = "description" then
        state.mrssDescriptionTemplateItem = templateItem
      else ' custom field
        if type(state.mrssCustomFieldTemplateItems) <> "roAssociativeArray" then
          state.mrssCustomFieldTemplateItems = { }
        end if
        state.mrssCustomFieldTemplateItems.AddReplace(elementName$, templateItem)
      end if
      
    next
    
    for each mediaRssCustomFieldTemplateItem in templateItemDescription.mediaRssCustomFieldTemplateItems
      templateItem = newMediaRssTextTemplateItem(bsp, state.templateItems, mediaRssCustomFieldTemplateItem)
      elementName$ = mediaRssCustomFieldTemplateItem.elementName
      if type(state.mrssCustomFieldTemplateItems) <> "roAssociativeArray" then
        state.mrssCustomFieldTemplateItems = { }
      end if
      state.mrssCustomFieldTemplateItems.AddReplace(elementName$, templateItem)
    next
    
    if (not templateItemDescription.mediaRssMediaTemplateItem.IsEmpty()) then
      state.mrssMediaTemplateItem = newMediaRssMediaTemplateItem(bsp, state.templateItems, templateItemDescription.mediaRssMediaTemplateItem)
    end if
  
    for each simpleRssTemplateItem in templateItemDescription.simpleRssTemplateItems
      
      templateItem = newSimpleRssTemplateItem(bsp, state.templateItems, simpleRssTemplateItem)
  
      if type(state.simpleRSSTemplateItems) <> "roAssociativeArray" then
        state.simpleRSSTemplateItems = { }
      end if
      
      simpleRSS = state.simpleRSSTemplateItems.Lookup(templateItem.id$)
      if type(simpleRSS) <> "roAssociativeArray" then
        simpleRSS = { }
        simpleRSS.currentIndex% = 0
  
        simpleRSS.displayTime% = simpleRssTemplateItem.displayTime
        
        simpleRSS.rssLiveDataFeeds = []
        rssLiveDataFeedIds = simpleRssTemplateItem.rssLiveDataFeedIds
        if rssLiveDataFeedIds.Count() > 0 then
          for each rssLiveDataFeedId in rssLiveDataFeedIds
            liveDataFeedId$ = CleanName(rssLiveDataFeedId)
            liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
            if type(liveDataFeed) = "roAssociativeArray" then
              simpleRSS.rssLiveDataFeeds.push(liveDataFeed)
            end if
          next
        end if
        
        simpleRSS.currentLiveDataFeedIndex% = 0
        
        simpleRSS.items = []
        
        state.simpleRSSTemplateItems.AddReplace(templateItem.id$, simpleRSS)
        
      end if
      
      simpleRSS.items.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 newMRSSPlaylistItem(bsp as object, zoneHSM as object, playlistItemDescription as object, state as object)
    
    state.slideTransition% = playlistItemDescription.slideTransition%
    liveDataFeedId$ = playlistItemDescription.liveDataFeedId
    if liveDataFeedId$ <> "" then
      state.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
    end if
    
    SetMRSSHandlers(state)
    
  end sub
  
  
  Sub newLocalPlaylistItem(bsp as object, playlistItemXML as object, state as object)
  stop  
    state.slideTransition% = 0
    
    attrs = playlistItemXML.defaultPlaylist.mediaRSSSpec.GetAttributes()
    if attrs = invalid then
      ' new format
      liveDataFeedId$ = playlistItemXML.defaultPlaylistDataFeedId.GetText()
      
      devicePlaylists = playlistItemXML.devicePlaylist
      for each devicePlaylist in devicePlaylists
        deviceName$ = devicePlaylist.deviceName.GetText()
        deviceLiveDataFeedId$ = devicePlaylist.dynamicPlaylistDataFeedId.GetText()
        if deviceName$ = bsp.sysInfo.deviceUniqueID$ then
          liveDataFeedId$ = CleanName(deviceLiveDataFeedId$)
          exit for
        end if
      next
      
      if liveDataFeedId$ <> "" then
        state.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
      end if
    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
        end if
      next
      
      liveDataFeedId$ = CleanName(name$)
      liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
      if type(liveDataFeed) <> "roAssociativeArray" then
        liveDataFeed = newLiveDataFeedFromMRSSFormat(bsp, liveDataFeedId$, url$, true)
  stop      
        bsp.liveDataFeeds.AddReplace(liveDataFeed.id$, liveDataFeed)
      end if
      state.liveDataFeed = liveDataFeed
    end if
    
    SetMRSSHandlers(state)
    
  end sub
  
  
  Sub SetMRSSHandlers(state as object)
    
    state.HStateEventHandler = STPlayingMediaRSSEventHandler
    SetMediaItemEventHandlers(state)
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    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
  
  
  Function GetBoolFromXML(xmlValue$ as string) as boolean
    
    value$ = lcase(xmlValue$)
    if value$ = "true" then
      return true
    else
      return false
    end if
    
  end function
  
  
  Sub newPlayFilePlaylistItem(bsp as object, playlistItemDescription as object, state as object)
    
    state.filesTable = { }
    
    ' BACONTODO'
    ''  state.slideTransition% = playlistItemDescription.slideTransition%
    state.slideTransition% = 0
    
    state.payload$ = ""
    
    state.specifyLocalFiles = not playlistItemDescription.useDataFeed
    
    state.useDefaultMedia = playlistItemDescription.useDefaultMedia
    if state.useDefaultMedia then
      state.defaultMediaFileName$ = playlistItemDescription.defaultMediaContentItem.fileName
      state.defaultMediaFileType$ = playlistItemDescription.defaultMediaContentItem.type
    end if
    
    state.useUserVariable = playlistItemDescription.useUserVariable
    if state.useUserVariable then
      state.userVariable = bsp.GetUserVariable(playlistItemDescription.userVariableName)
      if type(state.userVariable) <> "roAssociativeArray" then
        state.useUserVariable = false
      end if
    end if
    
    liveDataFeedId$ = playlistItemDescription.dataFeedId
    if liveDataFeedId$ <> "" then
      state.liveDataFeed = bsp.liveDataFeeds.Lookup(CleanName(liveDataFeedId$))
    else
      state.liveDataFeed = invalid
    end if
    
    if state.specifyLocalFiles then
      playFileItems = playlistItemDescription.contentItems
      for each playFileItem in playFileItems
        fileTableEntry = { }
        fileTableEntry.label$ = playFileItem.label
        fileTableEntry.export = playFileItem.export
        
        contentItem = playFileItem.contentItem
        fileTableEntry.fileName$ = contentItem.fileName
        fileTableEntry.fileType$ = contentItem.type
        if fileTableEntry.fileType$ = "video" or fileTableEntry.fileType$ = "audio" then
          fileTableEntry.probeData = GetProbeData(bsp.assetPoolFiles, fileTableEntry.fileName$)
        end if
        fileTableEntry.userVariable = bsp.GetUserVariable(contentItem.fileName)
        fileTableEntry.automaticallyLoop = true
        fileTableEntry.isEncrypted = false
        
        fileTableEntry.videoDisplayMode% = 0
        
        state.filesTable.AddReplace(playFileItem.key, fileTableEntry)
      next
    end if
    
    state.HStateEventHandler = STPlayFileEventHandler
    state.PopulatePlayFileFromLiveDataFeed = PopulatePlayFileFromLiveDataFeed
    SetMediaItemEventHandlers(state)
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    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, playlistItemDescription as object, state as object)
    
    state.url = newParameterValue(bsp, playlistItemDescription.url)
    
    state.HStateEventHandler = STStreamPlayingEventHandler
    SetMediaItemEventHandlers(state)
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    state.LaunchTimer = LaunchTimer
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    
  end sub
  
  
  Sub newMjpegStreamPlaylistItem(bsp as object, playlistItemDescription as object, state as object)
    
    state.url = newParameterValue(bsp, playlistItemDescription.url)
    state.rotation% = int(val(playlistItemDescription.rotation))
    
    state.HStateEventHandler = STMjpegPlayingEventHandler
    SetMediaItemEventHandlers(state)
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    state.LaunchTimer = LaunchTimer
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    
  end sub
  
  
  Sub newAudioPlaylistItem(bsp as object, playlistItemDescription as object, state as object, playlistItemBS as object)
    
    newMediaPlaylistItem(bsp, playlistItemDescription, state, playlistItemBS)
    
    playlistItemBS.probeData = GetProbeData(bsp.assetPoolFiles, playlistItemBS.fileName$)
    playlistItemBS.volume% = playlistItemDescription.volume
    
    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(playlistItemDescription as object, bsp as object, zoneHSM as object) as object
    
    item = { }
    
    item.textStrings = []
    
    item.userVariableName = playlistItemDescription.userVariableInTickerItem.userVariableName
    
    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)
      end if
    next
    
    item.isRSSFeed = false
    item.isUserVariable = true
    
    return item
    
  end function
  
  
  Function newTextPlaylistItem(playlistItemDescription as object) as object
    
    item = { }
    
    ' TODO - may need to do some adjustment to make this work for XML
    strings = playlistItemDescription.textItem.strings
    
    item.textStrings = CreateObject("roArray", strings.count(), true)
    
    for each textString in strings
      item.textStrings.push(textString.textString)
    next
    
    item.isRSSFeed = false
    item.isUserVariable = false
    
    return item
    
  end function
  
  
  Function newTwitterPlaylistItem(bsp as object, playlistItemDescription as object) as object
    
    item = { }
    
    twitterItem = playlistItemDescription.twitterItem
    
    twitterUserName$ = twitterItem.userName
    jsonUrl$ = "https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=" + twitterUserName$ + "&tweet_mode=extended"
    url = newTextParameterValue(jsonUrl$)
    
    authData = { }
    authData.AuthType = "OAuth 1.0a"
    authData.AuthToken = twitterItem.authToken
    authData.ConsumerKey = twitterItem.BSConsumerKey
    authData.EncryptedTwitterSecrets = twitterItem.encryptedTwitterSecrets
    
    updateInterval% = twitterItem.updateInterval
    
    liveDataFeed = newLiveDataFeedWithAuthData(bsp, url, authData, updateInterval%)
    
    if not bsp.liveDataFeeds.DoesExist(liveDataFeed.id$) then
      liveDataFeed.isJSON = true
      liveDataFeed.isTwitterFeed = true
      
      if twitterItem.restrictNumberOfTweets = "ByCount" then
        liveDataFeed.restrictNumberOfItems = true
        liveDataFeed.numberOfItemsToDisplay% = twitterItem.numberOfTweetsToShow
      else if twitterItem.restrictNumberOfTweets = "ByRecentDays" then
        ' TODO - implement restriction by number of days
        liveDataFeed.restrictNumberOfItems = false
        liveDataFeed.numberOfRecentDaysToDisplay% = twitterItem.numberOfRecentDaysForTweets
      else
        liveDataFeed.restrictNumberOfItems = false
      end if
  stop    
      bsp.liveDataFeeds.AddReplace(liveDataFeed.id$, liveDataFeed)
    else
      liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeed.id$)
    end if
    
    item.liveDataFeed = liveDataFeed
    item.rssTitle$ = item.liveDataFeed.id$
    
    item.twitterUserName$ = twitterUserName$ + ": "
    item.isRSSFeed = true
    item.isUserVariable = false
    
    return item
    
  end function
  
  
  Function newRSSDataFeedPlaylistItem(bsp as object, stateDescription as object) as object
    
    item = { }
    liveDataFeedId$ = stateDescription.rssDataFeedPlaylistItem.liveDataFeedId$
    item.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
    item.rssTitle$ = item.liveDataFeed.id$
    item.isRSSFeed = true
    item.isUserVariable = false
    
    return item
    
  end function
  
  
  Sub newBackgroundImagePlaylistItem(bsp as object, playlistItemDescription as object, state as object, playlistItemBS as object)
    
    newMediaPlaylistItem(bsp, playlistItemDescription, 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
    end if
    
    return viewMode%
    
  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
    end if
    
    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 top left" then
      slideTransition% = 6
    else if slideTransitionSpec$ = "Explode top right" then
      slideTransition% = 7
    else if slideTransitionSpec$ = "Explode bottom left" then
      slideTransition% = 8
    else if slideTransitionSpec$ = "Explode 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
    end if
    
    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)
    end if
    
    if type(zone.enabledRegions) <> "roList" then
      zone.enabledRegions = CreateObject("roList")
    end if
    
  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" then
      return
    end if
    
    ' 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()
    end if
    
    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
    end if
    
    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")
        
      end if
      
    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")
      
    end if
    
    return
    
  end sub
  
  
  Sub SetAudioVolumeLimits(zone as object, audioSettings as object)
    
    audioSettings.minVolume% = zone.minimumVolume%
    audioSettings.maxVolume% = zone.maximumVolume%
    
  end sub
  
  
  Sub SetAudioMode(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
      end if
      
      if type(zone.videoPlayer) = "roVideoPlayer" then
        zone.videoPlayer.SetAudioMode(mode%)
      end if
      
      if IsAudioPlayer(zone.audioPlayer) then
        zone.audioPlayer.SetAudioMode(mode%)
      end if
    end if
    
  end sub
  
  
  Function GetUsbAudioType(parameters as object, usbConnectorName$ as string) as string
  
    if parameters.DoesExist(usbConnectorName$) then
      parameter = parameters[usbConnectorName$]
      return parameter.GetCurrentParameterValue()
    else
      return "none"
    end if
  
  end function
  
  
  Function GetAudioOutputConnector(connectorSpec$ as string) as string
  
    connector$ = connectorSpec$
  
    if connectorSpec$ = "HDMI" then
      di = CreateObject("roDeviceInfo")
      if di.GetModel() = "AU335" then
        connector$ = "earc"
      endif
    endif
  
    return connector$
  
  end function
  
  
  Sub ExecuteSetAllAudioOutputsCommand(parameters as object)
  
    parameter = parameters["zoneId"]
    zoneId$ = parameter.GetCurrentParameterValue()
    
    parameter = parameters["analog1"]
    analog$ = parameter.GetCurrentParameterValue()
    
    parameter = parameters["hdmi"]
    hdmi$ = parameter.GetCurrentParameterValue()
    
    parameter = parameters["spdif"]
    spdif$ = parameter.GetCurrentParameterValue()
  
    usbTypeA$ = GetUsbAudioType(parameters, "usbTypeA")
    usbTypeC$ = GetUsbAudioType(parameters, "usbTypeC")
    usb700_1$ = GetUsbAudioType(parameters, "usb700_1")
    usb700_2$ = GetUsbAudioType(parameters, "usb700_2")
    usb700_3$ = GetUsbAudioType(parameters, "usb700_3")
    usb700_4$ = GetUsbAudioType(parameters, "usb700_4")
    usb700_5$ = GetUsbAudioType(parameters, "usb700_5")
    usb700_6$ = GetUsbAudioType(parameters, "usb700_6")
    usb700_7$ = GetUsbAudioType(parameters, "usb700_7")
    usb_1$ = GetUsbAudioType(parameters, "usb_1")
    usb_2$ = GetUsbAudioType(parameters, "usb_2")
    usb_3$ = GetUsbAudioType(parameters, "usb_3")
    usb_4$ = GetUsbAudioType(parameters, "usb_4")
    usb_5$ = GetUsbAudioType(parameters, "usb_5")
    usb_6$ = GetUsbAudioType(parameters, "usb_6")
  
    pcm = CreateObject("roArray", 1, true)
    compressed = CreateObject("roArray", 1, true)
    multichannel = CreateObject("roArray", 1, true)
    
    analogAudioOutput = CreateObject("roAudioOutput", "Analog:1")
    hdmiAudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector("HDMI"))
    spdifAudioOutput = CreateObject("roAudioOutput", "SPDIF")
    
    if lcase(analog$) <> "none" and lcase(analog$) <> "multichannel" then
      pcm.push(analogAudioOutput)
    end if
    
    if lcase(analog$) = "multichannel" then
      multichannel.push(analogAudioOutput)
    end if
    
    if lcase(hdmi$) = "passthrough" then
      compressed.push(hdmiAudioOutput)
    else if lcase(hdmi$) <> "none" then
      pcm.push(hdmiAudioOutput)
    end if
    
    if lcase(spdif$) = "passthrough" then
      compressed.push(spdifAudioOutput)
    else if lcase(spdif$) <> "none" then
      pcm.push(spdifAudioOutput)
    end if
  
    m.SetUSBAudioOutput("usbTypeC", usbTypeC$, pcm, multichannel)
    m.SetUSBAudioOutput("usbTypeA", usbTypeA$, pcm, multichannel)
    m.SetUSBAudioOutput("usb700_1", usb700_1$, pcm, multichannel)
    m.SetUSBAudioOutput("usb700_2", usb700_2$, pcm, multichannel)
    m.SetUSBAudioOutput("usb700_3", usb700_3$, pcm, multichannel)
    m.SetUSBAudioOutput("usb700_4", usb700_4$, pcm, multichannel)
    m.SetUSBAudioOutput("usb700_5", usb700_5$, pcm, multichannel)
    m.SetUSBAudioOutput("usb700_6", usb700_6$, pcm, multichannel)
    m.SetUSBAudioOutput("usb700_7", usb700_7$, pcm, multichannel)
    m.SetUSBAudioOutput("usb_1", usb_1$, pcm, multichannel)
    m.SetUSBAudioOutput("usb_2", usb_2$, pcm, multichannel)
    m.SetUSBAudioOutput("usb_3", usb_3$, pcm, multichannel)
    m.SetUSBAudioOutput("usb_4", usb_4$, pcm, multichannel)
    m.SetUSBAudioOutput("usb_5", usb_5$, pcm, multichannel)
    m.SetUSBAudioOutput("usb_6", usb_6$, pcm, multichannel)
  
    if pcm.Count() = 0 then
      noPCMAudioOutput = CreateObject("roAudioOutput", "none")
      pcm.push(noPCMAudioOutput)
    end if
    
    if compressed.Count() = 0 then
      noCompressedAudioOutput = CreateObject("roAudioOutput", "none")
      compressed.push(noCompressedAudioOutput)
    end if
    
    if multichannel.Count() = 0 then
      noMultichannelAudioOutput = CreateObject("roAudioOutput", "none")
      multichannel.push(noMultichannelAudioOutput)
    end if
    
    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)
      end if
      
      if IsAudioPlayer(zone.audioPlayer) then
        zone.audioPlayer.SetPcmAudioOutputs(pcm)
        zone.audioPlayer.SetCompressedAudioOutputs(compressed)
        zone.audioPlayer.SetMultichannelAudioOutputs(multichannel)
      end if
      
    end if
    
  end sub
  
  
  Sub SetUSBAudioOutput(connectorName$ as string, audioType$ as string, pcm as object, multichannel as object)
    connectorName$ = m.GetRuntimeUsbConnector(connectorName$)
    if IsUsbPort(connectorName$) then
      usbSpec = GetGlobalAA().usbConnectorNameToUsbSpec.Lookup(connectorName$)
      if usbSpec.audioOutputSpec <> "" then
        usbAudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector(usbSpec.audioOutputSpec))
        if type(usbAudioOutput) = "roAudioOutput" then
          if lcase(audioType$) = "pcm" then
            pcm.push(usbAudioOutput)
          else if lcase(audioType$) = "multichannel" then
            multichannel.push(usbAudioOutput)
          end if
        end if
      endif
    end if
  end sub
  
  
  Sub UnmuteAudioConnector(connector$ as string)
    
    audioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector(connector$))
    if type(audioOutput) = "roAudioOutput" then
      audioOutput.SetMute(false)
    end if
    
  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
    end if
    
    return false
    
  end function
  
  
  Function GetUsbPort(usbConnectorName as string, usbInternalHub as string, fid as string, category as string, usbTypeATargetFid as string) as boolean
    
    ' externalHub = GetGlobalAA().externalHub
    externalHub = ""
    
    ' TEDTODO - is the first one true if the usb700 is plugged into a USBTypeA on an XT? (USB B)?
    ' TEDTODO - look more carefully at the trailing / in the brightSignUsbPort
    usb700Port = mid(usbConnectorName, len("usb700") + 2)
    if instr(1, usbConnectorName, "usb700") = 1 then
      targetFid = "A/" + usb700Port
      targetFidWithExternalHub = "A" + externalHub + "/" + usb700Port
      brightSignUsbPort = "A"
      brightSignHub = "/" + usb700Port
    else if lcase(usbConnectorName) = "usbtypec" then
      targetFid = "A"
      targetFidWithExternalHub = "A" + externalHub
      brightSignUsbPort = "A"
      brightSignHub = ""
    else if lcase(usbConnectorName) = "usbtypea" then
      targetFid = usbTypeATargetFid
      targetFidWithExternalHub = usbTypeATargetFid + externalHub
      brightSignUsbPort = usbTypeATargetFid
      brightSignHub = ""
    else if lcase(usbConnectorName) = "usb_1" then
      targetFid = "A"
      targetFidWithExternalHub = "A" + externalHub
      brightSignUsbPort = "A"
      brightSignHub = ""
    else if lcase(usbConnectorName) = "usb_2" then
      targetFid = "B"
      targetFidWithExternalHub = "B" + externalHub
      brightSignUsbPort = "B"
      brightSignHub = ""
    else if lcase(usbConnectorName) = "usb_3" then
      targetFid = "C"
      targetFidWithExternalHub = "C" + externalHub
      brightSignUsbPort = "C"
      brightSignHub = ""
    else if lcase(usbConnectorName) = "usb_4" then
      targetFid = "D"
      targetFidWithExternalHub = "D" + externalHub
      brightSignUsbPort = "D"
      brightSignHub = ""
    else if lcase(usbConnectorName) = "usb_5" then
      targetFid = "E"
      targetFidWithExternalHub = "E" + externalHub
      brightSignUsbPort = "E"
      brightSignHub = ""
    else if lcase(usbConnectorName) = "usb_6" then
      targetFid = "F"
      targetFidWithExternalHub = "F" + externalHub
      brightSignUsbPort = "F"
      brightSignHub = ""
    else
      return false
    end if
  
    targetFidWithoutExtension = targetFidWithExternalHub + usbInternalHub
  
    if fid = (targetFidWithoutExtension + ".0") then
      spec = "USB:" + targetFidWithoutExtension + ".0"
    else if fid = (targetFidWithoutExtension + ".2") then
      spec = "USB:" + targetFidWithoutExtension + ".2"
    else
      return false
    endif
  
    gaa = GetGlobalAA()
    if gaa.usbConnectorNameToUsbSpec.DoesExist(usbConnectorName) then
      usbSpec = gaa.usbConnectorNameToUsbSpec.Lookup(usbConnectorName)
    else 
      usbSpec = {}
      usbSpec.brightSignUsbPort = brightSignUsbPort
      usbSpec.brightSignHub = brightSignHub
      usbSpec.internalHub = usbInternalHub
      usbSpec.externalHub = "" ' TEDTODO externalHub
      usbSpec.audioOutputSpec = ""
      usbSpec.hidOutputSpec = ""
      gaa.usbConnectorNameToUsbSpec.AddReplace(UsbConnectorName, usbSpec)
    endif
  
    if category = "HID" then
      usbSpec.hidOutputSpec = spec
    else
      usbSpec.audioOutputSpec = spec
    endif
  
    return true
      
  end function
  
  
  Sub GetConnectedUSBDeviceName(bsp as object, model as string, connectedUSBDevices as object, usbConnectorName as string, usbInternalHub as string)
  
    if lcase(model) = "au325" or lcase(model) = "hd1024" or lcase(model) = "xt1143" or lcase(model) = "xd1033" or lcase(model) = "xt1144" or lcase(model) = "xd1034" or lcase(model) = "ls423" or lcase(model) = "ls424" or lcase(model) = "hs124" or lcase(model) = "hs123" then
      
      usbTypeATargetFid = "B"
      if lcase(model) = "hd1024" or lcase(model) = "hs124" or lcase(model) = "hs123" then
        usbTypeATargetFid = "A"
      endif
  
      for each connectedUSBDevice in connectedUSBDevices
        
        fid = connectedUSBDevice.fid
        category = connectedUSBDevice.category
  
        if fid <> "" and ucase(category) = "HID" or ucase(category) = "AUDIO" then
  
          usbDeviceFound = GetUsbPort(usbConnectorName, usbInternalHub, fid, ucase(category), usbTypeATargetFid)
  
          if not usbDeviceFound then
  
            ' check for case where a Bose product is specified for Type C connector but device is instead plugged into USB 700-1 or vice versa
            if lcase(usbConnectorName) = "usb700_1" then
              usbDeviceFound = GetUsbPort("usbTypeC", usbInternalHub, fid, ucase(category), usbTypeATargetFid)
              if usbDeviceFound then
                bsp.replaceUSB700_1_with_USB_C = true
              endif
            else if lcase(usbConnectorName) = "usbtypec" then
              usbDeviceFound = GetUsbPort("usb700_1", usbInternalHub, fid, ucase(category), usbTypeATargetFid)
              if usbDeviceFound then
                bsp.replaceUSB_C_with_USB700_1 = true
              endif
            endif
  
          end If
  
        end if
        
      next
      
    end if
      
  end sub
  
  
  Sub BuildUSBDevicesByConnector(bsp as object, sign as object)
  
    di = CreateObject("roDeviceInfo")
    connectedUSBDevices = di.GetUSBTopology({ array : true })
  
    gaa = GetGlobalAA()
  
    gaa.usbConnectorNameToUsbSpec = {}
  
    for each connector in sign.boseProductsByConnector
      boseProduct = sign.boseProductsByConnector[connector]   
      GetConnectedUSBDeviceName(bsp, di.GetModel(), connectedUSBDevices, connector, boseProduct.usbInternalHub$)
    next
  
  end sub
  
  
  Sub UnmuteAllAudio()
    
    m.UnmuteAudioConnector("Analog:1")
    m.UnmuteAudioConnector("Analog:2")
    m.UnmuteAudioConnector("Analog:3")
    m.UnmuteAudioConnector("HDMI")
    m.UnmuteAudioConnector("SPDIF")
  
    for each usbConnectorName in GetGlobalAA().usbConnectorNameToUsbSpec
      usbSpec = GetGlobalAA().usbConnectorNameToUsbSpec.Lookup(usbConnectorName)
      if usbSpec.audioOutputSpec <> "" then
        m.UnmuteAudioConnector(usbSpec.audioOutputSpec)
      endif
    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", GetAudioOutputConnector(objectName$))
        if type(audioOutput) = "roAudioOutput" then
          audioOutput.SetMute(muteOn)
        end if
      end if
    end if
  end sub
  
  
  Sub MuteAudioOutputs(muteOn as boolean, parameters as object)
  
    m.MuteAudioOutput(muteOn, parameters, "analog1", "Analog:1")
    m.MuteAudioOutput(muteOn, parameters, "hdmi", "HDMI")
    m.MuteAudioOutput(muteOn, parameters, "spdif", "SPDIF")
    for each connector in parameters
      runtimeConnector$ = m.GetRuntimeUsbConnector(connector)
      if IsUsbPort(runtimeConnector$) then      
        usbSpec = GetGlobalAA().usbConnectorNameToUsbSpec.Lookup(runtimeConnector$)
        if usbSpec.audioOutputSpec <> "" then
          m.MuteAudioOutput(muteOn, parameters, connector, usbSpec.audioOutputSpec)
        endif
      endif
    next
    
  end sub
  
  
  Sub SetConnectorVolume(parameters as object)
    parameter = parameters["connector"]
    specifiedConnector$ = parameter.GetCurrentParameterValue()
    connector$ = m.GetRuntimeUsbConnector(specifiedConnector$)
    parameter = parameters["volume"]
    volume$ = parameter.GetCurrentParameterValue()
    volume% = int(val(volume$))
  
    if lcase(connector$) = "analog" then
      m.analogVolume% = ExecuteChangeConnectorVolume("Analog:1", volume%, m.sign.audio1MinVolume%, m.sign.audio1MaxVolume%)
    else if lcase(connector$) = "hdmi" then
      m.hdmiVolume% = ExecuteChangeConnectorVolume("HDMI", volume%, m.sign.hdmiMinVolume%, m.sign.hdmiMaxVolume%)
    else if lcase(connector$) = "spdif" then
      m.spdifVolume% = ExecuteChangeConnectorVolume("SPDIF", volume%, m.sign.spdifMinVolume%, m.sign.spdifMaxVolume%)
    else if IsUsbPort(connector$) then
      usbSpec = GetGlobalAA().usbConnectorNameToUsbSpec.Lookup(connector$)
      key$ = usbSpec.brightSignUsbPort + usbSpec.brightSignHub
      if usbSpec.audioOutputSpec <> "" then
        if specifiedConnector$ = "usbTypeA" then
          m.usbVolumeA% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usbTypeAMinVolume%, m.sign.usbTypeAMaxVolume%)
        else if specifiedConnector$ = "usbTypeC" then
          m.usbVolumeB% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usbTypeCMinVolume%, m.sign.usbTypeCMaxVolume%)
        else if specifiedConnector$ = "usb700_1" then
          m.usbVolumeA1% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb700_1MinVolume%, m.sign.usb700_1MaxVolume%)
        else if specifiedConnector$ = "usb700_2" then
          m.usbVolumeA2% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb700_2MinVolume%, m.sign.usb700_2MaxVolume%)
        else if specifiedConnector$ = "usb700_3" then
          m.usbVolumeA3% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb700_3MinVolume%, m.sign.usb700_3MaxVolume%)
        else if specifiedConnector$ = "usb700_4" then
          m.usbVolumeA4% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb700_4MinVolume%, m.sign.usb700_4MaxVolume%)
        else if specifiedConnector$ = "usb700_5" then
          m.usbVolumeA5% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb700_5MinVolume%, m.sign.usb700_5MaxVolume%)
        else if specifiedConnector$ = "usb700_6" then
          m.usbVolumeA6% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb700_6MinVolume%, m.sign.usb700_6MaxVolume%)
        else if specifiedConnector$ = "usb700_7" then
          m.usbVolumeA7% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb700_7MinVolume%, m.sign.usb700_7MaxVolume%)
        else if specifiedConnector$ = "usb_1" then
          m.usbVolumeA1% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb_1MinVolume%, m.sign.usb_1MaxVolume%)
        else if specifiedConnector$ = "usb_2" then
          m.usbVolumeA2% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb_2MinVolume%, m.sign.usb_2MaxVolume%)
        else if specifiedConnector$ = "usb_3" then
          m.usbVolumeA3% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb_3MinVolume%, m.sign.usb_3MaxVolume%)
        else if specifiedConnector$ = "usb_4" then
          m.usbVolumeA4% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb_4MinVolume%, m.sign.usb_4MaxVolume%)
        else if specifiedConnector$ = "usb_5" then
          m.usbVolumeA5% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb_5MinVolume%, m.sign.usb_5MaxVolume%)
        else if specifiedConnector$ = "usb_6" then
          m.usbVolumeA6% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, volume%, m.sign.usb_6MinVolume%, m.sign.usb_6MaxVolume%)
        else
          stop
        end if
      endif
    end if
    
  end sub
  
  
  Sub ChangeConnectorVolume(multiplier% as integer, parameters as object)
    parameter = parameters["connector"]
    specifiedConnector$ = parameter.GetCurrentParameterValue()
    connector$ = m.GetRuntimeUsbConnector(specifiedConnector$)
    parameter = parameters["volume"]
    volumeDelta$ = parameter.GetCurrentParameterValue()
    volumeDelta% = int(val(volumeDelta$)) * multiplier%
    
    if lcase(connector$) = "analog" or lcase(connector$) = "analog1" then
      m.analogVolume% = ExecuteChangeConnectorVolume("Analog:1", m.analogVolume% + volumeDelta%, m.sign.audio1MinVolume%, m.sign.audio1MaxVolume%)
    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 IsUsbPort(connector$) then
      usbSpec = GetGlobalAA().usbConnectorNameToUsbSpec.Lookup(connector$)
      key$ = usbSpec.brightSignUsbPort + usbSpec.brightSignHub
      if usbSpec.audioOutputSpec <> "" then 
        if specifiedConnector$ = "usbTypeA" then
          m.usbVolumeTypeA% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeTypeA% + volumeDelta%, m.sign.usbTypeAMinVolume%, m.sign.usbTypeAMaxVolume%)
        else if specifiedConnector$ = "usbTypeC" then
          m.usbVolumeTypeC% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeTypeC% + volumeDelta%, m.sign.usbTypeCMinVolume%, m.sign.usbTypeCMaxVolume%)
        else if specifiedConnector$ = "usb700_1" then
          m.usbVolumeA1% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA1% + volumeDelta%, m.sign.usb700_1MinVolume%, m.sign.usb700_1MaxVolume%)
        else if specifiedConnector$ = "usb700_2" then
          m.usbVolumeA2% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA2% + volumeDelta%, m.sign.usb700_2MinVolume%, m.sign.usb700_2MaxVolume%)
        else if specifiedConnector$ = "usb700_3" then
          m.usbVolumeA3% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA3% + volumeDelta%, m.sign.usb700_3MinVolume%, m.sign.usb700_3MaxVolume%)
        else if specifiedConnector$ = "usb700_4" then
          m.usbVolumeA4% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA4% + volumeDelta%, m.sign.usb700_4MinVolume%, m.sign.usb700_4MaxVolume%)
        else if specifiedConnector$ = "usb700_5" then
          m.usbVolumeA5% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA5% + volumeDelta%, m.sign.usb700_5MinVolume%, m.sign.usb700_5MaxVolume%)
        else if specifiedConnector$ = "usb700_6" then
          m.usbVolumeA6% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA6% + volumeDelta%, m.sign.usb700_6MinVolume%, m.sign.usb700_6MaxVolume%)
        else if specifiedConnector$ = "usb700_7" then
          m.usbVolumeA7% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA7% + volumeDelta%, m.sign.usb700_7MinVolume%, m.sign.usb700_7MaxVolume%)
        else if specifiedConnector$ = "usb_1" then
          m.usbVolumeA1% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA1% + volumeDelta%, m.sign.usb_1MinVolume%, m.sign.usb_1MaxVolume%)
        else if specifiedConnector$ = "usb_2" then
          m.usbVolumeA2% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA2% + volumeDelta%, m.sign.usb_2MinVolume%, m.sign.usb_2MaxVolume%)
        else if specifiedConnector$ = "usb_3" then
          m.usbVolumeA3% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA3% + volumeDelta%, m.sign.usb_3MinVolume%, m.sign.usb_3MaxVolume%)
        else if specifiedConnector$ = "usb_4" then
          m.usbVolumeA4% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA4% + volumeDelta%, m.sign.usb_4MinVolume%, m.sign.usb_4MaxVolume%)
        else if specifiedConnector$ = "usb_5" then
          m.usbVolumeA5% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA5% + volumeDelta%, m.sign.usb_5MinVolume%, m.sign.usb_5MaxVolume%)
        else if specifiedConnector$ = "usb_6" then
          m.usbVolumeA6% = ExecuteChangeConnectorVolume(usbSpec.audioOutputSpec, m.usbVolumeA6% + volumeDelta%, m.sign.usb_6MinVolume%, m.sign.usb_6MaxVolume%)
        else
          stop
        end if
      end if
    end if
    
  end sub
  
  
  Function ExecuteChangeConnectorVolume(audioOutputSpec as string, newVolume% as integer, minVolume% as integer, maxVolume% as integer) as integer
    
    audioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector(audioOutputSpec))
    if type(audioOutput) = "roAudioOutput" then
      if newVolume% > maxVolume% then
        newVolume% = maxVolume%
      else if newVolume% < minVolume%
        newVolume% = minVolume%
      end if
      audioOutput.SetVolume(newVolume%)
    end if
    
    ' print "ExecuteChangeConnectorVolume - connector = ";connector$;", volume = ";newVolume%
    
    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
      end if
      if IsAudioPlayer(zone.audioPlayer) then
        zone.audioPlayer.SetVolume(volume%)
        for i% = 0 to 5
          zone.audioChannelVolumes[i%] = volume%
        next
      end if
    end if
    
  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%
        end if
        m.ChangeChannelVolumes(zone.videoPlayer, zone.videoChannelVolumes, 63, volumeDelta%, minVolume%, maxVolume%)
      end if
      
      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%
        end if
        m.ChangeChannelVolumes(zone.audioPlayer, zone.audioChannelVolumes, 63, volumeDelta%, minVolume%, maxVolume%)
      end if
      
    end if
    
  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
      end if
      
      m.SetChannelVolumes(player, channelVolumes, int(val(channelMask$)), int(val(volume$)))
    end if
    
  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%
      end if
      
      m.ChangeChannelVolumes(player, channelVolumes, int(val(channelMask$)), volumeDelta%, minVolume%, maxVolume%)
      
    end if
    
  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$)))
    end if
    
  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%)
    end if
    
  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%)
    end if
    
  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
      end if
    end if
    
  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$)))
      end if
    end if
    
  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%])
      end if
    next
    
  end sub
  
  
  Function GetVideoZone(zone as object) as object
    
    if type(zone) = "roAssociativeArray" then
      if type(zone.videoPlayer) = "roVideoPlayer" then
        return zone
      end if
    end if
    
    if type(m.sign) = "roAssociativeArray" then
      if type(m.sign.videoZoneHSM) = "roAssociativeArray" and type(m.sign.videoZoneHSM.videoPlayer) = "roVideoPlayer" then
        return m.sign.videoZoneHSM
      end if
    end if
    
    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
      end if
    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%
        end if
        player.SetChannelVolumes(mask%, channelVolumes[i%])
      end if
    next
    
  end sub
  
  
  Sub ConfigureAudioResources()
    
    if type(m.videoPlayer) = "roVideoPlayer" then
      m.videoPlayer.ConfigureAudioResources()
    else if IsAudioPlayer(m.audioPlayer) then
      m.audioPlayer.ConfigureAudioResources()
    end if
    
  end sub
  
  
  Sub CecDisplayOn()
    
    m.SendCecCommand("400D", true)
    
  end sub
  
  
  Sub CecDisplayOff()
    
    m.SendCecCommand("4036", true)
    
  end sub
  
  
  Sub CecSetSourceToBrightSign()
    
    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 boolean)
    
    cec = CreateObject("roCecInterface")
    if type(cec) = "roCecInterface" then
      if not cecSubstituteSourceAddress then
        cec.UseInitiatorAddressFromPacket(true)
      end if
      b = CreateObject("roByteArray")
      b.fromhexstring(cecCommand$)
      cec.SendRawMessage(b)
      cec = invalid
    end if
    
  end sub
  
  
  Sub PauseVideo(zone as object)
    
    zone = m.GetVideoZone(zone)
    if type(zone) = "roAssociativeArray" then
      zone.videoPlayer.Pause()
    end if
    
  end sub
  
  
  Sub ResumeVideo(zone as object)
    
    zone = m.GetVideoZone(zone)
    if type(zone) = "roAssociativeArray" then
      zone.videoPlayer.Resume()
    end if
    
  end sub
  
  
  Sub SetPowerSaveMode(enablePowerSaveMode as boolean)
    
    videoMode = CreateObject("roVideoMode")
    if type(videoMode) = "roVideoMode" then
      videoMode.SetPowerSaveMode(enablePowerSaveMode)
    end if
    videoMode = invalid
    
  end sub
  
  
  Function GetAttachedFiles() as object
    
    return m.additionalPublishedFiles
    
  end function
  
  
  Function PostponeRestart() as boolean
    
    if m.dontChangePresentationUntilMediaEndEventReceived then
      m.restartPendingMediaEnd = true
    end if
    
    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 = { }
      prepareForRestartEvent["EventType"] = "PREPARE_FOR_RESTART"
      m.msgPort.PostMessage(prepareForRestartEvent)
      
      ' send internal message indicating that new content is available
      contentUpdatedEvent = { }
      contentUpdatedEvent["EventType"] = "CONTENT_UPDATED"
      m.msgPort.PostMessage(contentUpdatedEvent)
      
    end if
    
    return executeContentRestart
    
  end function
  
  
  'endregion
  
  'region Common Zone State Machine Methods
  ' *************************************************
  '
  ' Common Zone State Machine Methods
  '
  ' *************************************************
  
  Sub newZoneCommon(bsp as object, zoneDescription as object, zoneHSM as object)
    
    zoneHSM.audioPlayer = invalid
    zoneHSM.videoPlayer = invalid
    
    zoneHSM.name$ = zoneDescription.name$
    
    ' retrieve values from supplied bsdm parameters
    zoneHSM.originalWidth% = zoneDescription.originalWidth%
    zoneHSM.originalHeight% = zoneDescription.originalHeight%
    
    ' scale the zones if necessary
    scaleScreenElement(bsp, true, zoneHSM, zoneDescription)
    
    zoneHSM.type$ = zoneDescription.type
    zoneHSM.id$ = zoneDescription.id$
    
    ''    else
    ''        zoneHSM.name$ = zoneXML.name.GetText()
    
    ' scale the zones if necessary
    ''        zoneHSM.originalWidth% = int(val(zone.width.GetText()))
    ''        zoneHSM.originalHeight% = int(val(zone.height.GetText()))
    ''        scaleScreenElement(bsp, true, zoneHSM, zone)
    
    ''        zoneHSM.type$ = zone.type.GetText()
    ''       zoneHSM.id$ = zone.id.GetText()
    ''    endif
    
    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.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.conditionalTransitions) = "roArray" then
      for each conditionalTransition in transition.conditionalTransitions
        
        matchFound = false
        
        currentValue% = val(conditionalTransition.userVariable.GetCurrentValue())
        
        userVariableValue = conditionalTransition.userVariableValue.GetCurrentParameterValue()
        userVariableValue% = val(userVariableValue)
        
        if conditionalTransition.operator$ = "EQ" then
          if conditionalTransition.userVariable.GetCurrentValue() = userVariableValue then
            matchFound = true
          end if
        else if conditionalTransition.operator$ = "NEQ" then
          if conditionalTransition.userVariable.GetCurrentValue() <> userVariableValue then
            matchFound = true
          end if
        else if conditionalTransition.operator$ = "LT" then
          if currentValue% < userVariableValue% then
            matchFound = true
          end if
        else if conditionalTransition.operator$ = "LTE" then
          if currentValue% <= userVariableValue% then
            matchFound = true
          end if
        else if conditionalTransition.operator$ = "GT" then
          if currentValue% > userVariableValue% then
            matchFound = true
          end if
        else if conditionalTransition.operator$ = "GTE" then
          if currentValue% >= userVariableValue% then
            matchFound = true
          end if
        else if conditionalTransition.operator$ = "B" then
          userVariableValue2 = conditionalTransition.userVariableValue2.GetCurrentParameterValue()
          userVariableValue2% = val(userVariableValue2)
          if currentValue% >= userVariableValue% and currentValue% <= userVariableValue2% then
            matchFound = true
          end if
        end if
        
        if matchFound then
          
          if conditionalTransition.targetMediaStateIsPreviousState then
            nextState$ = m.stateMachine.previousStateName$
          else
            nextState$ = conditionalTransition.targetMediaState$
          end if
          
          nextState.nextState$ = nextState$
          nextState.actualTarget = conditionalTransition
          return nextState
        end if
        
      next
    end if
    
    if transition.targetMediaStateIsPreviousState then
      nextState$ = m.stateMachine.previousStateName$
    else
      nextState$ = transition.targetMediaState$
    end if
    
    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
    end if
    
    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
          end if
          
        else
          
          if nextState.type$ = "playFile" then
            if nextState.useUserVariable then
              userVariable = nextState.userVariable
              payload$ = userVariable.GetCurrentValue()
            end if
            
            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$
            end if
          end if
          
          exit while
          
        end if
        
      end if
      
    end while
    
    switchToNewPresentation = m.bsp.ExecuteTransitionCommands(m.stateMachine, actualTarget)
    
    if switchToNewPresentation then
      return "HANDLED"
    end if
    
    if nextState$ = "" then
      
      if transition.remainOnCurrentStateActions = "stop" then
        if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
          m.stateMachine.videoPlayer.Stop()
        end if
      else if transition.remainOnCurrentStateActions = "stopclear" then
        if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
          m.stateMachine.videoPlayer.StopClear()
        end if
        if type(m.stateMachine.imagePlayer) = "roImageWidget" then
          m.stateMachine.imagePlayer.StopDisplay()
        end if
      end if
      
      return "HANDLED"
      
    else
      stateData.nextState = m.stateMachine.stateTable[nextState$]
      stateData.nextState.payload$ = payload$
      
      m.UpdatePreviousCurrentStateNames()
      
      return "TRANSITION"
    end if
    
  end function
  
  
  Sub AssignWildcardInputToUserVariable(bsp as object, input$ as string)
    
    if type(m.variableToAssignFromWildcard) = "roAssociativeArray" then
      m.variableToAssignFromWildcard.SetCurrentValue(input$, true)
    end if
    
  end sub
  
  
  Sub AssignEventInputToUserVariable(bsp as object, input$ as string)
    
    if type(m.variableToAssignFromInput) = "roAssociativeArray" then
      
      m.variableToAssignFromInput.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
            end if
          end if
        next
      end if
      
      if userVariablesUpdated then
        userVariablesChanged = { }
        userVariablesChanged["EventType"] = "USER_VARIABLES_UPDATED"
        bsp.msgPort.PostMessage(userVariablesChanged)
        
        ' Notify controlling devices to refresh
        bsp.SendUDPNotification("refresh")
      end if
      
    end if
    
  end sub
  
  
  Sub SetMediaItemEventHandlers(state as object)
    state.MediaItemEventHandler = MediaItemEventHandler
    state.KeyboardPressEventHandler = KeyboardPressEventHandler
    state.DatagramEventHandler = DatagramEventHandler
    state.SerialStreamLineEventHandler = SerialStreamLineEventHandler
    state.GpsEventHandler = GpsEventHandler
    state.RemoteDownEventHandler = RemoteDownEventHandler
  end sub
  
  
  Function GpsEventHandler(event as object, stateData as object) as string
    
    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
        end if
      else
        logGPSEvent = true
      end if
      
      if logGPSEvent then
        currentTime.AddSeconds(30)
        m.nextTimeToLogGPSEvent$ = currentTime.GetString()
      end if
      
      if gpsData.fixActive then
        
        if logGPSEvent then
          m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_GPS_LOCATION, str(gpsData.latitude) + ":" + str(gpsData.longitude))
        end if
        
        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, "")
          end if
          
        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, "")
          end if
          
        next
        
      else
        if logGPSEvent then
          m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_GPS_NOT_LOCKED, "")
        end if
        
        m.bsp.gpsLocation.latitude = invalid
        m.bsp.gpsLocation.longitude = invalid
      end if
    else
      ' print "GPS not valid"
    end if
    
    stateData.nextState = m.superState
    return "SUPER"
    
  end function
  
  
  Function SerialStreamLineEventHandler(event as object, stateData as object) as string
  
    serialEvent$ = event.GetString()
  
    port$ = event.GetUserData()
  
    if not IsUsbPort(port$) then
      
      port% = int(val(port$))
      
      if m.bsp.gpsConfigured and m.bsp.sign.serialPortConfigurations[port%].gps then
        return GpsEventHandler(event, stateData)
      end if
      
    end if
    
    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
          ' 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, serialEvent$)
                  end if
                  
                  if matches.Count() > 1 and transition.assignWildcardToUserVariable then
                    transition.AssignWildcardInputToUserVariable(m.bsp, matches[1])
                  end if
                  
                  return m.ExecuteTransition(transition, stateData, serialEvent$)
                end if
              end if
            end if
          next
        end if
      end if
    end if
    
    m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "0")
    
    stateData.nextState = m.superState
    return "SUPER"
    
  end function
  
  
  Function DatagramEventHandler(event as object, stateData as object) as string
    
    ' 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
            end if
          end if
          
          ' 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()
          end if
          
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "synchronize-pre", synchronizeEvent$, "1")
          
          ' ?? return "HANDLED" ??
        end if
      end if
      
      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, "")
        end if
      end if
      
    end if
    
    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
        ' 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())
                end if
                
                if matches.Count() > 1 and transition.assignWildcardToUserVariable then
                  transition.AssignWildcardInputToUserVariable(m.bsp, matches[1])
                end if
                
                return m.ExecuteTransition(transition, stateData, udpEvent$)
              end if
            end if
          end if
        next
      end if
    end if
    
    m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "udp", event.GetString(), "0")
    
    stateData.nextState = m.superState
    return "SUPER"
    
  end function
  
  
  Function RemoteDownEventHandler(event as object, stateData as object) as string
  
    m.bsp.diagnostics.PrintDebug("Remote Event" + stri(event))
    
    irRemoteControl = m.bsp.sign.irRemoteControl
    manufacturerCode = irRemoteControl.manufacturerCode
    buttons = irRemoteControl.buttons
    targetButtonCode = event - (manufacturerCode * 256)
  
    buttonCode$ = StripLeadingSpaces(stri(targetButtonCode))
  
    remoteEvents = m.remoteEvents
    if type(remoteEvents) = "roAssociativeArray" then
      if buttons.DoesExist(buttonCode$) then
        button = buttons[buttonCode$]
        buttonDescription = button.buttonDescription
        if type(remoteEvents[buttonDescription]) = "roAssociativeArray" then
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "remote", buttonDescription, "1")
          return m.ExecuteTransition(m.remoteEvents[buttonDescription], stateData, buttonDescription)
        end if
      endif
  
    endif
  
    m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "remote", buttonCode$, "0")
  
    stateData.nextState = m.superState
    return "SUPER"
  
  end function
  
  
  Function KeyboardPressEventHandler(event as object, stateData as object) as string
    
    ' 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$
        end if
        checkUSBInputString = false
      else
        checkUSBInputString = true
      end if
    end if
    
    ' 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$ = ""
          end if
          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$ = ""
            end if
            return action$
          end if
        end if
      end if
    end if
    
    ' 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$
      end if
      
      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$ = ""
        end if
        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$ = ""
        end if
        return action$
      end if
      
    end if
    
    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$ = ""
    end if
    
    stateData.nextState = m.superState
    return "SUPER"
    
  end function
  
  
  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, "")
        end if
      end if
      
    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, "")
        end if
      end if
      
    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, "")
          end if
        end if
      end if
      
      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"
                end if
                
                ' 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%)
                end if
                
                ' restart timer
                LaunchTimeClockEventTimer(m, timeClockEvent)
                
                if not triggerEvent then
                  m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timeClock", "", "0")
                  return "HANDLED"
                end if
              end if
              
              m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timeClock", "", "1")
              return m.ExecuteTransition(timeClockEvent.transition, stateData, "")
              
            end if
          end if
        next
      end if
      
      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"
          end if
        end if
      end if
      
      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, "")
        end if
      end if
      m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "touch", touchIndex$, "0")
      
    else if type(event) = "roStreamLineEvent" then
      
      return m.SerialStreamLineEventHandler(event, stateData)
      
    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, "")
            end if
          next
        end if
      end if
  
    else if type(event) = "roIRDownEvent" then
  
      return m.RemoteDownEventHandler(event, stateData)
  
    else if type(event) = "roKeyboardPress" then
      
      return m.KeyboardPressEventHandler(event, stateData)
      
    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 = { }
          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$)
          end if
        end if
        
      end if
      
    ' Set user data to distinguish between presentation udp messages and bootstrap udp messages
    else if type(event) = "roDatagramEvent" and event.GetUserData() = "receiver" then
      
      return m.DatagramEventHandler(event, stateData)
      
    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"]
          end if
          
          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$)
          end if
          
          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, "")
          end if
          
        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, "")
          end if
          
        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$)
                      end if
                      
                      if matches.Count() > 1 and transition.assignWildcardToUserVariable then
                        transition.AssignWildcardInputToUserVariable(m.bsp, matches[1])
                      end if
                      
                      return m.ExecuteTransition(transition, stateData, sendZoneMessageParameter$)
                    end if
                  end if
                end if
              next
            end if
          end if
          
          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$)
          
          if pluginName$ = "initNode" then
            
            if pluginMessage$ = "webSocketEvent" and type(event["WebsocketEvent"]) = "roAssociativeArray" then
              
              if type(m.wssEvents) = "roAssociativeArray" then
                
                webSocketEvent = event["WebsocketEvent"] ' event from Eddie
                
                rivieraPort$ = ""
                
                globalAA = getGlobalAA()
                ' globalAA.eddieDumpFile.sendLine(event["WebsocketEvent"])
                
                m.bsp.diagnostics.PrintDebug("WEBSOCKET EVENT:")
                m.bsp.diagnostics.PrintDebug("header")
                m.bsp.diagnostics.PrintDebug(formatJson(event["WebsocketEvent"].header))
                
                m.bsp.diagnostics.PrintDebug("body")
                m.bsp.diagnostics.PrintDebug(formatJson(event["WebsocketEvent"].body))
                ' print "AUTORUN RECEIVED EVENT:"
                ' eventDateTime = m.bsp.systemTime.GetLocalDateTime()
                ' print eventDateTime.GetString()
                'print formatJson(event["WebsocketEvent"].body)
                'dumpJsonBody(event["WebsocketEvent"].body)
                'globalAA.eddieDumpFile.sendLine("**** NEW WebSocketEvent")
                'dumpJsonBody(globalAA.eddieDumpFile, event["WebsocketEvent"])
                ' find USB device that corresponds to this webSocketEvent
                for each usbConnector in m.bsp.sign.boseProductsByConnector
                  boseProduct = m.bsp.sign.boseProductsByConnector[usbConnector]
                  port$ = boseProduct.port$
                  fid$ = port$ + "." + boseProduct.usbNetInterfaceIndex$
                  eventFid$ = "USB " + webSocketEvent.fid
                  if eventFid$ = fid$ then
                    rivieraPort$ = usbConnector
                    exit for
                  end if
                next
                
                ' if fid specified in webSocketEvent doesn't match USB connector specified in bpf, discard event
                if rivieraPort$ <> "" then
                  
                  resource = webSocketEvent.header.resource ' event end point - definitely needs to be matched
                  
                  
                  wssPortEvents = m.wssEvents[rivieraPort$] ' wss events for this state for this port
                  
                  if type(wssPortEvents) = "roAssociativeArray" and wssPortEvents.DoesExist(resource) then ' is this state waiting for an event from this resource?
                  
                  wssEventInfo = wssPortEvents.Lookup(resource)
                  if type(wssEventInfo) = "roAssociativeArray" and type(wssEventInfo.wssEvent) = "roAssociativeArray" then
                    print "FriendlyId=" + wssEventInfo.wssevent.FriendlyId
                  end if
                  
                  boseProduct = m.bsp.sign.boseProductsByConnector[rivieraPort$]
                  wssCommunicationSpec = boseProduct.wssCommunicationSpec
                  
                  wssCommunicationSpecEvent = GetWssCommunicationSpecEventFromWebSocketEvent(wssCommunicationSpec, webSocketEvent)
                  
                  if wssCommunicationSpecEvent <> invalid then
                    
                    wssEventInfo = wssPortEvents.Lookup(resource) ' if yes, get the additional parameters for this event (from the autoplay)
                    
                    ' Look for a match
                    if type(wssEventInfo.wssEventTransitionEventSpecs) = "roAssociativeArray" then
                      ' multiple transitions exist for this state / event - see if the event matches one of them
                      ' look for a match for the event and any of the event/transition pairs for this state'
                      for each propertyName in wssEventInfo.wssEventTransitionEventSpecs
                        
                        ' check for an exact match
                        wssEventTransitionEventSpec = wssEventInfo.wssEventTransitionEventSpecs[propertyName]
                        
                        paramAttrs = m.MatchWssEvent(wssCommunicationSpecEvent, webSocketEvent, wssEventInfo.properties, wssEventTransitionEventSpec, propertyName)
                        
                        if paramAttrs.matchFound then
                          return m.ExecuteTransition(paramAttrs.transition, stateData, event.PluginMessage)
                        end if
                        
                      next
                      
                    else
                      ' **** is it necessary to do additional matching at this point?
                      transition = wssEventInfo.transition
                      return m.ExecuteTransition(transition, stateData, event.PluginMessage)
                    end if
                    
                  end if
                  
                end if
                
              end if
              
            end if
            
          end if
          
          stateData.nextState = m.superState
          return "SUPER"
          
        end if
        
        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)
                      end if
                      
                      if matches.Count() > 1 and transition.assignWildcardToUserVariable then
                        transition.AssignWildcardInputToUserVariable(m.bsp, matches[1])
                      end if
                      
                      return m.ExecuteTransition(transition, stateData, event.PluginMessage)
                    end if
                  end if
                end if
              next
            end if
          end if
        end if
        
        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"
          end if
        end if
        
        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 = { }
            internalSyncPlay["EventType"] = "INTERNAL_SYNC_PLAY"
            internalSyncPlay["EventParameter"] = internalSyncParameter$
            m.stateMachine.msgPort.PostMessage(internalSyncPlay)
            actedOn$ = "1"
          end if
        end if
        
        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, "")
          end if
        end if
        
        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, "")
          end if
        end if
        
        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
        end if
        
        if IsAudioPlayer(m.stateMachine.audioPlayer) then
          m.stateMachine.audioPlayer = invalid
        end if
        
        if type(m.stateMachine.imagePlayer) = "roImageWidget" then
          m.stateMachine.imagePlayer = invalid
        end if
        
        return "HANDLED"
        
      end if
    end if
    
  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")
    end if
    
  end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
  end function
  
  
  Sub dumpJsonBody(eddieDumpFile as object, jsonBody as object)
    for each propertyName in jsonBody
      ''    if propertyName = "enabled" then
      ''      stop
      ''    endif
      eddieDumpFile.sendLine(propertyName)
      propertyValue = jsonBody[propertyName]
      if type(propertyValue) <> "roAssociativeArray" then
        eddieDumpFile.sendLine(propertyValue)
      else
        dumpJsonBody(eddieDumpFile, propertyValue)
      end if
    next
  end sub
  
  ' m is bsp
  Sub WaitForSyncResponse(parameter$ as string)
    
    udpReceiver = CreateObject("roDatagramReceiver", m.udpReceivePort)
    ' Set user data to distinguish between presentation udp messages and bootstrap udp messages
    udpReceiver.SetUserData("receiver")
    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
      end if
    end while
    
  end sub
  
  
  Function EventMatchesWildcard(eventSpec as string, eventValue as string) as object
    
    ' look for regular expression match if spec includes wildcard
    if instr(1, eventSpec, "(.*)") > 0 then
      r = CreateObject("roRegEx", eventSpec, "i")
      if type(r) = "roRegex" then
        return r.match(eventValue)
      end if
    end if
    
    return []
    
  end function
  
  
  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
    end if
    
    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
      end if
    else if currentTime% < endTime% or currentTime% > startTime% then
      withinWindow = true
    end if
    
    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
    end if
    
    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
      end if
      
      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
          end if
          
          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$)
        end if
      end if
    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%)
        end if
        
        ' 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)
        end if
        
        ' 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.")
        
      end if
      
      systemTime = invalid
      
    end if
    
    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
      
    end if
    
    if type(m.timeClockEvents) = "roArray" then
      
      for each timeClockEvent in m.timeClockEvents
        LaunchTimeClockEventTimer(m, timeClockEvent)
      next
      
    end if
    
  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
    end if
    
    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.isEncrypted = false
      else if fileType$ = "video" then
        videoItem = { }
        videoItem.fileName$ = fileName$
        videoItem.probeData = fileTableEntry.probeData
        videoItem.isEncrypted = false
      end if
    else if type(m.imageItem) = "roAssociativeArray" then
      imageItem = { }
      imageItem.fileName$ = m.imageItem.fileName$
    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
      end if
    end if
    
    if type(imageItem) = "roAssociativeArray" then
      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$)
      end if
      
      zone.preloadedStateName$ = m.name$
      m.bsp.diagnostics.PrintDebug("Preloaded file in PreloadItem: " + imageItem.fileName$ + ", " + imageItemFilePath$)
    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)
      end if
      
      isEncrypted = m.bsp.encryptionByFile.DoesExist(videoItem.fileName$)
      'isEncrypted = m.bsp.contentEncrypted
      if isEncrypted then
        aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
        aa.AddReplace("EncryptionKey", videoItem.fileName$)
      end if
      
      ok = zone.videoPlayer.PreloadFile(aa)
      zone.preloadedStateName$ = m.name$
      m.bsp.diagnostics.PrintDebug("Preloaded file in PreloadItem: " + videoItem.fileName$)
    end if
    
  end sub
  
  'endregion
  
  'region Images State Machine
  ' *************************************************
  '
  ' Images State Machine
  '
  ' *************************************************
  Function newImagesZoneHSM(bsp as object, zoneDescription as object) as object
    
    zoneHSM = newHSM()
    zoneHSM.ConstructorHandler = ImageZoneConstructor
    zoneHSM.InitialPseudostateHandler = ImageZoneGetInitialState
    
    newZoneCommon(bsp, zoneDescription, zoneHSM)
    
    zoneHSM.imageMode% = zoneDescription.imageMode%
    
    zoneHSM.numImageItems% = 0
    
    return zoneHSM
    
  end function
  
  
  Function IsPortraitBottomLeft(portraitSpec$) as boolean
    
    if portraitSpec$ = "portrait" or portraitSpec$ = "portraitbottomleft" then
      return true
    end if
    
    return false
    
  end function
  
  
  Function IsPortraitBottomRight(portraitSpec$) as boolean
    
    if portraitSpec$ = "portraitbottomright" then
      return true
    end if
    
    return false
    
  end function
  
  
  Sub ImageZoneConstructor()
    
    m.InitializeZoneCommon(m.bsp.msgPort)
    
    zoneHSM = m
    
    if zoneHSM.numImageItems% > 0 then
      
      imagePlayer = CreateObject("roImageWidget", zoneHSM.rectangle)
      
      if IsPortraitBottomLeft(m.bsp.sign.monitorOrientation) then
        imagePlayer.SetTransform("rot90")
      else if IsPortraitBottomRight(m.bsp.sign.monitorOrientation) then
        imagePlayer.SetTransform("rot270")
      end if
      
      zoneHSM.imagePlayer = imagePlayer
      
      ' initialize image player parameters
      imagePlayer.SetDefaultMode(zoneHSM.imageMode%)
      
    else
      
      zoneHSM.imagePlayer = invalid
      
    end if
    
    m.CreateObjects()
    
    m.activeState = m.playlist.firstState
    if type(m.playlist.firstState) = "roAssociativeArray" then
      m.previousStateName$ = m.playlist.firstState.id$
    else
      m.previousStateName$ = ""
    end if
    
  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)
        
      end if
      
    end if
  else
    return m.MediaItemEventHandler(event, stateData)
  end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
  end function
  
  'endregion
  
  'region Enhanced Audio State Machine
  ' *************************************************
  '
  ' Enhanced Audio State Machine
  '
  ' *************************************************
  Function newEnhancedAudioZoneHSM(bsp as object, zoneDescription as object) as object
    
    zoneHSM = newHSM()
    zoneHSM.ConstructorHandler = EnhancedAudioZoneConstructor
    zoneHSM.InitializeAudioZoneCommon = InitializeAudioZoneCommon
    zoneHSM.InitialPseudostateHandler = EnhancedAudioZoneGetInitialState
    
    newZoneCommon(bsp, zoneDescription, zoneHSM)
    newAudioZoneCommon(zoneDescription, zoneHSM)
    zoneHSM.fadeLength% = zoneDescription.fadeLength
    
    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, zoneDescription as object) as object
    
    zoneHSM = newHSM()
    
    zoneHSM.InitializeAudioZoneCommon = InitializeAudioZoneCommon
    zoneHSM.ConstructorHandler = AudioZoneConstructor
    zoneHSM.InitialPseudostateHandler = AudioZoneGetInitialState
    
    newZoneCommon(bsp, zoneDescription, zoneHSM)
    newAudioZoneCommon(zoneDescription, zoneHSM)
    
    return zoneHSM
    
  end function
  
  
  ' TODO - is there any reason to go from json to zoneDescription to zoneHSM parameters now that XML has been eliminated?
  Sub newAudioZoneCommon(zoneDescription as object, zoneHSM as object)
  
    zoneHSM.audioMixMode$ = zoneDescription.audioMixMode$
    
    zoneHSM.analogOutput$ = zoneDescription.analogOutput$
    zoneHSM.hdmiOutput$ = zoneDescription.hdmiOutput$
    zoneHSM.spdifOutput$ = zoneDescription.spdifOutput$
    zoneHSM.usbOutputA$ = zoneDescription.usbOutputA$
    zoneHSM.usbOutputB$ = zoneDescription.usbOutputB$
    zoneHSM.usbOutputTypeA$ = zoneDescription.usbOutputTypeA$
    zoneHSM.usbOutputTypeC$ = zoneDescription.usbOutputTypeC$
    zoneHSM.usbOutput700_1$ = zoneDescription.usbOutput700_1$
    zoneHSM.usbOutput700_2$ = zoneDescription.usbOutput700_2$
    zoneHSM.usbOutput700_3$ = zoneDescription.usbOutput700_3$
    zoneHSM.usbOutput700_4$ = zoneDescription.usbOutput700_4$
    zoneHSM.usbOutput700_5$ = zoneDescription.usbOutput700_5$
    zoneHSM.usbOutput700_6$ = zoneDescription.usbOutput700_6$
    zoneHSM.usbOutput700_7$ = zoneDescription.usbOutput700_7$
    zoneHSM.usbOutput_1$ = zoneDescription.usbOutput_1$
    zoneHSM.usbOutput_2$ = zoneDescription.usbOutput_2$
    zoneHSM.usbOutput_3$ = zoneDescription.usbOutput_3$
    zoneHSM.usbOutput_4$ = zoneDescription.usbOutput_4$
    zoneHSM.usbOutput_5$ = zoneDescription.usbOutput_5$
    zoneHSM.usbOutput_6$ = zoneDescription.usbOutput_6$
    zoneHSM.minimumVolume% = zoneDescription.minimumVolume%
    zoneHSM.maximumVolume% = zoneDescription.maximumVolume%
    
    zoneHSM.initialAudioVolume% = zoneDescription.initialAudioVolume%
    
  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.SetVolume(zoneHSM.audioVolume%)
    audioPlayer.SetLoopMode(false)
    
    zoneHSM.ConfigureAudioResources()
    
    zoneHSM.audioPlayerAudioSettings = { }
    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$ = ""
    end if
    
    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, zoneDescription as object) as object
    
    zoneHSM = newHSM()
    
    zoneHSM.InitializeVideoZoneObjects = InitializeVideoZoneObjects
    zoneHSM.ConstructorHandler = VideoZoneConstructor
    zoneHSM.InitialPseudostateHandler = VideoZoneGetInitialState
    
    newZoneCommon(bsp, zoneDescription, zoneHSM)
    
    ' zone.properties
    
    zoneHSM.viewMode% = zoneDescription.viewMode%
    
    newAudioZoneCommon(zoneDescription, zoneHSM)
    
    zoneHSM.initialVideoVolume% = zoneDescription.initialVideoVolume%
    
    zoneHSM.zOrderFront = zoneDescription.zOrderFront
    
    return zoneHSM
    
  end function
  
  
  Sub SetAudioOutputAndMode(player as object)
  
    pcm = CreateObject("roArray", 1, true)
    compressed = CreateObject("roArray", 1, true)
    multichannel = CreateObject("roArray", 1, true)
    
    analogAudioOutput = CreateObject("roAudioOutput", "Analog:1")
    hdmiAudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector("HDMI"))
    spdifAudioOutput = CreateObject("roAudioOutput", "SPDIF")
    
    if lcase(m.analogOutput$) <> "none" and lcase(m.analogOutput$) <> "multichannel" then
      pcm.push(analogAudioOutput)
    end if
    
    if lcase(m.analogOutput$) = "multichannel" then
      multichannel.push(analogAudioOutput)
    end if
    
    if lcase(m.hdmiOutput$) = "passthrough" then
      compressed.push(hdmiAudioOutput)
    else if lcase(m.hdmiOutput$) <> "none" then
      pcm.push(hdmiAudioOutput)
    end if
    
    if lcase(m.spdifOutput$) = "passthrough" then
      compressed.push(spdifAudioOutput)
    else if lcase(m.spdifOutput$) <> "none" then
      pcm.push(spdifAudioOutput)
    end if
    
    gaa = GetGlobalAA()
    
    for each runtimeUsbConnectorName in gaa.usbConnectorNameToUsbSpec
  
      usbConnectorName = m.bsp.GetSpecifiedConnector(runtimeUsbConnectorName)
  
      usbSpec = gaa.usbConnectorNameToUsbSpec.Lookup(runtimeUsbConnectorName)
      if usbSpec.audioOutputSpec <> "" then
  
        usbAudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector(usbSpec.audioOutputSpec))
  
        if usbConnectorName = "usbTypeA" then
          spec$ = m.usbOutputTypeA$
        else if usbConnectorName = "usbTypeC" then
          spec$ = m.usbOutputTypeC$
        else if usbConnectorName = "usb700_1" then
          spec$ = m.usbOutput700_1$
        else if usbConnectorName = "usb700_2" then
          spec$ = m.usbOutput700_2$
        else if usbConnectorName = "usb700_3" then
          spec$ = m.usbOutput700_3$
        else if usbConnectorName = "usb700_4" then
          spec$ = m.usbOutput700_4$
        else if usbConnectorName = "usb700_5" then
          spec$ = m.usbOutput700_5$
        else if usbConnectorName = "usb700_6" then
          spec$ = m.usbOutput700_6$
        else if usbConnectorName = "usb700_7" then
          spec$ = m.usbOutput700_7$
        else if usbConnectorName = "usb_1" then
          spec$ = m.usbOutput_1$
        else if usbConnectorName = "usb_2" then
          spec$ = m.usbOutput_2$
        else if usbConnectorName = "usb_3" then
          spec$ = m.usbOutput_3$
        else if usbConnectorName = "usb_4" then
          spec$ = m.usbOutput_4$
        else if usbConnectorName = "usb_5" then
          spec$ = m.usbOutput_5$
        else if usbConnectorName = "usb_6" then
          spec$ = m.usbOutput_6$
        else
          stop
        end if
      
        if type(usbAudioOutput) = "roAudioOutput" then
          if lcase(spec$) = "pcm" then
            pcm.push(usbAudioOutput)
          else if lcase(spec$) = "multichannel" then
            multichannel.push(usbAudioOutput)
          end if
        end if
  
      endif
  
    next
    
    if pcm.Count() = 0 then
      noPCMAudioOutput = CreateObject("roAudioOutput", "none")
      pcm.push(noPCMAudioOutput)
    end if
    
    if compressed.Count() = 0 then
      noCompressedAudioOutput = CreateObject("roAudioOutput", "none")
      compressed.push(noCompressedAudioOutput)
    end if
    
    if multichannel.Count() = 0 then
      noMultichannelAudioOutput = CreateObject("roAudioOutput", "none")
      multichannel.push(noMultichannelAudioOutput)
    end if
    
    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)
    end if
    
  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 IsPortraitBottomLeft(m.bsp.sign.monitorOrientation) then
      videoPlayer.SetTransform("rot90")
    else if IsPortraitBottomRight(m.bsp.sign.monitorOrientation) then
      videoPlayer.SetTransform("rot270")
    end if
    
    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.SetVolume(zoneHSM.videoVolume%)
    
    zoneHSM.ConfigureAudioResources()
    
    if m.bsp.sysInfo.numberOfVideoPlanes = 1 then
      ' graphics should always be in front if there's only a single video plane
      videoPlayer.ToBack()
    else
      if zoneHSM.zOrderFront then
        videoPlayer.ToFront()
      else
        videoPlayer.ToBack()
      end if
    end if
    
    zoneHSM.videoPlayerAudioSettings = { }
    m.bsp.SetAudioVolumeLimits(zoneHSM, zoneHSM.videoPlayerAudioSettings)
      
    m.activeState = m.playlist.firstState
    if type(m.playlist.firstState) = "roAssociativeArray" then
      m.previousStateName$ = m.playlist.firstState.id$
    else
      m.previousStateName$ = ""
    end if
    
  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
    end if
    
    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%
          end if
          
          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
          end if
          
          ok = m.stateMachine.videoPlayer.PlayFile(aa)
          
          if ok = 0 then
            m.bsp.diagnostics.PrintDebug("Error playing rtsp file in STStreamPlayingEventHandler: url = " + url$)
            videoStreamPlaybackFailure = { }
            videoStreamPlaybackFailure["EventType"] = "VideoStreamPlaybackFailure"
            m.stateMachine.msgPort.PostMessage(videoStreamPlaybackFailure)
          end if
        else
          ok = audioPlayer.PlayFile({ Rtsp: m.rtspStream })
          if ok = 0 then
            m.bsp.diagnostics.PrintDebug("Error playing rtsp file in STStreamPlayingEventHandler: url = " + url$)
            audioStreamPlaybackFailure = { }
            audioStreamPlaybackFailure["EventType"] = "AudioStreamPlaybackFailure"
            m.stateMachine.msgPort.PostMessage(audioStreamPlaybackFailure)
          end if
        end if
        
        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"
        end if
        if type(m.videoEndEvent) = "roAssociativeArray" then
          return m.ExecuteTransition(m.videoEndEvent, stateData, "")
        end if
        PostMediaEndEvent(m.bsp.msgPort)
        
      else if event["EventType"] = "AudioStreamPlaybackFailure" then
        
        if m.bsp.ProcessMediaEndEvent() then
          return "HANDLED"
        end if
        if type(m.audioEndEvent) = "roAssociativeArray" then
          return m.ExecuteTransition(m.audioEndEvent, stateData, "")
        end if
        
      else
        
        return m.MediaItemEventHandler(event, stateData)
        
      end if
      
    end if
    
  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"
      end if
      if type(m.videoEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.videoEndEvent, stateData, "")
      end if
    end if
  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"
      end if
      if type(m.audioEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.audioEndEvent, stateData, "")
      end if
      PostMediaEndEvent(m.bsp.msgPort)
    end if
  else
    return m.MediaItemEventHandler(event, stateData)
  end if
  
  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$)
        end if
        
        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)
        end if
        
        if type(m.stateMachine.mjpegVideoPlayer) <> "roVideoPlayer" then
          m.stateMachine.mjpegVideoPlayer = CreateObject("roVideoPlayer")
          
          if IsPortraitBottomLeft(m.bsp.sign.monitorOrientation) then
            m.stateMachine.mjpegVideoPlayer.SetTransform("rot90")
          else if IsPortraitBottomRight(m.bsp.sign.monitorOrientation) then
            m.stateMachine.mjpegVideoPlayer.SetTransform("rot270")
          end if
          
          m.stateMachine.mjpegVideoPlayer.SetRectangle(m.stateMachine.rectangle)
          m.stateMachine.mjpegVideoPlayer.SetPort(m.bsp.msgPort)
        end if
        
        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%
        end if
        
        if m.stateMachine.mosaicDecoderName <> "" then
          aa.Decoder = m.stateMachine.mosaicDecoderName
        end if
        
        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)
        
      end if
      
    end if
    
  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"
      end if
      if type(m.videoEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.videoEndEvent, stateData, "")
      end if
      PostMediaEndEvent(m.bsp.msgPort)
    end if
  else
    return m.MediaItemEventHandler(event, stateData)
  end if
  
  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"
        end if
        if type(m.videoEndEvent) = "roAssociativeArray" then
          return m.ExecuteTransition(m.videoEndEvent, stateData, "")
        end if
        PostMediaEndEvent(m.bsp.msgPort)
        
      else
        
        return m.MediaItemEventHandler(event, stateData)
        
      end if
      
    end if
    
  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"
      end if
      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$)
      end if
      
      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"
        end if
        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "0")
      end if
    end if
  else
    return m.MediaItemEventHandler(event, stateData)
  end if
  
  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
    end if
    
    player.ClearEvents()
    
    if type(m.audioTimeCodeEvents) = "roAssociativeArray" then
      for each eventNum in m.audioTimeCodeEvents
        m.AddAudioTimeCodeEvent(m.audioTimeCodeEvents[eventNum].timeInMS%, int(val(eventNum)))
      next
    end if
    
  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%)
    end if
    
  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
    end if
    
  end sub
  
  
  Sub AddVideoTimeCodeEvent(timeInMS% as integer, eventNum% as integer)
    
    if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
      m.stateMachine.videoPlayer.AddEvent(eventNum%, timeInMS%)
    end if
    
  end sub
  
  'endregion
  
  'region Video or Images State Machine
  ' *************************************************
  '
  ' VideoOrImages State Machine
  '
  ' *************************************************
  Function newVideoOrImagesZoneHSM(bsp as object, zoneDescription as object) as object
    
    zoneHSM = newVideoZoneHSM(bsp, zoneDescription)
    zoneHSM.ConstructorHandler = VideoOrImagesZoneConstructor
    zoneHSM.InitialPseudostateHandler = VideoOrImagesZoneGetInitialState
    
    zoneHSM.imageMode% = zoneDescription.imageMode%
    
    zoneHSM.numImageItems% = 0
    
    return zoneHSM
    
  end function
  
  Sub VideoOrImagesZoneConstructor()
    
    m.InitializeVideoZoneObjects()
    
    zoneHSM = m
    
    ' create players
    if zoneHSM.numImageItems% > 0 then
      
      imagePlayer = CreateObject("roImageWidget", zoneHSM.rectangle)
      
      if IsPortraitBottomLeft(m.bsp.sign.monitorOrientation) then
        imagePlayer.SetTransform("rot90")
      else if IsPortraitBottomRight(m.bsp.sign.monitorOrientation) then
        imagePlayer.SetTransform("rot270")
      end if
      
      zoneHSM.imagePlayer = imagePlayer
      
      ' initialize image player parameters
      imagePlayer.SetDefaultMode(zoneHSM.imageMode%)
      
    else
      
      zoneHSM.imagePlayer = invalid
      
    end if
    
    zoneHSM.audioVolume% = zoneHSM.initialAudioVolume%
    
    zoneHSM.audioPlayerAudioSettings = { }
    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
      
      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 = { }
        fileTableEntry.fileName$ = url$
        fileTableEntry.filePath$ = filePath$
        
        if type(m.liveDataFeed.fileTypes) = "roArray" and m.liveDataFeed.fileTypes.Count() > i% then
          fileTableEntry.fileType$ = m.liveDataFeed.fileTypes[i%]
        end if
        
        fileTableEntry.automaticallyLoop = true
        fileTableEntry.isEncrypted = false
        fileTableEntry.videoDisplayMode% = 0
        m.filesTable.AddReplace(key$, fileTableEntry)
        
      next
      
    end if
    
  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()
        end if
        
        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")
          end if
          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.defaultMediaFileType$
          end if
          m.imageItem = invalid
          m.videoItem = invalid
          m.audioItem = invalid
          if fileType$ = "image" then
            m.imageItem = { }
            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$
              end if
              m.imageItem.userVariable = fileTableEntry.userVariable
            end if
            m.imageItem.slideTransition% = m.slideTransition%
            m.DisplayImage("playFile")
          else if fileType$ = "video" then
            m.videoItem = { }
            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$
              end if
              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
            end if
            m.LaunchVideo("playFile")
          else if fileType$ = "audio" then
            m.audioItem = { }
            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$
              end if
              m.audioItem.probeData = fileTableEntry.probeData
              m.audioItem.userVariable = fileTableEntry.userVariable
            end if
            m.LaunchAudio("playFile")
          end if
        end if
        
        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"
        
      end if
      
    end if
    
  else if type(m.videoItem) = "roAssociativeArray" and 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"
      end if
      if type(m.videoEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.videoEndEvent, stateData, "")
      end if
      PostMediaEndEvent(m.bsp.msgPort)
    end if
    
  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"
      end if
      if type(m.audioEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.audioEndEvent, stateData, "")
      end if
      PostMediaEndEvent(m.bsp.msgPort)
    end if
    
  end if
  
  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
  
  
  ' BACONTODO - possible to combine these two subs?
  Sub ConfigureIntraStateEventHandlersButton(navigationList as object)
    
    for each navigation in navigationList
      
      if IsBpNavigationEvent(navigation) then
        bpEvent = { }
        bpEvent.buttonPanelIndex% = int(val(GetButtonPanelIndexFromBpIndex(navigation.eventData.bpIndex)))
        bpEvent.buttonNumber$ = stri(navigation.eventData.buttonNumber)
        bpEvent.configuration$ = "press"
        m.bsp.ConfigureBPButton(bpEvent.buttonPanelIndex%, bpEvent.buttonNumber$, bpEvent)
      end if
      
      if navigation.eventName = "gpioUserEvent" then
        
        gpioEvent = { }
        gpioEvent.buttonNumber$ = StripLeadingSpaces(stri(navigation.eventData.buttonNumber))
        gpioEvent.configuration$ = "press"
        m.bsp.ConfigureGPIOButton(gpioEvent.buttonNumber$, gpioEvent)
      end if
      
    next
    
  end sub
  
  
  Function IsBpNavigationEvent(navigationEvent as object) as boolean
    
    if navigationEvent.eventName = "bp900AUserEvent" or navigationEvent.eventName = "bp900BUserEvent" or navigationEvent.eventName = "bp900CUserEvent" or navigationEvent.eventName = "bp900DUserEvent" then
      return true
    end if
    
    if navigationEvent.eventName = "bp200AUserEvent" or navigationEvent.eventName = "bp200BUserEvent" or navigationEvent.eventName = "bp200CUserEvent" or navigationEvent.eventName = "bp200DUserEvent" then
      return true
    end if
    
    return false
    
  end function
  
  
  Function GetButtonPanelIndexFromBpIndex(bpIndex$ as string) as string
    
    bpUserEventButtonPanelIndex$ = "0"
    if bpIndex$ = "a" then
      bpUserEventButtonPanelIndex$ = "0"
    else if bpIndex$ = "b" then
      bpUserEventButtonPanelIndex$ = "1"
    else if bpIndex$ = "c" then
      bpUserEventButtonPanelIndex$ = "2"
    else if bpIndex$ = "d" then
      bpUserEventButtonPanelIndex$ = "3"
    end if
    
    return bpUserEventButtonPanelIndex$
    
  end function
  
  
  Function GetMatchingNavigationEvent(navigationEventList as object, event as object) as object
    
    MEDIA_END = 8
    
    for each navigationEvent in navigationEventList
      
      if type(event) = "roAssociativeArray" and IsString(event["EventType"]) then
        
        if IsBpNavigationEvent(navigationEvent) and event["EventType"] = "BPControlDown" then
          
          bpIndex$ = event.ButtonPanelIndex
          bpNum$ = event["ButtonNumber"]
          m.bsp.diagnostics.PrintDebug("BP Press, button number " + bpNum$ + ", button index " + bpIndex$)
          navigationButtonNumber$ = StripLeadingSpaces(stri(navigationEvent.eventData.buttonNumber))
          
          bpUserEventButtonPanelIndex$ = GetButtonPanelIndexFromBpIndex(navigationEvent.eventData.bpIndex)
          
          if navigationButtonNumber$ = bpNum$ and bpUserEventButtonPanelIndex$ = bpIndex$ then
            m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "1")
            return true
          end if
          
        else if event["EventType"] = "GPIOControlDown" and navigationEvent.eventName = "gpioUserEvent" then
          if (event["ButtonNumber"] = StripLeadingSpaces(stri(navigationEvent.eventData.buttonNumber))) then
            m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioDown", event["ButtonNumber"], "1")
            return true
          end if
          
        else if navigationEvent.eventName = "zoneMessage" and event["EventType"] = "SEND_ZONE_MESSAGE"
          
          zoneMessage$ = event["EventParameter"]
          if navigationEvent.eventData.data = zoneMessage$ then
            m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "zoneMessage", zoneMessage$, "1")
            return true
          end if
          
        end if
        
      else if navigationEvent.eventName = "timeout" and type(event) = "roTimerEvent" and type(m.advanceOnImageTimeoutTimer) = "roTimer" and event.GetSourceIdentity() = m.advanceOnImageTimeoutTimer.GetIdentity() then
        return navigationEvent
  
      else if navigationEvent.eventName = "timeout" and type(event) = "roTimerEvent" and type(m.retreatOnImageTimeoutTimer) = "roTimer" and event.GetSourceIdentity() = m.retreatOnImageTimeoutTimer.GetIdentity() then
        return navigationEvent
  
      else if navigationEvent.eventName = "mediaEnd" and type(event) = "roVideoEvent" and event.GetInt() = MEDIA_END and type(m.stateMachine.videoPlayer) = "roVideoPlayer" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() then
        return navigationEvent
        
      else if navigationEvent.eventName = "mediaEnd" and type(event) = "roAudioEvent" and event.GetInt() = MEDIA_END and event.GetSourceIdentity() = m.stateMachine.audioPlayer.GetIdentity() then
        return navigationEvent
        
      else if navigationEvent.eventName = "serial" and type(event) = "roStreamLineEvent" then
        port = event.GetUserData()
        serialEvent$ = event.GetString()
        
        m.bsp.diagnostics.PrintDebug("Serial Line Event " + serialEvent$)
        
        if port = navigationEvent.eventData.port then
          if (serialEvent$ = navigationEvent.eventData.data or navigationEvent.eventData.data = "<*>") then
            m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", serialEvent$, "1")
            return true
          else
            if instr(1, navigationEvent.eventData.data, "(.*)") > 0 then
              r = CreateObject("roRegEx", navigationEvent.eventData.data, "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
                end if
              end if
            end if
          end if
        end if
        
      else if navigationEvent.eventName = "keyboard" and type(event) = "roKeyboardPress" 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$
        end if
        
        if navigationEvent.eventData.data = keyboardChar$ or navigationEvent.eventData.data = "<any>" then
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "keyboard", keyboardChar$, "1")
          return true
        end if
        
      end if
      
    next
    
    return invalid
    
  end function
  
  
  Function HandleIntraStateEvent(event as object, navigationEventList as object) as boolean
    
    MEDIA_END = 8
    
    navigationEvent = m.GetMatchingNavigationEvent(navigationEventList, event)
    if navigationEvent = invalid then
      return false
    end if
    
    return true
    
  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 list")
      m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_EMPTY_MEDIA_PLAYLIST, "")
      stop
    end if
    
    ' get current media item and launch playback
    item = m.items[itemIndex]
    
    if m.sendZoneMessage and not(m.statemachine.type$ = "EnhancedAudio") then
      
      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)
        end if
      end if
      
      ' send ZoneMessage using the file name as the message
      zoneMessageCmd = { }
      zoneMessageCmd["EventType"] = "SEND_ZONE_MESSAGE"
      zoneMessageCmd["EventParameter"] = fileNameWithoutExtension$
      m.bsp.msgPort.PostMessage(zoneMessageCmd)
      
    end if
    
    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
      end if
    end if
    
    if executePrevCommands then
      if type(m.transitionPreviousItemCmds) = "roArray" then
        for each cmd in m.transitionPreviousItemCmds
          m.bsp.ExecuteCmd(m.stateMachine, cmd.name$, cmd.parameters)
        next
      end if
    end if
    
    if item.type = "image" then
      
      m.imageItem = item
      m.imageItem.slideTransition% = m.slideTransition%
      m.imageItem.transitionDuration% = m.transitionDuration%
      
      if not m.firstItemDisplayed then
        m.PreDrawImage()
      end if
      
      m.DrawImage(true)
      
      if not m.firstItemDisplayed then
        m.PostDrawImage("imageList")
      else
        m.ClearVideo()
      end if
      
      ' if advancing on image timeout, set the timer
      if type(m.advanceOnImageTimeoutTimer) = "roTimer" then
        m.advanceOnImageTimeoutTimer.Stop()
      end if
      
      ' setup timeout event if the user specified one in the transitionToNextEventList
      timeoutEvent = m.GetTimeoutEvent(m.transitionToNextEventList)
      if type(timeoutEvent) = "roAssociativeArray" then
        if type(m.advanceOnImageTimeoutTimer) <> "roTimer" then
          m.advanceOnImageTimeoutTimer = CreateObject("roTimer")
          m.advanceOnImageTimeoutTimer.SetPort(m.stateMachine.msgPort)
        end if
        m.advanceOnImageTimeoutTimer.SetElapsed(timeoutevent.eventData.interval, 0)
        m.advanceOnImageTimeoutTimer.Start()
      end if
      
      ' setup timeout event if the user specified one in the transitionToPreviousEventList
      timeoutEvent = m.GetTimeoutEvent(m.transitionToPreviousEventList)
      if type(timeoutEvent) = "roAssociativeArray" then
        if type(m.retreatOnImageTimeoutTimer) <> "roTimer" then
          m.retreatOnImageTimeoutTimer = CreateObject("roTimer")
          m.retreatOnImageTimeoutTimer.SetPort(m.stateMachine.msgPort)
        end if
        m.retreatOnImageTimeoutTimer.SetElapsed(timeoutevent.eventData.interval, 0)
        m.retreatOnImageTimeoutTimer.Start()
      end if
  
    else if item.type = "video" then
      
      m.videoItem = item
      
      if not m.firstItemDisplayed then
        m.PrePlayVideo()
      end if
      
      m.PlayVideo(not m.firstItemDisplayed, true)
      
      if not m.firstItemDisplayed then
        m.PostPlayVideo("videoList")
      else
        m.stateMachine.ClearImagePlane()
      end if
      
    else if item.type = "audio" then
      
      m.audioItem = item
      
      if not m.firstItemDisplayed then
        m.PrePlayAudio()
      end if
      
      if m.stateMachine.type$ = "EnhancedAudio" then
        m.PlayMixerAudio(not m.firstItemDisplayed, m.playbackIndex%, playImmediate)
      else
        m.PlayAudio(not m.firstItemDisplayed, true)
      end if
      
      if not m.firstItemDisplayed then
        m.PostPlayAudio("audioList")
      end if
      
    end if
    
    m.firstItemDisplayed = true
    
  end sub
  
  
  Function GetTimeoutEvent(userEventList) as object
    
    for each userEvent in userEventList
      if userEvent.eventName = "timeout" then
        return userEvent
      end if
    next
    
    return invalid
    
  end function
  
  
  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
    end if
    
  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
      end if
    next
    
    m.LaunchMediaListPlaybackItem(playImmediate, false, executePrevCommands)
    
    m.playbackIndex% = m.playbackIndex% + 1
    if m.playbackIndex% >= m.numItems% then
      m.playbackIndex% = 0
    end if
    
  end sub
  
  
  Sub StartInactivityTimer()
  
  	if m.inactivityTimeout then
  		if type(m.inactivityTimer) = "roTimer" then
  			userData = {}
  			userData.id = "mediaList"
  			userData.state = m
  			m.inactivityTimer.SetUserData(userData)
  			m.inactivityTimer.SetElapsed(m.inactivityTime, 0)
  			m.inactivityTimer.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
    end if
    
  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")
    if type(videoMode) = "roVideoMode" then
      videoMode.Set3dMode(m.videoItem.videoDisplayMode%)
      videoMode = invalid
    end if
    
    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$)
    end if
    
    ' 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
      end if
    end if
    
    syncInProgress = false
    if type(m.stateMachine.syncInfo) = "roAssociativeArray" then
      syncInProgress = true
    end if
    
    m.stateMachine.videoPlayer.EnableSafeRegionTrimming(false)
    
    if not preloaded and not syncInProgress then
      m.stateMachine.videoPlayer.Stop()
    end if
    
    m.SetVideoTimeCodeEvents()
    
    if executeEntryCmds then
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
    end if
    
    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 = { }
        videoPlaybackFailure["EventType"] = "VideoPlaybackFailureEvent"
        m.stateMachine.msgPort.PostMessage(videoPlaybackFailure)
      end if
      
      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)
      end if
      
      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%
        end if
        
      end if
      
      if ItemIsEncrypted(m.videoItem) then
        aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
        aa.AddReplace("EncryptionKey", file$)
      end if
      
      if m.stateMachine.mosaicDecoderName <> "" then
        aa.Decoder = m.stateMachine.mosaicDecoderName
      end if
      
      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 = { }
        videoPlaybackFailure["EventType"] = "VideoPlaybackFailureEvent"
        m.stateMachine.msgPort.PostMessage(videoPlaybackFailure)
      end if
      
      if syncInProgress then
        m.bsp.diagnostics.PrintDebug("LaunchVideo: play synchronized file " + file$)
        m.stateMachine.syncInfo = invalid
      end if
    end if
    
    if type(m.videoItem.userVariable) = "roAssociativeArray" then
      m.videoItem.userVariable.Increment()
    end if
    
    ' 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$)
    end if
        
    if setTransition then
      m.stateMachine.imagePlayer.SetDefaultTransition(m.imageItem.slideTransition%)
    end if
    
    if type(m.imageItem.transitionDuration%) = "roInt" then
      m.stateMachine.imagePlayer.SetTransitionDuration(m.imageItem.transitionDuration%)
    end if
    
    if not true then
      stop
    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$)
        end if
        
        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$)
            end if
            
          end if
        end if
        
        if not preloaded then
          
          aa = { }
          aa.filename = filePath$
          
          if ItemIsEncrypted(m.imageItem) then
            aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
            aa.AddReplace("EncryptionKey", file$)
          end if
          
          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$)
          end if
          
        end if
        
      end if
      
    end if
        
    m.stateMachine.ShowImageWidget()
    
    m.stateMachine.preloadState = 0
    m.stateMachine.preloadedStateName$ = ""
    
    if type(m.imageItem.userVariable) = "roAssociativeArray" then
      m.imageItem.userVariable.Increment()
    end if
    
    ' 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()
    
    if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
      m.stateMachine.videoPlayer.StopClear()
    end if
    
  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()
    end if
    
  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
    end if
    
    player.SetLoopMode(loopMode%)
    
    player.Stop()
    
    m.SetAudioTimeCodeEvents()
    
    if executeEntryCmds then
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
    end if
    
    if type(m.audioItem.fileName$) = "roString" and len(m.audioItem.fileName$) > 0 then
      file$ = m.audioItem.fileName$
    else
      file$ = m.audioItem.filePath$
    end if
    
    if type(m.audioItem.filePath$) = "roString" then
      filePath$ = m.audioItem.filePath$
    else
      filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, m.audioItem.fileName$)
    end if
    
    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)
    end if
    
    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 = { }
      audioPlaybackFailure["EventType"] = "AudioPlaybackFailureEvent"
      m.stateMachine.msgPort.PostMessage(audioPlaybackFailure)
    end if
    
    m.stateMachine.ClearImagePlane()
    
    if type(m.audioItem.userVariable) = "roAssociativeArray" then
      m.audioItem.userVariable.Increment()
    end if
    
    ' 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 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, "")
        end if
        
      else
        
        return m.MediaItemEventHandler(event, stateData)
        
      end if
      
    end if
    
  else
    
    return m.MediaItemEventHandler(event, stateData)
    
  end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
  end function
  
  
  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()
          end if
          
          m.stateMachine.ClearImagePlane()
          
          if IsAudioPlayer(m.stateMachine.audioPlayer) then
            m.stateMachine.audioPlayer.Stop()
          end if
          
        end if
        
        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)
        
      end if
      
    end if
    
  else
    return m.MediaItemEventHandler(event, stateData)
  end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
  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()
        
        ' HDMI In
        m.stateMachine.videoPlayer.EnableSafeRegionTrimming(m.overscan)
        
        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%
          end if
          
        end if
        
        if m.stateMachine.mosaicDecoderName <> "" then
          aa.Decoder = m.stateMachine.mosaicDecoderName
        end if
        
        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)
        
      end if
      
    end if
    
  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"
      end if
      if type(m.videoEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.videoEndEvent, stateData, "")
      end if
      
      PostMediaEndEvent(m.bsp.msgPort)'
    end if
    
  else
    return m.MediaItemEventHandler(event, stateData)
  end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
  end function
  
  
  Sub SetUserAgentForHtmlWidget(bsp as object, htmlWidget as object)
    if 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$)
      end if
    end if
  end sub
  
  
  Function GetInterstitialHtmlToLaunchHtml() as string
    interstitialHtml$ = ""
    interstitialHtml$ = interstitialHtml$ + "<html>" + chr(10)
    interstitialHtml$ = interstitialHtml$ + "<body onload =" + chr(34) + "loadURL()" + chr(34) + ">" + chr(10)
    interstitialHtml$ = interstitialHtml$ + "<script type=" + chr(34) + "text/javascript" + chr(34) + ">function loadURL(){" + chr(10)
    interstitialHtml$ = interstitialHtml$ + "process.chdir(" + chr(34) + "/storage/pool1/<siteName>" + chr(34) + ");" + chr(10)
    interstitialHtml$ = interstitialHtml$ + "document.location = " + chr(34) + "file:///pool1:/<siteName>/<fileName>" + chr(34) + ";" + chr(10)
    interstitialHtml$ = interstitialHtml$ + "};" + chr(10)
    interstitialHtml$ = interstitialHtml$ + "</script>" + chr(10)
    interstitialHtml$ = interstitialHtml$ + "</body>" + chr(10)
    interstitialHtml$ = interstitialHtml$ + "</html>" + chr(10)
    return interstitialHtml$
  end function
  
  
  Function GetInterstitialHtmlUrl(prefix$, fileName$) as string
    ' if the last character in prefix$ is a '/', strip it to create a new string
    ' substitute instances of 'siteName'
    len% = len(prefix$)
    if mid(prefix$, len(prefix$), 1) = "/" then
      len% = len% - 1
    end if
    prefixSubstitution$ = mid(prefix$, 1, len%)
    
    interstitialHtml$ = GetInterstitialHtmlToLaunchHtml()
    
    ' replace <siteName> with actual siteName
    siteNameIndex% = instr(1, interstitialHtml$, "<siteName>")
    while siteNameIndex% > 0
      interstitialHtmlLength% = len(interstitialHtml$)
      interstitialHtml$ = mid(interstitialHtml$, 1, siteNameIndex% - 1) + prefixSubstitution$ + mid(interstitialHtml$, siteNameIndex% + len("<siteName>"))
      siteNameIndex% = instr(1, interstitialHtml$, "<siteName>")
    end while
    
    ' replace <fileName> with actual filename
    fileNameIndex% = instr(1, interstitialHtml$, "<fileName>")
    while fileNameIndex% > 0
      interstitialHtmlLength% = len(interstitialHtml$)
      interstitialHtml$ = mid(interstitialHtml$, 1, fileNameIndex% - 1) + fileName$ + mid(interstitialHtml$, fileNameIndex% + len("<fileName>"))
      fileNameIndex% = instr(1, interstitialHtml$, "<fileName>")
    end while
    
    interstitialFilePath$ = "tmp:/interstitial.html"
    interstitialFile = CreateObject("roCreateFile", interstitialFilePath$)
    interstitialFile.SendLine(interstitialHtml$)
    interstitialFile.Flush()
    interstitialFile = invalid
    
    url$ = "file:///tmp:/interstitial.html"
    return url$
    
  end function
  
  
  Sub LoadHtmlWidgetStaticInitialization()
    
    syncSpec = ReadSyncSpec()
    if not type(syncSpec) = "roSyncSpec" stop
      
      assetCollection = syncSpec.GetAssets("download")
      
      presentationName$ = m.bsp.sign.name$
      stateName$ = m.name$
      
      aa = { }
      
      aa.nodejs_enabled = m.isNodeServer
      
      if m.isNodeServer then
        m.view = CreateObject("roAssetCollectionView", m.bsp.assetPool, assetCollection)
      end if
      
      if m.bsp.sign.htmlEnableJavascriptConsole then
        aa.inspector_server = { port: 2999 }
      end if
      
      if IsPortraitBottomLeft(m.bsp.sign.monitorOrientation) then
        aa.transform = "rot90"
      else if IsPortraitBottomRight(m.bsp.sign.monitorOrientation) then
        aa.transform = "rot270"
      end if
  
      aa.port = m.bsp.msgPort
      
      security = { }
      security.websecurity = m.enableCrossDomainPolicyChecks
      security.camera_enabled = m.enableCamera
      security.insecure_https_enabled = m.ignoreHttpsCertificateErrors
      aa.security_params = security
      
      aa.brightsign_js_objects_enabled = m.enableBrightSignJavascriptObjects
      
      aa.mouse_enabled = m.enableMouseEvents
      
      if m.hwzOn then
        aa.hwz_default = "on"
      end if
      
      aa.javascript_enabled = true
      
      aa.focus_enabled = true
      
      if m.useUserStylesheet then
        fileName$ = GetPoolFilePath(m.bsp.assetPoolFiles, m.userStylesheet)
        aa.user_stylesheet = fileName$
      end if
      
      aa.fonts = []
      for each customFont in m.customFonts
        customFontPath$ = GetPoolFilePath(m.bsp.assetPoolFiles, customFont)
        aa.fonts.push(customFontPath$)
      next
      
      if m.isNodeServer or m.contentIsLocal then
        asset = { }
        asset.pool = m.bsp.assetPool
        asset.collection = assetCollection
        asset.uri_prefix = "/" + m.prefix$ + "/"
        asset.pool_prefix = m.prefix$
        
        aa.assets = []
        aa.assets.push(asset)
      end if
  
      if m.isNodeServer then
        m.url$ = GetInterstitialHtmlUrl(m.prefix$, m.filePath$)
      else if m.contentIsLocal then
        m.url$ = "file:///" + m.prefix$ + "/" + m.filePath$
      else
        m.url$ = m.url.GetCurrentParameterValue()
      end if
  
      m.url$ = m.url$ + m.queryString.GetCurrentParameterValue()
  
      aa.url = m.url$
      
      m.stateMachine.loadingHtmlWidget = CreateObject("roHtmlWidget", m.stateMachine.rectangle, aa)
      SetUserAgentForHtmlWidget(m.bsp, m.stateMachine.loadingHtmlWidget)
      
    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.LoadHtmlWidgetStaticInitialization()
          
          if not m.stateMachine.isVisible then
            m.stateMachine.loadingHtmlWidget.Hide()
          end if
          
          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)
          
        end if
        
      end if
      
    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()
          end if
        else if eventData.reason = "load-finished" then
          if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
            m.stateMachine.videoPlayer.StopClear()
          end if
          
          '				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
        end if
      end if
      
    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)
      end if
      
    else
      return m.MediaItemEventHandler(event, stateData)
    end if
    
    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)
      end if
    end if
    
  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()
        end if
        
        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%
                  end if
                end if
              end if
            end if
          end if
          m.currentInteractiveMenuItem = m.interactiveMenuItems[m.currentInteractiveMenuNavigationIndex%]
        end if
        
        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()
        end if
        
        if type(m.imageFileTimeoutTimer) = "roTimer" then
          m.imageFileTimeoutTimer.Stop()
        end if
        
        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
        end if
        
      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"
          end if
          
          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"
          end if
          
          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"
          end if
          
          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"
          end if
          
          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)
          end if
          
        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"
          end if
          
          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)
          end if
          
          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)
          end if
          
        end if
        
      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"
            end if
          end if
          
          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"
            end if
          end if
          
          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"
            end if
          end if
          
          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"
            end if
          end if
          
          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)
            end if
          end if
          
        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"
            end if
          end if
          
          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)
            end if
          end if
          
          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)
            end if
          end if
          
        end if
        
      end if
      
    end if
    
  else if type(event) = "roVideoEvent" and type(m.stateMachine.videoPlayer) = "roVideoPlayer" 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"
      end if
      
      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"
      end if
      
      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")
    end if
    
    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"
      end if
      
      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")
    end if
    
    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, "")
    end if
    
    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")
      end if
      return "HANDLED"
    end if
    
  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$ <> "" then
        return status$
      end if
      status$ = m.ConsumeSerialByteInput(stateData, m.downNavigation, m.currentInteractiveMenuItem.downNavigationIndex%, serialStreamInput, port$, "navigate")
      if status$ <> "" then
        return status$
      end if
      status$ = m.ConsumeSerialByteInput(stateData, m.leftNavigation, m.currentInteractiveMenuItem.leftNavigationIndex%, serialStreamInput, port$, "navigate")
      if status$ <> "" then
        return status$
      end if
      status$ = m.ConsumeSerialByteInput(stateData, m.rightNavigation, m.currentInteractiveMenuItem.rightNavigationIndex%, serialStreamInput, port$, "navigate")
      if status$ <> "" then
        return status$
      end if
      status$ = m.ConsumeSerialByteInput(stateData, m.enterNavigation, - 1, serialStreamInput, port$, "enter")
      if status$ <> "" then
        return status$
      end if
      
    else
      
      status$ = m.ConsumeSerialByteInput(stateData, m.backNavigation, - 1, serialStreamInput, port$, "back")
      if status$ <> "" then
        return status$
      end if
      status$ = m.ConsumeSerialByteInput(stateData, m.nextClipNavigation, - 1, serialStreamInput, port$, "nextClip")
      if status$ <> "" then
        return status$
      end if
      status$ = m.ConsumeSerialByteInput(stateData, m.previousClipNavigation, - 1, serialStreamInput, port$, "previousClip")
      if status$ <> "" then
        return status$
      end if
      
    end if
    
    
  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"
        end if
      end if
      
      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"
        end if
      end if
      
      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"
        end if
      end if
      
      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"
        end if
      end if
      
      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)
        end if
      end if
      
    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"
        end if
      end if
      
      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)
        end if
      end if
      
      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)
        end if
      end if
      
    end if
    
  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
    end if
    
  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$
    end if
    
    retVal = m.HandleInteractiveMenuEventInput(stateData, "keyboardChar$", keyboardChar$, "keyboard")
    if retVal <> invalid then
      return retVal
    end if
    
  end if
  
  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"
      end if
      
      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"
      end if
      
      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"
      end if
      
      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"
      end if
      
      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)
      end if
      
    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"
      end if
      
      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)
      end if
      
      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)
      end if
      
    end if
    
    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)
          end if
          return "HANDLED"
        end if
      end if
    end if
    
    return ""
    
  end function
  
  
  Function ExecuteInteractiveMenuEnter(stateData as object) as string
    
    if type(m.inactivityTimer) = "roTimer" then
      m.inactivityTimer.Stop()
    end if
    
    ' 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"
          end if
        end if
        
        m.bsp.ExecuteCmd(m.stateMachine, cmd.name$, cmd.parameters)
        
      next
      
    end if
    
    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)
        launchedPlayback = true
      end if
    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$]
      end if
      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
    end if
    
    if not launchedPlayback and type(m.inactivityTimer) = "roTimer" then
      m.RestartInteractiveMenuInactivityTimer()
    end if
    
    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
        end if
      else
        if m.currentInteractiveMenuNavigationIndex% < 0 then
          m.currentInteractiveMenuNavigationIndex% = m.interactiveMenuItems.Count() - 1
        end if
      end if
      
      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
      end if
      
      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)
          return "HANDLED"
        end if
      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
      end if
      
    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)
    end if
    
    if ItemIsEncrypted(interactiveMenuItem) then
      aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
      aa.AddReplace("EncryptionKey", file$)
    end if
    
    if m.stateMachine.mosaicDecoderName <> "" then
      aa.Decoder = m.stateMachine.mosaicDecoderName
    end if
    
    ok = m.stateMachine.videoPlayer.PlayFile(aa)
    if ok = 0 then stop
    
    m.stateMachine.ClearImagePlane()
    
    if type(userVariable) = "roAssociativeArray" then
      userVariable.Increment()
    end if
    
    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()
    end if
    
    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)
    end if
    
    ok = player.PlayFile(aa)
    if ok = 0 then stop
    
    m.stateMachine.ClearImagePlane()
    
    if type(userVariable) = "roAssociativeArray" then
      userVariable.Increment()
    end if
    
    m.playingAudioClip = true
    m.playingVideoClip = false
    m.displayingImage = false
  end sub
  
  
  Sub DisplayInteractiveMenuImage(file$ as string, userVariable as object, timeout% as integer, interactiveMenuItem as object)
    '	m.stateMachine.audioPlayer.Stop()
    m.stateMachine.videoPlayer.StopClear()
    filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, file$)
    
    if not true then
    else
      aa = { }
      aa.filename = filePath$
      if ItemIsEncrypted(interactiveMenuItem) then
        aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
        aa.AddReplace("EncryptionKey", file$)
      end if
      ok = m.stateMachine.imagePlayer.DisplayFile(aa)
    end if
    
    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)
    end if
    
    m.imageFileTimeoutTimer.SetElapsed(timeout%, 0)
    m.imageFileTimeoutTimer.Start()
    
    if type(userVariable) = "roAssociativeArray" then
      userVariable.Increment()
    end if
    
  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 not true then
          stop
        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$)
          end if
          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$)
          end if
        end if
        
        m.stateMachine.ShowImageWidget()
        
      else
        m.stateMachine.ClearImagePlane()
      end if
    end if
    
    ' draw backgrounds
    for each interactiveMenuItem in m.interactiveMenuItems
      m.DisplayNavigationOverlay(interactiveMenuItem.unselectedImage$, interactiveMenuItem)
    next
    
    ' draw foreground for first menu item
    if type(m.currentInteractiveMenuItem) = "roAssociativeArray" then
      m.DisplayNavigationOverlay(m.currentInteractiveMenuItem.selectedImage$, m.currentInteractiveMenuItem)
    end if
    
    if IsString(m.backgroundImage$) and m.backgroundImage$ <> "" and type(m.backgroundImageUserVariable) = "roAssociativeArray" then
      m.backgroundImageUserVariable.Increment()
    end if
    
  end sub
  
  
  Sub NavigateToMenuItem(navigationIndex% as integer)
    
    m.RestartInteractiveMenuInactivityTimer()
    
    if navigationIndex% >= 0 then
      m.DisplayNavigationOverlay(m.currentInteractiveMenuItem.unselectedImage$, m.currentInteractiveMenuItem)
      m.currentInteractiveMenuNavigationIndex% = navigationIndex%
      m.lastInteractiveMenuNavigationIndex% = m.currentInteractiveMenuNavigationIndex%
      m.currentInteractiveMenuItem = m.interactiveMenuItems[navigationIndex%]
      m.DisplayNavigationOverlay(m.currentInteractiveMenuItem.selectedImage$, m.currentInteractiveMenuItem)
    end if
    
  end sub
  
  
  Sub RestartInteractiveMenuInactivityTimer()
    
    if type(m.inactivityTimer) = "roTimer" then
      m.inactivityTimer.Stop()
      m.inactivityTimer.SetElapsed(0, m.mstimeoutValue%)
      m.inactivityTimer.Start()
    end if
    
  end sub
  
  
  Sub DisplayNavigationOverlay(fileName$ as string, interactiveMenuItem as object)
    
    if IsString(fileName$) and fileName$ <> "" then
      
      filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, fileName$)
      
      if not true then
        stop
      else
        m.bsp.diagnostics.PrintDebug("Overlay image in DisplayNavigationOverlay: " + fileName$)
        
        if type(m.bsp.encryptionByFile) = "roAssociativeArray" and m.bsp.encryptionByFile.DoesExist(fileName$) 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%)
        end if
        
        if ok = 0 then
          m.bsp.diagnostics.PrintDebug("Error in OverlayImage in DisplayNavigationOverlay: " + fileName$ + ", " + filePath$)
        end if
        
      end if
      
      m.stateMachine.ShowImageWidget()
      
    end if
    
  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
    end if
    
    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% }
      end if
    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% }
    end if
    
  end sub
  
  
  Function TemplateUsesAnyUserVariable() as boolean
    
    for each templateItem in m.templateItems
      if templateItem.type$ = "userVariableTemplateItem" then
        return true
      end if
    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
          end if
        end if
      end if
    next
    
  end function
  
  
  Function TemplateUsesSystemVariable() as boolean
    
    for each templateItem in m.templateItems
      if templateItem.type$ = "systemVariableTextTemplateItem"
        return true
      end if
    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$)
    end if
    
    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$
          end if
        end if
        
        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$
            end if
          end if
        end if
        
        m.stateMachine.canvasWidget.SetLayer(templateObject, i% + 1)
      else
        m.stateMachine.canvasWidget.ClearLayer(i% + 1)
      end if
    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$ = "systemVariableTemplateItem" 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$ = "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"] = ""
          end if
        end if
        
        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)
        end if
        
      else if templateItem.type$ = "indexedLiveTextDataItem" or templateItem.type$ = "titledLiveTextDataItem" then
        liveDataFeed = templateItem.liveDataFeed
        if m.liveDataFeeds.DoesExist(liveDataFeed.id$) then
          if templateItem.type$ = "indexedLiveTextDataItem" then
            indexStr$ = templateItem.index.GetCurrentParameterValue()
            index% = int(val(indexStr$))
            if index% > 0 then
              index% = index% - 1
            end if
            if type(liveDataFeed.articles) = "roArray" then
              if index% <= (liveDataFeed.articles.count() - 1) then
                textValue$ = liveDataFeed.articles[index%]
              else
                textValue$ = ""
              end if
              
              text = { }
              text["text"] = textValue$
              m.BuildTextTemplateItem(templateItem, text)
            end if
          else
            title$ = templateItem.title.GetCurrentParameterValue()
            if type(liveDataFeed.articlesByTitle) = "roAssociativeArray" then
              if liveDataFeed.articlesByTitle.DoesExist(title$) then
                textValue$ = liveDataFeed.articlesByTitle.Lookup(title$)
              else
                textValue$ = ""
              end if
              
              text = { }
              text["text"] = textValue$
              m.BuildTextTemplateItem(templateItem, text)
            end if
          end if
        end if
        
      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)
        end if
        
      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"] = ""
          end if
          
          image["filename"] = templateItem.fileName$
          image["CompositionMode"] = "source-over"
          
          ' TemplateToDo - do the images care about the stretch/crop mode?
          ' m.SetBackgroundImageSizeLocation(backgroundImage)
          BuildBaseTemplateItem(templateItem, image)
        end if
        
      end if
      
      m.BuildTemplateItem(text, image, templateItem)
      
    next
    
    ' now add any simple rss items
    if type(m.simpleRSSTemplateItems) = "roAssociativeArray" then
      for each simpleRSSId in m.simpleRSSTemplateItems
        simpleRSS = m.simpleRSSTemplateItems.Lookup(simpleRSSId)
        if type(simpleRSS) = "roAssociativeArray" then
          liveDataFeed = simpleRSS.rssLiveDataFeeds[simpleRSS.currentLiveDataFeedIndex%]
          if m.liveDataFeeds.DoesExist(liveDataFeed.id$) then
    
            if type(liveDataFeed.articles) = "roArray" then
              
              if simpleRSS.currentIndex% >= liveDataFeed.articles.count() then
                simpleRSS.currentIndex% = 0
              end if
              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 lcase(templateItem.elementName$) = "title" then
                    textValue$ = liveDataFeed.articleTitles[index%]
                  else
                    textValue$ = liveDataFeed.articles[index%]
                  end if
                  
                  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()
                end if
              end if
            end if
          end if
        end if
      next
    end if
    
  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)
      end if
      
      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)
          end if
          
          m.stateMachine.templateObjectsByLayer[backgroundLayer%].push(backgroundColor)
          
        end if
        
      else
        
        m.stateMachine.templateObjectsByLayer[contentLayer%].push(image)
        
      end if
      
    end if
    
  end sub
  
  
  Sub SetupTemplateMRSS()
    
    if type(m.mrssTitleTemplateItem) = "roAssociativeArray" then
      m.mrssTitleTemplateItem.textString$ = invalid
    end if
    
    if type(m.mrssDescriptionTemplateItem) = "roAssociativeArray" then
      m.mrssDescriptionTemplateItem.textString$ = invalid
    end if
    
    if type(m.mrssImageTemplateItem) = "roAssociativeArray" or type(m.mrssMediaTemplateItem) = "roAssociativeArray" then
      m.mrssMediaTemplateItem.fileName$ = invalid
    end if
    
    if type(m.mrssCustomFieldTemplateItems) = "roAssociativeArray" then
      for each customFieldName in m.mrssCustomFieldTemplateItems
        customFieldTemplateItem = m.mrssCustomFieldTemplateItems[customFieldName]
        customFieldTemplateItem.textString$ = invalid
      next
    end if
    
  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()
          end if
          
        end if
        
        ' reset indices on entry to the state
        if type(m.simpleRSSTemplateItems) = "roAssociativeArray" then
          for each simpleRSSId in m.simpleRSSTemplateItems
            simpleRSS = m.simpleRSSTemplateItems.Lookup(simpleRSSId)
            if type(simpleRSS) = "roAssociativeArray" then
              simpleRSS.currentIndex% = 0
              simpleRSS.currentLiveDataFeedIndex% = 0
              simpleRSS.rssItemTimer = invalid
            end if
          next
        end if
        
        m.liveDataFeeds = { }
        for each liveDataFeedId in m.bsp.liveDataFeeds
          liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
          m.liveDataFeeds.AddReplace(liveDataFeedId, 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()
        end if
        
        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)
        end if
        
        m.stateMachine.canvasWidget.EnableAutoRedraw(0)
        
        maxLayer% = 1
        if type(m.stateMachine.templateObjectsByLayer) = "roArray" then
          maxLayer% = m.stateMachine.templateObjectsByLayer.Count()
        end if
        
        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)
          end if
          
          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$
            end if
          end if
          
          m.stateMachine.canvasWidget.SetLayer(backgroundImage, 0)
          
        else
          
          m.stateMachine.canvasWidget.ClearLayer(0)
          
        end if
        
        ' 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$
              end if
            end if
            
            m.stateMachine.canvasWidget.SetLayer(templateObject, i% + 1)
          end if
        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)
        end if
        
        m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)
        
      else if event["EventType"] = "USER_VARIABLES_UPDATED" then
        
        if m.TemplateUsesAnyUserVariable() then
          m.RedisplayTemplateItems()
        end if
        
        return "HANDLED"
        
      else if event["EventType"] = "SYSTEM_VARIABLE_UPDATED" then
        
        if m.TemplateUsesSystemVariable() then
          m.RedisplayTemplateItems()
        end if
        
        return "HANDLED"
        
      else if event["EventType"] = "USER_VARIABLE_CHANGE" then
        
        userVariable = event["UserVariable"]
        
        if m.TemplateUsesUserVariable(userVariable) then
          m.RedisplayTemplateItems()
        end if
        
        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.id$, liveDataFeed)
        
        m.RedisplayTemplateItems()
        
        return "HANDLED"
        
      else
        
        return m.MediaItemEventHandler(event, stateData)
        
      end if
      
    end if
    
  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()
      end if
      
      return "HANDLED"
      
    end if
    
    if type(m.mrssItemTimer) = "roTimer" and event.GetSourceIdentity() = m.mrssItemTimer.GetIdentity() then
      
      m.GetMRSSTemplateItem()
      return "HANDLED"
      
    end if
    
    if type(m.simpleRSSTemplateItems) = "roAssociativeArray" then
      for each simpleRSSId in m.simpleRSSTemplateItems
        simpleRSS = m.simpleRSSTemplateItems.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.id$) 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
                    end if
                    liveDataFeed = simpleRSS.rssLiveDataFeeds[simpleRSS.currentLiveDataFeedIndex%]
                    if m.liveDataFeeds.DoesExist(liveDataFeed.id$) then
                      if type(liveDataFeed.articles) = "roArray" then
                        itemExists = true
                      end if
                    end if
                  else
                    itemExists = true
                  end if
                end if
              end if
              
              if itemExists then
                m.RedisplayTemplateItems()
                
                ' restart timer
                simpleRSS.rssItemTimer.SetElapsed(simpleRSS.displayTime%, 0)
                simpleRSS.rssItemTimer.Start()
              end if
              
              return "HANDLED"
              
            end if
          end if
        end if
      next
    end if
    
    return m.MediaItemEventHandler(event, stateData)
    
  else
    
    return m.MediaItemEventHandler(event, stateData)
    
  end if
  
  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%
    end if
    
    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
        end if
      end if
      
      mrssLiveDataFeedIndex% = mrssLiveDataFeedIndex% + 1
      if mrssLiveDataFeedIndex% >= m.mrssLiveDataFeeds.Count() then
        mrssLiveDataFeedIndex% = 0
      end if
      
      if mrssLiveDataFeedIndex% = startingIndex% then
        ' search has wrapped around - nothing was found - continue waiting
        m.LaunchWaitForContentTimer()
        return false
      end if
      
    end while
    
  end function
  
  
  Function GetNextMRSSTemplateItem() as object
    
    if m.currentFeed.items.Count() = 0 then
      return invalid
    end if
    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
        end if
        
        contentFound = m.FindMRSSContent()
        
        if not contentFound then
          return invalid
        end if
        
      end if
      
      displayItem = m.currentFeed.items[m.displayIndex]
      
      if displayItem = invalid then
        return invalid
      end if
      
      filePath$ = GetPoolFilePath(m.mrssLiveDataFeed.assetPoolFiles, displayItem.url)
      if filePath$ <> "" then
        foundItem = true
        displayItem.filePath$ = filePath$
      end if
      
      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()
      end if
      if type(m.stateMachine.imagePlayer) = "roImageWidget" then
        m.stateMachine.imagePlayer.StopDisplay()
      end if
      
      m.ClearTemplateItems()
      
      m.LaunchWaitForContentTimer()
      return
      
    end if
    
    ' 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()
    end if
    
    if type(m.mrssTitleTemplateItem) = "roAssociativeArray" then
      m.mrssTitleTemplateItem.textString$ = item.title
    end if
    
    if type(m.mrssDescriptionTemplateItem) = "roAssociativeArray" then
      m.mrssDescriptionTemplateItem.textString$ = item.description
    end if
    
    ' 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
      end if
    end if
    
    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
        end if
        
      next
      
    end if
    
    ' 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
        end if
      end if
      
      m.PlayVideo(false, true)
    else
      if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
        m.stateMachine.videoPlayer.StopClear()
      end if
    end if
    
    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()
        
        ' set default transition
        if type(m.stateMachine.imagePlayer) = "roImageWidget" then
          m.stateMachine.imagePlayer.SetDefaultTransition(m.slideTransition%)
        end if
        
        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.id$, 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 = { }
              mrssNotFullyLoadedPlaybackEvent["EventType"] = "MRSSNotFullyLoadedPlaybackEvent"
              mrssNotFullyLoadedPlaybackEvent["EventParameter"] = m.liveDataFeed.id$
              m.stateMachine.msgPort.PostMessage(mrssNotFullyLoadedPlaybackEvent)
            else
              m.AdvanceToNextMRSSItem()
            end if
          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 = { }
            mrssNotFullyLoadedPlaybackEvent["EventType"] = "MRSSNotFullyLoadedPlaybackEvent"
            mrssNotFullyLoadedPlaybackEvent["EventParameter"] = m.liveDataFeed.id$
            m.stateMachine.msgPort.PostMessage(mrssNotFullyLoadedPlaybackEvent)
            return "HANDLED"
          end if
          
          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"
          end if
          
          if m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray" then
            return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
          else
            m.AdvanceToNextMRSSItem()
            return "HANDLED"
          end if
          
        else if event["EventType"] = "MRSSNotFullyLoadedPlaybackEvent" then
          liveDataFeedId$ = event["EventParameter"]
          if liveDataFeedId$ = m.liveDataFeed.id$ 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()
            end if
          end if
          return "HANDLED"
          
        else if event["EventType"] = "MRSS_SPEC_UPDATED" then
          
          updatedLiveDataFeed = event["LiveDataFeed"]
          
          if updatedLiveDataFeed.id$ = m.liveDataFeed.id$ 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.id$, 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"
                end if
              end if
              
              ' 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
              
            end if
            
          end if
          
          return "HANDLED"
          
        else
          
          return m.MediaItemEventHandler(event, stateData)
          
        end if
        
      end if
      
    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()
            end if
            
            '					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
            
          end if
        end if
      end if
      
    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"
        end if
        
      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
            end if
            m.AdvanceToNextMRSSItem()
          else
            m.LaunchWaitForContentTimer()
          end if
        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()
        end if
        
        return "HANDLED"
        
      end if
      
      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"
      end if
      
    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"
        end if
        
      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"
        end if
        
      end if
      
    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"
      end if
      
    else
      
      return m.MediaItemEventHandler(event, stateData)
      
    end if
    
    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)
    end if
    
    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.id$, 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
              end if
              m.AdvanceToNextMRSSItem()
            else
              m.LaunchWaitForContentTimer()
            end if
            return
          end if
          
        end if
        
      end if
      
      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
        end if
      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
        end if
      end if
      
      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
    end if
    
  end sub
  
  
  Sub ProtectMRSSItem(displayItem as object)
    
    m.playingItemAssetCollection = CreateObject("roAssetCollection")
    
    asset = { }
    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
    end if
    m.playingItemAssetCollection.AddAsset(asset)
    
    '	m.ProtectMRSSFeed( "playing-" + m.currentFeed.title, m.playingItemAssetCollection )
    m.ProtectMRSSFeed("playing-" + m.liveDataFeed.id$, 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
    end if
    
    if isImage(displayItem) then
      m.imageItem = { }
      
      if IsString(displayItem.title) then
        m.imageItem.fileName$ = displayItem.title
      else
        m.imageItem.fileName$ = "noTitle"
      end if
      
      m.imageItem.filePath$ = filePath$
      m.imageItem.userVariable = invalid
      m.imageItem.imageTimeout% = displayItem.duration
      m.imageItem.isEncrypted = isEncrypted
      
      ' m.DisplayImage("mrss")
      m.DrawImage(false)
      
      if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
        m.stateMachine.videoPlayer.StopClear()
      end if
      
      if type(m.imageTimeoutTimer) = "roTimer" then
        m.imageTimeoutTimer.Stop()
      end if
      
      if type(m.imageTimeoutTimer) <> "roTimer" then
        m.imageTimeoutTimer = CreateObject("roTimer")
        m.imageTimeoutTimer.SetPort(m.stateMachine.msgPort)
      end if
      m.imageTimeoutTimer.SetElapsed(m.imageItem.imageTimeout%, 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%)
      end if
      
    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
        end if
        
        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
        end if
        
        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)
          end if
          
          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$)
          end if
          
          ' playback logging
          m.stateMachine.LogPlayStart("audio", fileName$)
          
        end if
        
      end if
      
    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)
      end if
      
      if IsPortraitBottomLeft(m.bsp.sign.monitorOrientation) then
        m.stateMachine.loadingHtmlWidget.SetPortrait(true)
      else if IsPortraitBottomRight(m.bsp.sign.monitorOrientation) then
        m.stateMachine.loadingHtmlWidget.SetTransform("rot270")
      end if
      
      m.stateMachine.loadingHtmlWidget.SetPort(m.bsp.msgPort)
      
      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()
      end if
      
      if type(m.imageTimeoutTimer) <> "roTimer" then
        m.imageTimeoutTimer = CreateObject("roTimer")
        m.imageTimeoutTimer.SetPort(m.stateMachine.msgPort)
      end if
      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)
      end if
      
    end if
    
    if not m.firstItemDisplayed then
      m.firstItemDisplayed = true
    end if
    
  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%
      end if
    end while
    if p% > 0 then
      return mid(path$, p% + 1)
    end if
    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%)
    
    if executeEntryCmds then
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
    end if
    
    file$ = m.audioItem.fileName$
    
    if type(m.audioItem.filePath$) = "roString" then
      filePath$ = m.audioItem.filePath$
    else
      filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, m.audioItem.fileName$)
    end if
    
    track = { }
    track["Filename"] = filePath$
    track["QueueNext"] = 1
    track["UserString"] = playbackIndex%
    
    fadeLength% = m.stateMachine.fadelength% * 1000
    track["FadeInLength"] = fadeLength%
    track["FadeOutLength"] = fadeLength%
    
    if playImmediate then
      track["FadeCurrentPlayNext"] = 0
    end if
    
    ok = m.stateMachine.audioPlayer.PlayFile(track)
    
    m.stateMachine.ClearImagePlane()
    
    if type(m.audioItem.userVariable) = "roAssociativeArray" then
      m.audioItem.userVariable.Increment()
    end if
    
    ' 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")
        end if
        
        return "HANDLED"
        
      else if event["EventType"] = "AudioPlaybackFailureEvent" then
        
        if m.bsp.ProcessMediaEndEvent() then
          return "HANDLED"
        end if
        if type(m.audioEndEvent) = "roAssociativeArray" then
          return m.ExecuteTransition(m.audioEndEvent, stateData, "")
        end if
        
      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)
        
      end if
      
    end if
    
  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"
      end if
      if type(m.audioEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.audioEndEvent, stateData, "")
      end if
      PostMediaEndEvent(m.bsp.msgPort)
    end if
    
  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"
      end if
      if type(m.audioEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.audioEndEvent, stateData, "")
      end if
      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"
        end if
        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "audioTimeCode", "", "0")
      end if
    end if
  else
    return m.MediaItemEventHandler(event, stateData)
  end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
  end function
  
  'endregion
  
  
  'region Control State Machine
  ' *************************************************
  '
  ' Control State Machine
  '
  ' *************************************************
  Function newControlZoneHSM(bsp as object, zoneXML as object) as object
  
    zoneHSM = newHSM()
    zoneHSM.ConstructorHandler = ControlZoneConstructor
    zoneHSM.InitialPseudostateHandler = ControlZoneGetInitialState
    
    newZoneCommon(bsp, zoneXML, zoneHSM)
    
    return zoneHSM
    
  end function
  
  
  Sub ControlZoneConstructor()
    
    m.InitializeZoneCommon(m.bsp.msgPort)
    
    zoneHSM = m
    zoneHSM.isVideoZone = false
    
    m.activeState = m.playlist.firstState
    if type(m.playlist.firstState) = "roAssociativeArray" then
      m.previousStateName$ = m.playlist.firstState.id$
    else
      m.previousStateName$ = ""
    end if
    
    m.CreateObjects()
    
  end sub
  
    
  Function ControlZoneGetInitialState() as object
    
    return m.activeState
    
  end function
  
  
  
  '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 IsPortraitBottomLeft(m.bsp.sign.monitorOrientation) then
      videoPlayer.SetTransform("rot90")
    else if m.bsp.sign.monitorOrientation = "portraitbottomonright" then
      videoPlayer.SetTransform("rot270")
    end if
    
    videoPlayer.SetRectangle(zoneHSM.rectangle)
    videoPlayer.ToBack()
    
    videoPlayer.SetPort(zoneHSM.msgPort)
    
    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$ = ""
    end if
    
    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$)
        
        ok = m.stateMachine.videoPlayer.PlayStaticImage(aa)
        
        if ok = 0 then
          m.bsp.diagnostics.PrintDebug("Error displaying file in LaunchBackgroundImage: " + file$ + ", " + filePath$)
        else
          m.bsp.diagnostics.PrintDebug("LaunchBackgroundImage: display file " + file$)
        end if
        
        if type(m.backgroundImageItem.userVariable) = "roAssociativeArray" then
          m.backgroundImageItem.userVariable.Increment()
        end if
        
        ' 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)
        
      end if
      
    end if
  else
    return m.MediaItemEventHandler(event, stateData)
  end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
  end function
  
  'endregion
  
  'region Clock State Machine
  ' *************************************************
  '
  ' Clock State Machine
  '
  ' *************************************************
  Function newClockZoneHSM(bsp as object, zoneData as object) as object
    
    zoneHSM = newHSM()
    zoneHSM.ConstructorHandler = ClockZoneConstructor
    zoneHSM.InitialPseudostateHandler = ClockZoneGetInitialState
    
    newZoneCommon(bsp, zoneData, zoneHSM)
  
    zoneHSM.clockData = zoneData
    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
  
    syncSpec = ReadSyncSpec()
    assetCollection = syncSpec.GetAssets("download")
    aa = { }
    aa.assets = []
    asset = { }
    
    asset.pool = m.bsp.assetPool
    asset.collection = assetCollection
    asset.uri_prefix = "/Default_PresentationBsDateTimeWidget//"
    asset.pool_prefix = "Default_PresentationBsDateTimeWidget/"
    aa.assets.push(asset)
  
    if IsPortraitBottomLeft(m.bsp.sign.monitorOrientation) then
      aa.transform = "rot90"
      zoneHSM.clockData.AddReplace("isViewportPortrait", true)
    else if IsPortraitBottomRight(m.bsp.sign.monitorOrientation) then
      aa.transform = "rot270"
      zoneHSM.clockData.AddReplace("isViewportPortrait", true)
    end if
  
    aa.url = "file:///Default_PresentationBsDateTimeWidget//bsDateTimeWidget.html"
    aa.port = m.bsp.msgPort
    aa.javascript_enabled = true
    aa.focus_enabled = false
    aa.brightsign_js_objects_enabled = true
    aa.nodejs_enabled = true
    zoneHSM.widget = CreateObject("roHtmlWidget", zoneHSM.rectangle, aa)
  
    userData = { }
    userData.htmlWidgetId = "clockZoneWidget"
    zoneHSM.widget.SetUserData(userData)
    
  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")
          m.stateMachine.widget.Show()
          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")
          
        end if
        
      end if
  
    else if type(event) = "roHtmlWidgetEvent" then
      
      userData = event.getUserData()
  
      if type(userData) = "roAssociativeArray" and IsString(userData.htmlWidgetId) and userData.htmlWidgetId = "clockZoneWidget" 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)
        
          else if eventData.reason = "load-finished" then
            widgetDataString = FormatJson(m.stateMachine.clockData)
            m.stateMachine.widget.PostJSMessage({ widget: widgetDataString })
          end if
        end if
      end if
    end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
  end function
  
  'endregion
  
  'region Ticker State Machine
  ' *************************************************
  '
  ' Ticker State Machine
  '
  ' *************************************************
  Function newTickerZoneHSM(bsp as object, sign as object, zoneDescription as object) as object
    
    zoneHSM = newHSM()
    zoneHSM.ConstructorHandler = TickerZoneConstructor
    zoneHSM.InitialPseudostateHandler = TickerZoneGetInitialState
    
    newZoneCommon(bsp, zoneDescription, zoneHSM)
    
    zoneHSM.numberOfLines% = zoneDescription.numberOfLines%
    zoneHSM.delay% = zoneDescription.delay%
    
    zoneHSM.rotation% = zoneDescription.rotation%
    
    zoneHSM.alignment% = zoneDescription.alignment%
    
    zoneHSM.scrollingMethod% = zoneDescription.scrollingMethod%
    
    zoneHSM.scrollSpeed% = zoneDescription.scrollSpeed%
    
    zoneHSM.foregroundTextColor% = zoneDescription.foregroundTextColor%
    zoneHSM.backgroundTextColor% = zoneDescription.backgroundTextColor%
    zoneHSM.font$ = zoneDescription.font$
    
    zoneHSM.backgroundBitmapFile$ = zoneDescription.backgroundBitmapFile$
    zoneHSM.stretch = zoneDescription.stretch
    
    zoneHSM.safeTextRegionX% = zoneDescription.safeTextRegionX%
    zoneHSM.safeTextRegionY% = zoneDescription.safeTextRegionY%
    zoneHSM.safeTextRegionWidth% = zoneDescription.safeTextRegionWidth%
    zoneHSM.safeTextRegionHeight% = zoneDescription.safeTextRegionHeight%
    
    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 = { }
    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%)
    end if
    
    if type(zoneHSM.foregroundTextColor%) = "roInt" then
      zoneHSM.widget.SetForegroundColor(zoneHSM.foregroundTextColor%)
    end if
    
    if type(zoneHSM.backgroundTextColor%) = "roInt" then
      zoneHSM.widget.SetBackgroundColor(zoneHSM.backgroundTextColor%)
    end if
    
    if zoneHSM.font$ <> "" and zoneHSM.font$ <> "System" then
      fontPath$ = GetPoolFilePath(m.bsp.assetPoolFiles, zoneHSM.font$)
      zoneHSM.widget.SetFont(fontPath$)
    end if
    
    if IsString(zoneHSM.backgroundBitmapFile$) then
      backgroundBitmapFilePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, zoneHSM.backgroundBitmapFile$)
      aa = { }
      m.bsp.SetEncryptionAttributes(aa, zoneHSM.backgroundBitmapFile$, backgroundBitmapFilePath$)
      zoneHSM.widget.SetBackgroundBitmap(aa, zoneHSM.stretch)
    end if
    
    if type(zoneHSM.safeTextRegionX%) = "roInt" then
      r = CreateObject("roRectangle", zoneHSM.safeTextRegionX%, zoneHSM.safeTextRegionY%, zoneHSM.safeTextRegionWidth%, zoneHSM.safeTextRegionHeight%)
      zoneHSM.widget.SetSafeTextRegion(r)
      r = invalid
    end if
    
    m.includesRSSFeeds = false
    for each rssDataFeedItem in m.rssDataFeedItems
      if rssDataFeedItem.isRSSFeed then
        m.includesRSSFeeds = true
      end if
    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$)
      end if
    end if
    
  end sub
  
  
  Function TickerZoneGetInitialState() as object
    
    if m.includesRSSFeeds then
      return m.stRSSDataFeedInitialLoad
    else
      return m.stRSSDataFeedPlaying
    end if
    
  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.id$ = rssDataFeedItem.liveDataFeed.id$ then
              rssDataFeedItem.loadAttemptComplete = true
            else if not rssDataFeedItem.loadAttemptComplete then
              allLoadsComplete = false
            end if
          end if
        next
        
        if allLoadsComplete then
          stateData.nextState = m.stateMachine.STRSSDataFeedPlaying
          return "TRANSITION"
        else
          return "HANDLED"
        end if
        
      else if event["EventType"] = "PREPARE_FOR_RESTART" then
        
        m.bsp.diagnostics.PrintDebug(m.id$ + " - PREPARE_FOR_RESTART")
        m.stateMachine.widget = invalid
        return "HANDLED"
        
      end if
      
    end if
    
  end if
  
  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
      end if
    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.id$ = rssDataFeedItem.liveDataFeed.id$ then
              rssDataFeedItemLoaded = true
              exit for
            end if
          end if
        next
        
        if rssDataFeedItemLoaded then
          
          m.PopulateRSSDataFeedWidget()
          
        end if
        
      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
              end if
            next
          end if
        next
        
        if rssDataFeedsUpdated then
          m.PopulateRSSDataFeedWidget()
        end if
        
        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"
        
      end if
      
    end if
    
  end if
  
  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.PlayingEventUrlHandler = PlayingEventUrlHandler
    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
        end if
      end if
    next
    
    activeScheduledPresentation = m.bsp.schedule.activeScheduledEvent
    if type(activeScheduledPresentation) = "roAssociativeArray" then
      return m.stPlaying
    else
      return m.stWaiting
    end if
    
  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
    end if
    
  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()
        end if
        
        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.json") then
          
          m.bsp.assetCollection = currentSyncSpec.GetAssets("download")
          m.bsp.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.assetPool, m.bsp.assetCollection)
          
        else if currentSyncSpec.ReadFromFile("current-sync.xml") then
          
          m.bsp.assetCollection = currentSyncSpec.GetAssets("download")
          m.bsp.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.assetPool, m.bsp.assetCollection)
          
        end if
        
        m.bsp.Restart("")
        
        activeScheduledPresentation = m.bsp.schedule.activeScheduledEvent
        if type(activeScheduledPresentation) = "roAssociativeArray" then
          stateData.nextState = m.stateMachine.stPlaying
        else
          stateData.nextState = m.stateMachine.stWaiting
        end if
        
        return "TRANSITION"
        
      end if
      
    end if
    
  else if type(event) = "roControlDisconnected" then
    
    userData$ = "Port unspecified"
    if type(event.getUserData()) = "roString" then
      userData$ = event.getUserData()
    end if
    
    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 = { }
    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$
      end if
      
      ok = TakeSnapshot(m.bsp.systemTime, presentationName$)
      
      m.bsp.remoteSnapshotTimer.SetElapsed(m.bsp.globalAA.remoteSnapshotInterval, 0)
      m.bsp.remoteSnapshotTimer.Start()
      
    end if
    
    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 = { }
        prepareForRestartEvent["EventType"] = "PREPARE_FOR_RESTART"
        m.bsp.msgPort.PostMessage(prepareForRestartEvent)
        
        ' send internal message indicating that new content is available
        contentUpdatedEvent = { }
        contentUpdatedEvent["EventType"] = "CONTENT_UPDATED"
        m.bsp.msgPort.PostMessage(contentUpdatedEvent)
        
      end if
      
      return "HANDLED"
    end if
    
    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"
        
      end if
      
    end if
    
    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()
          end if
        end if
      next
    end if
    
  else if type(event) = "roHdmiEdidChanged" then
    
    edid = m.bsp.videoMode.GetEdidIdentity(true)
    UpdateEdidValues(edid, m.bsp.sysInfo)
    edid = invalid
    
    m.bsp.UpdateEdidUserVariables(true)
    
    systemVariableChanged = { }
    systemVariableChanged["EventType"] = "SYSTEM_VARIABLE_UPDATED"
    m.bsp.msgPort.PostMessage(systemVariableChanged)
    
  else if type(event) = "roAssetFetcherEvent" then
    
    userData$ = event.GetUserData()
    
    for each liveDataFeedId in m.bsp.liveDataFeeds
      if userData$ = liveDataFeedId then
        liveDataFeed = m.bsp.liveDataFeeds.Lookup(userData$)
        liveDataFeed.HandleLiveDataFeedContentDownloadAssetFetcherEvent(event)
        return "HANDLED"
      end if
    next
    
  else if type(event) = "roAssetFetcherProgressEvent" then
    
    print "### File download progress ";event.GetFileName();" unknown"
    
    userData$ = event.GetUserData()
    
    for each liveDataFeedId in m.bsp.liveDataFeeds
      if userData$ = liveDataFeedId then
        liveDataFeed = m.bsp.liveDataFeeds.Lookup(userData$)
        liveDataFeed.HandleLiveDataFeedContentDownloadAssetFetcherProgressEvent(event)
        return "HANDLED"
      end if
    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
            end if
          end if
        end if
      end if
      nc = invalid
    else
      if networkInterface% = 0 then
        m.bsp.sysInfo.ipAddressWired$ = "Invalid"
      else if networkInterface% = 1 then
        m.bsp.sysInfo.ipAddressWireless$ = "Invalid"
      end if
    end if
    
    m.bsp.UpdateIPAddressUserVariables(true)
    
    systemVariableChanged = { }
    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
    end if
    
    if blcIndex% <> -1 then
      blcIdentifier$ = "BLC" + stri(blcIndex%) + ":"
    end if
    
    ' 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$ = ""
    end if
    
    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%)
    end if
    
    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%)
    end if
    
  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 = { }
              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
              
            end if
            
            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()
                end if
                
                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)
                    
                  end if
                  
                  exit for
                end if
                
              else if action$ = "deleteLogs" then
                
                if displayStatus then
                  tw.Clear()
                  tw.PushString("Deleting log files.")
                  tw.Show()
                end if
                
                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()
                end if
                
                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()
                  end if
                  
                  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$
                  end if
                  day$ = StripLeadingSpaces(stri(dtLocal.GetDay()))
                  if len(day$) = 1 then
                    day$ = "0" + day$
                  end if
                  hour$ = StripLeadingSpaces(stri(dtLocal.GetHour()))
                  if len(hour$) = 1 then
                    hour$ = "0" + hour$
                  end if
                  minute$ = StripLeadingSpaces(stri(dtLocal.GetMinute()))
                  if len(minute$) = 1 then
                    minute$ = "0" + minute$
                  end if
                  '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$)
                    end if
                    
                    variablesFile = invalid
                    
                  else
                    
                    errorEncountered = true
                    m.bsp.diagnostics.PrintDebug("copyVariablesDB failed - create file failed")
                    
                  end if
                  
                  if errorEncountered then
                    
                    if displayStatus then
                      
                      tw.Clear()
                      tw.PushString("Error encountered while copying variables database.")
                      tw.Show()
                      
                      sleep(5000)
                      
                    end if
                    
                    exit for
                    
                  end if
                  
                else if displayStatus then
                  
                  tw.Clear()
                  tw.PushString("No variables to copy.")
                  tw.Show()
                  
                  sleep(3000)
                  
                end if
                
              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$)
                end if
                
                RebootSystem()
                
                return "HANDLED"
                
              end if
            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.")
              end if
              
              tw.Show()
              
              sleep(5000)
              
              tw = invalid
              
            end if
            
            ' 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()
            
          end if
        end if
      end if
    end if
    
  else if type(event) = "roBtEvent" then
    
    m.bsp.btManager.HandleEvent(event)
    return "HANDLED"
    
  end if
  
  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()
            end if
          end if
        end if
        
        if globalAA.bsp.networkingHSM.pendingSnapshotsToUpload.DoesExist(fileToDelete) then
          globalAA.bsp.diagnostics.PrintDebug("DeleteExcessSnapshots: removed from pendingSnapshotsToUpload: " + fileToDelete)
          globalAA.bsp.networkingHSM.pendingSnapshotsToUpload.Delete(fileToDelete)
        end if
      end if
      
      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")
    if type(videoMode) <> "roVideoMode" then
      globalAA.bsp.diagnostics.PrintDebug("TakeSnapshot: cannot take snapshot as device does not support video mode")
      return
    end if
    
    ' create a file name based on the current date/time
    dtLocal = systemTime.GetLocalDateTime()
    
    ' strip illegal characters from string
    dateTime$ = GetISODateTimeString(dtLocal)
  
    rotationDegree% = 0
    if IsNonEmptyString(globalAA.remoteSnapshotOrientation) then
      orientation$ = globalAA.remoteSnapshotOrientation
      if IsPortraitBottomLeft(lcase(orientation$)) then
        rotationDegree% = 90
      else if IsPortraitBottomRight(lcase(orientation$)) then
        rotationDegree% = 270
      end if
    end if
    
    aa = { }
    aa.filename = "snapshots/" + dateTime$ + ".jpg"
    aa.Width = videoMode.GetResX()
    aa.Height = videoMode.GetResY()
    aa.filetype = "JPEG"
    aa.quality = globalAA.remoteSnapshotJpegQualityLevel
    aa.rotation = rotationDegree%
    aa.Async = 0
    
    if type(activePresentation$) = "roString" then
      aa.description = activePresentation$
    end if
    
    if type(globalAA.bsp) = "roAssociativeArray" then
      globalAA.bsp.diagnostics.PrintDebug("------------------------------------------- SCREENSHOT " + aa.filename + "-------------------------------------")
    end if
    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, "")
      end if
    else
      globalAA.listOfSnapshotFiles.push(dateTime$ + ".jpg")
      
      ' upload snapshot to BSN if it's initialized
      if type(globalAA.bsp) = "roAssociativeArray" then
        snapshotCaptured = { }
        snapshotCaptured["EventType"] = "SNAPSHOT_CAPTURED"
        snapshotCaptured["SnapshotName"] = dateTime$ + ".jpg"
        globalAA.bsp.msgPort.PostMessage(snapshotCaptured)
      end if
      
    end if
    
    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)
    end if
    
    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")
    ' BSNRT '
    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$ = ""
    end if
    
    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$ = ""
    end if
    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")
    ' BSNRT '
    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$ = ""
    end if
    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
    end if
    
    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())
    end if
    
  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%)
      end if
      httpMonth = mid(httpDateTime, 9, 3)
      for month% = 0 to 11
        if months[month%] = httpMonth then
          dateTime.SetMonth(month% + 1)
          exit for
        end if
      next
      year% = val(mid(httpDateTime, 13, 4))
      if year% > 0 and year% < dateTime.GetYear() then
        dateTime.SetYear(year%)
      end if
      hour% = val(mid(httpDateTime, 18, 2))
      if hour% < 24 then
        dateTime.SetHour(hour%)
      end if
      minute% = val(mid(httpDateTime, 21, 2))
      if minute% < 60 then
        dateTime.SetMinute(minute%)
      end if
      second% = val(mid(httpDateTime, 24, 2))
      if second% < 60 then
        dateTime.SetSecond(second%)
      end if
    end if
    
    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)
    end if
    
  end sub
  
  
  Function SetDeviceSetupSplashScreen(setupType as String, msgPort as Object) as object
  
  	filepath = "sys:/web-client/postDeviceSetupSplashScreen/dist/index.html"
  	file = CreateObject("roReadFile", filepath)
  	
  	if file <> invalid then
  	  filepath = "file:/" + filepath
  		videoMode = CreateObject("roVideoMode")
  		resX = videoMode.GetResX()
  		resY = videoMode.GetResY()
  		r = CreateObject("roRectangle", 0, 0, resX, resY)
  		config = {
  			url: filepath
  			brightsign_js_objects_enabled: true
  			nodejs_enabled: true
  		}
  		htmlWidget = CreateObject("roHtmlWidget", r, config)
  		if type(htmlWidget) = "roHtmlWidget" then
  			htmlWidget.SetPort(msgPort)
  			htmlWidget.AllowJavascriptUrls({ all: "*" })
  			htmlWidget.EnableJavascript(true)
  			sleep(5000)
  			if setupType = "lfn" then
  				htmlWidget.PostJSMessage({
  					htmlcommand: "setupsplashscreenmessage",
  					headermsg: "Congratulations, your BrightSign player is set up!",
  					message: "Use BrightAuthor:connected to publish content via Local File Networking."
  				})
  			end if
  
  			htmlWidget.Show()
  			return htmlWidget
  		else
  			stop
  		endif
  	endif
  
  	return invalid
  
  End Function
  
  
  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
        end if
        
        return "HANDLED"
        
      else if event["EventType"] = "EXIT_SIGNAL" then
        
        m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
        
      end if
      
    end if
    
  end if
  
  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())
            
          end if
          
          ' 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 liveDataFeedId in m.bsp.liveDataFeeds
            liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
            liveDataFeed.ReadFeedContent()
          next
          
          ' load live data feeds
          m.liveDataFeeds = { }
          m.bsp.liveDataFeedsByTimer = { }
          
          ' queue live data feeds for downloading
          m.bsp.liveDataFeedsToDownload = []
          for each liveDataFeedId in m.bsp.liveDataFeeds
            liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
            m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
          next
          
          ' launch playback
          m.bsp.StartPlayback()
          
        end if
        
        return "HANDLED"
        
      else if event["EventType"] = "UPDATE_DATA_FEED" then
        
        dataFeedId$ = event["DataFeedId"]
        for each liveDataFeedId in m.bsp.liveDataFeeds
          liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
          if dataFeedId$ = liveDataFeedId then
            liveDataFeed.forceUpdate = true
            m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
          end if
        next
        
      else if event["EventType"] = "UPDATE_DATA_FEED_BY_CATEGORY" then
        
        categoryName$ = event["Name"]
        for each liveDataFeedId in m.bsp.liveDataFeeds
          liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
          if liveDataFeed.title$ = categoryName$ then
            liveDataFeed.forceUpdate = true
            m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
          end if
        next
        
      else if event["EventType"] = "UPDATE_ALL_DATA_FEEDS" then
        
        for each liveDataFeedId in m.bsp.liveDataFeeds
          liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
          if liveDataFeed.autoGenerateUserVariables then
            liveDataFeed.forceUpdate = true
            m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
          end if
        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.id$ 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"
                end if
              end if
            end if
          next
        next
        
      else if event["EventType"] = "EXIT_SIGNAL" then
        
        m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
        
      end if
      
      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)
        
      end if
      
    end if
    
  end if
  
  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)
        end if
      end if
      
      ' requeue the feed that failed
      m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
    end if
    
  else if type(event) = "roUrlEvent" then
    
    return m.PlayingEventUrlHandler(event, stateData)
    
  end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
  end function
  
  
  Function PlayingEventUrlHandler(event as object, stateData as object) as object
    
    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)
        end if
        
        ' 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.id$ + "', 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.id$ + "', last modified = " + lastModifiedTime.GetString() + ", currentModified = " + liveDataFeed.currentModifiedTime.GetString())
              end if
            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
            end if
          end if
          
          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%
          end if
          
        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" then
              if (liveDataFeed.isDynamicPlaylist or liveDataFeed.isLiveMediaFeed) then
                liveDataFeed.ParseMRSSFeed(liveDataFeed.rssFileName$)
                liveDataFeed.ConvertMRSSFormatToContent()
              else
                liveDataFeed.ParseCustomContentFormat(liveDataFeed.rssFileName$)
              endif
            else
              liveDataFeed.ParseSimpleRSSFeed(liveDataFeed.rssFileName$)
            end if
            
            ' 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$)
                end if
              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"]
                  end if
                  
                  item.position% = position%
                  position% = position% + 1
                  
                  liveDataFeed.items.push(item)
                end for
              end if
            end if
          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 = { }
          end if
          
          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()
          end if
          
          if liveDataFeed.autoGenerateUserVariables then
            m.bsp.CreateUserVariablesFromDataFeed(liveDataFeed)
          end if
          
          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.id$ = liveDataFeed.id$ then
                  description = liveDataFeed.articlesByTitle[title]
                  userVariable.SetCurrentValue(description, true)
                  updatedUserVariables.AddReplace(title, userVariable)
                end if
              end if
            next
            
            m.UpdateTimeClockEvents(updatedUserVariables)
            
          end if
          
          ' send internal message indicating that the data feed has been updated
          liveTextDataUpdatedEvent = { }
          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
          end if
          
          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 = { }
        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()
        end if
        
        ' 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
      end if
      
      ' set a timer to update this live data feed
      liveDataFeed.RestartLiveDataFeedDownloadTimer(updateInterval%)
    end if
    
    return "HANDLED"
    
  end function
  
  
  Sub UpdateDataFeed(parameters as object)
  
    updateDataFeedParameter = parameters["dataFeed"]
    dataFeedId$ = updateDataFeedParameter.GetCurrentParameterValue()
    
    updateDataFeedMsg = { }
    updateDataFeedMsg["EventType"] = "UPDATE_DATA_FEED"
    updateDataFeedMsg["DataFeedId"] = dataFeedId$
    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$
    end if
    
    sectionId% = m.GetDBSectionId(sectionName$)
    if sectionId% < 0 then
      m.AddDBSection(sectionName$)
      sectionId% = m.GetDBSectionId(sectionName$)
    end if
    
    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%)
        end if
      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%)
        end if
      next
      
    end if
    
  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
            end if
            
            if type(zoneHSM.videoPlayer) = "roVideoPlayer" then
              zoneHSM.videoPlayer.Stop()
              zoneHSM.videoPlayer = invalid
            end if
            
            zoneHSM.ClearImagePlane()
            
          next
        end if
        
        m.bsp.sign = invalid
        
        videoMode = CreateObject("roVideoMode")
        if type(videoMode) = "roVideoMode" then
          resX = videoMode.GetResX()
          resY = videoMode.GetResY()
          videoMode.SetBackgroundColor(0)
          videoMode = invalid
          
          ' display update message on the screen
          twParams = { }
          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.")
        end if
        
        ' 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$
        end if
        
        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 = { }
          usbUpdateErrorEvent["EventType"] = "USB_UPDATE_ERROR"
          usbUpdateErrorEvent["Message"] = "Update files are corrupt."
          m.stateMachine.msgPort.PostMessage(usbUpdateErrorEvent)
          return "HANDLED"
        end if
        
        ' 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
        end if
        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
          end if
        else if usbContentUpdatePassword$ <> "" then
          ok = m.stateMachine.newSync.VerifySignature(signature$, usbContentUpdatePassword$)
          if not ok then
            securityError = true
          end if
        end if
        
        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 = { }
          usbUpdateErrorEvent["EventType"] = "USB_UPDATE_ERROR"
          usbUpdateErrorEvent["Message"] = "Update failed - an incorrect password was provided."
          m.stateMachine.msgPort.PostMessage(usbUpdateErrorEvent)
          return "HANDLED"
        end if
        
        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 = { }
            usbTransitionEvent["EventType"] = "USB_PERFORM_TRANSITION"
            m.stateMachine.msgPort.PostMessage(usbTransitionEvent)
            return "HANDLED"
            
          end if
          
          ' 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 = { }
          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 = { }
            usbUpdateErrorEvent["EventType"] = "USB_UPDATE_ERROR"
            usbUpdateErrorEvent["Message"] = "Unable to perform update."
            m.stateMachine.msgPort.PostMessage(usbUpdateErrorEvent)
            return "HANDLED"
          end if
          
          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 = { }
            updateSyncSpecMatchesEvent["EventType"] = "UPDATE_SYNC_SPEC_MATCHES"
            m.stateMachine.msgPort.PostMessage(updateSyncSpecMatchesEvent)
            return "HANDLED"
          end if
          
          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 = { }
            usbUpdateErrorEvent["EventType"] = "USB_UPDATE_ERROR"
            usbUpdateErrorEvent["Message"] = errorMsg
            m.stateMachine.msgPort.PostMessage(usbUpdateErrorEvent)
          end if
          
        end if
        
        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"
        
      end if
      
    end if
    
  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.")
    end if
    
    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"
      end if
      
      return "HANDLED"
      
    end if
    
    ' 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()
    
  end if
  
  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)
      end if
      
    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)."
  end if
  
  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)."
  end if
  
  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
        end if
        
        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
        end if
        
      end if
      
      ' Save to current-sync.xml then do cleanup
      ' BACONTODO - json or xml?
      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
      
    end if
    
    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()
        end if
        
        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"
        
      end if
      
    end if
    
  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()
    
  end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
  end function
  
  
  Function CheckForUSBUpdate(storagePath$ as string) as object
    
    syncSpecFilePath$ = storagePath$ + "/update/local-sync.xml"
    if FileExists(syncSpecFilePath$) then
      return true
    end if
    
    syncSpecFilePath$ = storagePath$ + "/fwUpdate/fw-sync.xml"
    if FileExists(syncSpecFilePath$) then
      return true
    end if
    
    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
                      end if
                      
                      if type(timeClockEvent.timer) = "roTimer" then
                        timeClockEvent.timer.Stop()
                      else if setTimer then
                        timeClockEvent.timer = CreateObject("roTimer")
                      end if
                      
                      if setTimer then
                        timeClockEvent.timer.SetDateTime(dateTime)
                        timeClockEvent.timer.SetPort(zoneHSM.msgPort)
                        timeClockEvent.timer.Start()
                      end if
                    else
                      m.bsp.diagnostics.PrintDebug("Timeout specification " + dateTime$ + " is invalid")
                      m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_INVALID_DATE_TIME_SPEC, dateTime$)
                    end if
                  end if
                end if
              next
            end if
          end if
        next
      end if
    end if
    
  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 = { }
          paramItem.name = nameVal[0]
          paramItem.value = nameVal[1]
          paramArray.push(paramItem)
        end if
      next
    end if
    ' 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
        end if
      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$ + "&"
      end if
    end for
    
    ' create signature base string
    if authenticationData.DoesExist("HttpMethod") and type(authenticationData.HttpMethod) = "roString" then
      sigBase$ = authenticationData.HttpMethod + "&"
    else
      sigBase$ = "GET&"
    end if
    
    if (queryIndex > 0)
      normUrl$ = left(url$, queryIndex - 1)
    else
      normUrl$ = url$
    end if
    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]])
    end if
  end sub
  
  
  Sub QueueRetrieveLiveDataFeed(liveDataFeeds as object, liveDataFeed as object)
    
    ' 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.id$)
      
      ' launch download of first feed
      if m.liveDataFeedsToDownload.Count() = 1 then
        m.RetrieveLiveDataFeed(liveDataFeeds, liveDataFeed)
      end if
    end if
    
  end sub
  
  
  Sub AdvanceToNextLiveDataFeedInQueue(liveDataFeeds as object)
    
    ' Remove top entry
    m.liveDataFeedsToDownload.Shift()
    
    if m.liveDataFeedsToDownload.Count() > 0 then
      liveDataFeedId = m.liveDataFeedsToDownload[0]
      liveDataFeed = m.liveDataFeeds.Lookup(liveDataFeedId)
      m.RetrieveLiveDataFeed(liveDataFeeds, liveDataFeed)
    end if
    
  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$)
    end if
    
    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.id$ + ": " + userAgent$)
        end if
      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$)
      end if
    end if
    if userAgent$ <> "" then
      liveDataFeed.rssURLXfer.SetUserAgent(userAgent$)
    else
      liveDataFeed.rssURLXfer.SetUserAgent(m.userAgent$)
    end if
    
    ' 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())
        end if
      end if
    end if
    
    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
  
  
  '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.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 = { }
    NetworkingStateMachine.DeviceDownloadProgressItemsPendingUpload = { }
    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.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.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
      end if
    end if
    nc = invalid
    
    globalAA = GetGlobalAA()
    
    ' 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.json") and not m.currentSync.ReadFromFile("localToBSN-sync.json") then
      m.diagnostics.PrintDebug("### No current sync state available")
      return false
    end if
    
    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"))
    
    timezone = m.currentSync.LookupMetadata("client", "timezone")
    if timezone <> "" then
      m.systemTime.SetTimeZone(timezone)
    end if
    
    m.diagnostics.PrintTimestamp()
    m.diagnostics.PrintDebug("### Current active sync list suggests next URL of " + nextURL)
    
    if nextURL = "" then stop
    if m.eventURL = "" then stop
    
    ' BSNRT '
    m.accessToken$ = m.currentSync.LookupMetadata("server", "accessToken")
    m.user$ = m.currentSync.LookupMetadata("server", "user")
    m.password$ = m.currentSync.LookupMetadata("server", "password")
    if m.user$ <> "" or m.password$ <> "" then
      m.setUserAndPassword = true
      enableBasicAuthentication$ = m.currentSync.LookupMetadata("server", "enableBasicAuthentication")
      if lcase(enableBasicAuthentication$) = "true" then
        m.enableBasicAuthentication = true
      else
        m.enableBasicAuthentication = false
      end if
    else
      m.setUserAndPassword = false
      m.enableBasicAuthentication = false
    end if
    
    useWireless$ = m.currentSync.LookupMetadata("client", "useWireless")
    if not m.modelSupportsWifi then useWireless$ = "no"
    
    if useWireless$ = "yes" then
      m.useWireless = true
    else
      m.useWireless = false
    end if
    
    ' 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")
    
    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")
    end if
    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")
    end if
    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
    end if
    
    ' check for timeBetweenNetConnects override
    tbnco$ = m.bsp.registrySettings.tbnco$
    if tbnco$ <> "" then
      timeBetweenNetConnects$ = tbnco$
    end if
    
    m.timeBetweenNetConnects% = val(timeBetweenNetConnects$)
    m.diagnostics.PrintDebug("### Time between net connects = " + timeBetweenNetConnects$)
    m.currentTimeBetweenNetConnects% = m.timeBetweenNetConnects%
    m.networkTimerDownload = { }
    m.networkTimerDownload.timerType = "TIMERTYPEPERIODIC"
    m.networkTimerDownload.timerInterval = m.timeBetweenNetConnects%
    
    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
    end if
    
    ' 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%
      end if
    else
      wiredRL% = m.wiredRateLimits.rlOutsideWindow%
      wirelessRL% = m.wirelessRateLimits.rlOutsideWindow%
    end if
    
    '  remote snapshot values
    m.SetRemoteSnapshotUrls(m.currentSync)
    m.pendingSnapshotsToUpload = { }
    
    m.bsp.registrySettings.remoteDwsEnabled$ = m.currentSync.LookupMetadata("client", "remoteDwsEnabled")
    
    ' diagnostic web server
    dwsRebootRequired = GetAndSaveDWSParams(m.currentSync, m.bsp.registrySettings)
    
    SetDownloadRateLimit(m.diagnostics, 0, wiredRL%)
    
    if m.useWireless then
      SetDownloadRateLimit(m.diagnostics, 1, wirelessRL%)
    end if
    
    m.bsp.obfuscatedEncryptionKey = ""
  
    if dwsRebootRequired then RebootSystem()
    
    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 (sqsUrlItems.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]
      
    end if
    
  end sub
  
  
  Function GetAndSaveDwsParams(syncSpec as object, registrySettings as object)
  
    dwsEnabled$ = syncSpec.LookupMetadata("client", "dwsEnabled")
    ' In case there is no dwsEnabled$ param in the sync-spec file
    ' don't setup DWS (this is a non-BSN presentation publish case)
    if not dwsEnabled$ = "" then
      dwsPassword$ = syncSpec.LookupMetadata("client", "dwsPassword")
      dwsAA = { }
      if dwsEnabled$ = "yes" or registrySettings.remoteDwsEnabled$ = "yes" then
        dwsAA["port"] = "80"
        dwsAA["password"] = dwsPassword$
      else if dwsEnabled$ = "no" and registrySettings.remoteDwsEnabled$ = "no" then
        dwsAA["port"] = 0
      end if
      
      ' supervisor sets DWSPassword value: true to "--PASSWORDSET--" and sends to
      ' BSN during registration and settings message.
      ' It helps us to determine if user changed the password via property sheet or local DWS
      if not dwsAA["password"] = "--PASSWORDSET--" then
        nc = CreateObject("roNetworkConfiguration", 0)
        if type(nc) <> "roNetworkConfiguration" then
          nc = CreateObject("roNetworkConfiguration", 1)
        end if
        if type(nc) = "roNetworkConfiguration" then
          dwsRebootRequired = nc.SetupDWS(dwsAA)
          nc = invalid
          return dwsRebootRequired
        end if
      end if
    end if
    
    return false
  
  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
      end if
    else
      if not(((secondsSinceMidnight% >= startOfRangeInSeconds%) and (secondsSinceMidnight% < secondsPerDay%)) or (secondsSinceMidnight% < (endOfRangeInSeconds% - secondsPerDay%))) then
        notInDownloadWindow = true
      end if
    end if
    
    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$))
      end if
    else if updateIfNotSpecified or rateLimitModeOutsideWindow$ <> "" then
      rateLimits.rlOutsideWindow% = -1
    end if
    
    if rateLimitModeInWindow$ = "unlimited" then
      rateLimits.rlInWindow% = 0
    else if rateLimitModeInWindow$ = "specified" then
      if rateLimitRateInWindow$ <> "" then
        rateLimits.rlInWindow% = int(val(rateLimitRateInWindow$))
      end if
    else if updateIfNotSpecified or rateLimitModeInWindow$ <> "" then
      rateLimits.rlInWindow% = -1
    end if
    
  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$
    end if
    
    return url$
    
  end function
  
  
  Sub AddUploadHeaders(url as object, contentDisposition$)
    
    '    url.SetHeaders({})
    url.SetHeaders(GetServerMetadata(m.bsp.registrySection, 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 = { }
        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"
          end if
        end if
        
        m.filesToDownload.AddReplace(downloadFile.hash, fileToDownload)
      end if
      
      if IsString(downloadFile.chargeable) then
        if lcase(downloadFile.chargeable) = "yes" then
          m.chargeableFiles[downloadFile.name] = true
        end if
      end if
      
    next
    
  end sub
  
  
  Sub PushDeviceDownloadProgressItem(fileItem as object, type$ as string, currentFilePercentage$ as string, status$ as string)
    
    if type(fileItem) <> "roAssociativeArray" then
      return
    end if
    
    deviceDownloadProgressItem = { }
    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$
    end if
    
    m.DeviceDownloadProgressItems.AddReplace(fileItem.name, deviceDownloadProgressItem)
    
  end sub
  
  
  Sub AddDeviceDownloadProgressItem(fileItem as object, currentFilePercentage$ as string, status$ as string)
    
    if type(fileItem) <> "roAssociativeArray" then
      return
    end if
    
    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")
    end if
    
    ' 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$)
    end if
    
    ' 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$
      end if
      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")
    end if
    
    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")
    end if
    
    ' 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()
    end if
    
    
    ' 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 liveDataFeedId in m.bsp.liveDataFeeds
      liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
      for each fileToDownloadKey in liveDataFeed.feedContentFilesToDownload
        fileToDownload = liveDataFeed.feedContentFilesToDownload.Lookup(fileToDownloadKey)
        if type(fileToDownload) = "roAssociativeArray" then
          m.PushDeviceDownloadProgressItem(fileToDownload, "fileInSyncSpec", fileToDownload.currentFilePercentage$, fileToDownload.status$)
        end if
      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 = { }
    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")
    end if
    
    ' 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$)
    end if
    
    ' 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()
      end if
      if m.DeviceDownloadItems.Count() > 100 then
        m.diagnostics.PrintDebug("### UploadDeviceDownload - clear items from queue")
        m.DeviceDownloadItems.Clear()
      end if
      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")
    end if
    
    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$)
    end if
    
    ' 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
      fullFilePath = m.uploadLogFolder + "/" + file
      if not m.logging.inErrorState then
        m.diagnostics.PrintDebug("### UploadLogFiles " + file + " to " + m.uploadLogFileURL$)
        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
        end if
      else
        ' if any of the files get back 401 or 403, put the rest of files in FailedUpload folder
        target$ = m.uploadLogFailedFolder + "/" + file
        ok = MoveFile(fullFilePath, target$)
      end if
    next
    
  end sub
  
  
  Sub UploadLogFileHandler(msg as object)
    responseCode = msg.GetResponseCode()
    if responseCode = 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$)
        end if
        m.logFileUpload = invalid
      end if
      
    else
      if responseCode = 401 or responseCode = 403 then
        m.logging.inErrorState = true
      end if
  
      if IsString(m.logFileUpload) then
        m.diagnostics.PrintDebug("### Failed to upload log file " + m.logFileUpload + ", error code = " + str(responseCode))
  
        if responseCode = 400 then
          DeleteFile(m.logFileUpload)
        else if responseCode = 413 then
          target$ = m.uploadLogArchiveFolder + "/" + m.logFile$.Left(m.logFile$.Len()-4)+"-413.log"
          ok = MoveFile(m.logFileUpload, target$)
        else
          ' move file so that the script doesn't try to upload it again immediately
          target$ = m.uploadLogFailedFolder + "/" + m.logFile$
          ok = MoveFile(m.logFileUpload, target$)
        end if
  
      end if
      
      m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_LOGFILE_UPLOAD_FAILURE, str(responseCode))
      
    end if
    
    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")
    end if
    
    ' 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$)
    end if
    
    ' 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")
    end if
    
    ' 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$)
    end if
    
    ' 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(GetServerMetadata(m.bsp.registrySection, 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")
    end if
    
    ok = url.AsyncPostFromString("UploadTrafficDownload")
    if not ok then
      m.diagnostics.PrintDebug("### SendTrafficUpload - AsyncPostFromString failed")
      return false
    end if
    
    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 = { }
    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 = { }
    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$)
    end if
    
    ' 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()
      end if
      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()
    end if
    
  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$)
    end if
    
    ' 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()
      end if
      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()
    end if
    
  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%)
          end if
          
        end if
        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$)
        end if
      
      end if
      
    end if
    
  else if type(event) = "roTimerEvent" then
    
    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%)
        end if
        
        ' 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%)
          
        end if
        
        return "HANDLED"
      end if
      
    end if
    
    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 = { }
        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%)
        end if
        
        ' 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"
      end if
      
    end if
    
    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)
          end if
        end if
        m.stateMachine.retrySnapshotUploadTimer = invalid
        return "HANDLED"
      end if
      
    end if
    
    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)
      end if
    end if
    
  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())
        end if
        
        m.stateMachine.uploadSnapshotToBSNEEUrl = invalid
        
        return "HANDLED"
        
      end if
    end if
    
    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())
        end if
      end if
    end if
    
    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())
        end if
      end if
    end if
    
    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()
          end if
          
        end if
        
        ' indicate that transfer is no longer in progress
        m.stateMachine.uploadSnapshotUrl = invalid
        
        return "HANDLED"
      end if
    end if
    
    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)
          end if
          
        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()
          end if
          
        end if
        
        return "HANDLED"
      end if
    end if
    
    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()))
        end if
        m.stateMachine.deviceDownloadUploadURL = invalid
        m.stateMachine.UploadDeviceDownload()
        return "HANDLED"
      end if
    end if
    
    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()))
        end if
        m.stateMachine.deviceDownloadProgressUploadURL = invalid
        m.stateMachine.UploadDeviceDownloadProgressItems()
        return "HANDLED"
      end if
    end if
    
    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"
      end if
    end if
    
    if type (m.stateMachine.eventUploadURL) = "roUrlTransfer" then
      if event.GetSourceIdentity() = m.stateMachine.eventUploadURL.GetIdentity() then
        m.stateMachine.eventUploadURL = invalid
        m.stateMachine.UploadEvent()
        return "HANDLED"
      end if
    end if
    
    if type (m.stateMachine.deviceErrorUploadURL) = "roUrlTransfer" then
      if event.GetSourceIdentity() = m.stateMachine.deviceErrorUploadURL.GetIdentity() then
        m.stateMachine.deviceErrorUploadURL = invalid
        m.stateMachine.UploadDeviceError()
        return "HANDLED"
      end if
    end if
    
    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#)
          end if
        end if
        return "HANDLED"
      end if
    end if
    
    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#)
          end if
        end if
        return "HANDLED"
      end if
    end if
    
  end if
  
  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
  
  
  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 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()
    end if
    
  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
    end if
    
    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
    end if
    
    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
    end if
    
  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.")
    end if
    
    m.assetFetcher = invalid
    
  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")
        
        end if
      
      end if
    
    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"
        
        end if
      
      end if
  
    ' Set user data to distinguish between presentation udp messages and bootstrap udp messages
    else if type(event) = "roDatagramEvent" and event.GetUserData() = "bootstrap" then
  
      jsonobject = ParseJson(event.GetString())
  
      if jsonobject.message = "checkforcontent" then
        m.bsp.diagnostics.PrintDebug("===Successfully read checkforcontent udp message.")
        stateData.nextState = m.stateMachine.stRetrievingSyncList
        return "TRANSITION"
      end if
  
    ' BCN-4613: Player should check for content upon network hotplug
    else if type(event) = "roNetworkAttached" then
  
      stateData.nextState = m.stateMachine.stRetrievingSyncList
      return "TRANSITION"
    
    end if
  
    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")
        
      end if
      
    end if
    
  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"
      
    end if
    
  end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
  end function
  
  
  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
    end if
    
    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
      end if
    end if
    
    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.enableBasicAuthentication)
    m.xfer.SetMinimumTransferRate(10, 240)
    m.xfer.SetHeaders(GetServerMetadata(m.bsp.registrySection, 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")
    end if
    
    ' 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.json") then
      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)
          end if
        end if
      end if
    end if
    
    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
  
  
  Function GetServerMetadata(registry as object, serverMetadata as object) as object
    accessToken$ = registry.Read("access_token")
  
    ' add the accessToken from registry in the request header 
    serverMetadata.AddReplace("accessToken", accessToken$)
  
    ' if refresh Token or bsnrt exist in checkforcontent header,
    ' BSN will exchange for a new accessToken. So delete them in the request headers.
    if (serverMetadata.DoesExist("refreshToken")) then
      serverMetadata.Delete("refreshToken")
    end if
  
    if (serverMetadata.DoesExist("registrationToken")) then
      serverMetadata.Delete("registrationToken")
    end if
  
    return serverMetadata
  end function
  
  
  Sub AddLocalToBSNHeaders(xfer as object)
    
    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)
    end if
    
    xfer.AddHeader("remoteDwsEnabled", m.bsp.registrySettings.remoteDwsEnabled$)
    
    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$)
    end if
    
    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"))
    end if
    
    ' 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"
    end if
    xfer.AddHeader("useDHCP_2", useDHCP$)
    if useDHCP$ = "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"))
    end if
    
    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$)
    end if
    
  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.json") or tempSpec.ReadFromFile("current-sync.xml") or tempSpec.ReadFromFile("localToBSN-sync.xml") then
      urlXfer.AddHeader("storage-current-used", str(tempRealizer.EstimateRealizedSizeInMegabytes(tempSpec)))
    end if
    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") then
        m.bsp.obfuscatedEncryptionKey = headers["bsn-content-passphrase"]
      else
        m.bsp.obfuscatedEncryptionKey = ""
      end if
      
      ' 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
      end if
      
      ' check for forced log upload
      forceLogUpload$ = LCase(m.stateMachine.newSync.LookupMetadata("client", "forceLogUpload"))
      if forceLogUpload$ = "true" then
        m.stateMachine.logging.WriteDiagnosticLogEntryForForceLogUpload(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_RECEIVED, "FORCE LOG UPLOAD")
        m.bsp.LogActivePresentation()
      end if
      
      ' 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
          end if
        end if
        
        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$ = ""
        end if
      end if
      
      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
        end if
        
        return m.stateMachine.stWaitForTimeout
      end if
      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
        end if
      end if
      
      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
      end if
      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
        end if
      end if
      
      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
        end if
      end if
      
      ' 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$
        end if
        
        if unitNamingMethod$ <> unitNamingMethodFromRegistry$ then
          m.bsp.WriteRegistrySetting("unm", unitNamingMethod$)
          m.bsp.registrySettings.unitNamingMethod$ = unitNamingMethod$
        end if
        
        if unitDescription$ <> unitDescriptionFromRegistry$ then
          m.bsp.WriteRegistrySetting("ud", unitDescription$)
          m.bsp.registrySettings.unitDescription$ = unitDescription$
        end if
        
        proxyFromSyncSpec$ = m.stateMachine.newSync.LookupMetadata("client", "proxy")
        if proxyFromSyncSpec$ <> "" then
          useProxy$ = "yes"
        else
          useProxy$ = "no"
        end if
        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()
        end if
        
        nc = CreateObject("roNetworkConfiguration", 1)
        if type(nc) = "roNetworkConfiguration" then
          ok = nc.SetProxy(proxySpec$)
          nc.Apply()
        end if
        
        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)
          end if
        next
        
        ' bypass proxy servers
        nc = CreateObject("roNetworkConfiguration", 0)
        if type(nc) = "roNetworkConfiguration" then
          ok = nc.SetProxyBypass(bypassProxyHosts)
          nc.Apply()
        end if
        
        nc = CreateObject("roNetworkConfiguration", 1)
        if type(nc) = "roNetworkConfiguration" then
          ok = nc.SetProxyBypass(bypassProxyHosts)
          nc.Apply()
        end if
        
        nc = invalid
        
      end if
      
      ' 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
      end if
      
      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")
            end if
            wirelessNetworkingParameters.useWireless = true
            wirelessNetworkingParameters.ssid$ = ssid$
            wirelessNetworkingParameters.passphrase$ = passphrase$
            wirelessNetworkingParameters.timeServer$ = timeServer$
            
            m.ConfigureNetwork(wirelessNetworkingParameters, m.bsp.registrySettings.wirelessNetworkingParameters, "")
          end if
          
          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")
          end if
          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")
          end if
          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()
            end if
          end if
          
        end if
        
      end if
      
      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$
      end if
      
      if m.stateMachine.useWireless then
        if ssidFromRegistry$ <> ssid$ then
          m.bsp.WriteRegistrySetting("ss", ssid$)
          m.bsp.registrySettings.ssid$ = ssid$
        end if
        
        if passphraseFromRegistry$ <> passphrase$ then
          m.bsp.WriteRegistrySetting("pp", passphrase$)
          m.bsp.registrySettings.passphrase$ = passphrase$
        end if
      end if
      
      timeServerFromRegistry$ = m.bsp.registrySettings.timeServer$
      if timeServerFromRegistry$ <> timeServer$ then
        m.bsp.WriteRegistrySetting("ts", timeServer$)
        m.bsp.registrySettings.timeServer$ = timeServer$
      end if
      
      ' 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
      end if
      beacon2FromRegistry$ = m.bsp.registrySettings.beacon2
      if beacon2Data <> beacon2FromRegistry$ then
        m.bsp.WriteRegistrySetting("beacon2", beacon2Data)
        m.bsp.registrySettings.beacon2 = beacon2Data
        beaconsChanged = true
      end if
      
      if beaconsChanged then
        ' Update beacons with changed data and restart
        m.bsp.btManager.UpdatePersistentBeacons()
      end if
      
      ' Retrieve latest net connect spec information from sync spec
      
      timeBetweenNetConnects$ = m.stateMachine.newSync.LookupMetadata("client", "timeBetweenNetConnects")
      
      if timeBetweenNetConnects$ <> "" then
        
        ' check for timeBetweenNetConnects override
        tbnco$ = m.bsp.registrySettings.tbnco$
        if tbnco$ <> "" then
          timeBetweenNetConnects$ = tbnco$
        end if
        
        ' 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$)
        end if
        
      end if
      
      ' clear any existing timers associated with rate limitings / content download window
      if type(m.stateMachine.contentDownloadWindowStartTimer) = "roTimer" then
        m.stateMachine.contentDownloadWindowStartTimer.Stop()
      end if
      if type(m.stateMachine.contentDownloadWindowEndTimer) = "roTimer" then
        m.stateMachine.contentDownloadWindowEndTimer.Stop()
      end if
      
      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")
      end if
      ' 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")
      end if
      ' 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()
      
      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
      end if
      
      ' 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
          end if
          
          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%)
          end if
          
          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%)
          end if
          
        end if
        
      else
        
        SetDownloadRateLimit(m.bsp.diagnostics, 0, m.stateMachine.wiredRateLimits.rlOutsideWindow%)
        
        if m.stateMachine.useWireless then
          SetDownloadRateLimit(m.bsp.diagnostics, 1, m.stateMachine.wirelessRateLimits.rlOutsideWindow%)
        end if
        
      end if
      
      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())
        end if
        
        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", eventData$, str(event.GetResponseCode()))
      else
        print "received 503"
      end if
      
    end if
    
    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)
    end if
    
    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"))
      ' change remoteSnapshotDisplayPortrait to remoteSnapshotOrientation for BCN-6961
      orientationFromSyncSpec = syncSpec.LookupMetadata("client", "deviceScreenShotsOrientation")
      if IsNonEmptyString(orientationFromSyncSpec) then
        registrySection.Write("remoteSnapshotOrientation", orientationFromSyncSpec)
      else
        registrySection.Write("remoteSnapshotOrientation", "Landscape")
      end if
    end if
    
    ' remember current settings
    currentEnableRemoteSnapshot = globalAA.enableRemoteSnapshot
    currentRemoteSnapshotInterval = globalAA.remoteSnapshotInterval
    currentRemoteSnapshotMaxImages = globalAA.remoteSnapshotMaxImages
    currentRemoteSnapshotJpegQualityLevel = globalAA.remoteSnapshotJpegQualityLevel
    currentRemoteSnapshotOrientation = globalAA.remoteSnapshotOrientation
    
    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")))
      orientationFromRegistry = registrySection.Read("remoteSnapshotOrientation")
      if IsNonEmptyString(orientationFromRegistry) then
        globalAA.remoteSnapshotOrientation = orientationFromRegistry
      else
        globalAA.remoteSnapshotOrientation = "Landscape"
      end if
    end if
    
    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()
        end if
      end if
    end if
  end if
  
  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()"
    end if
  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$)
    end if
    
    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.SetIP4Broadcast(networkingParameters.broadcast$)
        nc.SetIP4Gateway(networkingParameters.gateway$)
        
        dnsServers = []
        if networkingParameters.dns1$ <> "" then
          dnsServers.push(networkingParameters.dns1$)
        end if
        if networkingParameters.dns2$ <> "" then
          dnsServers.push(networkingParameters.dns2$)
        end if
        if networkingParameters.dns3$ <> "" then
          dnsServers.push(networkingParameters.dns3$)
        end if
        if dnsServers.Count() > 0 then
          ok = nc.SetDNSServers([])
          ok = nc.SetDNSServers(dnsServers)
        end if
        
      else
        
        nc.SetDHCP()
        
      end if
      
      nc.SetRoutingMetric(int(val(networkingParameters.networkConnectionPriority$)))
      
      if networkingParameters.useWireless then
        nc.SetWiFiESSID(networkingParameters.ssid$)
        nc.SetObfuscatedWifiPassphrase(networkingParameters.passphrase$)
      end if
      
      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$
        end if
        
        if networkingParameters.useDHCP$ = "no" then
          
          if registryNetworkingParameters.useDHCP$ <> "no" then
            m.bsp.WriteRegistrySetting("dhcp" + registryKeySuffix$, "no")
            registryNetworkingParameters.useDHCP$ = "no"
          end if
          
          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$
          end if
          
          if subnetMaskFromRegistry$ <> networkingParameters.subnetMask$ then
            m.bsp.WriteRegistrySetting("sm" + registryKeySuffix$, networkingParameters.subnetMask$)
            registryNetworkingParameters.subnetMask$ = networkingParameters.subnetMask$
          end if
          
          if gatewayFromRegistry$ <> networkingParameters.gateway$ then
            m.bsp.WriteRegistrySetting("gw" + registryKeySuffix$, networkingParameters.gateway$)
            registryNetworkingParameters.gateway$ = networkingParameters.gateway$
          end if
          
          if dns1FromRegistry$ <> networkingParameters.dns1$ then
            m.bsp.WriteRegistrySetting("d1" + registryKeySuffix$, networkingParameters.dns1$)
            registryNetworkingParameters.dns1$ = networkingParameters.dns1$
          end if
          
          if dns2FromRegistry$ <> networkingParameters.dns2$ then
            m.bsp.WriteRegistrySetting("d2" + registryKeySuffix$, networkingParameters.dns2$)
            registryNetworkingParameters.dns2$ = networkingParameters.dns2$
          end if
          
          if dns3FromRegistry$ <> networkingParameters.dns3$ then
            m.bsp.WriteRegistrySetting("d3" + registryKeySuffix$, networkingParameters.dns3$)
            registryNetworkingParameters.dns3$ = networkingParameters.dns3$
          end if
          
        else
          
          if registryNetworkingParameters.useDHCP$ <> "yes" then
            m.bsp.WriteRegistrySetting("dhcp" + registryKeySuffix$, "yes")
            registryNetworkingParameters.useDHCP$ = "yes"
          end if
          
        end if
        
      end if
      
    else
      
      m.bsp.diagnostics.PrintDebug("Unable to create roNetworkConfiguration - index = " + stri(networkConfigurationIndex%))
      
    end if
    
  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"
        end if
        
        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"
        end if
        
      end if
      
    end if
    
  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")
    end if
    
    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"
      end if
      
      return "HANDLED"
      
    end if
    
  end if
  
  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.enableBasicAuthentication)
    m.stateMachine.assetFetcher.SetMinimumTransferRate(1000, 60)
    m.stateMachine.assetFetcher.SetHeaders(GetServerMetadata(m.bsp.registrySection, 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()
  end if
  
  if m.stateMachine.proxy_mode then
    m.stateMachine.assetFetcher.AddHeader("Roku-Cache-Request", "Yes")
  end if
  
  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
    end if
  else
    m.stateMachine.assetFetcher.AsyncSuggestCache(m.stateMachine.newSync)
  end if
  
  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#))
        end if
        file = invalid
      end if
      
    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())
      end if
      
      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
      end if
      
    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
        end if
      else
        m.bsp.diagnostics.PrintDebug("### Proxy mode sync complete")
      end if
      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
      end if
      
      ' 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
      dwsRebootRequired = GetAndSaveDWSParams(m.stateMachine.newSync, m.bsp.registrySettings)
  
      m.stateMachine.stRetrievingSyncList.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "dwsEnabled"), m.bsp.registrySettings.dwsEnabled$, "dwse")
      m.stateMachine.stRetrievingSyncList.UpdateRegistrySetting(m.stateMachine.newSync.LookupMetadata("client", "dwsPassword"), m.bsp.registrySettings.dwsPassword$, "dwsp")
      
      m.bsp.WriteRegistrySetting("rdwse", m.bsp.registrySettings.remoteDwsEnabled$)
      
      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
        end if
        
        ' reboot if successful
        rebootRequired = true
        
      end if
      
      ' Save to current-sync.json then do cleanup
      jsonSyncSpec$ = m.stateMachine.newSync.WriteToString({ format : "json" })
      ok = WriteAsciiFile("current-sync.json", jsonSyncSpec$)
      if not ok then stop
      
      timezone = m.stateMachine.newSync.LookupMetadata("client", "timezone")
      if timezone <> "" then
        m.stateMachine.systemTime.SetTimeZone(timezone)
      end if
      
      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()
      end if
      
      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()
      end if
      
      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.json")
      if globalAA.autoscheduleFilePath$ = "" then stop
      
      currentSyncFileName$ = "current-sync.json"
      
      globalAA.boseProductsFilePath$ = GetPoolFilePath(m.stateMachine.assetPoolFiles, "PartnerProducts.json")
      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(currentSyncFileName$) then stop
      
      m.bsp.diagnostics.PrintTimestamp()
      m.bsp.diagnostics.PrintDebug("### return from HandleAssetFetcherEvent")
      
      debugOn = false
      if syncSpecValueTrue(m.stateMachine.currentSync.LookupMetadata("client", "enableSerialDebugging")) then
        debugOn = true
      end if
      m.bsp.diagnostics.UpdateDebugOn(debugOn)
      
      systemLogDebugOn = false
      if syncSpecValueTrue(m.stateMachine.currentSync.LookupMetadata("client", "enableSystemLogDebugging")) then
        systemLogDebugOn = true
      end if
      m.bsp.diagnostics.UpdateSystemLogDebugOn(systemLogDebugOn)
      
      m.bsp.contentEncrypted = false
      deviceCustomization = CreateObject("roDeviceCustomization")
      deviceCustomization.StoreObfuscatedEncryptionKey("AesCtrHmac", m.bsp.obfuscatedEncryptionKey)
      if m.bsp.obfuscatedEncryptionKey <> "" then
        m.bsp.contentEncrypted = true
      end if
      
      m.bsp.SetPerFileEncryptionStatus(m.stateMachine.currentSync)
      
      ' send internal message to prepare for restart
      prepareForRestartEvent = { }
      prepareForRestartEvent["EventType"] = "PREPARE_FOR_RESTART"
      m.stateMachine.msgPort.PostMessage(prepareForRestartEvent)
      
      ' send internal message indicating that new content is available
      contentUpdatedEvent = { }
      contentUpdatedEvent["EventType"] = "CONTENT_UPDATED"
      m.stateMachine.msgPort.PostMessage(contentUpdatedEvent)
      
      return m.stateMachine.stWaitForTimeout
      
    end if
    
  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 = { }
    
    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 = { }
        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()
        end if
        
        m.state$ = "ButtonDown"
        
      end if
      
    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
        end if
        
        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 = { }
          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()
          
        end if
      end if
    end if
    
  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
      end if
      m.configuration$ = "press"
      return
    end if
    
    ' 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$))
    end if
    
    ' 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()
      end if
    end if
    
    ' 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 = { }
    
    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"
      end if
    end if
    
    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 = { }
        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()
        end if
        
        m.state$ = "ButtonDown"
        
      end if
      
    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
        end if
        
        gpioControlUp = { }
        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 = { }
          gpioControlDown["EventType"] = "GPIOControlDown"
          gpioControlDown["ButtonNumber"] = StripLeadingSpaces(str(m.buttonNumber%))
          m.msgPort.PostMessage(gpioControlDown)
          
          m.timer.SetElapsed(0, m.repeatInterval%)
          m.timer.Start()
          
        end if
      end if
    end if
    
  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
        end if
      end if
      
      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
          end if
        end if
        '			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()))
            end if
          end if
        end if
  
        if type(msg) = "roHttpEvent" then
          
          userdata = msg.GetUserData()
          if type(userdata) = "roAssociativeArray" and type(userdata.HandleEvent) = "roFunction" then
            userData.HandleEvent(userData, msg)
          end if
  
        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)
              end if
            next
          next
          
          for i% = 0 to 7
            if type(m.gpioSM[i%]) = "roAssociativeArray" then
              m.gpioSM[i%].EventHandler(msg)
            end if
          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
            
          end if
          
          if m.networkingActive then
            m.networkingHSM.Dispatch(msg)
          end if
          
        end if
        
      end if
      
    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 + ".json"
      jsonFileName$ = m.assetPoolFiles.GetPoolFilePath(autoplayFileName$)
      if jsonFileName$ = "" then
        m.diagnostics.PrintDebug("switchPresentation: target presentation not found - " + presentationName$)
        return false
      end if
      
      ' send internal message to prepare for restart
      prepareForRestartEvent = { }
      prepareForRestartEvent["EventType"] = "PREPARE_FOR_RESTART"
      m.msgPort.PostMessage(prepareForRestartEvent)
      
      ' send switch presentation internal message
      switchPresentationEvent = { }
      switchPresentationEvent["EventType"] = "SWITCH_PRESENTATION"
      switchPresentationEvent["Presentation"] = presentation.presentationName
      m.msgPort.PostMessage(switchPresentationEvent)
      
      return true
      
    end if
    
    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
        end if
        
        m.ExecuteCmd(zoneHSM, cmd.name$, cmd.parameters)
        
      next
      
    end if
    
  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
            end if
            
          end if
          
        end if
        
        m.ExecuteCmd(zoneHSM, transitionCmd.name$, transitionCmd.parameters)
        
      next
      
    end if
    
    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$)
    end if
    
    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 = { }
    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 then
      return ""
    end if
    
    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$))
    end if
    
    return parameter%
  end function
  
  
  Sub ExecuteSendWssCommand(parameters as object)
    
    gaa = GetGlobalAA()
    
    port$ = parameters["port"].getCurrentParameterValue()
    
    speakerFound = false
    
    for each key in gaa.usbDevicesByConnector
      usbDeviceName = gaa.usbDevicesByConnector[key]
      if usbDeviceName = port$ then
        speakerFound = true
        exit for
      end if
    next
    
    if not speakerFound then
      m.diagnostics.PrintDebug("ExecuteSendWssCommand: speaker not found at connector: " + port$)
      return
    end if
    
    ' check to see whether or not the speaker specified in this command has been discovered
    if type(m.bose) <> "roAssociativeArray" or type(m.bose.speakers) <> "roArray" or m.bose.speakers.Count() = 0 or type(m.bose.speakers[0].address) <> "roString" then
      m.diagnostics.PrintDebug("ExecuteSendWssCommand: no speakers discovered yet")
      return
    end if
    
    speakerDiscovered = false
    
    for i% = 0 to m.bose.speakers.Count() - 1
      
      speaker = m.bose.speakers[i%]
      if speaker.fid <> invalid then
        
        speakerFid$ = speaker.fid
        
        index = instr(1, speakerFid$, mid(port$, 5))
        if index = 1 then
          speakerDiscovered = true
          exit for
        end if
        
      end if
      
    next
    
    if not speakerDiscovered then
      m.diagnostics.PrintDebug("ExecuteSendWssCommand: speaker not discovered at connector: " + port$)
      return
    end if
    
    wssCommand = parameters.wssCommand
    wssCommandName$ = wssCommand.getCurrentParameterValue()
    
    ' command data from wssCommunicationSpec
    
    boseProduct = m.sign.boseProductsByConnector[port$]
    wssCommunicationSpec = boseProduct.wssCommunicationSpec
    commands = wssCommunicationSpec.commands
    
    ' get structure for this specific command
    wssCommandTemplate = commands[wssCommandName$]
    
    ' start building command
    
    ' header - start with fixed parameters from the template or otherwise fixed
    
    header = { }
    header.resource = wssCommandTemplate.header.resource
    header.version = wssCommandTemplate.header.version
    header.msgtype = "REQUEST"
    header.token = "las9kdfjaslkjdbhgsdkKbldkfbvnl?adkfjnvlk"
    header.method = wssCommandTemplate.header.method
    header.device = "GUID"
    header.reqID = 36
    
    ' body
    body = { }
    for each parameterName in wssCommandTemplate.body
      if wssCommandTemplate.body.DoesExist(parameterName) then
        wssParameter = wssCommandTemplate.body.Lookup(parameterName)
        if type(wssParameter) = "roAssociativeArray" and wssParameter.DoesExist("propertyType") and wssParameter.DoesExist("uniqueName") then
          ' command parameter that is set by the user
          wssParameterId = wssParameter.uniqueName
          if parameters.DoesExist(wssParameterId) then
            parameterValue = parameters[wssParameterId].GetCurrentParameterValue()
            if wssParameter.propertyType = "${INT}" then
              parameterValue = int(val(parameterValue))
            end if
            body[parameterName] = parameterValue
          end if
        else
          '' Bose pending change - do anything here? I think this represents a hardcoded value. Maybe it is in fact an integer if that's what is needed.
          body[parameterName] = wssParameter
        end if
      end if
    next
    
    bsWebSocket = { }
    bsWebSocket.header = header
    bsWebSocket.body = body
    
    ' Send the REST command to initNode plugin
    aa = {
      fid : speakerFid$,
      bsWebSocket : FormatJson(bsWebSocket)
    }
    pluginMessageCmd = {
      EventType : "SEND_PLUGIN_MESSAGE",
      PluginName : "initNode",
      PluginMessage : "boseWebsocket",
      BoseWebsocketArray : aa
    }
    
    m.diagnostics.PrintDebug("WebSocket command:")
    m.diagnostics.PrintDebug(formatJson(pluginMessageCmd))
    
    globalAA = getGlobalAA()
    globalAA.eddieDumpFile.sendLine("**** NEW WssCommand")
    dumpJsonBody(globalAA.eddieDumpFile, pluginMessageCmd)
    globalAA.eddieDumpFile.flush()
    
    m.msgPort.PostMessage(pluginMessageCmd)
    
  end sub
  
  
  ' m is bsp
  
  Sub ExecuteGpioOnCommand(zoneHSM as object, command$ as string, parameters as object)
    
    gpioNumberParameter = parameters["gpioNumber"]
    gpioNumber$ = gpioNumberParameter.GetCurrentParameterValue()
  
    m.diagnostics.PrintDebug("Turn on gpioNumber " + gpioNumber$)
    if IsControlPort(m.controlPort) then
      if gpioNumber$ = "-1" and m.sysinfo.deviceModel$ = "AU325" then
        m.controlPort.SetOutputState(1, 1)
        m.controlPort.SetOutputState(3, 1)
        m.controlPort.SetOutputState(5, 1)
        m.controlPort.SetOutputState(7, 1)
      else
        m.controlPort.SetOutputState(int(val(gpioNumber$)), 1)
      endif
    end if
  end sub
  
  
  Sub ExecuteGpioOffCommand(zoneHSM as object, command$ as string, parameters as object)
  
    gpioNumberParameter = parameters["gpioNumber"]
    gpioNumber$ = gpioNumberParameter.GetCurrentParameterValue()
  
    m.diagnostics.PrintDebug("Turn off gpioNumber " + gpioNumber$)
    if IsControlPort(m.controlPort) then
      if gpioNumber$ = "-1" and m.sysinfo.deviceModel$ = "AU325" then
        m.controlPort.SetOutputState(1, 0)
        m.controlPort.SetOutputState(3, 0)
        m.controlPort.SetOutputState(5, 0)
        m.controlPort.SetOutputState(7, 0)
      else
        m.controlPort.SetOutputState(int(val(gpioNumber$)), 0)
      endif
    end if
  end sub
  
  
  Sub ExecuteGpioSetStateCommand(zoneHSM as object, command$ as string, parameters as object)
    gpioStateParameter = parameters["gpioState"]
    gpioState$ = gpioStateParameter.GetCurrentParameterValue()
    m.diagnostics.PrintDebug("Set GPIO's to " + gpioState$)
    if IsControlPort(m.controlPort) then
      m.controlPort.SetWholeState(int(val(gpioState$)))
    end if
  end sub
  
  
  Sub ExecuteSerialSendStringCommand(zoneHSM as object, command$ as string, parameters as object)
    
    portParameter = parameters["port"]
    port$ = portParameter.GetCurrentParameterValue()
    
    port$ = zoneHSM.bsp.GetRuntimeUsbConnector(port$)
  
    serialStringParameter = parameters["message"]
    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$)
      end if
    end if
    
  end sub
  
  
  Sub ExecuteSendSerialBlockCommand(zoneHSM as object, command$ as string, parameters as object)
    
    portParameter = parameters["port"]
    port$ = portParameter.GetCurrentParameterValue()
    
    port$ = zoneHSM.bsp.GetRuntimeUsbConnector(port$)
  
    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$)
      end if
    end if
    
  end sub
  
  
  Sub ExecuteSendSerialByteCommand(zoneHSM as object, command$ as string, parameters as object)
    
    portParameter = parameters["port"]
    port$ = portParameter.GetCurrentParameterValue()
    
    port$ = zoneHSM.bsp.GetRuntimeUsbConnector(port$)
  
    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$)))
      end if
    end if
    
  end sub
  
  
  Sub ExecuteSendSerialBytesCommand(zoneHSM as object, command$ as string, parameters as object)
    
    portParameter = parameters["port"]
    port$ = portParameter.GetCurrentParameterValue()
    
    port$ = zoneHSM.bsp.GetRuntimeUsbConnector(port$)
  
    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)))
            end if
            byteString$ = mid(byteString$, commaPosition + 1)
          end while
        end if
      end if
    end if
    
  end sub
  
  
  Sub ExecuteSendUDPCommand(zoneHSM as object, command$ as string, parameters as object)
    
    udpStringParameter = parameters["message"]
    udpString$ = udpStringParameter.GetCurrentParameterValue()
    m.diagnostics.PrintDebug("Send UDP command " + udpString$)
    m.udpSender.Send(udpString$)
    
  end sub
  
  Sub ExecuteSendUDPBytesCommand(zoneHSM as object, command$ as string, parameters as object)
    
    byteValueParameter = parameters["message"]
    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
    end if
    
    m.diagnostics.PrintDebug("Send UDP command " + ba.ToHexString())
    m.udpSender.Send(ba)
    
  end sub
  
  Sub ExecuteSendProntoIRRemote(zoneHSM as object, command$ as string, parameters as object)
    irRemoteOutParameter = parameters["message"]
    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)
    end if
    
    if type(m.remote) = "roIRRemote" then
      m.remote.Send("PHC", irRemoteOut$)
    end if
    
  end sub
  
  Sub ExecuteSendIRRemoteCommand(zoneHSM as object, command$ as string, parameters as object)
    irRemoteOutParameter = parameters["message"]
    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"
    end if
    
    if type(m.remote) <> "roIRRemote" then
      m.remote = CreateObject("roIRRemote")
      m.remote.SetPort(m.msgPort)
    end if
    
    if type(m.remote) = "roIRRemote" then
      irRemoteOut% = int(val(irRemoteOut$))
      m.remote.Send(protocol$, irRemoteOut%)
    end if
    
  end sub
  
  Sub ExecuteSendBLC400OutputCommand(zoneHSM as object, command$ as string, parameters as object)
    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$))
      controlCmd[0] = channels%
      
      if effect$ = "intensity" then
        
        time% = GetIntegerParameterValue(parameters, "time", 0)
        intensity% = GetIntegerParameterValue(parameters, "intensity", 100)
  
        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
        
        blinkRateParameter = parameters["blinkRate"]
        blinkRate$ = blinkRateParameter.GetCurrentParameterValue()
  
        if blinkRate$ = "fast" then
          blinkRate% = BLINK_SPEED_FAST%
        else if blinkRate$ = "medium" then
          blinkRate% = BLINK_SPEED_MEDIUM%
        else
          blinkRate% = BLINK_SPEED_SLOW%
        end if
        
        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
        
        time% = GetIntegerParameterValue(parameters, "time", 0)
        minimumIntensity% = GetIntegerParameterValue(parameters, "minimumIntensity", 0)
        maximumIntensity% = GetIntegerParameterValue(parameters, "maximumIntensity", 100)
  
        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
        
        time% = GetIntegerParameterValue(parameters, "time", 0)
        intensity% = GetIntegerParameterValue(parameters, "intensity", 100)
  
        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%
        end if
        
        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%
          end if
          
        end if
        
        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
        
      end if
      
      m.blcs[controllerIndex%].SetOutputValues(controlCmd)
      
    end if
    
  end sub
  
  Sub ExecuteSendBPOutputCommand(zoneHSM as object, command$ as string, parameters as object)
    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)
          end if
        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)
        end if
      end if
      
    end if
    
  end sub
  
  Sub ExecuteSynchronizeCommand(zoneHSM as object, command$ as string, parameters as object)
    synchronizeKeywordParameter = parameters["message"]
    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 = { }
      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
        end if
      end if
      ' currently only support preload / synchronizing with images and videos
      if preloadRequired then
        zoneHSM.preloadState.PreloadItem()
      end if
      
      sleep(300)
      
      ' m.udpSender.Send("ply-" + synchronizeKeyword$)
      
      if type(m.udpReceiver) = "roDatagramReceiver" then
        udpReceiverExists = true
        m.udpReceiver = 0
      else
        udpReceiverExists = false
      end if
      
      m.WaitForSyncResponse(synchronizeKeyword$)
      
      if udpReceiverExists then
        m.udpReceiver = CreateObject("roDatagramReceiver", m.udpReceivePort)
        ' Set user data to distinguish between presentation udp messages and bootstrap udp messages
        m.udpReceiver.SetUserData("receiver")
        m.udpReceiver.SetPort(m.msgPort)
      end if
      
    end if
    
    
  end sub
  
  Sub ExecuteSendZoneMessageCommand(zoneHSM as object, command$ as string, parameters as object)
    m.diagnostics.PrintDebug("Execute sendZoneMessage command")
    
    zoneMessageParameter = parameters["message"]
    sendZoneMessageParameter$ = zoneMessageParameter.GetCurrentParameterValue()
    
    ' send ZoneMessage message
    zoneMessageCmd = { }
    zoneMessageCmd["EventType"] = "SEND_ZONE_MESSAGE"
    zoneMessageCmd["EventParameter"] = sendZoneMessageParameter$
    m.msgPort.PostMessage(zoneMessageCmd)
  end sub
  
  Sub ExecuteSendPluginMessageCommand(zoneHSM as object, command$ as string, parameters as object)
    m.diagnostics.PrintDebug("Execute sendPluginMessage command")
    
    pluginName = parameters["pluginName"]
    pluginMessageParameter = parameters["message"]
    
    pluginName$ = pluginName.GetCurrentParameterValue()
    sendPluginMessageParameter$ = pluginMessageParameter.GetCurrentParameterValue()
    ' send ZoneMessage message
    pluginMessageCmd = { }
    pluginMessageCmd["EventType"] = "SEND_PLUGIN_MESSAGE"
    pluginMessageCmd["PluginName"] = pluginName$
    pluginMessageCmd["PluginMessage"] = sendPluginMessageParameter$
    m.msgPort.PostMessage(pluginMessageCmd)
    
  end sub
  
  Sub ExecuteResizeZoneCommand(zoneHSM as object, command$ as string, parameters as object)
    m.diagnostics.PrintDebug("Execute resizeZone command")
    
    zoneId$ = parameters["zoneId"].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)
      end if
      
      if type(zone.mjpegvideoPlayer) = "roVideoPlayer" then
        zone.mjpegVideoPlayer.SetRectangle(r)
      end if
      
      if type(zone.imagePlayer) = "roImageWidget" then
        zone.imagePlayer.SetRectangle(r)
      end if
      
      if type(zone.displayedHtmlWidget) = "roHtmlWidget" then
        zone.displayedHtmlWidget.SetRectangle(r)
      end if
      
      if type(zone.canvasWidget) = "roCanvasWidget" then
        zone.canvasWidget.SetRectangle(r)
      end if
      
      if type(zone.widget) = "roHtmlWidget" or type(zone.widget) = "roTextWidget" then
        ok = zone.widget.SetRectangle(r)
      end if
      
    end if
    
  end sub
  
  Sub ExecuteHideZoneCommand(zoneHSM as object, command$ as string, parameters as object)
    m.diagnostics.PrintDebug("Execute hideZone command")
    
    zoneId$ = parameters["zoneId"].GetCurrentParameterValue()
    zoneHSM = m.GetZone(zoneId$)
    
    if type(zoneHSM) = "roAssociativeArray" then
      
      if not zoneHSM.isVisible then
        return
      end if
      
      if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
        
        if type(zoneHSM.videoPlayer) = "roVideoPlayer" then zoneHSM.videoPlayer.Hide()
        
        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 type(zoneHSM.videoPlayer) = "roVideoPlayer" then zoneHSM.videoPlayer.Hide()
        
      else if zoneHSM.type$ = "Clock" then
        
        if type(zoneHSM.widget) <> "Invalid" then zoneHSM.widget.Hide()
        
      else if zoneHSM.type$ = "Ticker" then
        
        zoneHSM.widget.Hide()
        
      else if zoneHSM.type$ = "BackgroundImage" then
        
      end if
      
      zoneHSM.isVisible = false
      
    end if
  end sub
  
  Sub ExecuteShowZoneCommand(zoneHSM as object, command$ as string, parameters as object)
    m.diagnostics.PrintDebug("Execute showZone command")
    
    zoneId$ = parameters["zoneId"].GetCurrentParameterValue()
    zoneHSM = m.GetZone(zoneId$)
    
    if type(zoneHSM) = "roAssociativeArray" then
      
      if zoneHSM.isVisible then
        return
      end if
      
      if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
        
        if type(zoneHSM.videoPlayer) = "roVideoPlayer" then zoneHSM.videoPlayer.Show()
        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 type(zoneHSM.videoPlayer) = "roVideoPlayer" then zoneHSM.videoPlayer.Show()
        
      else if zoneHSM.type$ = "Clock" then
        
        zoneHSM.widget.Show()
        
      else if zoneHSM.type$ = "Ticker" then
        
        zoneHSM.widget.Show()
        
      else if zoneHSM.type$ = "BackgroundImage" then
        
      end if
      
      zoneHSM.isVisible = true
      
    end if
  end sub
  
  Sub ExecutePauseZonePlaybackCommand(zoneHSM as object, command$ as string, parameters as object)
    
    m.diagnostics.PrintDebug("Execute pauseZonePlayback command")
    
    zoneId$ = parameters["zoneId"].GetCurrentParameterValue()
    zoneHSM = m.GetZone(zoneId$)
    
    if type(zoneHSM) = "roAssociativeArray" then
  
      if type(zoneHSM.videoPlayer) = "roVideoPlayer" then
        zoneHSM.videoPlayer.Pause()
      endif
  
      if IsAudioPlayer(zoneHSM.audioPlayer) then
        zoneHSM.audioPlayer.Pause()
      endif
  
    endif
  
  end sub
  
  Sub ExecuteResumeZonePlaybackCommand(zoneHSM as object, command$ as string, parameters as object)
    
    m.diagnostics.PrintDebug("Execute resumeZonePlayback command")
    
    zoneId$ = parameters["zoneId"].GetCurrentParameterValue()
    zoneHSM = m.GetZone(zoneId$)
    
    if type(zoneHSM) = "roAssociativeArray" then
  
      if type(zoneHSM.videoPlayer) = "roVideoPlayer" then
        zoneHSM.videoPlayer.Resume()
      endif
  
      if IsAudioPlayer(zoneHSM.audioPlayer) then
        zoneHSM.audioPlayer.Resume()
      endif
  
    endif
  
  end sub
  
  Sub ExecuteInternalSynchronizeCommand(zoneHSM as object, command$ as string, parameters as object)
    m.diagnostics.PrintDebug("Execute internalSynchronize command")
    
    internalSyncParameter = parameters["message"]
    internalSyncParameter$ = internalSyncParameter.GetCurrentParameterValue()
    
    ' send InternalSyncPreload message
    internalSyncPreload = { }
    internalSyncPreload["EventType"] = "INTERNAL_SYNC_PRELOAD"
    internalSyncPreload["EventParameter"] = internalSyncParameter$
    m.msgPort.PostMessage(internalSyncPreload)
    
    ' send InternalSyncMasterPreload message
    internalSyncMasterPreload = { }
    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()
          end if
        end if
      end if
    end if
    
  end sub
  
  
  Sub ExecuteCecSendStringCommand(zoneHSM as object, command$ as string, parameters as object)
    
    cecCommandParameter = parameters["cecString"]
    cecCommand$ = cecCommandParameter.GetCurrentParameterValue()
    
    cecSubstituteSourceAddressParameter = parameters["cecSubstituteSourceAddress"]
    cecSubstituteSourceAddress$ = cecSubstituteSourceAddressParameter.GetCurrentParameterValue()
    if lcase(cecSubstituteSourceAddress$) = "true" then
      cecSubstituteSourceAddress = true
    else
      cecSubstituteSourceAddress = false
    end if
    
    m.diagnostics.PrintDebug("cecSendString " + cecCommand$ + " cecSubstituteSourceAddress " + cecSubstituteSourceAddress$)
    m.SendCecCommand(cecCommand$, cecSubstituteSourceAddress)
    
  end sub
  
  
  Sub ExecuteCecPhilipsSetVolumeCommand(zoneHSM as object, command$ as string, parameters as object)
    volumeParameter = parameters["volume"]
    volume$ = volumeParameter.GetCurrentParameterValue()
    
    m.diagnostics.PrintDebug("Set cec Philips volume to " + volume$)
    volume% = int(val(volume$))
    m.CecPhilipsSetVolume(volume%)
  end sub
  
  
  Sub ExecutePauseCommand(zoneHSM as object, command$ as string, parameters as object)
    pauseTimeParameter = parameters["pauseTime"]
    pauseTime$ = pauseTimeParameter.GetCurrentParameterValue()
    
    m.diagnostics.PrintDebug("Pause for " + pauseTime$ + " milliseconds")
    pauseTime% = int(val(pauseTime$))
    sleep(pauseTime%)
  end sub
  
  
  Sub ExecuteSetVariableCommand(zoneHSM as object, command$ as string, parameters as object)
    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$)
    end if
  end sub
  
  
  Sub ExecuteBeaconStartCommand(zoneHSM as object, command$ as string, parameters as object)
    nameParameter = parameters["beaconName"]
    name$ = nameParameter.GetCurrentParameterValue()
    m.btManager.StartBeacon(name$)
  end sub
  
  
  Sub ExecuteBeaconStopCommand(zoneHSM as object, command$ as string, parameters as object)
    nameParameter = parameters["beaconName"]
    name$ = nameParameter.GetCurrentParameterValue()
    m.btManager.StopBeacon(name$)
  end sub
  
  
  ' m is bsp
  Sub ExecuteCmd(zoneHSM as object, command$ as string, parameters as object)
    
    m.diagnostics.PrintDebug("ExecuteCmd " + command$)
    
    if command$ = "gpioOnCommand" then
      m.ExecuteGpioOnCommand(zoneHSM, command$, parameters)
    else if command$ = "gpioOffCommand" then
      m.ExecuteGpioOffCommand(zoneHSM, command$, parameters)
    else if command$ = "gpioSetStateCommand" then
      m.ExecuteGpioSetStateCommand(zoneHSM, command$, parameters)
    else if command$ = "sendWss" then
      m.ExecuteSendWssCommand(parameters)
    else if command$ = "setAllAudioOutputs" then
      m.ExecuteSetAllAudioOutputsCommand(parameters)
    else if command$ = "setAudioMode" then
      m.SetAudioMode(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$ = "serialSendStringCommand" or command$ = "sendSerialStringCommand" or command$ = "serialSendString" then
      m.ExecuteSerialSendStringCommand(zoneHSM, command$, parameters)
    else if command$ = "sendSerialBlockCommand" then
      m.ExecuteSendSerialBlockCommand(zoneHSM, command$, parameters)
    else if command$ = "sendSerialByteCommand" then
      m.ExecuteSendSerialByteCommand(zoneHSM, command$, parameters)
    else if command$ = "sendSerialBytesCommand" then
      m.ExecuteSendSerialBytesCommand(zoneHSM, command$, parameters)
    else if command$ = "sendUDPCommand" then
      m.ExecuteSendUDPCommand(zoneHSM, command$, parameters)
    else if command$ = "sendUDPBytesCommand" then
      m.ExecuteSendUDPBytesCommand(zoneHSM, command$, parameters)
    else if command$ = "sendProntoIRRemote" then
      m.ExecuteSendProntoIRRemote(zoneHSM, command$, parameters)
    else if command$ = "sendIRRemote" then
      m.ExecuteSendIRRemoteCommand(zoneHSM, command$, parameters)
    else if command$ = "sendBLC400Output" then
      m.ExecuteSendBLC400OutputCommand(zoneHSM, command$, parameters)
    else if command$ = "sendBPOutput" then
      m.ExecuteSendBPOutputCommand(zoneHSM, command$, parameters)
    else if command$ = "synchronize" then
      m.ExecuteSynchronizeCommand(zoneHSM, command$, parameters)
    else if command$ = "sendZoneMessage" then
      m.ExecuteSendZoneMessageCommand(zoneHSM, command$, parameters)
    else if command$ = "sendPluginMessage" then
      m.ExecuteSendPluginMessageCommand(zoneHSM, command$, parameters)
    else if command$ = "resizeZone" then
      m.ExecuteResizeZoneCommand(zoneHSM, command$, parameters)
    else if command$ = "hideZone" then
      m.ExecuteHideZoneCommand(zoneHSM, command$, parameters)
    else if command$ = "showZone" then
      m.ExecuteShowZoneCommand(zoneHSM, command$, parameters)
    else if command$ = "pauseZonePlayback" then
      m.ExecutePauseZonePlaybackCommand(zoneHSM, command$, parameters)
    else if command$ = "resumeZonePlayback" then
      m.ExecuteResumeZonePlaybackCommand(zoneHSM, command$, parameters)
    else if command$ = "internalSynchronize" then
      m.ExecuteInternalSynchronizeCommand(zoneHSM, command$, parameters)
    else if command$ = "reboot" then
      RebootSystem()
    else if command$ = "cecDisplayOn" then
      m.CecDisplayOn()
    else if command$ = "cecDisplayOff" then
      m.CecDisplayOff()
    else if command$ = "cecSetSourceToBrightSign" then
      m.CecSetSourceToBrightSign()
    else if command$ = "cecSendString" then
      m.ExecuteCecSendStringCommand(zoneHSM, command$, parameters)
    else if command$ = "cecPhilipsSetVolume" then
      m.ExecuteCecPhilipsSetVolumeCommand(zoneHSM, command$, parameters)
    else if command$ = "pauseVideo" then
      m.PauseVideo(zoneHSM)
    else if command$ = "resumeVideo" then
      m.ResumeVideo(zoneHSM)
    else if command$ = "enablePowerSaveMode" then
      m.SetPowerSaveMode(true)
    else if command$ = "disablePowerSaveMode" then
      m.SetPowerSaveMode(false)
    else if command$ = "pause" then
      m.ExecutePauseCommand(zoneHSM, command$, parameters)
    else if command$ = "setVariable" then
      m.ExecuteSetVariableCommand(zoneHSM, command$, parameters)
    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
      zoneHSM.ConfigureAudioResources()
    else if command$ = "updateDataFeed" then
      m.UpdateDataFeed(parameters)
    else if command$ = "beaconStart" then
      m.ExecuteBeaconStartCommand(zoneHSM, command$, parameters)
    else if command$ = "beaconStop" then
      m.ExecuteBeaconStopCommand(zoneHSM, command$, parameters)
    end if
    
  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$)
    end if
    
  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$)
    end if
    
  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$
    end if
    
  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 = { }
    
    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.WriteDiagnosticLogEntryForForceLogUpload = WriteDiagnosticLogEntryForForceLogUpload
    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.inErrorState = 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
    end if
    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$)
    end if
    
    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"
      end if
  
      ' this pattern is used in PushLogFile to check serial number
      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$
      end if
      day$ = StripLeadingSpaces(stri(dtLocal.GetDay()))
      if len(day$) = 1 then
        day$ = "0" + day$
      end if
      dateString$ = year$ + month$ + day$
      
      logDate$ = m.bsp.registrySettings.logDate$
      
      if logDate$ = "" or logCounter$ = "" then
        logCounter$ = "000"
      else if logDate$ <> dateString$ then
        logCounter$ = "000"
      end if
      logDate$ = dateString$
  
      ' this pattern is used in PushLogFile to check serial number
      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)
      
    end if
    
    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$)
    end if
    
    ' 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$)
      end if
    end if
    
    ' 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$)
    end if
    
    ' 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$
    end if
    
    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$)
      end if
      
    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
      end if
      
      if m.bsp.variablesDBExists then
        
        logFile = CreateObject("roAppendFile", filePath$)
        if type(logFile) <> "roAppendFile" then
          return
        end if
        
        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
          end if
        next
        
        logFile.Flush()
        
        if openedDB then
          m.bsp.userVariablesDB = invalid
        end if
        
      end if
      
    end if
    
  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")
    end if
    
    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()
    end if
    
    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")
    end if
    
    if type(m.cutoverTimer) = "roTimer" then
      m.cutoverTimer.Stop()
      m.cutoverTimer = invalid
    end if
    
    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()
    end if
    
    m.InitializeCutoverTimer()
    
  end sub
  
  
  Sub InitializeCutoverTimer()
    
    if type(m.cutoverTimer) = "roTimer" then
      m.cutoverTimer.stop()
    end if
    
    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
      end if
  
      ' randomize upload log time +- 30 mins
      ' Rnd(61) returns random integer in [1, 61], so randomDiff% is in [-30, 30]
      randomDiff% = Rnd(61) - 31
      ' minute% is in [0, 59], so newMinute% is in [-30, 89]
      newMinute% = minute% + randomDiff%
      if newMinute% <0 then
        if hour% = 0 then
          randomHour% = 23
        else
          randomHour% = hour% - 1
        end if
        randomMinute% = newMinute% + 60
      else if newMinute% >59 then
        if hour% = 23 then
          randomHour% = 0
        else
          randomHour% = hour% + 1
        end if
        randomMinute% = newMinute% - 60
      else
        randomMinute% = newMinute%
        randomHour% = hour%
      end if
  
      m.cutoverTimer.SetDate( - 1, - 1, - 1)
      m.cutoverTimer.SetTime(randomHour%, randomMinute%, 0)
      
    end if
    
    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()
    end if
    
    ok = m.CopyLogFiles(storagePath$, "currentLog")
    if not ok then
      return ok
    end if
    
    ok = m.CopyLogFiles(storagePath$, "logs")
    if not ok then
      return ok
    end if
    
    ok = m.CopyLogFiles(storagePath$, "failedLogs")
    if not ok then
      return ok
    end if
    
    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 then
        return ok
      end if
      
    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
    end if
    
    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()
        
      end while
      
    end if
    
  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%
        end if
      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$)
      end if
      
    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$ = ""
    end if
    
    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$
    end if
    
    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()
    end if
    
  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
  
    ' If log file size is larger than 5MB, rotate file
    if m.logFile.CurrentPosition() >= 5242880 then
      m.CutoverLogFile(false)
      if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return
    end if
    
    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
  
  
  ' This function is only called in the case of force log upload
  Sub WriteDiagnosticLogEntryForForceLogUpload(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.CutoverLogFile(true)
  
  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
      ' check if serial number in log file matches device serial number
      ' this check is to prevent when SD card from one working player gets inserted to another new player
      start = file.instr(".") + 1
      length = file.instr("-") - start
      logSerial = file.mid(start, length)
  
      fullFilePath$ = m.uploadLogFailedFolder + "/" + file
  
      if logSerial = m.deviceUniqueID$ then
        target$ = m.uploadLogFolder + "/" + file
        ok = MoveFile(fullFilePath$, target$)
      else
        ok = DeleteFile(fullFilePath$)
      end if
  
    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" or m.inErrorState then return
    
    m.logFile.Flush()
    m.MoveCurrentLog()
    m.logFile = m.CreateLogFile()
    
    if forceUpload or m.uploadLogFilesAtSpecificTime then
      m.PushLogFile(forceUpload)
    end if
    
    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
      end if
    next
    
    m.logFile = m.CreateLogFile()
    
  end sub
  
  'endregion
  
  'region Hierarchical State Machine
  ' *************************************************
  '
  ' Hierarchical State Machine Implementation
  '
  ' *************************************************
  Function newHSM() as object
    
    HSM = { }
    
    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 = { }
    
    ' empty event used to get super states
    emptyEvent = { }
    emptyEvent["EventType"] = "EMPTY_SIGNAL"
    
    ' entry event
    entryEvent = { }
    entryEvent["EventType"] = "ENTRY_SIGNAL"
    
    ' init event
    initEvent = { }
    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
        end if
        
        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 = { }
        
        ' empty event used to get super states
        emptyEvent = { }
        emptyEvent["EventType"] = "EMPTY_SIGNAL"
        
        ' entry event
        entryEvent = { }
        entryEvent["EventType"] = "ENTRY_SIGNAL"
        
        ' exit event
        exitEvent = { }
        exitEvent["EventType"] = "EXIT_SIGNAL"
        
        ' init event
        initEvent = { }
        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)
            end if
            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)
          end if
        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
        end if
      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)
        end if
        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
        end if
      end while
    end while
  end if
  end if
  end if
  end if
  end if
  end if
  
  ' 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")
    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
  
  end if
  
  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 = { }
    
    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 = { }
    
    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")
    end if
    
    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")
    end if
    
  end sub
  
  
  Sub PrintDebug(debugStr$ as string)
    
    if type(m) <> "roAssociativeArray" then stop
    
    if m.debug then
      
      print debugStr$
      
    end if
    
    if m.systemLogDebug then
      m.systemLog.SendLine(debugStr$)
    end if
    
    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())
    end if
    
    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 GetHexColor(colorAttrs as object) as string
    
    ba = CreateObject("roByteArray")
    
    ba[0] = colorAttrs["a"]
    alpha$ = ba.ToHexString()
    
    ba[0] = colorAttrs["r"]
    red$ = ba.ToHexString()
    
    ba[0] = colorAttrs["g"]
    green$ = ba.ToHexString()
    
    ba[0] = 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() then
      return false
    end if
    
    numBytesToMatch% = baSpec.Count()
    numBytesInInput% = baInput.Count()
    startByteInInput% = numBytesInInput% - numBytesToMatch%
    
    for i% = 0 to baSpec.Count() - 1
      if baInput[startByteInInput% + i%] <> baSpec[i%] then
        return false
      end if
    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)
    end while
    
    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 = { }
    
    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_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
    end if
    
    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
    end if
    
    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
    end if
    
    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 Miscellaneous functions
  
  
  Function FileExists(filePath$ as string) as boolean
    
    file = CreateObject("roReadFile", filePath$)
    if not type(file) = "roReadFile"
      return false
    end if
    
    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.id$ + ".xml"
    
    m.isMRSSFeed = m.FeedIsMRSS(feedFileName$)
    if not m.isMRSSFeed and m.parser$ = "" then
      return
    end if
    
    m.bsp.diagnostics.PrintDebug("Read existing content for feed " + m.id$ + ".")
    
    file = CreateObject("roReadFile", feedFileName$)
    if type(file) <> "roReadFile" then
      return
    end if
    
    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 = { }
        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
        end if
        m.assetCollection.AddAsset(asset)
      end if
    next
    
    if not m.bsp.feedPool.ProtectAssets("display-" + m.id$, 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
    end if
    
    m.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.feedPool, m.assetCollection)
    
  end sub
  
  
  Function FeedIsMRSS(fileName$ as string)
    
    xml = ReadAsciiFile(fileName$)
    if len(xml) = 0 then
      return false
    end if
    
    feedXML = CreateObject("roXMLElement")
    if not feedXML.Parse(xml) then
      return false
    end if
    
    if feedXML.HasAttribute("xmlns:media") then
      attrs = feedXML.GetAttributes()
      if attrs["xmlns:media"] = "http://search.yahoo.com/mrss/" then
        return true
      end if
    end if
    
    return false
    
  end function
  
  
  Sub DownloadMRSSContent()
    
    m.bsp.diagnostics.PrintDebug("DownloadMRSSContent")
    
    if type(m.assetFetcher) = "roAssetFetcher" then
      return
    end if
    
    fileNameOnCard$ = "feed_cache/" + m.id$ + ".xml"
    
    ' write the mrss feed to the card
    CopyFile(m.rssFileName$, fileNameOnCard$)
    
    m.bsp.diagnostics.PrintDebug("Download new content for feed " + m.id$ + ".")
    m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_START_MRSS_FEED_CONTENT_DOWNLOAD, m.id$)
    
    ' 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 = { }
        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
        end if
        
        m.assetCollection.AddAsset(asset)
      end if
      
      ' 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"
          end if
        end if
      end if
      
      if IsString(fileToDownload.hash) then
        m.feedContentFilesToDownload.AddReplace(fileToDownload.hash, fileToDownload)
      end if
      
    next
    
    if type(m.bsp.networkingHSM) = "roAssociativeArray" then
      m.bsp.networkingHSM.UploadDeviceDownloadProgressFileList()
      m.bsp.networkingHSM.FileListPendingUpload = false
    end if
    
    if not m.bsp.feedPool.ProtectAssets("download-" + m.id$, 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
    end if
    
    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.id$)
    
    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
    end if
    
    ' tell the states to switch over to the new spec immediately
    m.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.feedPool, m.assetCollection)
    
    mrssSpecUpdatedEvent = { }
    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$)
    end if
    
    ' 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
    end if
    if m.feed.displayOn <> invalid then
      m.displayOn = m.feed.displayOn
    end if
    
  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$ <> "" then
        return true
      end if
    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$ = "" then
          return false
        end if
      end if
    next
    
    return true
    
  end function
  
  Sub mrssFeed_PopulateFeedItems(filePath$ as string)
    
    m.items = []
    
    xml = ReadAsciiFile(filePath$)
    if len(xml) = 0 then
      return
    end if
    
    ' check for encryption
    if getGlobalAA().bsp.contentEncrypted then
      isEncrypted = true
    else
      isEncrypted = false
    end if
    
    mrssFeedXML = CreateObject("roXMLElement")
    if not mrssFeedXML.Parse(xml) then
      stop
    end if
    
    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 lcase(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%
    end if
    
  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
      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%
      end if
    end if
    
  end sub
  
  
  Function newCustomContentMRSSItem( xml as Object ) As Object
  
  	item = { url:"no_url", title:"no_title", medium:"no_medium", type: "no_type", guid: "" }
  	contentPresent = false
  	for each elt in xml.GetBody()
  		name = elt.GetName()
  		if name = "title" then
  			item.title = elt.GetBody()
  		else if name = "description" then
  			item.url = elt.GetBody()
  		else if name = "medium" then
  			item.medium = elt.GetBody()
  		else if name = "type" then
  			item.type = elt.GetBody()
      endif
  	next
  
    ' set medium from either type of medium
    if item.medium = "no_medium" then
      if item.type <> "no_type" then
        item.medium = getMediumFromMimeType(item.type)
      endif
    endif
  
    return item
  
  End function
  
  
  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 lcase(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 = { }
        end if
        
        if elt.GetBody() = invalid then
          value = ""
        else
          value = elt.GetBody()
        end if
        
        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
    end if
    
    if (contentPresent) then
      return item
    else
      return invalid
    end if
    
  end function
  
  Sub mrssFeed_ParseFeedByPlugin(filePath$ as string)
    
    items = CreateObject("roArray", 1, true)
    metadata = { }
    
    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 = ""
          end if
          if IsString(item.description) then
            mrssItem.description = item.description
          else
            mrssItem.description = ""
          end if
          if IsString(item.description) then
            mrssItem.duration = Val(item.duration)
          else
            mrssItem.duration = 15
          end if
          if IsString(item.type) then
            mrssItem.type = item.type
          else
            mrssItem.type = ""
          end if
          if IsString(item.medium) then
            mrssItem.medium = item.medium
          else
            mrssItem.medium = "no_medium"
          end if
          if IsString(item.size) then
            mrssItem.size = Val(item.size)
          else
            mrssItem.size = 0
          end if
          if IsString(item.probeData) then
            mrssItem.probeData = item.probeData
          else
            mrssItem.probeData = ""
          end if
          
          ' 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
          end if
          
          ' if plugin specified custom fields, just copy them in
          if type(item.mrssCustomFields) = "roAssociativeArray" then
            mrssItem.mrssCustomFields = item.mrssCustomFields
          end if
          
          m.items.Push(mrssItem)
        end if
      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)
      end if
      if metadata.title <> invalid and IsString(metadata.title) then
        m.title = metadata.title
      end if
      if metadata.playtime <> invalid and IsString(metadata.playtime) then
        m.playtime = metadata.playtime
      end if
      if metadata.displayOn <> invalid and IsString(metadata.displayOn) then
        m.displayOn = metadata.displayOn
      end if
    end if
    
  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
  
      m.ParseRSSWithParserPlugin( filePath$)
  		
  	else
  
  		if type(m.isJSON) = "roBoolean" and m.isJSON then
  
        success = m.ParseJSONRSS(filePath$)
  
  		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
        end if
      next
      
      return true
      
    end if
    
    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()
    end if
    
  end sub
  
  
  Sub ReadLiveFeedContent()
    
    filePath$ = "feed_cache/" + m.id$ + ".xml"
    
    ok = true
    if m.isDynamicPlaylist or m.isLiveMediaFeed then
      m.ParseMRSSFeed(filePath$)
      m.ConvertMRSSFormatToContent()
    else
      ok = m.ParseCustomContentFormat(filePath$)
    end if
    
    if ok then
      m.itemUrls = m.articles
      m.fileUrls = m.articles
      if type(m.articleMediaTypes) = "roArray" then
        m.fileTypes = m.articleMediaTypes
      end if
      
      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
            end if
            index% = index% + 1
          next
        next
      end if
      
      ' 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 = { }
        asset.link = url
        asset.name = url
        
        if type(m.assets) = "roArray" then
          asset.hash = m.assets[index%].hash
        end if
        
        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
        end if
      next
      
      ' protect these assets
      if not m.bsp.feedPool.ProtectAssets("current-" + m.id$, 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
      end if
      
      ' post message indicating load complete
      contentDataFeedLoaded = { }
      contentDataFeedLoaded["EventType"] = "CONTENT_DATA_FEED_LOADED"
      contentDataFeedLoaded["Name"] = m.id$
      m.bsp.msgPort.PostMessage(contentDataFeedLoaded)
      
    end if
    
  end sub
  
  
  Sub DownloadLiveFeedContent()
    
    m.bsp.diagnostics.PrintDebug("DownloadLiveFeedContent")
    
    if type(m.parser$) = "roString" and m.parser$ <> "" then
      compareArticleTitles = false
    else
      compareArticleTitles = true
    end if
    
    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
      end if
      
      ' 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
      
    end if
    
    ' 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.id$ + ". No need to download content.")
        
        ' post message indicating no need to download content
        contentDataFeedUnchanged = { }
        contentDataFeedUnchanged["EventType"] = "CONTENT_DATA_FEED_UNCHANGED"
        contentDataFeedUnchanged["Name"] = m.id$
        m.bsp.msgPort.PostMessage(contentDataFeedUnchanged)
        
        return
      end if
      
    end if
    
    ' write the rss feed to the card
    CopyFile(m.rssFileName$, "feed_cache/" + m.id$ + ".xml")
    
    m.bsp.diagnostics.PrintDebug("Download new content for feed " + m.id$ + ".")
    m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_START_LIVE_DATA_FEED_CONTENT_DOWNLOAD, m.id$)
    
    ' 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 = { }
      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%]
      end if
      
      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.id$, 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
    end if
    
    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.id$)
    
    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
    end if
    
  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)
      end if
      
      m.timer.SetElapsed(timespan%, 0)
      m.timer.Start()
      
      m.bsp.liveDataFeedsByTimer.AddReplace(stri(m.timer.GetIdentity()), m)
      
    end if
    
  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")
      end if
    end if
    
    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)
          end if
        end if
      end if
      
    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())
        end if
        
      end if
      
      ' 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)
          end if
          
        next
      end if
      
      if type(m.bsp.networkingHSM) = "roAssociativeArray" then
        m.bsp.networkingHSM.UploadDeviceDownloadProgressFileList()
        m.bsp.networkingHSM.FileListPendingUpload = false
      end if
      
      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.id$, 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
        end if
        
        if not m.bsp.feedPool.UnprotectAssets("new-" + m.id$) 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())
        end if
        
        ' 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%]
          end if
          
          ' 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 = { }
        contentDataFeedLoaded["EventType"] = "CONTENT_DATA_FEED_LOADED"
        contentDataFeedLoaded["Name"] = m.id$
        m.bsp.msgPort.PostMessage(contentDataFeedLoaded)
        
      else
        
        ' unprotect old assets, keep protection on new (now current) assets
        if not m.bsp.feedPool.ProtectAssets("display-" + m.id$, 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
        end if
        
        if not m.bsp.feedPool.UnprotectAssets("download-" + m.id$) 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())
        end if
        
        ' post message indicating load complete
        mrssDataFeedLoaded = { }
        mrssDataFeedLoaded["EventType"] = "MRSS_DATA_FEED_LOADED"
        mrssDataFeedLoaded["Name"] = m.id$
        m.bsp.msgPort.PostMessage(mrssDataFeedLoaded)
        
      end if
      
    end if
    
  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 = { }
    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 ParseRSSWithParserPlugin( filePath$ as String) as Boolean
  
    success = true
    
    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
  
    return success
  
  End Function
  
  
  Function ParseJSONRSS(filePath$) as Boolean
  
    success = true
  
    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
  
    return success
  
  End Function
  
  
  Function ParseCustomContentFormat( filePath$ as String)
  
  	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
    
      m.ParseRSSWithParserPlugin( filePath$)
  	
    else
  	
    	if type(m.isJSON) = "roBoolean" and m.isJSON then
    
        success = m.ParseJSONRSS(filePath$)
  	
    	else
  
        xml = ReadAsciiFile( filePath$ )
        
        if len(xml) = 0 then
          return false
        endif
  
        mrssFeedXML = CreateObject("roXMLElement")
        if not mrssFeedXML.Parse(xml) stop
  
        items = []
  
        for each elt in mrssFeedXML.GetBody().Peek().GetBody()
          name = elt.GetName()
          if name = "item" then
            item = newCustomContentMRSSItem(elt)
            items.Push( item )
          end if
        next
  
        ' 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 items
          m.articles.push(item.url)
          m.articleTitles.push(item.title)
          m.articlesByTitle.AddReplace(item.title, item.url)
          m.articleMediaTypes.push(item.medium)
        next
  
      endif
  
    endif
  
  	return success
  
  End Function
  
  
  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 then
        return true
      end if
    end if
    
    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
        end if
      else if start_b% >= len(b$) then
        return 1
      end if
      
      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
        end if
      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
        end if
        
        ' Otherwise we've dealt with this character
        start_a% = start_a% + 1
        start_b% = start_b% + 1
      end if
    end while
    
  end function
  
  
  Function IsDigit(a$ as string) as boolean
    
    if asc(a$) >= 48 and asc(a$) <= 57 then
      return true
    end if
    
    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)
      end if
      
    end while
    
    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
    
    btm.btManager = CreateObject("roBtManager")
    if type(btm.btManager) = "roBtManager" then
      btm.btManager.SetPort(m.msgPort)
      btm.UpdatePersistentBeacons()
    endif
    
    if m.btleSupported then
      btm.btleSupported = true
    end if
    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
    
    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 }
      end if
    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 }
      end if
    else if beaconType$ = "EddystoneUid" then
      txp% = Val(beaconXml.txlevel.GetText())
      beaconData = GetEddystoneUidBeaconData(beaconXml.beaconId.GetText(), beaconXml.data1.GetText(), txp%, persistent)
    end if
    
    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
        end if
      end if
    end if
    
    return beacon
  end function
  
  Function ParsePresentationBeacons(beaconsXml as object) as boolean
    beacons = { }
    ' BACONTODO'
    
    ''	if 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 not m.presentationBeacons.IsEmpty() then
      m.presentationBeacons = { }
      m.SetBtAdvertising()
    end if
  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)
      end if
      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)
      end if
      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 }
    end if
    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()
    end if
  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 }
          end if
        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 }
          end if
        else if beaconType% = 2 then
          beaconType$ = "EddystoneUid"
          beaconData = GetEddystoneUidBeaconData(beaconInputData.BeaconId, beaconInputData.Data1, beaconInputData.TxLevel, true)
        end if
        
        if beaconData <> invalid then
          beacon = { name: beaconName$, type: beaconType$, data: beaconData, activate: true, isActive: false }
          m.persistentBeacons.AddReplace(beacon.name, beacon)
        end if
      end if
    end if
    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()
    end if
    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()
    end if
    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
          end if
          m.btleClientServiceData.push(txPower)
          m.btleClientServiceData.push(0)
          ' restart advertising to set connectable flag for all beacons
          success = m.SetBtAdvertising()
        end if
      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")
      end if
    end if
    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()
    end if
    return success
  end function
  
  Sub SetBtleStatus(status% as integer)
    newStatus% = status% and 255
    if newStatus% <> m.btleStatus% then
      m.btleStatus% = newStatus%
      m.SetBtAdvertising()
    end if
  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%)
        end if
        ' 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
            end if
          end if
        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)
        end if
        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%) + ")"
          end if
          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
              end if
            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$)
          end if
        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")
        end if
      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")
      end if
    end if
    return success
  end function
  
  
  Function ReadSyncSpec() as object
    
    currentSync = CreateObject("roSyncSpec")
    
    if not currentSync.ReadFromFile("current-sync.json") then
      if not currentSync.ReadFromFile("local-sync.json") then
        if not currentSync.ReadFromFile("localToBSN-sync.json") then
          if not currentSync.ReadFromFile("localSetupToStandalone-sync.json") then
            currentSync = invalid
          end if
        end if
      end if
    end if
    
    return currentSync
    
  end function
  
  
  Function GetLocalSetupToStandaloneSyncSpecFileName()
    return "localSetupToStandalone-sync.json"
  end function
  
  
  Function GetAutoscheduleFilePath(BSP as object)
    
    return GetPoolFilePath(BSP.assetPoolFiles, "autoschedule.json")
    
  end function
  
  
  Function GetAutoschedule(autoscheduleFilePath$)
    
    return m.jsonAutoschedule(autoscheduleFilePath$)
    
  end function
  
  
  Function GetAutoplayFileName(presentationName$)
    
    return "autoplay-" + presentationName$ + ".json"
    
  end function
  
  
  Function GetAutoplay(autoplayPath$)
    
    autoplayContainer = ParseJSON(ReadAsciiFile(autoplayPath$))
    
    ' high level check for validity of autoplay file
    if type(autoplayContainer) <> "roAssociativeArray" or type(autoplayContainer.BrightAuthor) <> "roAssociativeArray" then
      print "Invalid autoplay file - name not BrightAuthor"
      stop
    end if
    
    autoplay = autoplayContainer.BrightAuthor
    
    if type(autoplay.version) <> "Integer" then print "Invalid JSON file - version not found" : stop
    
    return autoplay
    
  end function
  
  
  Function GetAutoplayVersion(autoplay)
    
    version% = autoplay.version
    return version%
    
  end function
  
  
  Function ParseAutoplay(BrightAuthor as object, bsp as object)
    
    autoplay = { }
    meta = { }
    autoplay.meta = meta
    
    jsonParseAutoplay(BrightAuthor, meta, bsp)
    
    return autoplay
    
  end function
  
  
  Function ParseZones(bsp as object, BrightAuthor as object, Sign as object)
    
    return jsonParseZones(BrightAuthor, Sign)
    
  end function
  
  
  Function GetColor(colorAttrs as object) as integer
    
    alpha = colorAttrs["a"]
    red% = colorAttrs["r"]
    green% = colorAttrs["g"]
    blue% = colorAttrs["b"]
    
    color_spec% = (alpha * 256 * 256 * 256) + (red% * 256 * 256) + (green% * 256) + blue%
    return color_spec%
    
  end function
  
  
  ' region Schedule parsing'
  Sub GetStartingPresentation(schedule as object)
    
    ' 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)
    end if
    
    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)
        end if
        
        nextInterruptionStartTime = schedule.GetNextScheduledEventTime(schedule.scheduledInterruptions)
        
        if endDateTime = invalid then
          if nextInterruptionStartTime <> invalid then
            schedule.activeScheduledEventEndDateTime = nextInterruptionStartTime
          end if
        else
          if nextInterruptionStartTime = invalid then
            schedule.activeScheduledEventEndDateTime = endDateTime
          else
            if endDateTime.GetString() < nextInterruptionStartTime.GetString() then
              schedule.activeScheduledEventEndDateTime = endDateTime
            else
              schedule.activeScheduledEventEndDateTime = nextInterruptionStartTime
            end if
          end if
        end if
      end if
    end if
    
  end sub
  
  
  Function jsonAutoschedule(jsonFileName$ as string)
    
    autoScheduleJSON = ParseJSON(ReadAsciiFile(jsonFileName$))
    
    schedule = jsonNewSchedule(autoScheduleJSON)
    
    if type(schedule.activeScheduledEvent) = "roAssociativeArray" then
      
      presentation$ = schedule.activeScheduledEvent.presentationName$
      m.activePresentation$ = presentation$
      
      autoplayFileName$ = "autoplay-" + presentation$ + ".json"
      
      ' find the autoplay file in the pool folder
      currentSync = ReadSyncSpec()
      if not type(currentSync) = "roSyncSpec" then
        stop
      end if
      
      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
      
    end if
    
    return schedule
    
  end function
  
  
  Function jsonNewSchedule(autoScheduleJSON as object) as object
    
    scheduledPresentations = autoScheduleJSON.scheduledPresentations
    numScheduledPresentations% = scheduledPresentations.Count()
    
    schedule = { }
    schedule.allScheduledEvents = CreateObject("roArray", numScheduledPresentations%, true)
    schedule.scheduledEvents = CreateObject("roArray", numScheduledPresentations%, true)
    schedule.scheduledInterruptions = CreateObject("roArray", 1, true)
    
    for each scheduledPresentation in scheduledPresentations
      scheduledPresentationBS = jsonNewScheduledEvent(scheduledPresentation)
      
      schedule.allScheduledEvents.push(scheduledPresentationBS)
      
      if scheduledPresentationBS.interruption then
        schedule.scheduledInterruptions.push(scheduledPresentationBS)
      else
        schedule.scheduledEvents.push(scheduledPresentationBS)
      end if
    next
    
    GetStartingPresentation(schedule)
    
    return schedule
    
  end function
  
  
  Function jsonNewScheduledEvent(scheduledEventJSON as object) as object
    
    scheduledEventBS = { }
    
    if type(scheduledEventJSON.presentationToSchedule) = "roAssociativeArray" then
      scheduledEventBS.presentationName$ = scheduledEventJSON.presentationToSchedule.name
    end if
    
    dateTime$ = scheduledEventJSON.dateTime
    scheduledEventBS.dateTime = FixDateTime(dateTime$)
    
    scheduledEventBS.duration% = scheduledEventJSON.duration
    
    if scheduledEventJSON.allDayEveryDay then
      scheduledEventBS.allDayEveryDay = true
    else
      scheduledEventBS.allDayEveryDay = false
    end if
    
    scheduledEventBS.recurrence = scheduledEventJSON.recurrence
    scheduledEventBS.recurrencePattern$ = scheduledEventJSON.recurrencePattern
    scheduledEventBS.recurrencePatternDaily$ = scheduledEventJSON.recurrencePatternDaily
    scheduledEventBS.recurrencePatternDaysOfWeek% = scheduledEventJSON.recurrencePatternDaysOfWeek
    
    ' TODO - bug that it's not written I think
    if not scheduledEventJSON.recurrenceStartDate = invalid then
      dateTime$ = scheduledEventJSON.recurrenceStartDate
      scheduledEventBS.recurrenceStartDate = FixDateTime(dateTime$)
    end if
    
    scheduledEventBS.recurrenceGoesForever = scheduledEventJSON.recurrenceGoesForever
    
    dateTime$ = scheduledEventJSON.recurrenceEndDate
    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
    
    ' TODO - bug that it's not initialized I think
    scheduledEventBS.interruption = false
    if not scheduledEventJSON.interruption = invalid then
      scheduledEventBS.interruption = scheduledEventJSON.interruption
    end if
    
    return scheduledEventBS
    
  end function
  
  
  Sub jsonParseAutoplay(BrightAuthor as object, meta as object, bsp as object)
    
    meta.publishedModel = BrightAuthor.meta.model
    meta.name = BrightAuthor.meta.name
    meta.videoMode = BrightAuthor.meta.videoMode
    meta.forceResolution = BrightAuthor.meta.forceResolution
    meta.tenBitColorEnabled = BrightAuthor.meta.tenBitColorEnabled
    
    meta.dolbyVisionEnabled = BrightAuthor.meta.dolbyVisionEnabled
    meta.fullResGraphicsEnabled = BrightAuthor.meta.fullResGraphicsEnabled
    meta.stretchedVideoWall = []
    meta.audioAutoLevel = BrightAuthor.meta.audioAutoLevel
    
    meta.videoConnector = BrightAuthor.meta.videoConnector
    meta.monitorOrientation = lcase(BrightAuthor.meta.monitorOrientation)
    
    meta.deviceWebPageDisplay = BrightAuthor.meta.deviceWebPageDisplay
    meta.customDeviceWebPage = BrightAuthor.meta.customDeviceWebPage
    
    meta.alphabetizeVariableNames = BrightAuthor.meta.alphabetizeVariableNames
    
    meta.htmlEnableJavascriptConsole = BrightAuthor.meta.htmlEnableJavascriptConsole
    
    meta.backgroundScreenColor = GetColor(BrightAuthor.meta.backgroundScreenColor)
    meta.dontChangePresentationUntilMediaEndEventReceived = BrightAuthor.meta.delayScheduleChangeUntilMediaEndEvent
    meta.delayScheduleChangeUntilMediaEndEvent = BrightAuthor.meta.delayScheduleChangeUntilMediaEndEvent
    meta.languageKey = BrightAuthor.meta.languageKey 'BACONTODO - check case'
    
    meta.bp900AConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp900a"].configureAutomatically
    meta.bp900BConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp900b"].configureAutomatically
    meta.bp900CConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp900c"].configureAutomatically
    meta.bp900DConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp900d"].configureAutomatically
    meta.bp200AConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp200a"].configureAutomatically
    meta.bp200BConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp200b"].configureAutomatically
    meta.bp200CConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp200c"].configureAutomatically
    meta.bp200DConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp200d"].configureAutomatically
    
    meta.bp900AConfiguration% = BrightAuthor.meta.buttonPanels["bp900a"].configuration
    meta.bp900BConfiguration% = BrightAuthor.meta.buttonPanels["bp900b"].configuration
    meta.bp900CConfiguration% = BrightAuthor.meta.buttonPanels["bp900c"].configuration
    meta.bp900DConfiguration% = BrightAuthor.meta.buttonPanels["bp900d"].configuration
    meta.bp200AConfiguration% = BrightAuthor.meta.buttonPanels["bp200a"].configuration
    meta.bp200BConfiguration% = BrightAuthor.meta.buttonPanels["bp200b"].configuration
    meta.bp200CConfiguration% = BrightAuthor.meta.buttonPanels["bp200c"].configuration
    meta.bp200DConfiguration% = BrightAuthor.meta.buttonPanels["bp200d"].configuration
    
    ' remote configuration
    meta.irRemoteControl = BrightAuthor.meta.irRemote.irRemoteControl
    meta.irInConfiguration = BrightAuthor.meta.irRemote.irInConfiguration
    meta.irOutConfiguration = BrightAuthor.meta.irRemote.irOutConfiguration
  
    ' serial ports'
    meta.serialPortConfigurations = CreateObject("roArray", 1, true)
    
    for each serialPortConfigurationSpec in BrightAuthor.meta.serialPortConfigurations
      
      serialPortConfiguration = { }
      serialPortConfiguration.serialPortSpeed% = serialPortConfigurationSpec.baudRate
      serialPortConfiguration.protocol$ = serialPortConfigurationSpec.protocol
      serialPortConfiguration.sendEol$ = GetEolFromSpec(serialPortConfigurationSpec.sendEol)
      serialPortConfiguration.receiveEol$ = GetEolFromSpec(serialPortConfigurationSpec.receiveEol)
      serialPortConfiguration.invertSignals = serialPortConfigurationSpec.invertSignals
      
      dataBits$ = stri(serialPortConfigurationSpec.dataBits)
      parity$ = serialPortConfigurationSpec.parity
      stopBits$ = stri(serialPortConfigurationSpec.stopBits)
      serialPortConfiguration.serialPortMode = StripLeadingSpaces(dataBits$) + StripLeadingSpaces(parity$) + StripLeadingSpaces(stopBits$)
      port% = val(serialPortConfigurationSpec.port)
      serialPortConfiguration.port = port%
      
      connectedDevice$ = serialPortConfigurationSpec.connectedDevice
      if connectedDevice$ = "GPS" then
        serialPortConfiguration.gps = true
      else
        serialPortConfiguration.gps = false
      end if
      
      meta.serialPortConfigurations[port%] = serialPortConfiguration
    next
    
    ' parse parser plugins
    meta.parserPlugins = CreateObject("roArray", 1, true)
    for each parserPluginSpec in BrightAuthor.meta.parserPlugins
      parserPlugin = jsonParseParserPlugin(parserPluginSpec)
      meta.parserPlugins.push(parserPlugin)
    next
    
    ' first pass parse of user variables
    meta.userVariableSpecs = []
    
    userVariablesJson = BrightAuthor.meta.userVariables
    
    for each userVariableJson in userVariablesJson
      
      userVariableSpec = { }
      userVariableSpec.name = userVariableJson.name
      userVariableSpec.defaultValue = userVariableJson.defaultValue
      userVariableSpec.access = userVariableJSON.access
      userVariableSpec.systemVariable$ = userVariableJson.systemVariable
      
      ' record networked information - parse in 2nd pass
      userVariableSpec.url = ""
      userVariableSpec.liveDataFeedId = userVariableJson.liveDataFeedId
      
      url$ = userVariableJson.url
      if url$ <> "" then
        userVariableSpec.url$ = url$
      end if
      
      meta.userVariableSpecs.push(userVariableSpec)
    next
    
    ' parse live data feeds
    meta.liveDataFeedDescriptions = []
    liveDataFeedsJson = BrightAuthor.meta.liveDataFeeds
    for each liveDataFeedJson in liveDataFeedsJson
      liveDataFeedDescription = jsonParseLiveDataFeed(liveDataFeedJson)
      meta.liveDataFeedDescriptions.push(liveDataFeedDescription)
    next
    meta.resetVariablesOnPresentationStart = BrightAuthor.meta.resetVariablesOnPresentationStart
    
    ' parse HTML sites
    meta.htmlSiteDescriptions = []
    for each htmlSiteJson in BrightAuthor.meta.htmlSites
      htmlSiteDescription = jsonParseHtmlSite(bsp, htmlSiteJson)
      meta.htmlSiteDescriptions.push(htmlSiteDescription)
    next
    
    ' parse presentation identifiers
    meta.presentationIdentifiers = []
    if type(BrightAuthor.meta.presentationIdentifiers) = "roArray" then
      for each presentationIdentifier in BrightAuthor.meta.presentationIdentifiers
        meta.presentationIdentifiers.push(presentationIdentifier)
      next
    end if
    
    ' parse beacons
    meta.beacons = []
    
    ' get list of additional files to publish
    meta.additionalPublishedFiles = []
    if type(BrightAuthor.meta.auxiliaryFiles) = "roArray" then
      for each auxiliaryFile in BrightAuthor.meta.auxiliaryFiles
        meta.additionalPublishedFiles.push(auxiliaryFile)
      next
    end if
    
    meta.boseProducts = BrightAuthor.meta.partnerProducts
    
    meta.udpReceivePort = BrightAuthor.meta.udpReceiverPort
    meta.udpSendPort = BrightAuthor.meta.udpDestinationPort
    meta.udpAddressType = BrightAuthor.meta.udpDestinationAddressType 'BACONTODO - check case'
    
    if meta.udpAddressType = "" then meta.udpAddressType = "IPAddress"
    meta.udpAddress = BrightAuthor.meta.udpDestinationAddress
    
    meta.enableEnhancedSynchronization = false
    meta.deviceIsSyncMaster = false
    meta.ptpDomain$ = "0"
    if type(BrightAuthor.meta.enableEnhancedSynchronization) = "roAssociativeArray" and BrightAuthor.meta.enableEnhancedSynchronization.deviceIsSyncMaster <> invalid then
      meta.enableEnhancedSynchronization = true
      meta.deviceIsSyncMaster = BrightAuthor.meta.enableEnhancedSynchronization.deviceIsSyncMaster
      meta.ptpDomain$ = StripLeadingSpaces(stri(BrightAuthor.meta.enableEnhancedSynchronization.ptpDomain))
    else
    end if
    
    meta.flipCoordinates = BrightAuthor.meta.flipCoordinates
    meta.touchCursorDisplayMode = lcase(BrightAuthor.meta.touchCursorDisplayMode)
      
    meta.gpio = CreateObject("roArray", 8, true)
    for i% = 0 to 7
      meta.gpio[i%] = BrightAuthor.meta.gpio[i%]
    next
    
    meta.audioConfiguration = BrightAuthor.meta.audioConfiguration
    
    audioSignPropertyMap = BrightAuthor.meta.audioSignPropertyMap
    meta.audio1MinVolume = audioSignPropertyMap["analog1"].min
    meta.audio1MaxVolume = audioSignPropertyMap["analog1"].max
    meta.hdmiMinVolume = audioSignPropertyMap["hdmi"].min
    meta.hdmiMaxVolume = audioSignPropertyMap["hdmi"].max
    meta.spdifMinVolume = audioSignPropertyMap["spdif"].min
    meta.spdifMaxVolume = audioSignPropertyMap["spdif"].max
  
    meta.usbTypeAMinVolume = audioSignPropertyMap["usbTypeA"].min
    meta.usbTypeAMaxVolume = audioSignPropertyMap["usbTypeA"].max
    meta.usbTypeCMinVolume = audioSignPropertyMap["usbTypeC"].min
    meta.usbTypeCMaxVolume = audioSignPropertyMap["usbTypeC"].max
    meta.usb700_1MinVolume = audioSignPropertyMap["usb700_1"].min
    meta.usb700_1MaxVolume = audioSignPropertyMap["usb700_1"].max
    meta.usb700_2MinVolume = audioSignPropertyMap["usb700_2"].min
    meta.usb700_2MaxVolume = audioSignPropertyMap["usb700_2"].max
    meta.usb700_3MinVolume = audioSignPropertyMap["usb700_3"].min
    meta.usb700_3MaxVolume = audioSignPropertyMap["usb700_3"].max
    meta.usb700_4MinVolume = audioSignPropertyMap["usb700_4"].min
    meta.usb700_4MaxVolume = audioSignPropertyMap["usb700_4"].max
    meta.usb700_5MinVolume = audioSignPropertyMap["usb700_5"].min
    meta.usb700_5MaxVolume = audioSignPropertyMap["usb700_5"].max
    meta.usb700_6MinVolume = audioSignPropertyMap["usb700_6"].min
    meta.usb700_6MaxVolume = audioSignPropertyMap["usb700_6"].max
    meta.usb700_7MinVolume = audioSignPropertyMap["usb700_7"].min
    meta.usb700_7MaxVolume = audioSignPropertyMap["usb700_7"].max
    meta.usb_1MinVolume = audioSignPropertyMap["usb_1"].min
    meta.usb_1MaxVolume = audioSignPropertyMap["usb_1"].max
    meta.usb_2MinVolume = audioSignPropertyMap["usb_2"].min
    meta.usb_2MaxVolume = audioSignPropertyMap["usb_2"].max
    meta.usb_3MinVolume = audioSignPropertyMap["usb_3"].min
    meta.usb_3MaxVolume = audioSignPropertyMap["usb_3"].max
    meta.usb_4MinVolume = audioSignPropertyMap["usb_4"].min
    meta.usb_4MaxVolume = audioSignPropertyMap["usb_4"].max
    meta.usb_5MinVolume = audioSignPropertyMap["usb_5"].min
    meta.usb_5MaxVolume = audioSignPropertyMap["usb_5"].max
    meta.usb_6MinVolume = audioSignPropertyMap["usb_6"].min
    meta.usb_6MaxVolume = audioSignPropertyMap["usb_6"].max
    
    meta.inactivityTimeout = BrightAuthor.meta.inactivityTimeout
    meta.inactivityTime = BrightAuthor.meta.inactivityTime
    
    meta.graphicsZOrder = BrightAuthor.meta.graphicsZOrder 'BACONTODO - check case'
    
    meta.isMosaic = false
    
  end sub
  
  
  Function jsonParseZones(BrightAuthor as object, Sign as object)
    
    zoneList = BrightAuthor.zones
    numZones% = zoneList.Count()
    
    zoneDescriptions = CreateObject("roArray", numZones%, true)
    
    for each zoneSpec in zoneList
      zoneDescription = jsonParseZoneSpec(zoneSpec, Sign)
      zoneDescriptions.push(zoneDescription)
    next
    
    return zoneDescriptions
    
  end function
  
  
  ' BACONTODO add support for AudioOnly'
  Function jsonParseZoneSpec(zoneSpec as object, sign as object)
    
    zoneDescription = { }
    
    '' common zone parameters
    zoneDescription.name$ = zoneSpec.name
    
    zoneDescription.originalWidth% = zoneSpec.absolutePosition.width
    zoneDescription.originalHeight% = zoneSpec.absolutePosition.height
    
    zoneDescription.x = zoneSpec.absolutePosition.x
    zoneDescription.y = zoneSpec.absolutePosition.y
    zoneDescription.width = zoneSpec.absolutePosition.width
    zoneDescription.height = zoneSpec.absolutePosition.height
    
    zoneDescription.type$ = zoneSpec.type
    zoneDescription.id$ = zoneSpec.id
    
    ' VideoOrImagesZone'
    if zoneDescription.type$ = "VideoOrImages" then
      zoneDescription.imageMode% = GetImageModeValue(zoneSpec.zoneSpecificParameters.imageMode)
      zoneDescription.numImageItems% = 0
    end if
    
    ' Ticker '
    if lcase(zoneDescription.type$) = "ticker" then
      
      zoneDescription.numberOfLines% = zoneSpec.zoneSpecificParameters.textWidget.numberOfLines
      
      zoneDescription.delay% = zoneSpec.zoneSpecificParameters.textWidget.delay
      
      zoneDescription.rotation% = 0
      rotation$ = zoneSpec.zoneSpecificParameters.textWidget.rotation
      if rotation$ = "90" then
        zoneDescription.rotation% = 3
      else if rotation$ = "180" then
        zoneDescription.rotation% = 2
      else if rotation$ = "270" then
        zoneDescription.rotation% = 1
      end if
      
      alignment$ = lcase(zoneSpec.zoneSpecificParameters.textWidget.alignment)
      if alignment$ = "center" then
        zoneDescription.alignment% = 1
      else if alignment$ = "right" then
        zoneDescription.alignment% = 2
      else
        zoneDescription.alignment% = 0
      end if
      
      scrollingMethod$ = lcase(zoneSpec.zoneSpecificParameters.textWidget.scrollingMethod)
      zoneDescription.scrollingMethod% = 0
      if scrollingMethod$ = "statictext" then
        zoneDescription.scrollingMethod% = 1
      else if scrollingMethod$ = "scrolling" then
        zoneDescription.scrollingMethod% = 3
      end if
      
      zoneDescription.scrollSpeed% = zoneSpec.zoneSpecificParameters.scrollSpeed
      
      widget = zoneSpec.zoneSpecificParameters.widget
      
      foregroundTextColor = widget.foregroundTextColor
      backgroundTextColor = widget.backgroundTextColor
      
      zoneDescription.foregroundTextColor% = GetColor(foregroundTextColor)
      zoneDescription.backgroundTextColor% = GetColor(backgroundTextColor)
      
      zoneDescription.font$ = widget.font
      
      if type(widget.backgroundBitmap) = "roAssociativeArray" then
        zoneDescription.backgroundBitmapFile$ = widget.backgroundBitmap.file
        zoneDescription.stretch = widget.backgroundBitmap.stretch
      else
        zoneDescription.backgroundBitmapFile$ = ""
        zoneDescription.stretch = false
      end if
      
      safeTextRegion = widget.safeTextRegion
      if type(safeTextRegion) = "roAssociativeArray" then
        zoneDescription.safeTextRegionX% = safeTextRegion.x
        zoneDescription.safeTextRegionY% = safeTextRegion.y
        zoneDescription.safeTextRegionWidth% = safeTextRegion.width
        zoneDescription.safeTextRegionHeight% = safeTextRegion.height
      end if
      
    end if
  
   ' Clock '
    if lcase(zoneDescription.type$) = "clock" then
  
      rotation = zoneSpec.zoneSpecificParameters.rotation
      if rotation <> invalid and IsNonEmptyString(rotation) then
        zoneDescription.AddReplace("rotation", rotation)
      end if
  
      clockFormat = zoneSpec.zoneSpecificParameters.clockFormat
      if clockFormat <> invalid and IsNonEmptyString(clockFormat) then
        zoneDescription.AddReplace("clockFormat", clockFormat)
      end if
      
      widget = zoneSpec.zoneSpecificParameters.widget
      
      foregroundTextColor = widget.foregroundTextColor
      if type(foregroundTextColor) = "roAssociativeArray" then
        zoneDescription.AddReplace("foregroundTextColor", foregroundTextColor)
      end if
  
      backgroundTextColor = widget.backgroundTextColor
      if type(backgroundTextColor) = "roAssociativeArray" then
        zoneDescription.AddReplace("backgroundTextColor", backgroundTextColor)
      end if
  
      backgroundBitmapFileName = widget.backgroundBitmapFileName
      if backgroundBitmapFileName <> invalid and IsNonEmptyString(backgroundBitmapFileName) then
        filePath = GetPoolFilePath(m.bsp.assetPoolFiles, backgroundBitmapFileName)
        zoneDescription.AddReplace("backgroundBitmapFilePath", backgroundBitmapFileName)
        zoneDescription.AddReplace("stretchBitmapFile", widget.stretchBitmapFile)
      end if
  
      fontFileName = widget.fontFileName
      if fontFileName <> invalid and IsNonEmptyString(fontFileName) then
        filePath = GetPoolFilePath(m.bsp.assetPoolFiles, fontFileName)
        zoneDescription.AddReplace("fontFilePath", fontFileName)
      end if
  
      safeTextRegion = widget.safeTextRegion
      if type(safeTextRegion) = "roAssociativeArray" then
        zoneDescription.AddReplace("safeTextRegion", safeTextRegion)
      end if
      
    end if
    
    ' Video parameters'
    if zoneDescription.type$ = "VideoOrImages" or zoneDescription.type$ = "VideoOnly" then
      zoneDescription.viewMode% = GetViewModeValue(zoneSpec.zoneSpecificParameters.viewMode)
      zoneDescription.initialVideoVolume% = zoneSpec.zoneSpecificParameters.videoVolume
      zoneDescription.zOrderFront = zoneSpec.zoneSpecificParameters.zOrderFront
    end if
  
    ' Audio parameters'
    if zoneDescription.type$ = "VideoOrImages" or zoneDescription.type$ = "VideoOnly" or zoneDescription.type$ = "AudioOnly" or zoneDescription.type$ = "EnhancedAudio" then
      zoneDescription.audioMixMode$ = zoneSpec.zoneSpecificParameters.audioMixMode
      zoneDescription.analogOutput$ = zoneSpec.zoneSpecificParameters.analogOutput
      zoneDescription.hdmiOutput$ = zoneSpec.zoneSpecificParameters.hdmiOutput
      zoneDescription.spdifOutput$ = zoneSpec.zoneSpecificParameters.spdifOutput
      zoneDescription.usbOutputA$ = zoneSpec.zoneSpecificParameters.usbOutputA
      zoneDescription.usbOutputB$ = zoneSpec.zoneSpecificParameters.usbOutputB
      zoneDescription.usbOutputTypeA$ = zoneSpec.zoneSpecificParameters.usbOutputTypeA
      zoneDescription.usbOutputTypeC$ = zoneSpec.zoneSpecificParameters.usbOutputTypeC
      zoneDescription.usbOutput700_1$ = zoneSpec.zoneSpecificParameters.usbOutput700_1
      zoneDescription.usbOutput700_2$ = zoneSpec.zoneSpecificParameters.usbOutput700_2
      zoneDescription.usbOutput700_3$ = zoneSpec.zoneSpecificParameters.usbOutput700_3
      zoneDescription.usbOutput700_4$ = zoneSpec.zoneSpecificParameters.usbOutput700_4
      zoneDescription.usbOutput700_5$ = zoneSpec.zoneSpecificParameters.usbOutput700_5
      zoneDescription.usbOutput700_6$ = zoneSpec.zoneSpecificParameters.usbOutput700_6
      zoneDescription.usbOutput700_7$ = zoneSpec.zoneSpecificParameters.usbOutput700_7
      zoneDescription.usbOutput_1$ = zoneSpec.zoneSpecificParameters.usbOutput_1
      zoneDescription.usbOutput_2$ = zoneSpec.zoneSpecificParameters.usbOutput_2
      zoneDescription.usbOutput_3$ = zoneSpec.zoneSpecificParameters.usbOutput_3
      zoneDescription.usbOutput_4$ = zoneSpec.zoneSpecificParameters.usbOutput_4
      zoneDescription.usbOutput_5$ = zoneSpec.zoneSpecificParameters.usbOutput_5
      zoneDescription.usbOutput_6$ = zoneSpec.zoneSpecificParameters.usbOutput_6
      zoneDescription.minimumVolume% = zoneSpec.zoneSpecificParameters.minimumVolume
      zoneDescription.maximumVolume% = zoneSpec.zoneSpecificParameters.maximumVolume
      zoneDescription.initialAudioVolume% = zoneSpec.zoneSpecificParameters.audioVolume
    end if
    
    ' EnhancedAudioZone parameters
    if zoneDescription.type$ = "EnhancedAudio" then
      zoneDescription.fadeLength = zoneSpec.zoneSpecificParameters.fadeLength
    end if
    
    ' ImagesZone - BACONTODO - this seems inconsistent with xml version
    if zoneDescription.type$ = "Images" then
      zoneDescription.numImageItems% = 0
      zoneDescription.imageMode% = GetImageModeValue(zoneSpec.zoneSpecificParameters.imageMode)
    end if
    
    ' BACONTODO - zone type independent?
    zoneDescription.playlist = jsonParsePlaylistSpec(zoneDescription, zoneSpec.playlist)
    
    return zoneDescription
    
  end function
  
  
  Function jsonParseHtmlSite(bsp as object, htmlSiteJson as object) as object
    
    htmlSiteDescription = { }
    htmlSiteDescription.name$ = htmlSiteJson.name
    htmlSiteDescription.enableNode = htmlSiteJson.enableNode
    htmlSiteDescription.queryString = jsonParseParameterValue(htmlSiteJson.queryString)
    htmlSiteDescription.contentIsLocal = htmlSiteJson.contentIsLocal
    if htmlSiteDescription.contentIsLocal then
      htmlSiteDescription.prefix$ = htmlSiteJson.prefix
      htmlSiteDescription.filePath$ = htmlSiteJson.fileName
    else
      htmlSiteDescription.url = jsonParseParameterValue(htmlSiteJson.url)
    end if
    
    return htmlSiteDescription
    
  end function
  
  
  Function jsonParsePlaylistSpec(zoneDescription as object, playlistSpec as object)
    
    playlistDescription = { }
    
    playlistDescription.name = playlistSpec.name
    playlistDescription.type = playlistSpec.type
    playlistDescription.initialMediaStateName = playlistSpec.initialMediaStateName
    
    ' playlistSpec.states
    playlistDescription.stateDescriptions = []
    for each stateSpec in playlistSpec.states
      stateDescription = jsonParseState(stateSpec)
      playlistDescription.stateDescriptions.push(stateDescription)
    next
    
    ' playlistSpec.transitions
    playlistDescription.transitionDescriptions = []
    for each transitionSpec in playlistSpec.transitions
      transitionDescription = jsonParseTransition(transitionSpec)
      playlistDescription.transitionDescriptions.push(transitionDescription)
    next
    
    return playlistDescription
    
  end function
  
  
  Function jsonParseStreamItem(streamItemSpec as object)
    
    streamItemSpec.url = jsonParseParameterValue(streamItemSpec.url.params)
    return streamItemSpec
    
  end function
  
  
  Function jsonParseState(stateSpec as object)
    
    stateDescription = { }
    
    stateDescription.name = stateSpec.name
    if type(stateSpec.imageItem) = "roAssociativeArray" then
      stateDescription.imageItem = stateSpec.imageItem
      if type(stateSpec.imageItem.slideTransition) = "roString" then
        stateDescription.imageItem.slideTransition% = GetSlideTransitionValue(stateSpec.imageItem.slideTransition)
      else
        stateDescription.imageItem.slideTransition% = 0
      end if
      stateDescription.type = "image"
    else if type(stateSpec.videoItem) = "roAssociativeArray" then
      stateDescription.videoItem = stateSpec.videoItem
      stateDescription.type = "video"
    else if type(stateSpec.audioItem) = "roAssociativeArray" then
      stateDescription.audioItem = stateSpec.audioItem
      stateDescription.type = "audio"
    else if type(stateSpec.html5Item) = "roAssociativeArray" then
      stateDescription.html5Item = stateSpec.html5Item
      stateDescription.html5Item.name$ = stateSpec.name
      stateDescription.html5Item.htmlSiteName$ = stateSpec.html5Item.htmlSiteName
      stateDescription.type = "html5"
    else if type(stateSpec.rssDataFeedPlaylistItem) = "roAssociativeArray" then
      stateDescription.rssDataFeedPlaylistItem = jsonParseRSSItem(stateSpec.rssDataFeedPlaylistItem)
      stateDescription.type = "rss"
    else if type(stateSpec.liveVideoItem) = "roAssociativeArray" then
      stateDescription.liveVideoItem = stateSpec.liveVideoItem
      stateDescription.type = "liveVideo"
    else if type(stateSpec.videoStreamItem) = "roAssociativeArray" then
      stateDescription.videoStreamItem = jsonParseStreamItem(stateSpec.videoStreamItem)
      stateDescription.type = "videoStream"
    else if type(stateSpec.audioStreamItem) = "roAssociativeArray" then
      stateDescription.audioStreamItem = jsonParseStreamItem(stateSpec.audioStreamItem)
      stateDescription.type = "audioStream"
    else if type(stateSpec.mjpegStreamItem) = "roAssociativeArray" then
      stateDescription.mjpegStreamItem = jsonParseStreamItem(stateSpec.mjpegStreamItem)
      stateDescription.type = "mjpegStream"
    else if type(stateSpec.mrssDataFeedItem) = "roAssociativeArray" then
      stateDescription.mrssDataFeedPlaylistItem = stateSpec.mrssDataFeedItem
      stateDescription.mrssDataFeedPlaylistItem.slideTransition% = 0 ' TODO
      stateDescription.type = "mrssDataFeed"
    else if type(stateSpec.backgroundImageItem) = "roAssociativeArray" then
      stateDescription.backgroundImageItem = stateSpec.backgroundImageItem
      stateDescription.type = "backgroundImage"
    else if type(stateSpec.textItem) = "roAssociativeArray" then
      stateDescription.textItem = stateSpec.textItem
      stateDescription.type = "textItem"
    else if type(stateSpec.mediaListItem) = "roAssociativeArray" then
      stateDescription.mediaListItem = stateSpec.mediaListItem
      stateDescription.type = "mediaList"
    else if type(stateSpec.playFileItem) = "roAssociativeArray" then
      stateDescription.playFileItem = stateSpec.playFileItem
      stateDescription.type = "playFile"
    else if type(stateSpec.templatePlaylistItem) = "roAssociativeArray" then
      stateDescription.templatePlaylistItem = stateSpec.templatePlaylistItem
      stateDescription.type = "template"
    else if type(stateSpec.twitterItem) = "roAssociativeArray" then
      stateDescription.twitterItem = stateSpec.twitterItem
      stateDescription.type = "twitter"
    else if type(stateSpec.eventHandlerItem) = "roAssociativeArray" then
      stateDescription.eventHandlerItem = stateSpec.eventHandlerItem
      stateDescription.type = "eventHandler"
    else if type(stateSpec.superStateItem) = "roAssociativeArray" then
      stateDescription.superStateItem = stateSpec.superStateItem
      stateDescription.type = "superState"
    else if type(stateSpec.userVariableInTickerItem) = "roAssociativeArray" then
      stateDescription.userVariableInTickerItem = stateSpec.userVariableInTickerItem
      stateDescription.type = "userVariableInTickerItem"
    end if
    
    stateDescription.brightSignCmd = stateSpec.entryCommands
    stateDescription.brightSignExitCommands = stateSpec.exitCommands
    
    return stateDescription
    
  end function
  
  
  Function jsonParseRSSItem(playlistItemJson as object) as object
    
    rssPlaylistItemDescription = { }
    rssPlaylistItemDescription.liveDataFeedId$ = playlistItemJson.liveDataFeedId
    return rssPlaylistItemDescription
    
  end function
  
  
  Function getStringFromJsonParameter(parameter as object) as string
    if parameter = invalid then
      return ""
    end if
    return parameter
  end function
  
  
  Function jsonParseTransition(transitionSpec as object)
    
    transitionDescription = { }
    
    transitionDescription.sourceMediaState = getStringFromJsonParameter(transitionSpec.sourceMediaState)
    transitionDescription.targetMediaState = getStringFromJsonParameter(transitionSpec.targetMediaState)
    
    transitionDescription.commands = transitionSpec.commands
    
    transitionDescription.assignInputToUserVariable = transitionSpec.assignInputToUserVariable
    if transitionSpec.assignInputToUserVariable then
      transitionDescription.variableToAssignFromInput$ = transitionSpec.variableToAssignFromInput
    else
      transitionDescription.variableToAssignFromInput$ = ""
    end if
    
    transitionDescription.assignWildcardToUserVariable = transitionSpec.assignWildcardToUserVariable
    if transitionSpec.assignWildcardToUserVariable then
      transitionDescription.variableToAssign$ = transitionSpec.variableToAssignFromWildcard
    else
      transitionDescription.variableToAssign$ = ""
    end if
    
    targetAction = GetTargetActionFromEventAction(transitionSpec.eventAction)
    transitionDescription.targetMediaStateIsPreviousState = targetAction.targetMediaStateIsPreviousState
    transitionDescription.remainOnCurrentStateActions = targetAction.remainOnCurrentStateActions
    
    transitionDescription.conditionalTransitions = transitionSpec.conditionalTransitions
    
    transitionDescription.userEvent = transitionSpec.userEvent
    
    eventName = transitionSpec.userEvent.name
    eventData = transitionSpec.userEvent.data
    
    if eventName = "rectangularTouchEvent" then
      
      eventData.x = eventData.region.x
      eventData.y = eventData.region.y
      eventData.width = eventData.region.width
      eventData.height = eventData.region.height
      
    else if eventName = "bp900AUserEvent" or eventName = "bp900BUserEvent" or eventName = "bp900CUserEvent" or eventName = "bp200AUserEvent" or eventName = "bp200BUserEvent" or eventName = "bp200CUserEvent" then
      
      bpIndex$ = eventData.bpIndex
      if bpIndex$ = "a" then
        eventData.buttonPanelIndex% = 0
      else if bpIndex$ = "b" then
        eventData.buttonPanelIndex% = 1
      else if bpIndex$ = "c" then
        eventData.buttonPanelIndex% = 2
      else
        eventData.buttonPanelIndex% = 3
      end if
      
      eventData.buttonNumber$ = StripLeadingSpaces(stri(eventData.buttonNumber))
      
      if type(eventData.pressContinuous) = "roAssociativeArray" then
        eventData.configuration$ = "pressContinuous"
        eventData.initialHoldoff$ = StripLeadingSpaces(stri(eventData.pressContinuous.initialHoldoff))
        eventData.repeatInterval$ = StripLeadingSpaces(stri(eventData.pressContinuous.repeatInterval))
      else
        eventData.configuration$ = "press"
      end if
      
    else if eventName = "gpioUserEvent" then
      
      eventData.buttonDirection$ = lcase(eventData.buttonDirection)
      
      eventData.buttonNumber$ = StripLeadingSpaces(stri(eventData.buttonNumber))
      
      if type(eventData.pressContinuous) = "roAssociativeArray" then
        eventData.configuration$ = "pressContinuous"
        eventData.initialHoldoff$ = StripLeadingSpaces(stri(eventData.pressContinuous.initialHoldoff))
        eventData.repeatInterval$ = StripLeadingSpaces(stri(eventData.pressContinuous.repeatInterval))
      else
        eventData.configuration$ = "press"
      end if
      
    end if
    
    return transitionDescription
    
  end function
  
  
  Function jsonParseParameterValue(parameterValueJson as object) as object
    
    parameterValueDescription = { }
    
    parameterValueItems = []
    for each parameterValueItemSpec in parameterValueJson
      parameterValueItem = { }
      parameterValueItem.type = parameterValueItemSpec.type
      parameterValueItem.value = parameterValueItemSpec.value
      parameterValueItems.push(parameterValueItemSpec)
    next
    
    parameterValueDescription.parameterValueItems = parameterValueItems
    
    return parameterValueDescription
    
  end function
  
  
  Function newParameterValue(bsp as object, parameterValueDescription as object) as object
    
    parameterValue = { }
    parameterValue.GetCurrentParameterValue = GetCurrentParameterValue
    parameterValue.GetVariableName = GetVariableName
    
    parameterValue.parameterValueItems = []
    if type(parameterValueDescription) = "roAssociativeArray" then
      
      parameterValueItems = parameterValueDescription.parameterValueItems
      if parameterValueItems.count() = 0 then
        parameterValue.parameterValueItems.push(newParameterValueItemText(""))
      else
        for each parameterValueItem in parameterValueItems
          if lcase(parameterValueItem.type) = "text" then
            parameterValue.parameterValueItems.push(newParameterValueItemText(parameterValueItem.value))
          else if lcase(parameterValueItem.type) = "uservariable" then
            parameterValue.parameterValueItems.push(jsonNewParameterValueItemUserVariable(bsp, parameterValueItem))
          end if
        next
      end if
    end if
    
    return parameterValue
    
  end function
  
  
  Function jsonNewParameterValueItemUserVariable(bsp as object, parameterValueItemJsonUserVariable) as object
    
    parameterValueItem = { }
    parameterValueItem.GetCurrentValue = GetCurrentUserVariableParameterValue
    
    parameterValueItem.type$ = "userVariable"
    
    userVariableName$ = parameterValueItemJsonUserVariable.value
    
    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$)
    end if
    
    return parameterValueItem
    
  end function
  
  
  Function jsonGetTotalSpaceRequired(filesToCopy as object, deletionCandidates as object) as object
    
    filesToPublish$ = ReadAsciiFile("filesToPublish.json")
    if filesToPublish$ = "" then stop
    
    ' create the list of files that need to be copied. this is the list of files in filesToPublish that are not in listOfPoolFiles
    filesToPublish = ParseJson(filesToPublish$)
    
    ' determine total space required
    totalSpaceRequired! = 0
    
    for each file in filesToPublish.file
      fullFileName$ = file.fullFileName
      o = deletionCandidates.Lookup(fullFileName$)
      if not IsString(o) then ' file is not already on the card
      fileItem = { }
      fileItem.fileName$ = file.fileName
      fileItem.filePath$ = file.filePath
      fileItem.hashValue$ = file.hashValue
      fileItem.fileSize$ = file.fileSize
      
      filesToCopy.AddReplace(fullFileName$, fileItem) ' files that need to be copied to the card
      
      fileSize% = val(fileItem.fileSize$)
      totalSpaceRequired! = totalSpaceRequired! + fileSize%
    end if
  next
  
  filesToPublish = invalid
  
  return totalSpaceRequired!
  
  end function
  
  
  Sub jsonGetFilesToDelete(deletionCandidates as object, oldLocationDeletionCandidates as object)
    
    stop ' not implemented yet
    filesToPublish$ = ReadAsciiFile("filesToPublish.json")
    stop
  end sub
  
  Function jsonParseLiveDataFeed(liveDataFeedJson)
    
    liveDataFeedDescription = { }
    liveDataFeedDescription.id$ = liveDataFeedJson.id
    
    liveDataFeedDescription.isLiveBSNDataFeed = false
    liveDataFeedDescription.isLiveMediaFeed = false
    liveDataFeedDescription.isDynamicPlaylist = false
    
    if liveDataFeedJson.type = "BSNDataFeed" then
      liveDataFeedDescription.isLiveBSNDataFeed = true
    else if liveDataFeedJson.type = "BSNMediaFeed" then
      liveDataFeedDescription.isLiveMediaFeed = true
    else if liveDataFeedJson.type = "BSNDynamicPlaylist" then
      liveDataFeedDescription.isDynamicPlaylist = true
    else if liveDataFeedJson.type = "BSNTaggedPlaylist" then
      liveDataFeedDescription.isLiveMediaFeed = true
    end if
    
    
    if IsString(liveDataFeedJson.url) then
      liveDataFeedDescription.urlPV = newTextParameterValue(liveDataFeedJson.url)
    else
      ' BACONTODO - review carefully'
      liveDataFeedDescription.urlPV = jsonParseParameterValue(liveDataFeedJson.url)
      ''    liveDataFeedDescription.urlPV = jsonParseParameterValue(liveDataFeedJson.url.params)
      ' liveDataFeedDescription.urlPV = liveDataFeedJson.url    ' xml saves it as urlSpec$'
    end if
    
    liveDataFeedDescription.parserPluginName = liveDataFeedJson.parserPluginName
    liveDataFeedDescription.uvParserPluginName = ""
    
    liveDataFeedDescription.updateInterval% = liveDataFeedJson.updateInterval
    liveDataFeedDescription.useHeadRequest = liveDataFeedJson.useHeadRequest
    liveDataFeedDescription.usage$ = lcase(liveDataFeedJson.usage)
    liveDataFeedDescription.autoGenerateUserVariables = liveDataFeedJson.autoGenerateUserVariables
    liveDataFeedDescription.userVariableAccess$ = lcase(liveDataFeedJson.userVariableAccess)
    return liveDataFeedDescription
    
  end function
  
  
  Function syncSpecValueTrue(syncSpecValue$) as boolean
    if lcase(syncSpecValue$) = "yes" or syncSpecValue$ = "1" or lcase(syncSpecValue$) = "true" then
      return true
    end if
    return false
  end function
  
  Function syncSpecValueFalse(syncSpecValue$) as boolean
    if lcase(syncSpecValue$) = "no" or syncSpecValue$ = "0" or lcase(syncSpecValue$) = "false" then
      return true
    end if
    return false
  end function
  
  
  Function GetSynchronizerFilesToTransfer(userData as object, e as object)
    
    mVar = userData.mVar
    
    print "respond to GetSynchronizerFilesToTransfer request"
    
    MoveFile(e.GetRequestBodyFile(), "filesInSite.json")
    
    filesToTransfer = GetDifferentOrMissingFiles()
    jsonStr$ = FormatJson(filesToTransfer)
    e.SetResponseBodyString(jsonStr$)
    e.SendResponse(200)
    
  end function
  
  
  Sub SynchronizerFilePosted(userData as object, e as object)
    
    mVar = userData.mVar
    
    destinationFilename = e.GetRequestHeader("Destination-Filename")
    print "File posted to ";destinationFileName
    ok = MoveFile(e.GetRequestBodyFile(), destinationFilename)
    if not ok then
      regex = CreateObject("roRegEx", "/", "i")
      parts = regex.Split(destinationFilename)
      if parts.Count() > 1 then
        dirName$ = ""
        for i% = 0 to (parts.Count() - 2)
          dirName$ = dirName$ + parts[i%] + "\"
          
          ' check to see if directory already exits
          dir = CreateObject("roReadFile", dirName$)
          if type(dir) <> "roReadFile" then
            ok = CreateDirectory(dirName$)
            if not ok then
              stop
            end if
          end if
        next
        
        ' directories have been created - try again
        ok = MoveFile(e.GetRequestBodyFile(), destinationFilename)
        if ok then
          print "Move successful after directory creation"
        end if
        
      end if
    end if
    
    if not ok then
      stop
    end if
    
    e.SetResponseBodyString("RECEIVED")
    e.SendResponse(200)
    
  end sub
  
  
  Function GetDifferentOrMissingFiles() as object
    
    filesToTransfer = []
    
    filesInSite$ = ReadAsciiFile("filesInSite.json")
    if filesInSite$ = "" then stop
    
    filesInSite = ParseJson(filesInSite$)
    
    for each fileInSiteO in filesInSite.file
      
      fileInSite = { }
      fileInSite.name = fileInSiteO.fileName
      fileInSite.relativePath = fileInSiteO.filePath
      fileInSite.sha1 = fileInSiteO.hashValue
      fileInSite.size = fileInSiteO.fileSize
      
      fileOnCardIdentical = false
      
      file = CreateObject("roReadFile", fileInSite.relativePath)
      if type(file) = "roReadFile" then
        
        file.SeekToEnd()
        size% = file.CurrentPosition()
        
        if size% > 0 then
          
          ' file exists on card and is non zero size - see if it is the same file
          if size% = fileInSite.size then
            
            ' size is identical, check sha1
            sha1 = GetSHA1(fileInSite.relativePath)
            
            if lcase(sha1) = lcase(fileInSite.sha1) then
              ' sha1 is identical - files are the same
              fileOnCardIdentical = true
            end if
            
          end if
        end if
      end if
      
      ' if the file is not on the card or is different, add it to the list of files that need to be downloaded
      if not fileOnCardIdentical then
        filesToTransfer.push(fileInSite)
      end if
      
    next
    
    return filesToTransfer
    
  end function
  
  
  Function GetSHA1(path as string) as string
    
    ba = CreateObject("roByteArray")
    ok = ba.ReadFile(path)
    hashGen = CreateObject("roHashGenerator", "SHA1")
    return hashGen.hash(ba).ToHexString()
    
  end function
  
  
  Sub RestartScript(userData as object, e as object)
    RestartScript()
  end sub
  'endregion
  
  
  'region MediaList
  REM *******************************************************
  REM *******************************************************
  REM ***************                    ********************
  REM *************** MediaList          ********************
  REM ***************                    ********************
  REM *******************************************************
  REM *******************************************************
  REM
  REM
  
  Sub newMediaListPlaylistItem(bsp as object, playlistItemDescription as object, zoneHSM as object, state as object, playlistItemBS as object)
  
    if playlistItemDescription.playbackType = "FromBeginning" then
      state.playFromBeginning = true
    else
      state.playFromBeginning = false
    end if
    
    state.shuffle = playlistItemDescription.shuffle
    
    if type(playlistItemDescription.inactivityTimeout) = "Boolean" then
      state.inactivityTimeout = playlistItemDescription.inactivityTimeout
    else
      state.inactivityTimeout = false
    endif
    state.inactivityTime = playlistItemDescription.inactivityTime
  
    state.slideTransition% = GetSlideTransitionValue(playlistItemDescription.transitionEffect.transitionType)
    state.transitionDuration% = playlistItemDescription.transitionEffect.transitionDuration
    
    state.sendZoneMessage = playlistItemDescription.sendZoneMessage
    
    if type(playlistItemDescription.startIndex) = "Integer" and playlistItemDescription.startIndex > 0 then
      state.specifiedStartIndex% = playlistItemDescription.startIndex - 1
    else
      state.specifiedStartIndex% = 0
    end if
    state.startIndex% = state.specifiedStartIndex%
    
    state.populateFromMediaLibrary = not playlistItemDescription.useDataFeed
    
    liveDataFeedId$ = playlistItemDescription.dataFeedId
    if liveDataFeedId$ <> "" then
      state.liveDataFeed = bsp.liveDataFeeds.Lookup(CleanName(liveDataFeedId$))
    else
      state.liveDataFeed = invalid
    end if
    
    ' parse BrightSignCmdsTransitionNextItem
    state.transitionNextItemCmds = []
    transitionNextItemCmds = playlistItemDescription.transitionToNextCommands
    if transitionNextItemCmds.Count() > 0 then
      for each cmd in transitionNextItemCmds
        newCmd(bsp, cmd, state.transitionNextItemCmds)
      next
    end if
    
    ' parse BrightSignCmdsTransitionPreviousItem
    state.transitionPreviousItemCmds = []
    transitionPreviousItemCmds = playlistItemDescription.transitionToPreviousCommands
    if transitionPreviousItemCmds.Count() > 0 then
      for each cmd in transitionPreviousItemCmds
        newCmd(bsp, cmd, state.transitionPreviousItemCmds)
      next
    end if
    
    
    ' Note - this is different from BA Classic. If the zone types supports images, then increment variable
    if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
      zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
    end if
    
    state.transitionToNextEventList = []
    for each transitionToNextEvent in playlistItemDescription.transitionToNextEventList
      
      eventName = transitionToNextEvent.name
      eventData = transitionToNextEvent.data
      
      transitionToNextEvent = { }
      transitionToNextEvent.eventName = eventName
      transitionToNextEvent.eventData = eventData
  
      state.transitionToNextEventList.push(transitionToNextEvent)
  
    next
    
    state.transitionToPreviousEventList = []
    for each transitionToPreviousEvent in playlistItemDescription.transitionToPreviousEventList
      
      eventName = transitionToPreviousEvent.name
      eventData = transitionToPreviousEvent.data
      
      transitionToPreviousEvent = { }
      transitionToPreviousEvent.eventName = eventName
      transitionToPreviousEvent.eventData = eventData
      
      state.transitionToPreviousEventList.push(transitionToPreviousEvent)
    next
    
    ConfigureMediaListStateIO(bsp, playlistItemDescription.transitionToNextEventList)
    ConfigureMediaListStateIO(bsp, playlistItemDescription.transitionToPreviousEventList)
    
    if state.populateFromMediaLibrary then
      
      state.numItems% = playlistItemDescription.contentItems.count()
      state.items = CreateObject("roArray", state.numItems%, true)
      
      for each itemDescription in playlistItemDescription.contentItems
        
        item = { }
        
        item.type = itemDescription.type
        if item.type = "video" then
          newVideoPlaylistItem(bsp, itemDescription, state, item)
        else if item.type = "image" then
          newMediaPlaylistItem(bsp, itemDescription, state, item)
          item.slideTransition% = state.slideTransition%
          item.transitionDuration% = state.transitionDuration%
        else if item.type = "audio" or item.type = "audioItem" then
          newAudioPlaylistItem(bsp, itemDescription, state, item)
        end if
        
        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
      
    end if
    
    state.playbackActive = false
    state.playbackIndex% = state.startIndex%
    
    state.HStateEventHandler = STDisplayingMediaListItemEventHandler
    state.PopulateMediaListFromLiveDataFeed = PopulateMediaListFromLiveDataFeed
    state.ShuffleMediaListContent = ShuffleMediaListContent
    state.ConfigureIntraStateEventHandlersButton = ConfigureIntraStateEventHandlersButton
    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.GetMatchingNavigationEvent = GetMatchingNavigationEvent
    state.HandleIntraStateEvent = HandleIntraStateEvent
    state.LaunchMediaListPlaybackItem = LaunchMediaListPlaybackItem
    state.GetTimeoutEvent = GetTimeoutEvent
    state.AdvanceMediaListPlayback = AdvanceMediaListPlayback
    state.RetreatMediaListPlayback = RetreatMediaListPlayback
    
    state.AddAudioTimeCodeEvent = AddAudioTimeCodeEvent
    state.SetAudioTimeCodeEvents = SetAudioTimeCodeEvents
    
    state.AddVideoTimeCodeEvent = AddVideoTimeCodeEvent
    state.SetVideoTimeCodeEvents = SetVideoTimeCodeEvents
    
    SetMediaItemEventHandlers(state)
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    state.LaunchTimer = LaunchTimer
    state.PreloadItem = PreloadItem
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
    
    state.AtEndOfMediaList = AtEndOfMediaList
    
  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 data feed content has been loaded
            if type(m.liveDataFeed.assetPoolFiles) = "roAssetPoolFiles" then
              m.PopulateMediaListFromLiveDataFeed()
            end if
          end if
          
          if m.inactivityTimeout then
            m.inactivityTimer = CreateObject("roTimer")
            m.inactivityTimer.SetPort(m.bsp.msgPort)
          endif
  
          m.ConfigureIntraStateEventHandlersButton(m.transitionToNextEventList)
          m.ConfigureIntraStateEventHandlersButton(m.transitionToPreviousEventList)
          
          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
          end if
          
          ' reset playback index if appropriate
          if m.playFromBeginning then
            m.playbackIndex% = m.startIndex%
          end if
        
          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%
              end if
              m.playbackIndex% = m.startIndex%
            end if
            
            ' reshuffle media list if appropriate
            if m.playbackIndex% = m.startIndex% and m.shuffle then
              m.ShuffleMediaListContent()
            end if
            
            m.AdvanceMediaListPlayback(true, false)
            
          else
            
            m.playbackActive = false
            
          end if
          
          return "HANDLED"
        
        else if event["EventType"] = "VideoPlaybackFailureEvent" then
          
          if m.bsp.ProcessMediaEndEvent() then
            return "HANDLED"
          end if
          if type(m.videoEndEvent) = "roAssociativeArray" then
            return m.ExecuteTransition(m.videoEndEvent, stateData, "")
          end if
          PostMediaEndEvent(m.bsp.msgPort)
          
        else if event["EventType"] = "AudioPlaybackFailureEvent" then
          
          if m.bsp.ProcessMediaEndEvent() then
            return "HANDLED"
          end if
          if type(m.audioEndEvent) = "roAssociativeArray" then
            return m.ExecuteTransition(m.audioEndEvent, stateData, "")
          end if
        
        else if event["EventType"] = "CONTENT_DATA_FEED_LOADED" then
          
          if type(m.liveDataFeed) = "roAssociativeArray" and event["Name"] = m.liveDataFeed.id$ 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%
            end if
            m.playbackIndex% = m.startIndex%
            
            if m.numItems% > 0 then
              if m.shuffle then
                m.ShuffleMediaListContent()
              end if
              
              if not m.playbackActive then
                m.playbackActive = true
                m.AdvanceMediaListPlayback(true, false)
              end if
            end if
            
            return "HANDLED"
            
          end if
        
        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"
          
        end if
      
      end if
    
    ' 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 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"
        end if
  
        if m.transitionToNextEventList.count() > 0 then
          for each transitionToNextEvent in m.transitionToNextEventList
            if transitionToNextEvent.eventName = "mediaEnd" then    ' equivalent to m.advanceOnMediaEnd in BrightAuthor
              if not(m.playbackIndex% = m.startIndex% and type(m.videoEndEvent) = "roAssociativeArray") then
                m.AdvanceMediaListPlayback(true, true)
                return "HANDLED"
              endif
            endif
          next
        endif
  
        if type(m.videoEndEvent) = "roAssociativeArray" then
          return m.ExecuteTransition(m.videoEndEvent, stateData, "")
        end if
        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"
          end if
        end if
        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "0")
      end if
  
    else if 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
          end if
          
          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)
              end if
            end if
            
            ' send ZoneMessage using the file name as the message
            zoneMessageCmd = { }
            zoneMessageCmd["EventType"] = "SEND_ZONE_MESSAGE"
            zoneMessageCmd["EventParameter"] = fileNameWithoutExtension$
            m.bsp.msgPort.PostMessage(zoneMessageCmd)
            
          end if
          
          
          if not(m.playbackIndex% = m.startIndex% and type(m.audioEndEvent) = "roAssociativeArray") then
            m.PlayMixerAudio(false, m.playbackIndex%, false)
          end if
          
          ' 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"
        end if
        
      else if event.GetInt() = MEDIA_END then
      
        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
        if m.bsp.ProcessMediaEndEvent() then
          return "HANDLED"
        end if
        if type(m.audioEndEvent) = "roAssociativeArray" then
          return m.ExecuteTransition(m.audioEndEvent, stateData, "")
        end if
        PostMediaEndEvent(m.bsp.msgPort)
        
      end if
      
    else if 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"
        end if
        
        if m.transitionToNextEventList.count() > 0 then
          for each transitionToNextEvent in m.transitionToNextEventList
            if transitionToNextEvent.eventName = "mediaEnd" then    ' equivalent to m.advanceOnMediaEnd in BrightAuthor
              if not(m.playbackIndex% = m.startIndex% and type(m.videoEndEvent) = "roAssociativeArray") then
                m.AdvanceMediaListPlayback(true, true)
                return "HANDLED"
              endif
            endif
          next
        endif
  
        if type(m.audioEndEvent) = "roAssociativeArray" then
          return m.ExecuteTransition(m.audioEndEvent, stateData, "")
        end if
        PostMediaEndEvent(m.bsp.msgPort)
      end if
      
    end if
  
    ' event received
    if m.transitionToNextEventList.count() > 0 then
      advance = m.HandleIntraStateEvent(event, m.transitionToNextEventList)
      if advance then
        m.AdvanceMediaListPlayback(true, true)
        return "HANDLED"
      end if
    end if
  
    if m.transitionToPreviousEventList.count() > 0 then
      retreat = m.HandleIntraStateEvent(event, m.transitionToPreviousEventList)
      if retreat then
        m.RetreatMediaListPlayback(true, true)
        return "HANDLED"
      end if
    end if
  
    return m.MediaItemEventHandler(event, stateData)
  
  end function
  
  
  Sub ConfigureMediaListStateIO(bsp as object, eventList as object)
    
    for each event in eventList
      
      if event.name = "bp900AUserEvent" or event.name = "bp900BUserEvent" or event.name = "bp900CUserEvent" or event.name = "bp900DUserEvent" or event.name = "bp200AUserEvent" or event.name = "bp200BUserEvent" or event.name = "bp200CUserEvent" or event.name = "bp200DUserEvent" then
        bpUserEventButtonNumber$ = stri(event.data.buttonNumber)
        bpUserEventButtonPanelIndex$ = GetButtonPanelIndexFromBpIndex(event.data.bpIndex)
        bpIndex% = int(val(bpUserEventButtonPanelIndex$))
        bsp.ConfigureBPInput(bpIndex%, bpUserEventButtonNumber$)
      else if event.name = "gpioUserEvent" then
        bsp.ConfigureGpioInput(stri(event.data.buttonNumber))
      else if event.name = "serial" then
        bsp.CreateSerial(bsp, event.data.port, false)
      end if
      
    next
    
  end sub
  
  
  Sub PopulateMediaListFromLiveDataFeed()
    
    if type(m.liveDataFeed) = "roAssociativeArray" and 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 type(m.liveDataFeed.fileTypes) = "roArray" and m.liveDataFeed.fileTypes.Count() > i% then
          item.type = m.liveDataFeed.fileTypes[i%]
        end if
        
        if item.type = "image" then
          item.slideTransition% = 0
        else if item.type = "video" then
          item.videoDisplayMode% = 0
        end if
        
        m.items.push(item)
        
      next
      
      m.playbackIndices = CreateObject("roArray", m.numItems%, true)
      for i% = 0 to m.numItems% - 1
        m.playbackIndices[i%] = i%
      next
      
    end if
    
  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%
        end if
      next
    next
    
  end sub
  
  
  Function AtEndOfMediaList(event as object) as boolean
    
    MEDIA_END = 8
    endOfMediaEvent = false
    
    if type(event) = "roVideoEvent" and type(m.stateMachine.videoPlayer) = "roVideoPlayer" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() and event.GetInt() = MEDIA_END then
      endOfMediaEvent = true
    else if m.stateMachine.type$ = "EnhancedAudio" and type(event) = "roAudioEventMx" and event.GetInt() = MEDIA_END then
      endOfMediaEvent = true
    else if m.stateMachine.type$ <> "EnhancedAudio" and IsAudioEvent(m.stateMachine, event) and event.GetInt() = MEDIA_END then
      endOfMediaEvent = true
    else if type(event) = "roTimerEvent" then
  
      ' if this is a timer event for some random timer, it's not necessarily the end of the media list
      ' fix for the case of a serial port retry timer - Bose issue.
      sourceIdentity$ = stri(event.getSourceIdentity())
      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
            return false
          endif
        next
      endif
  
      if (m.playbackIndex% <> m.startIndex% or type(m.mstimeoutEvent) <> "roAssociativeArray") then
        endOfMediaEvent = true
      end if
  
    endif
    
    if endOfMediaEvent and m.playbackIndex% = 0 then
      return true
    else
      return false
    end if
    
  end function
  
  'endregion
  
  'region LocalFileNetworking
  
  Sub PopulateDeviceConfigurationJson(mVar as object) as object
    out = { }
    
    modelObject = CreateObject("roDeviceInfo")
    out.AddReplace("model", modelObject.GetModel())
    out.AddReplace("family", modelObject.GetFamily())
    out.AddReplace("activePresentation", mVar.activePresentation$) ' TODO check with Ted as active presentation label attribute removed
    out.AddReplace("autorunVersion", mVar.sysInfo.autorunVersion$)
    out.AddReplace("firmwareVersion", mVar.sysInfo.deviceFWVersion$)
    out.AddReplace("functionality", mVar.lwsConfig$)
    out.AddReplace("serialNumber", mVar.sysInfo.deviceUniqueID$)
    out.AddReplace("snapshotId", invalid)
    out.AddReplace("snapshotOrientation", invalid)
    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)
          out.AddReplace("snapshotId", id$) ' TODO check with Ted as snapshot id label attribute removed
          
          if IsNonEmptyString(mVar.globalAA.remoteSnapshotOrientation) then
            out.AddReplace("snapshotOrientation", mVar.globalAA.remoteSnapshotOrientation)
            ' snapshotDisplayPortrait changed to snapshotOrientation for BCN-6961
            ' TODO check with Ted as snapshot display portrait label attribute removed
            ' TODO check with Ted as value changed from string "true" to boolean true
          else
            out.AddReplace("snapshotOrientation", "Landscape")
            ' snapshotDisplayPortrait changed to snapshotOrientation for BCN-6961
            ' TODO check with Ted as snapshot display portrait label attribute removed
            ' TODO check with Ted as value changed from string "false" to boolean false
          end if
        end if
      end if
    end if
    
    out.AddReplace("unitName", mVar.registrySettings.unitName$)
    out.AddReplace("unitNamingMethod", mVar.registrySettings.unitNamingMethod$)
    out.AddReplace("unitDescription", mVar.registrySettings.unitDescription$)
    out.AddReplace("udpNotificationAddress", mVar.udpNotificationAddress$)
    out.AddReplace("udpNotifcationPort", StripLeadingSpaces(stri(mVar.udpNotificationPort%)))
    out.AddReplace("contentPort", "8008")
    
    if mVar.NetworkingIsActive() then
      out.AddReplace("bsnActive", true) ' TODO check with Ted for BA compatibility with boolean value
    else
      out.AddReplace("bsnActive", false) ' TODO check with Ted for BA compatibility with boolean value
    end if
    
    
    return out
  end sub
  
  Sub GetDeviceConfigurationJson(userData as object, e as object)
    
    mVar = userData.mVar
    
    respBody = PopulateDeviceConfigurationJson(mVar)
    
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(respBody))
    e.SendResponse(200)
    
  end sub
  
  
  Sub PopulateDeviceStatusJson(mVar as object) as object
    out = { }
    modelObject = CreateObject("roDeviceInfo")
    out.AddReplace("uptime", modelObject.GetDeviceUptime()) ' TODO check with Ted as device update label attribute removed
    return out
  end sub
  
  Sub GetDeviceStatusJson(userData as object, e as object)
    
    mVar = userData.mVar
    
    respBody = PopulateDeviceStatusJson(mVar)
    
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(respBody))
    e.SendResponse(200)
    
  end sub
  
  Sub PopulateSnapshotConfigurationJson(mVar as object) as object
    
    globalAA = GetGlobalAA()
    out = { }
    
    out.AddReplace("count", StripLeadingSpaces(stri(globalAA.listOfSnapshotFiles.Count()))) ' TODO check with Ted change count from attribute to value
    ' TODO check with Ted change Count > count
    
    if globalAA.enableRemoteSnapshot <> invalid then
      if globalAA.enableRemoteSnapshot then
        out.AddReplace("enabled", true) ' TODO check with Ted change enabled from attribute to value
        ' TODO check with Ted change Enabled > enabled
        ' TODO check with Ted change enabled value from boolean string to boolean
      else
        out.AddReplace("enabled", false) ' TODO check with Ted change enabled from attribute to value
        ' TODO check with Ted change Enabled > enabled
        ' TODO check with Ted change enabled value from boolean string to boolean
      end if
    end if
    if globalAA.remoteSnapshotInterval <> invalid then
      out.AddReplace("interval", StripLeadingSpaces(stri(globalAA.remoteSnapshotInterval))) ' TODO check with Ted change internval from attribute to value
      ' TODO check with Ted change Interval > interval
    else
      out.AddReplace("interval", invalid)
    end if
    if globalAA.remoteSnapshotMaxImages <> invalid then
      out.AddReplace("maxImages", StripLeadingSpaces(stri(globalAA.remoteSnapshotMaxImages))) ' TODO check with Ted change MaxImages > maxImages                                                                                             ' TODO check with Ted change maxImages from attribute to value
    else
      out.AddReplace("maxImages", invalid)
    end if
    if globalAA.remoteSnapshotJpegQualityLevel <> invalid then
      out.AddReplace("quality", StripLeadingSpaces(stri(globalAA.remoteSnapshotJpegQualityLevel))) ' TODO check with Ted change Quality > quality
    else
      out.AddReplace("quality", invalid)
    end if
    if mVar.videoMode <> invalid then
      out.AddReplace("resX", StripLeadingSpaces(stri(mVar.videoMode.GetOutputResX()))) ' TODO check with Ted change resX from attribute to value
      ' TODO check with Ted change ResX > resX
      out.AddReplace("resY", StripLeadingSpaces(stri(mVar.videoMode.GetOutputResY()))) ' TODO check with Ted change resY from attribute to value
      ' TODO check with Ted change ResY > resY
    else
      out.AddReplace("resX", invalid)
      out.AddReplace("resY", invalid)
      
    end if
    
    if IsNonEmptyString(globalAA.remoteSnapshotOrientation) then
      out.AddReplace("orientation", globalAA.remoteSnapshotOrientation) ' displayPortraitMode changed to orientation for BCN-6961
      ' TODO check with Ted change displayPortraitMode from attribute to value
      ' TODO check with Ted change DisplayPortraitMode > displayPortraitMode
      ' TODO check with Ted change displayPortraitMode value from boolean string to boolean
    else
      out.AddReplace("orientation", "Landscape") ' displayPortraitMode changed to orientation for BCN-6961
      ' TODO check with Ted change displayPortraitMode from attribute to value
      ' TODO check with Ted change DisplayPortraitMode > displayPortraitMode
      ' TODO check with Ted change displayPortraitMode value from boolean string to boolean
    end if
    
    ' CanConfigure attribute notifies clients if snapshot configuration can be managed by this unit
    out.AddReplace("canConfigure", true) ' TODO check with Ted change canConfigure from attribute to value
    ' TODO check with Ted change CanConfigure > canConfigure
    ' TODO check with Ted change canConfigure value from boolean string to boolean
    
    return out
  end sub
  
  Function GetSnapshotConfigurationJson(userData as object, e as object)
    
    mVar = userData.mVar
    globalAA = GetGlobalAA()
    
    startTimeSpecified = false
    startDT = CreateObject("roDateTime")
    startTime$ = e.GetRequestParam("startTime") ' TODO check with Ted change request param starttime > startTime
    
    if startTime$ <> "" then
      startTimeSpecified = startDT.FromIsoString(startTime$ + ".000")
    end if
    
    respBody = PopulateSnapshotConfigurationJson(mVar)
    
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(respBody))
    e.SendResponse(200)
    
  end function
  
  Function PostSnapshotConfigurationJson(userData as object, e as object)
    
    mVar = userData.mVar
    
    ' only set the override timer if appropriate (networking configured / active)
    overrideModeActive = false
    
    args = e.GetFormData()
    if args.enableRemoteSnapshot = invalid or args.remoteSnapshotInterval = invalid or args.remoteSnapshotMaxImages = invalid or args.remoteSnapshotJpegQualityLevel = invalid or args.remoteSnapshotOrientation = invalid then
      
      e.SendResponse(400) ' TODO check with Ted, add fallback for invalid request params instead of player crash
      
    else
      
      if mVar.NetworkingIsActive() then
        
        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
        end if
      end if
      
      mVar.diagnostics.PrintDebug("### SetSnapshotConfiguration")
      mVar.logging.WriteDiagnosticLogEntry(mVar.diagnosticCodes.EVENT_SET_SNAPSHOT_CONFIGURATION, "")
      
      ' 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
        if not e.SendResponse(403) then stop
        return 0
      end if
      
      globalAA = GetGlobalAA()
      
      ' remember current settings
      currentEnableRemoteSnapshot = globalAA.enableRemoteSnapshot
      currentRemoteSnapshotInterval = globalAA.remoteSnapshotInterval
      currentRemoteSnapshotMaxImages = globalAA.remoteSnapshotMaxImages
      currentRemoteSnapshotJpegQualityLevel = globalAA.remoteSnapshotJpegQualityLevel
      currentRemoteSnapshotOrientation = globalAA.remoteSnapshotOrientation
      
      ' set global values from form data
      args = e.GetFormData()
      if args.enableRemoteSnapshot <> invalid then
        globalAA.enableRemoteSnapshot = GetBoolFromString(args.enableRemoteSnapshot, false)
      else
        globalAA.enableRemoteSnapshot = false ' TODO check with Ted, added default to prevent player crash when invalid params provided
      end if
      if args.remoteSnapshotInterval <> invalid then
        globalAA.remoteSnapshotInterval = int(val(args.remoteSnapshotInterval))
      else
        globalAA.remoteSnapshotInterval = 15 * 60 ' TODO check with Ted, added default to prevent player crash when invalid params provided
      end if
      if args.remoteSnapshotMaxImages <> invalid then
        globalAA.remoteSnapshotMaxImages = int(val(args.remoteSnapshotMaxImages))
      else
        globalAA.remoteSnapshotMaxImages = 5 ' TODO check with Ted, added default to prevent player crash when invalid params provided
      end if
      if args.remoteSnapshotJpegQualityLevel <> invalid then
        globalAA.remoteSnapshotJpegQualityLevel = int(val(args.remoteSnapshotJpegQualityLevel))
      else
        globalAA.remoteSnapshotJpegQualityLevel = 50 ' TODO check with Ted, added default to prevent player crash when invalid params provided
      end if
      ' remoteSnapshotDisplayPortrait changed to remoteSnapshotOrientation for BCN-6961
      if IsNonEmptyString(args.remoteSnapshotOrientation) then
        globalAA.remoteSnapshotOrientation = args.remoteSnapshotOrientation
      else
        globalAA.remoteSnapshotOrientation = "Landscape" ' TODO check with Ted, added default to prevent player crash when invalid params provided
      end if
      
      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("remoteSnapshotOrientation", globalAA.remoteSnapshotOrientation)
      
    end if
    
    if globalAA.enableRemoteSnapshot then
      
      DeleteExcessSnapshots(globalAA)
      
      if not currentEnableRemoteSnapshot or globalAA.remoteSnapshotInterval <> currentRemoteSnapshotInterval then
        mVar.InitiateRemoteSnapshotTimer()
      end if
      
    else if currentEnableRemoteSnapshot then
      
      mVar.RemoveRemoteSnapshotTimer()
      
    end if
    
    e.SendResponse(200)
    
  end if
  
  end function
  
  Sub PopulateSnapshotHistoryJson(mVar as object, startTimeSpecified as boolean, startTime as object) as object
    globalAA = GetGlobalAA()
    out = { }
    
    out.AddReplace("snapshots", createObject("roArray", 0, true)) ' TODO check with Ted change BrightSignSnapshots > snapshots
    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 = { } 'item
          itemElem.AddReplace("id", time$)
          itemElem.AddReplace("time", time$)
          out.brightSignSnapshots.push(itemElem)
          
        end if
        
      end if
    next
    return out
  end sub
  
  Function GetSnapshotHistoryJson(userData as object, e as object)
    
    mVar = userData.mVar
    globalAA = GetGlobalAA()
    
    startTimeSpecified = false
    startDT = CreateObject("roDateTime")
    startTime$ = e.GetRequestParam("startTime") ' TODO check with Ted change request param starttime > startTime
    
    if startTime$ <> "" then
      startTimeSpecified = startDT.FromIsoString(startTime$ + ".000")
    end if
    
    respBody = PopulateSnapshotHistoryJson(mVar, startTimeSpecified, startDT)
    
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(respBody))
    e.SendResponse(200)
    
  end function
  
  Function GetSnapshotJson(userData as object, e as object)
    
    mVar = userData.mVar
    
    globalAA = GetGlobalAA()
    listOfSnapshotFiles = globalAA.listOfSnapshotFiles
    
    snapshotID$ = e.GetRequestParam("id") ' TODO check with Ted ID > 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
        end if
      next
    end if
    
    e.AddResponseHeader("Content-type", "text/plain") ' TODO check with Ted remove utf encoding
    
    if snapshotID$ <> "" then
      e.SetResponseBodyString("Snapshot file corresponding to id " + snapshotID$ + " not found")
    else
      e.SetResponseBodyString("Snapshot id not specified.")
    end if
    
    e.SendResponse(404)
    
  end function
  
  Sub PopulateCardSizeLimitsJson(mVar as object) as object
    
    out = { }
    
    out.AddReplace("limitStorageSpace", mVar.limitStorageSpace)
    
    if mVar.spaceLimitedByAbsoluteSize = "true" then
      out.AddReplace("limitStorageType", "Absolute") ' TODO synch with bauwdm types
    else if mVar.spaceLimitedByAbsoluteSize = "false" then
      out.AddReplace("limitStorageType", "Percent") ' TODO synch with bauwdm types
    else
      out.AddReplace("limitStorageType", invalid)
    end if
    out.AddReplace("publishDataAbsolute", mVar.publishedDataSizeLimitMB)
    out.AddReplace("publishDataPercent", mVar.publishedDataSizeLimitPercentage)
    out.AddReplace("dynamicDataAbsolute", mVar.dynamicDataSizeLimitMB)
    out.AddReplace("dynamicDataPercent", mVar.dynamicDataSizeLimitPercentage)
    out.AddReplace("htmlDataAbsolute", mVar.htmlDataSizeLimitMB)
    out.AddReplace("htmlDataPercent", mVar.htmlDataSizeLimitPercentage)
    out.AddReplace("htmlLocalStorageAbsolute", mVar.htmlLocalStorageSizeLimitMB)
    out.AddReplace("htmlLocalStoragePercent", mVar.htmlLocalStorageSizeLimitPercentage)
    out.AddReplace("htmlIndexedDbAbsolute", mVar.htmlIndexedDBSizeLimitMB)
    out.AddReplace("htmlIndexedDbPercent", mVar.htmlIndexedDBSizeLimitPercentage)
    
    return out
    
  end sub
  
  Sub GetCardSizeLimitsJson(userData as object, e as object)
    print "respond to GetCardSizeLimitsJson request"
    
    mVar = userData.mVar
    
    respBody = PopulateCardSizeLimitsJson(mVar)
    
    e.SetResponseBodyString(FormatJson(respBody))
    e.SendResponse(200)
    
  end sub
  
  Sub PostCardSizeLimitsJson(userData as object, e as object)
    ' TODO check with Ted changed to post with post data passed as body
  
    mVar = userData.mVar
    
    args = e.GetFormData()
    
    if args.limitStorageEnabled <> invalid and lcase(args.limitStorageEnabled) = "true" then
      if args.limitStorageType <> invalid and lcase(args.limitStorageType) = "absolute" then
        if args.publishDataAbsolute <> invalid and args.dynamicDataAbsolute <> invalid and args.htmlDataAbsolute <> invalid and args.htmlLocalStorageAbsolute <> invalid and args.htmlIndexedDbAbsolute <> invalid then
          mVar.publishedDataSizeLimitMB = ConvertToInt(args.publishDataAbsolute) ' TODO check with Ted publishedDataSizeLimitMB > publishDataAbsolute
          mVar.dynamicDataSizeLimitMB = ConvertToInt(args.dynamicDataAbsolute) ' TODO check with Ted dynamicDataSizeLimitMB > dynamicDataAbsolute
          mVar.htmlDataSizeLimitMB = ConvertToInt(args.htmlDataAbsolute) ' TODO check with Ted htmlDataSizeLimitMB > htmlDataAbsolute
          mVar.htmlLocalStorageSizeLimitMB = ConvertToInt(args.htmlLocalStorageAbsolute) ' TODO check with Ted htmlLocalStorageSizeLimitMB > htmlLocalStorageAbsolute
          mVar.htmlIndexedDBSizeLimitMB = ConvertToInt(args.htmlDataAbsolute) ' TODO check with Ted htmlIndexedDBSizeLimitMB > htmlIndexedDbAbsolute
        else
          e.SendResponse(400) ' TODO check with Ted, add fallback for invalid request params instead of player crash
          return
        end if
        mVar.spaceLimitedByAbsoluteSize = "true" ' TODO check with Ted changed type from sting to boolean
      else if args.limitStorageType <> invalid and lcase(args.limitStorageType) = "percent" then
        if args.publishDataPercent <> invalid and args.dynamicDataPercent <> invalid and args.htmlDataPercent <> invalid and args.htmlLocalStoragePercent <> invalid and args.htmlIndexedDbPercent <> invalid then
          mVar.publishedDataSizeLimitPercentage = ConvertToInt(args.publishDataPercent) ' TODO check with Ted publishedDataSizeLimitPercentage > publishDataPercent
          mVar.dynamicDataSizeLimitPercentage = ConvertToInt(args.dynamicDataPercent) ' TODO check with Ted dynamicDataSizeLimitPercentage > dynamicDataPercent
          mVar.htmlDataSizeLimitPercentage = ConvertToInt(args.htmlDataPercent) ' TODO check with Ted htmlDataSizeLimitPercentage > htmlDataAbsolute
          mVar.htmlLocalStorageSizeLimitPercentage = ConvertToInt(args.htmlLocalStoragePercent) ' TODO check with Ted htmlLocalStorageSizeLimitPercentage > htmlLocalStorageAbsolute
          mVar.htmlIndexedDBSizeLimitPercentage = ConvertToInt(args.htmlIndexedDbPercent) ' TODO check with Ted htmlIndexedDBSizeLimitPercentage > htmlIndexedDbAbsolute
        else
          e.SendResponse(400) ' TODO check with Ted, add fallback for invalid request params instead of player crash
          return
        end if
        mVar.spaceLimitedByAbsoluteSize = "false" ' TODO check with Ted changed type from sting to boolean
      else
        e.SendResponse(400)
        return
      end if
      mVar.limitStorageSpace = true
    else if args.limitStorageEnabled <> invalid and lcase(args.limitStorageEnabled) = "false" then
      mVar.limitStorageSpace = false
    else
      e.SendResponse(400)
      return
    end if
    e.SendResponse(200)
    
  end sub
  
  Sub PostPrepareForTransferJson(userData as object, e as object)
    
    mVar = userData.mVar
    
    if e.GetRequestBodyFile() = invalid then
      e.SendResponse(400) ' TODO check with Ted, add fallback for invalid request params instead of player crash
      return
    end if
    
    reqFile = CreateObject("roReadFile", e.GetRequestBodyFile())
    
    if reqFile.ReadByteIfAvailable() < 0 then
      e.SendResponse(400) ' TODO check with Ted, add fallback for invalid request params instead of player crash
      return
    end if
    
    MoveFile(e.GetRequestBodyFile(), "filesToPublish.json")
    
    filesToCopy = mVar.FreeSpaceOnDrive()
    if type(filesToCopy) = "roAssociativeArray" then
      respBody = { }
      respBody.AddReplace("family", mVar.sysInfo.deviceFamily$) ' TODO check with Ted Family > family
      respBody.AddReplace("model", mVar.sysInfo.deviceModel$) ' TODO check with Ted Model > model
      respBody.AddReplace("fwVersion", mVar.sysInfo.deviceFWVersion$) ' TODO check with Ted FWVersion > fwVersion
      respBody.AddReplace("fwVersionNumber", StripLeadingSpaces(stri(mVar.sysInfo.deviceFWVersionNumber%))) ' TODO check with Ted FWVersionNumber > fwVersionNumber
      
      respBody.AddReplace("file", createObject("roArray", 0, true))
      
      for each key in filesToCopy
        
        ' TODO validate each file item
        ' TODO does this need to check what's in the pool already?
        fileItem = filesToCopy[key]
        
        if fileItem = invalid or fileItem.fileName = invalid or fileItem.filePath = invalid or fileItem.hash = invalid or fileItem.size = invalid then
          e.SendResponse(400)
          return
        end if
        
        file = { }
        
        file.AddReplace("fileName", fileItem.fileName) ' TODO check with Ted name change fileName > sourceFileName
        file.AddReplace("filePath", fileItem.filePath)' TODO check with Ted name change filePath > sourceFilePath
        file.AddReplace("hash", fileItem.hash)
        file.AddReplace("size", fileItem.size) ' TODO check with Ted name change fileSize > size
        
        respBody.file.push(file)
        
      next
      e.SetResponseBodyString(FormatJson(respBody))
      e.SendResponse(200)
      
    else
      ' the following call is ignored on a post
      '	e.SetResponseBodyString("413")
      e.SendResponse(400)
      return
    end if
    
  end sub
  
  Sub PostFileJson(userData as object, e as object)
    
    if e.GetRequestBodyFile() = invalid then
      e.SendResponse(400) ' TODO check with Ted, add fallback for invalid request params instead of player crash
      return
    end if
    
    reqFile = CreateObject("roReadFile", e.GetRequestBodyFile())
    
    if reqFile.ReadByteIfAvailable() < 0 or e.GetRequestHeader("Destination-Filename") = "" then
      e.SendResponse(400) ' TODO check with Ted, add fallback for invalid request params instead of player crash
      return
    end if
    
    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)
    
    ' TODO this will silently fail for path depth > 2
    fullFilePath$ = currentDir$ + fileParts[1]
    
    MoveFile(e.GetRequestBodyFile(), fullFilePath$)
    
    e.SendResponse(200)
    
  end sub
  
  Sub PostSyncSpecJson(userData as object, e as object)
    
    print "respond to SyncSpecPostedJson request"
    
    if e.GetRequestBodyFile() = invalid then
      e.SendResponse(400) ' TODO check with Ted, add fallback for invalid request params instead of player crash
      return
    end if
    
    reqFile = CreateObject("roReadFile", e.GetRequestBodyFile())
    
    if reqFile.ReadByteIfAvailable() < 0 then
      e.SendResponse(400) ' TODO check with Ted, add fallback for invalid request params instead of player crash
      return
    end if
    
    EVENT_REALIZE_SUCCESS = 101
    
    mVar = userData.mVar
    
    newSyncFileName$ = "new-sync.json"
    
    if ReadAsciiFile("local-sync.json") <> "" then
      oldSyncFileName$ = "local-sync.json"
    end if
    
    autoScheduleFileName$ = "autoschedule.json"
    
    MoveFile(e.GetRequestBodyFile(), newSyncFileName$)
    e.SendResponse(200)
    
    oldSync = CreateObject("roSyncSpec")
    ok = oldSync.ReadFromFile(oldSyncFileName$)
    if not ok then stop
    
    newSync = CreateObject("roSyncSpec")
    ok = newSync.ReadFromFile(newSyncFileName$)
    if not ok then stop
    
    oldSyncSpecScriptsOnly = oldSync.FilterFiles("download", { group: "script" })
    newSyncSpecScriptsOnly = newSync.FilterFiles("download", { group: "script" })
    
    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
      end if
      
      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(newSyncFileName$)
        newSync = invalid
        return
      else
        ' destroy the Post LFN Device Setup Splash Screen
        mVar.registrySection.Delete("susse")
        mVar.registrySection.Flush()
  
        mVar.deviceSetupSplashScreen = invalid
        rebootRequired = true
      end if
      
    end if
    
    jsonSyncSpec$ = newSync.WriteToString({ format : "json" })
    ok = WriteAsciiFile("local-sync.json", jsonSyncSpec$)
    if not ok then stop
    
    ' cause fsync
    CreateObject("roReadFile", "local-sync.json")
    
    if rebootRequired then
      mVar.diagnostics.PrintDebug("### new script or upgrade found - reboot")
      RebootSystem()
    end if
    
    globalAA = GetGlobalAA()
    globalAA.autoscheduleFilePath$ = GetPoolFilePath(mVar.assetPoolFiles, autoScheduleFileName$)
    globalAA.boseProductsFilePath$ = GetPoolFilePath(mVar.assetPoolFiles, "PartnerProducts.json")
    if globalAA.autoscheduleFilePath$ = "" then stop
    
    metadata = newSync.GetMetadata("client")
    if metadata.DoesExist("obfuscatedPassphrase") then
      obfuscatedPassphrase$ = newSync.LookupMetadata("client", "obfuscatedPassphrase")
      deviceCustomization = CreateObject("roDeviceCustomization")
      deviceCustomization.StoreObfuscatedEncryptionKey("AesCtrHmac", obfuscatedPassphrase$)
      mVar.contentEncrypted = true
    end if
    
    mVar.SetPoolSizes(newSync)
    
    DeleteFile(newSyncFileName$)
    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
  