content: Library "autoplugins.brs"
  
  'region Main
  Sub Main()
    
    autorunVersion$ = "10.0.89" 'Bacon in development
    customAutorunVersion$ = "10.0.0"
  
    globalAA = getGlobalAA()
  
    ' create global registry section to be used throughout script
    globalAA.registrySection = CreateObject("roRegistrySection", "networking")
    if type(globalAA.registrySection) <> "roRegistrySection" then
      stop
    end if
  
    globalAA.msgPort = CreateObject("roMessagePort")
    globalAA.msgPort.SetWatchdogTimeout(60)
    
    modelObject = CreateObject("roDeviceInfo")
    InitializeSysInfo(modelObject, autorunVersion$, customAutorunVersion$)
    InitSupervisorSupport()
    LoadRegistrySettings()
    InitializeRecordPlayback()
    InitializeSyncSpecAndSettings()
  
    debugParams = EnableDebugging()
    
    sysFlags = { }
    sysFlags.debugOn = debugParams.serialDebugOn
    sysFlags.systemLogDebugOn = debugParams.systemLogDebugOn
    
    sysInfo = globalAA.sysInfo
    
    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
  
      ' GetScreenModes is a function introduced in Series 5 players. 
      ' Therefore we need to check if it's supported before use to avoid errors in lower series.
      if findMemberFunction(videoMode, "GetScreenModes") <> invalid then
        ' OS supports multi screen
        screenModes = videoMode.GetScreenModes()
        for each screen in screenModes
          edid = videoMode.GetEdidIdentity(screen.name)
          UpdateEdidValues(edid, sysInfo, screen.name)
        next
      end if
  
      ' always update HDMI without "-n" suffixes Edid variables to maintain backwards compatibility
      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)
  
    CheckFirmwareVersion(modelObject, sysInfo, systemTime)
  
    diagnosticCodes = newDiagnosticCodes()
  
    RunBsp(sysFlags, sysInfo, diagnosticCodes, systemTime)
    
  end sub
  
  
  Sub InitializeSysInfo(modelObject as object, autorunVersion$ as string, customAutorunVersion$ as string) as object
  
    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
  
    GetGlobalAA().sysInfo = sysInfo
  
  end sub
  
  
  Function GetVideoMode() as object
    return CreateObject("roVideoMode")
  end function
  
  
  ' Note that the check only needed for series 4 and older models
  ' Series 5 and newer models should always support BSN.cloud so no need to check
  Sub CheckFirmwareVersion(modelObject as object, sysInfo as object, systemTime as object)
    ' check to see whether or not the current firmware meets the minimum compatibility requirements
    ' note that the return value for the GetVersionNumber() method does not include any additional version numbers after the first three
    ' the calculation is major*65536 + minor*256 + build
    versionNumber% = modelObject.GetVersionNumber()
    
    if sysInfo.deviceFamily$ = "pantera" then
      minVersionNumber% = 524407
      minVersion$ = "8.0.119"
    else if sysInfo.deviceFamily$ = "pagani" then
      minVersionNumber% = 524407
      minVersion$ = "8.0.119"
    else if sysInfo.deviceFamily$ = "impala" then
      minVersionNumber% = 524407
      minVersion$ = "8.0.119"
    else if sysInfo.deviceFamily$ = "malibu" then
      minVersionNumber% = 524407
      minVersion$ = "8.0.119"
    else if sysInfo.deviceFamily$ = "tiger" then
      minVersionNumber% = 524407
      minVersion$ = "8.0.119"
    else ' no supported devices should hit this else
      minVersionNumber% = 524407
      minVersion$ = "8.0.119"
    end if
    
    if versionNumber% < minVersionNumber% then
      osUpgradeMsg$ = "Firmware needs to be upgraded to " + minVersion$ + " or greater"
      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(osUpgradeMsg$)
        tw.Show()
      else
        ' for a model that doesn't support video, print the message instead
        systemLog = CreateObject("roSystemLog")
        systemLog.SendLine(osUpgradeMsg$)
      end if
      
      if GetActiveSettings().deviceScreenShotsEnabled 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
  end sub
  
  
  ' setup snapshot capability as early as possible
  Sub InitRemoteSnapshots(sysInfo as object)
  
    globalAA = GetGlobalAA()
  
    ok = CreateDirectory("snapshots") ' fails if storage is write protected or formatted as ntfs, but ignore return value at this point
    
    ' 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, videoConnector$ as string)
    
    suffix$ = "$"
    if videoConnector$ <> "" then suffix$ = "_" + videoConnector$ + "$"
  
    if type(edid) = "roAssociativeArray" then
      sysInfo["edidMonitorSerialNumber"+suffix$] = edid.serial_number_string
      sysInfo["edidYearOfManufacture"+suffix$] = edid.year_of_manufacture
      sysInfo["edidMonitorName"+suffix$] = edid.monitor_name
      sysInfo["edidManufacturer"+suffix$] = edid.manufacturer
      sysInfo["edidUnspecifiedText"+suffix$] = edid.text_string
      sysInfo["edidSerialNumber"+suffix$] = StripLeadingSpaces(stri(edid.serial_number))
      sysInfo["edidManufacturerProductCode"+suffix$] = edid.product
      sysInfo["edidWeekOfManufacture"+suffix$] = edid.week_of_manufacture
    else
      sysInfo["edidMonitorSerialNumber"+suffix$] = ""
      sysInfo["edidYearOfManufacture"+suffix$] = ""
      sysInfo["edidMonitorName"+suffix$] = ""
      sysInfo["edidManufacturer"+suffix$] = ""
      sysInfo["edidUnspecifiedText"+suffix$] = ""
      sysInfo["edidSerialNumber"+suffix$] = ""
      sysInfo["edidManufacturerProductCode"+suffix$] = ""
      sysInfo["edidWeekOfManufacture"+suffix$] = ""
    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 GetActiveSettings().deviceScreenShotsEnabled then
        sleep(1000) ' sleep here ensures that graphics makes it to the screen before the snapshot is taken
        activePresentation$ = ""
        if globalAA.bsp.activePresentation$ <> invalid then
          activePresentation$ = globalAA.bsp.activePresentation$
        end if
        TakeSnapshot(globalAA.bsp.systemTime, 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 = GetActiveSyncSpecSettings().enableSerialDebugging
    debugParams.systemLogDebugOn = GetActiveSyncSpecSettings().enableSystemLogDebugging
    return debugParams
  end function
  
  
  Sub WriteRegistrySetting(key$ as string, value$ as string)
    globalAA = GetGlobalAA()
    globalAA.registrySection.Write(key$, value$)
  end sub
  
  
  Sub LoadRegistrySettings()
  
    globalAA = GetGlobalAA()
  
    globalAA.registrySettings = {}
  
    ReadCachedRegistrySettings(globalAA.registrySection, globalAA.registrySettings)
  
    ' read legacy settings even if the supervisor manages the settings - but don't use them unless required
    ReadLegacyRegistrySettings(globalAA.registrySection, globalAA.registrySettings)
  
  end sub
  
  
  Sub MergeLegacyRegistrySettingsIntoGlobalSettings(settings as object)
  
    registrySection = GetGlobalAA().registrySection
  
    settings.setupType = registrySection.Read("sut")
    settings.unitName = registrySection.Read("un")
    settings.unitDescription = registrySection.Read("ud")
    settings.unitNamingMethod = registrySection.Read("unm")
    settings.lwsConfig = registrySection.Read("nlws")
    settings.lwsUserName = registrySection.Read("nlwsu")
    settings.lwsPassword = registrySection.Read("nlwsp")
    settings.lwsEnableUpdateNotifications = GetBoolFromString(registrySection.Read("nlwseun"), true)
    settings.inheritNetworkProperties = registrySection.Read("inp")
  
  end sub
  
  
  Sub AddRegistrySettingsNotInStandaloneSyncSpecIntoGlobalSettings(registrySection as object, settings as object)
    ' new supervisor, standalone- invoked on startup
    '   bacon standalone publish does not include device screen shot parameters
    '   bacon network does not support editing individual device screen shot parameteters
    settings.deviceScreenShotsEnabled = GetValidBool(registrySection.Read("enableRemoteSnapshot"), false)
    settings.deviceScreenShotsInterval = GetIntFromString(registrySection.Read("remoteSnapshotInterval"))
    settings.deviceScreenShotsCountLimit = GetIntFromString(registrySection.Read("remoteSnapshotMaxImages"))
    settings.deviceScreenShotsQuality = GetIntFromString(registrySection.Read("remoteSnapshotJpegQualityLevel"))
    settings.deviceScreenShotsOrientation = registrySection.Read("remoteSnapshotOrientation")
  end sub
  
  
  ' Registry settings that are used by both settings and non settings code; i.e., not known to supervisor
  Sub ReadCachedRegistrySettings(registrySection as object, registrySettings as object)
  
    registrySettings.setupSplashScreenEnabled = registrySection.Read("susse")
    
    registrySettings.OnlyDownloadIfCached$ = registrySection.Read("OnlyDownloadIfCached")
    
    registrySettings.timeBetweenNetConnects$ = registrySection.Read("tbnc")
    
    registrySettings.logDate$ = registrySection.Read("ld")
    registrySettings.logCounter$ = registrySection.Read("lc")
    
    registrySettings.idleScreenColor$ = registrySection.Read("isc")
    if registrySettings.idleScreenColor$ = "" then
      registrySettings.idleScreenColor$ = "FF000000"
    end if
    
    registrySettings.usbContentUpdatePassword$ = registrySection.Read("uup")
    
    registrySettings.lastBSNConnectionTime = registrySection.Read("lastBSNConnectionTime")
    
    'Open datagramSocket for autorun and bootstrap to communicate
    registrySettings.daUdpSocketPort$ = registrySection.Read("daUdpSocketPort")
    registrySettings.wsUdpSocketPort$ = registrySection.Read("wsUdpSocketPort")
    
    registrySettings.bwConfigUserName = registrySection.Read("bwConfigUserName")
    registrySettings.bwConfigPassword = registrySection.Read("bwConfigPassword")
  
  end sub
  
  
  Sub RunBsp(sysFlags as object, sysInfo as object, diagnosticCodes as object, systemTime as object)
    
    globalAA = GetGlobalAA()
    msgPort = globalAA.msgPort
  
    BSP = newBSP(sysFlags, msgPort, systemTime)
      
    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
  
    registrySettings = globalAA.registrySettings
    if globalAA.ccloud = invalid then
      ' open datagramSocket for autorun and bootstrap to communicate
      ' create object for bootstrap udp messages
      BSP.dgSocket = CreateObject("roDatagramSocket")
      if registrySettings.daUdpSocketPort$ = "" then
        registrySettings.daUdpSocketPort% = 8888
      else
        registrySettings.daUdpSocketPort% = int(val(registrySettings.daUdpSocketPort$))
      end if
      BSP.dgSocket.BindToLocalPort(registrySettings.daUdpSocketPort%)
      BSP.dgSocket.SetUserData("bootstrap")
      BSP.dgSocket.SetPort(msgPort)
  
      if registrySettings.wsUdpSocketPort$ = "" then
        registrySettings.wsUdpSocketPort% = 9999
      else
        registrySettings.wsUdpSocketPort% = int(val(registrySettings.wsUdpSocketPort$))
      end if
    endif
  
    ' 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(BuildUserData("bpEvent", "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(BuildUserData("bpEvent", "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(BuildUserData("bpEvent", "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(BuildUserData("bpEvent", "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
    settings = globalAA.settings
    if settings.lwsConfig = "c" and BSP.sysInfo.storageIsWriteProtected then DisplayStorageDeviceLockedMessage()
    lwsEnabled = false
    if settings.lwsConfig = "c" or settings.lwsConfig = "s" then
      lwsEnabled = true
    end if
    
    if lwsEnabled then
  
      BSP.localServer = CreateObject("roHttpServer", { port: 8080 })
      BSP.localServer.SetPort(msgPort)
      
      if lwsEnabled then
        
        lwsUserName$ = settings.lwsUserName
        lwsPassword$ = settings.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.GetDeviceSerialNumberJsonAA = { HandleEvent: GetDeviceSerialNumberJson, mVar: BSP }
  
      BSP.GetSnapshotConfigurationJsonAA = { HandleEvent: GetSnapshotConfigurationJson, 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.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/device/configuration/serialNumber", user_data: BSP.GetDeviceSerialNumberJsonAA})
      
      BSP.localServer.AddGetFromEvent({ url_path: "/v2/snapshot/configuration", user_data: BSP.GetSnapshotConfigurationJsonAA, 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: "/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$ = settings.unitName
      unitNamingMethod$ = settings.unitNamingMethod
      
      unitDescription$ = settings.unitDescription
      
      if settings.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
        BSP.diagnostics.PrintDebug("Unable to create roNetworkAdvertisement for BRIGHTSIGN-LWS-SERVICE on port 8080")
      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 = GetActiveSyncSpec()
    activeSyncSpecType = GetActiveSyncSpecType()
    activeSettings = GetActiveSettings()  
    activeSyncSpecSettings = GetActiveSyncSpecSettings()
  
    BSP.contentEncrypted = false
  
    if activeSyncSpecType = "local" then
          
      BSP.assetCollection = activeSyncSpec.GetAssets("download")
      BSP.assetPoolFiles = CreateObject("roAssetPoolFiles", BSP.assetPool, BSP.assetCollection)
      
      ' update registry setting for USB content updates if necessary
      metadata = activeSyncSpec.GetMetadata("client")
      if metadata.DoesExist("usbUpdatePassword") then
        usbUpdatePassphrase$ = activeSyncSpec.LookupMetadata("client", "usbUpdatePassword")
        if usbUpdatePassphrase$ <> registrySettings.usbContentUpdatePassword$ then
          registrySettings.usbContentUpdatePassword$ = usbUpdatePassphrase$
          WriteRegistrySetting("uup", usbUpdatePassphrase$)
        end if
      end if
  
      BSP.GetSupportedFeatures()
      
      obfuscatedPassphrase$ = activeSyncSpecSettings.obfuscatedPassphrase
      if obfuscatedPassphrase$ <> "" then
        deviceCustomization = CreateObject("roDeviceCustomization")
        deviceCustomization.StoreObfuscatedEncryptionKey("AesCtrHmac", obfuscatedPassphrase$)
        BSP.contentEncrypted = true
      end if
  
      BSP.networkingActive = false
  
    else if activeSyncSpecType = "network" or activeSyncSpecType = "localToBsn" then
  
      ' if the device is configured for networking, require that the storage is writable
      if BSP.sysInfo.storageIsWriteProtected then DisplayStorageDeviceLockedMessage()
      
      BSP.networkingHSM = newNetworkingStateMachine(BSP, BSP.msgPort)
      
      BSP.networkingHSM.SetSystemInfo(sysInfo, diagnosticCodes)
      BSP.logging.networking = BSP.networkingHSM
      
      BSP.assetCollection = activeSyncSpec.GetAssets("download")
      BSP.assetPoolFiles = CreateObject("roAssetPoolFiles", BSP.assetPool, BSP.assetCollection)
      
      BSP.downloadFiles = activeSyncSpec.GetFileList("download")
      
      BSP.networkingActive = true
      
      BSP.GetSupportedFeatures()
      
      deviceCustomization = CreateObject("roDeviceCustomization")
      if deviceCustomization.IsEncryptionKeyPresent("AesCtrHmac") then
        BSP.contentEncrypted = true
      end if
      
      BSP.SetPerFileEncryptionStatus(activeSyncSpec)
      
      BSP.networkingHSM.Initialize()
      
    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")
  
    ' initialize logging parameters
    playbackLoggingEnabled = activeSettings.playbackLoggingEnabled
    eventLoggingEnabled = activeSettings.eventLoggingEnabled
    diagnosticLoggingEnabled = activeSettings.diagnosticLoggingEnabled
    stateLoggingEnabled = activeSettings.stateLoggingEnabled
    variableLoggingEnabled = activeSettings.variableLoggingEnabled
    uploadLogFilesAtBoot = activeSettings.uploadLogFilesAtBoot
    uploadLogFilesAtSpecificTime = activeSettings.uploadLogFilesAtSpecificTime
  
    if (activeSyncSpecType = "network" or activeSyncSpecType = "localToBsn") and IsInteger(activeSettings.uploadLogFilesTime)
      uploadLogFilesTime% = activeSettings.uploadLogFilesTime
    else
      uploadLogFilesTime% = 0
    end if
  
    ' 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(activeSyncSpecSettings)
    
    ' Read and parse BoseProducts.json
    BSP.boseProductSpecs = ReadBoseProductsFile()
    
    ' 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
    
    ' Network priorities, etc for data feeds
    BSP.mrssDataFeedsBindingPriorityIndex = 0  
    BSP.mrssDataFeedsNumRetries% = 0
    BSP.mrssMaxRetries% = 3
    
    BSP.textDataFeedsBindingPriorityIndex = 0  
    BSP.textDataFeedsNumRetries% = 0
    BSP.textMaxRetries% = 3
    
    ' 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.brightSignDevicesInWall = {}
  
    BSP.bwUdpSender = CreateObject("roDatagramSender")
    BSP.bwUdpSender.SetDestination("BCAST-LOCAL-SUBNETS", 5111)
  
    BSP.bwUdpReceiver = CreateObject("roDatagramReceiver", 5111)
    BSP.bwUdpReceiver.SetUserData("brightWall")
    BSP.bwUdpReceiver.SetPort(BSP.msgPort)
  
    if type(BSP.brightWallConfiguratorServer) = "roHttpServer" then
      BSP.AddBrightWallConfiguratorServerHandlers(BSP.brightWallConfiguratorServer)
    endif
    
    BSP.InitializePlaybackEventsTimer()
  
    BSP.EventLoop()
    
  end sub
  
  
  ' m is bsp
  Sub AddBrightWallConfiguratorServerHandlers(brightWallConfiguratorServer)
  
      getIsBrightWallAA = { HandleEvent: GetIsBrightWallHandler, mVar: m }
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/GetIsBrightWall", user_data: getIsBrightWallAA })
  
      getBrightWallConfigurationAA = { HandleEvent: GetBrightWallConfiguration, mVar: m }
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/GetBrightWallConfiguration", user_data: getBrightWallConfigurationAA })
  
      getBrightWallDeviceListAA = { HandleEvent: GetBrightWallDeviceList, mVar: m }
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/GetBrightWallDeviceList", user_data: getBrightWallDeviceListAA })
  
      setBrightWallPositionAA = { HandleEvent: SetBrightWallPosition, mVar: m }
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/SetBrightWallPosition", user_data: setBrightWallPositionAA })
  
      setBrightWallIsMasterAA = { HandleEvent: SetBrightWallIsMaster, mVar: m }
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/SetBrightWallIsMaster", user_data: setBrightWallIsMasterAA })
  
      exitConfiguratorAA = { HandleEvent: ExitConfigurator, mVar: m }
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/ExitConfigurator", user_data: exitConfiguratorAA })
  
      reenterConfiguratorAA = { HandleEvent: ReenterConfigurator, mVar: m }
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/ReenterConfigurator", user_data: reenterConfiguratorAA })
  
      reenterDeviceSetupAA = { HandleEvent: ReenterDeviceSetup, mVar: m }
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/ReenterDeviceSetup", user_data: reenterDeviceSetupAA })
  
      launchAlignmentToolAA = { HandleEvent: LaunchAlignmentTool, mVar: m }
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/LaunchAlignmentTool", user_data: launchAlignmentToolAA })
  
      exitAlignmentToolAA = { HandleEvent: ExitAlignmentTool, mVar: m }
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/ExitAlignmentTool", user_data: exitAlignmentToolAA })
  
      rebootBrightWallAA = { HandleEvent: RebootBrightWall, mVar: m }
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/RebootBrightWall", user_data: rebootBrightWallAA })
  
      setBezelDimensionsAA = { HandleEvent: SetBezelDimensions, mVar: m }
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/SetBezelDimensions", user_data: setBezelDimensionsAA })
  
  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$)
    
    videoMode = CreateObject("roVideoMode")
    if type(videoMode) = "roVideoMode" then
      m.setSyncDomainSupported = true
    else
      m.setSyncDomainSupported = false
    endif
    m.forceResolutionSupported = true
    m.showHideVideoZoneSupported = true
    m.bypassProxySupported = true
    m.contentEncryptionSupported = true
    m.htmlSetTransformSupported = true
    m.httpWidgetGetUserAgentSupported = true
    m.mosaicModeSupported = 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
  
  
  Function GetRequestedBrowserStorageSpace(storageSpaceLimits as object) as integer
  
    browser_storage% = 0
  
    if storageSpaceLimits.maximumHTMLLocalStoragePoolSizeMB% > 0 then
      if storageSpaceLimits.maximumHTMLLocalStoragePoolSizeMB% >= 2048 then
        storageSpaceLimits.maximumHTMLLocalStoragePoolSizeMB% = 2047
      end if
      browser_storage% = browser_storage% + storageSpaceLimits.maximumHTMLLocalStoragePoolSizeMB%
    end if
  
    if storageSpaceLimits.maximumHTMLDataPoolSizeMB% > 0 then
      if storageSpaceLimits.maximumHTMLDataPoolSizeMB% >= 2048 then
        storageSpaceLimits.maximumHTMLDataPoolSizeMB% = 2047
      end if
      browser_storage% = browser_storage% + storageSpaceLimits.maximumHTMLDataPoolSizeMB%
    end if
  
    if storageSpaceLimits.maximumHTMLIndexedDBPoolSizeMB% > 0 then
      if storageSpaceLimits.maximumHTMLIndexedDBPoolSizeMB% >= 2048 then
        storageSpaceLimits.maximumHTMLIndexedDBPoolSizeMB% = 2047
      end if
      browser_storage% = browser_storage% + storageSpaceLimits.maximumHTMLIndexedDBPoolSizeMB%
    end if
  
    browser_storage% = browser_storage% * 1024 * 1024
  
    return browser_storage%
  
  end function
  
  
  Function GetStorageSpaceLimits(syncSpecSettings as object) as object
  
    spaceLimitedByAbsoluteSize = syncSpecSettings.spaceLimitedByAbsoluteSize
    publishedDataSizeLimitMB = syncSpecSettings.publishedDataSizeLimitMB
    publishedDataSizeLimitPercentage = syncSpecSettings.publishedDataSizeLimitPercentage
    dynamicDataSizeLimitMB = syncSpecSettings.dynamicDataSizeLimitMB
    dynamicDataSizeLimitPercentage = syncSpecSettings.dynamicDataSizeLimitPercentage
    htmlDataSizeLimitPercentage = syncSpecSettings.htmlDataSizeLimitPercentage
    htmlDataSizeLimitMB = syncSpecSettings.htmlDataSizeLimitMB
    htmlLocalStorageSizeLimitPercentage = syncSpecSettings.htmlLocalStorageSizeLimitPercentage
    htmlLocalStorageSizeLimitMB = syncSpecSettings.htmlLocalStorageSizeLimitMB
    htmlIndexedDBSizeLimitPercentage = syncSpecSettings.htmlIndexedDBSizeLimitPercentage
    htmlIndexedDBSizeLimitMB = syncSpecSettings.htmlIndexedDBSizeLimitMB
  
    storageSpaceLimits = {}
  
    if not spaceLimitedByAbsoluteSize then
      
      ' convert from percentages to absolute values
      du = CreateObject("roStorageInfo", "./")
      totalCardSizeMB% = du.GetSizeInMegabytes()
      
      ' pool size for published data
      publishedDataSizeLimitPercentage% = int(val(publishedDataSizeLimitPercentage))
      storageSpaceLimits.maximumPublishedDataPoolSizeMB% = publishedDataSizeLimitPercentage% * totalCardSizeMB% / 100
      
      ' pool size for dynamic data
      dynamicDataSizeLimitPercentage% = int(val(dynamicDataSizeLimitPercentage))
      storageSpaceLimits.maximumDynamicDataPoolSizeMB% = dynamicDataSizeLimitPercentage% * totalCardSizeMB% / 100
      
      ' size for html data
      htmlDataSizeLimitPercentage% = int(val(htmlDataSizeLimitPercentage))
      storageSpaceLimits.maximumHTMLDataPoolSizeMB% = htmlDataSizeLimitPercentage% * totalCardSizeMB% / 100
      
      ' size for html local storage
      htmlLocalStorageSizeLimitPercentage% = 0
      storageSpaceLimits.maximumHTMLLocalStoragePoolSizeMB% = 0
      if htmlLocalStorageSizeLimitPercentage <> "" then
        htmlLocalStorageSizeLimitPercentage% = int(val(htmlLocalStorageSizeLimitPercentage))
        storageSpaceLimits.maximumHTMLLocalStoragePoolSizeMB% = htmlLocalStorageSizeLimitPercentage% * totalCardSizeMB% / 100
      end if
      
      ' size for html indexed db
      htmlIndexedDBSizeLimitPercentage% = 0
      storageSpaceLimits.maximumHTMLIndexedDBPoolSizeMB% = 0
      if htmlIndexedDBSizeLimitPercentage <> "" then
        htmlIndexedDBSizeLimitPercentage% = int(val(htmlIndexedDBSizeLimitPercentage))
        storageSpaceLimits.maximumHTMLIndexedDBPoolSizeMB% = htmlIndexedDBSizeLimitPercentage% * totalCardSizeMB% / 100
      end if
      
    else
      
      storageSpaceLimits.maximumPublishedDataPoolSizeMB% = int(val(publishedDataSizeLimitMB))
      storageSpaceLimits.maximumDynamicDataPoolSizeMB% = int(val(dynamicDataSizeLimitMB))
      
      storageSpaceLimits.maximumHTMLDataPoolSizeMB% = int(val(htmlDataSizeLimitMB))
      
      storageSpaceLimits.maximumHTMLLocalStoragePoolSizeMB% = 0
      if htmlLocalStorageSizeLimitMB <> "" then
        storageSpaceLimits.maximumHTMLLocalStoragePoolSizeMB% = int(val(htmlLocalStorageSizeLimitMB))
      end if
      
      storageSpaceLimits.maximumHTMLIndexedDBPoolSizeMB% = 0
      if htmlIndexedDBSizeLimitMB <> "" then
        storageSpaceLimits.maximumHTMLIndexedDBPoolSizeMB% = int(val(htmlIndexedDBSizeLimitMB))
      end if
      
    end if
  
    return storageSpaceLimits
      
  end function
  
  
  Sub SetPoolSizes(syncSpecSettings as object) as object
  
    limitStorageSpace = syncSpecSettings.limitStorageSpace
    
    if limitStorageSpace then
      storageSpaceLimits = GetStorageSpaceLimits(syncSpecSettings)    
      ok = m.assetPool.SetMaximumPoolSizeMegabytes(storageSpaceLimits.maximumPublishedDataPoolSizeMB%)
      ok = m.feedPool.SetMaximumPoolSizeMegabytes(storageSpaceLimits.maximumDynamicDataPoolSizeMB%)
    else
      ' clear prior settings
      ok = m.assetPool.SetMaximumPoolSizeMegabytes(-1)
      ok = m.feedPool.SetMaximumPoolSizeMegabytes(-1)    
    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 IsNetworkAvailable(network_interface as Dynamic) as boolean
      
      nc = CreateObject("roNetworkConfiguration", network_interface)
      if type(nc) <> "roNetworkConfiguration" then
        return false
      endif
  
      currentConfig = nc.GetCurrentConfig()
      if type(currentConfig) <> "roAssociativeArray" then
        return false
      endif
  
      return currentConfig.link
  
    end function
  
  
    Function GetBindingByPriority(dataType as string, priorityIndex as integer) as object
  
      aa = {}
  
      globalAA = GetGlobalAA()
  
      globalAA.bsp.diagnostics.PrintTimestamp()
      globalAA.bsp.diagnostics.PrintDebug("GetBindingByPriority entry:")
      globalAA.bsp.diagnostics.PrintDebug("dataType: " + dataType)
      globalAA.bsp.diagnostics.PrintDebug("priorityIndex: " + stri(priorityIndex))
  
      if globalAA.networkInterfacePriorityLists.DoesExist(dataType) then
  
        priorityList = globalAA.networkInterfacePriorityLists.Lookup(dataType)
  
        while (priorityIndex < priorityList.count())
  
          interface = priorityList[priorityIndex]
          
          ' check to see if interface has link
          if interface.networkInterface = "eth0" then
            network_interface = 0
          else if interface.networkInterface = "wlan0" then
            network_interface = 1
          else
            network_interface = interface.networkInterface
          endif
  
          bindingDiagnostic$ = GetBindingDiagnostic("Selected network_interface: ", network_interface)
          globalAA.bsp.diagnostics.PrintDebug(bindingDiagnostic$)
  
          nc = CreateObject("roNetworkConfiguration", network_interface)
          if type(nc) = "roNetworkConfiguration" then
            globalAA.bsp.diagnostics.PrintDebug("roNetworkConfiguration succeeded for selected network_interface")
            currentConfig = nc.GetCurrentConfig()
            if type(currentConfig) = "roAssociativeArray" then
              globalAA.bsp.diagnostics.PrintDebug("GetCurrentConfig succeeded for selected network_interface")
              if currentConfig.link then
                bindingDiagnostic$ = GetBindingDiagnostic("Return network_interface: ", network_interface)
                globalAA.bsp.diagnostics.PrintDebug(bindingDiagnostic$)
                aa = {}
                aa.network_interface = network_interface
                aa.priorityIndex = priorityIndex
                return aa
              else
                globalAA.bsp.diagnostics.PrintDebug("link false for selected network_interface")
              endif
            else
                globalAA.bsp.diagnostics.PrintDebug("GetCurrentConfig failed for selected network_interface")
            endif
          else
            globalAA.bsp.diagnostics.PrintDebug("roNetworkConfiguration failed for selected network_interface")
          endif
        
          priorityIndex = priorityIndex + 1
  
        end while
  
      endif
  
      globalAA.bsp.diagnostics.PrintDebug("GetBindingByPriority returns -1")
  
      aa.network_interface = -1
      aa.priorityIndex = 0
      return aa
  
    end function
  
  
    Function GetBinding(dataType as string, priorityIndex as integer) as object
  
      globalAA = GetGlobalAA()
  
      if not globalAA.supervisorSupportsUsbNetworkInterfaces and globalAA.cellularModemActive then
        aa = {}
        aa.network_interface = 2
        aa.priorityIndex = 0
        return aa
      endif
  
      return GetBindingByPriority(dataType, priorityIndex)
  
    end function
  
  
    Function GetBindingDiagnostic(diagnostic$ as string, binding) as string
      if not IsString(binding) then
        binding = stri(binding)
      endif
      return diagnostic$ + 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
      
      BSP.systemTime = systemTime
      
      BSP.diagnostics = newDiagnostics(sysFlags)
      
      BSP.Restart = Restart
      BSP.StartPlayback = StartPlayback
      
      BSP.LoadBrightWallDeviceSetup = LoadBrightWallDeviceSetup
  
      BSP.SetupDeviceWebPageServer = SetupDeviceWebPageServer
  
      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.ExecuteSynchronizePlayFile = ExecuteSynchronizePlayFile
      BSP.ExecuteSynchronizeMediaListEntry = ExecuteSynchronizeMediaListEntry
      BSP.ExecuteSynchronizeMediaListForward = ExecuteSynchronizeMediaListForward
      BSP.ExecuteSynchronizeMediaListBackward = ExecuteSynchronizeMediaListBackward
      BSP.ExecuteSynchronizeMediaListHelper = ExecuteSynchronizeMediaListHelper
      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.ExecuteSetAllAudioOutputsCommand = ExecuteSetAllAudioOutputsCommand
  
      BSP.EventLoop = EventLoop
      BSP.InitializePlaybackEventsTimer = InitializePlaybackEventsTimer
      BSP.RecordEvent = RecordEvent
      BSP.PlaybackEvent = ExecutePlaybackEvent
      BSP.GetControlEvent = GetControlEvent
  
      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.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.CreateDatagramReceiver = CreateDatagramReceiver
      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.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.GetRuntimeUsbConnector = GetRuntimeUsbConnector
      BSP.GetSpecifiedConnector = GetSpecifiedConnector
  
      ' json functions
      BSP.jsonAutoschedule = jsonAutoschedule
  
      BSP.ResendAutorunCapabilitiesToSupervisor = ResendAutorunCapabilitiesToSupervisor
      BSP.SendAutorunCapabilitiesToSupervisorViaUdp = SendAutorunCapabilitiesToSupervisorViaUdp
      
      BSP.UpdateVariable = UpdateVariable    
      BSP.UpdateUserVariablesFromBrightWallPayload = UpdateUserVariablesFromBrightWallPayload
  
      BSP.BroadcastUserVariableUpdatesToBrightWall = BroadcastUserVariableUpdatesToBrightWall
      BSP.GetUserVariablesAsPayload = GetUserVariablesAsPayload
  
      BSP.GetCurrentTimeAsIsoString = GetCurrentTimeAsIsoString
  
      BSP.ProcessBrightWallUdpMessage = ProcessBrightWallUdpMessage
      BSP.AddBrightWallConfiguratorServerHandlers = AddBrightWallConfiguratorServerHandlers
  
      return BSP
      
    end function
    
    'endregion
    
    Sub GetBrightWallConfiguratorPage(userData as object, e as object)
  
      mVar = userData.mVar
  
      e.AddResponseHeader("Content-type", "text/html; charset=utf-8")
      
      webPageContents$ = ReadAsciiFile(mVar.brightWallConfiguratorPage$)
      e.SetResponseBodyString(webPageContents$)
      if not e.SendResponse(200) then
        stop
      end if
  
    end sub
    
  
    'region Local Web server
    Sub GetConfigurationPage(userData as object, e as object)
      
      mVar = userData.mVar
      
      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)
      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)
         
      mVar = userData.mVar
      
      args = e.GetFormData()
      
      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
  
      mVar.BroadcastUserVariableUpdatesToBrightWall(args)
  
    end sub
    
    
    Sub PopulateIDData(mVar as object, root as object)
      
      settings = GetGlobalAA().settings
  
      unitName$ = settings.unitName
      unitNamingMethod$ = settings.unitNamingMethod
      unitDescription$ = settings.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
            
            if state.type$ = "mediaList" then          
              for each event in state.transitionToNextEventList
                if event.eventName = "udp" then
                  if event.eventData.export then
                    if IsString(event.eventData.label) then
                      udpLabel$ = event.eventData.label
                    else
                      udpLabel$ = event.eventName
                    end if
                    
                    if not udpEvents.DoesExist(udpLabel$) then
                      udpEvents.AddReplace(udpLabel$, newUDPItem(udpLabel$, event.eventData.data))
                    end if
                  endif
                endif
              next
  
              for each event in state.transitionToPreviousEventList
                if event.eventName = "udp" then
                  if event.eventData.export then
                    if IsString(event.eventData.label) then
                      udpLabel$ = event.eventData.label
                    else
                      udpLabel$ = event.eventName
                    end if
                    
                    if not udpEvents.DoesExist(udpLabel$) then
                      udpEvents.AddReplace(udpLabel$, newUDPItem(udpLabel$, event.eventData.data))
                    end if
                  endif
                endif
              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
      
      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
      
      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 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)
      
      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
    
    
    ' Called by BrightSignApp, not LFN
    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())))
    
      settings = GetActiveSettings()
  
      if settings.deviceScreenShotsEnabled then
        strVal = "true"
      else
        strVal = "false"
      end if
      root.AddAttribute("Enabled", strVal)
      root.AddAttribute("Interval", StripLeadingSpaces(stri(settings.deviceScreenShotsInterval)))
      root.AddAttribute("MaxImages", StripLeadingSpaces(stri(settings.deviceScreenShotsCountLimit)))
      root.AddAttribute("Quality", StripLeadingSpaces(stri(settings.deviceScreenShotsQuality)))
  
      if lcase(settings.deviceScreenShotsOrientation) = "landscape" then
          remoteSnapshotDisplayPortrait = "false"
      else
          remoteSnapshotDisplayPortrait = "true"
      endif
      root.AddAttribute("DisplayPortraitMode", remoteSnapshotDisplayPortrait)
      root.AddAttribute("Orientation", settings.deviceScreenShotsOrientation)
  
      root.AddAttribute("ResX", StripLeadingSpaces(stri(mVar.videoMode.GetOutputResX())))
      root.AddAttribute("ResY", StripLeadingSpaces(stri(mVar.videoMode.GetOutputResY())))
      
      ' 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 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
    
    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
  
  
  '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
  
  '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 newNodeApp(bsp as object, nodeAppDescription as object) as object
    nodeApp = { }
    nodeApp.name$ = nodeAppDescription.name$
    nodeApp.prefix$ = nodeAppDescription.prefix$
    nodeApp.filePath$ = nodeAppDescription.filePath$
    return nodeApp
  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()
    currentSyncSpec = GetActiveSyncSpec()
    if not type(currentSyncSpec) = "roSyncSpec" then
      stop
    end if
    
    m.restartPendingMediaEnd = false
    m.dontChangePresentationUntilMediaEndEventReceived = false
    
    m.currentUserVariables = { }
    m.liveDataFeeds = { }
    m.presentations = { }
    m.nodeApps = { }
    m.htmlSites = { }
    m.scriptPlugins = []
    m.videoModePlugins = []
    m.parserPlugins = []
    m.additionalPublishedFiles = []
    
    m.bypassProxyHosts = []
    
    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
    
    ' Check the signType of the activePresentation to see if it matches the registry settings.
    ' That is, is the signType for a BrightWall and does that match the registry settings? And other combinations of the two.
    if (autoplayPath$ <> "") then
      SetSignType(autoplayPath$)
    endif
  
    if IsBoolean(GetGlobalAA().registrySettings.brightwall_setup_screen_enabled) and GetGlobalAA().registrySettings.brightwall_setup_screen_enabled then
  
      sign = invalid
      m.sign = invalid
  
      m.LoadBrightWallDeviceSetup()
  
    else if (autoplayPath$ <> "") then
  
      brightAuthor = GetAutoplay(autoplayPath$)
      
      m.diagnostics.PrintTimestamp()
      m.diagnostics.PrintDebug("### create sign object from autoplay")
      
      version% = GetAutoplayVersion(brightAuthor)
      m.sysInfo.baconVersion$ = GetBaconVersion(brightAuthor)
  
      sign = newSign(brightAuthor, m.globalVariables, m, m.msgPort, m.controlPort, version%)
      
      m.LogActivePresentation()
      
    else
      sign = invalid
      if globalAA.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(globalAA.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
    
    if type(m.brightWallConfiguratorServer) = "roHttpServer" then
      m.brightWallConfiguratorServer = invalid
    endif
  
    zoneHSM = invalid
    m.dispatchingZone = invalid
    m.sign = invalid
    RunGarbageCollector()
    
    m.sign = sign
      
    if type(m.sign) = "roAssociativeArray" then
  
      if m.sign.enableSettingsHandler <> globalAA.supervisorEnableSettingsHandler then
  
        if m.sign.enableSettingsHandler then
          enableSettingsHandler = GetEnableSettingsHandler()
        else
          enableSettingsHandler = false    
        endif
  
        globalAA.supervisorEnableSettingsHandler = enableSettingsHandler
        SendAutorunCapabilitiesToSupervisorViaPost([], "")
  
      endif
  
      ' 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()
  
    else
  
      globalAA.supervisorEnableSettingsHandler = GetEnableSettingsHandler()
      SendAutorunCapabilitiesToSupervisorViaPost([], "")
  
    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
    settings = globalAA.settings
    if type(m.sign) = "roAssociativeArray" and (settings.lwsConfig = "c" or settings.lwsConfig = "s") then
      
      lwsUserName$ = settings.lwsUserName
      lwsPassword$ = settings.lwsPassword
      
      if (len(lwsUserName$) + len(lwsPassword$)) > 0 then
        credentials = { }
        credentials.AddReplace(lwsUserName$, lwsPassword$)
      else
        credentials = invalid
      end if
      
      ' support HTTPS configuration if certificate files exists
      deviceWebPageKeyFileName$ = "dwp.key"
      deviceWebPageCertFileName$ = "dwp.pem"
      serverOptions = { port: 8008 }
  
      if FileExists(deviceWebPageCertFileName$) then
        httpsOptions = { certificate_file: deviceWebPageCertFileName$ }
        if FileExists(deviceWebPageKeyFileName$) then 
          httpsOptions.AddReplace("key_file", deviceWebPageKeyFileName$)
        end if
        serverOptions.AddReplace("https", httpsOptions)
      end if
  
      if not m.sign.isBrightWall or GetGlobalAA().registrySettings.sync_master then
  
        m.sign.localServer = CreateObject("roHttpServer", serverOptions)
  
        if (m.sign.localServer = invalid or type(m.sign.localServer) <> "roHttpServer") then
          ' print the error and continue the process
          m.diagnostics.PrintDebug("### failed to create user variable web server")
        else
  
          m.sign.localServer.SetPort(m.msgPort)
          
          m.sign.GetUserVarsAA = { HandleEvent: GetUserVars, mVar: m }
          m.sign.GetConfigurationPageAA = { HandleEvent: GetConfigurationPage, mVar: m }
          m.sign.GetUDPEventsAA = { HandleEvent: GetUDPEvents, mVar: m }
          m.sign.SendUdpRestAA = { HandleEvent: SendUdpRest, mVar: m }
          
          m.sign.SetValuesAA = { HandleEvent: SetValues, mVar: m }
          m.sign.SetValuesByCategoryAA = { HandleEvent: SetValuesByCategory, mVar: m }
          
          m.sign.GetUserVariableCategoriesAA = { HandleEvent: GetUserVariableCategories, mVar: m }
          m.sign.GetUserVariablesByCategoryAA = { HandleEvent: GetUserVariablesByCategory, mVar: m }
          
          m.sign.UpdateFeedByCategoryAA = { HandleEvent: UpdateFeedByCategory, mVar: m }
          m.sign.UpdateAllFeedsAA = { HandleEvent: UpdateAllFeeds, mVar: m }
          
          m.sign.localServer.AddGetFromFile({ url_path: "/GetAutorun", content_type: "text/plain; charset=utf-8", filename: "autorun.brs" })
          
          m.sign.localServer.AddGetFromEvent({ url_path: "/GetUserVars", user_data: m.sign.GetUserVarsAA })
          m.sign.localServer.AddGetFromEvent({ url_path: "/", user_data: m.sign.GetConfigurationPageAA, passwords: credentials })
          m.sign.localServer.AddGetFromEvent({ url_path: "/GetUDPEvents", user_data: m.sign.GetUDPEventsAA })
          
          m.sign.localServer.AddGetFromEvent({ url_path: "/GetUserVariableCategories", user_data: m.sign.GetUserVariableCategoriesAA })
          m.sign.localServer.AddGetFromEvent({ url_path: "/GetUserVariablesByCategory", user_data: m.sign.GetUserVariablesByCategoryAA })
          
          m.sign.localServer.AddGetFromEvent({ url_path: "/UpdateFeedByCategory", user_data: m.sign.UpdateFeedByCategoryAA })
          m.sign.localServer.AddGetFromEvent({ url_path: "/UpdateAllFeeds", user_data: m.sign.UpdateAllFeedsAA })
          
          m.sign.localServer.AddPostToFormData({ url_path: "/SetValues", user_data: m.sign.SetValuesAA, passwords: credentials })
          m.sign.localServer.AddPostToFormData({ url_path: "/SetValuesByCategory", user_data: m.sign.SetValuesByCategoryAA, passwords: credentials })
          m.sign.localServer.AddPostToFormData({ url_path: "/SendUDP", user_data: m.sign.SendUdpRestAA })
  
        endif
  
      endif
  
    end if
    
    userVariables = m.currentUserVariables
    
      ' by default, process roStreamEnd events - initialize value here and give plugin a chance to override this behaviour.
    m.pluginProcessesStreamEndEvent = false
  
    ' Start BrightWallConfigurator server if either BrightWallSetupScreen is enabled or the published sign is for a BrightWall.
    if (IsBoolean(GetGlobalAA().registrySettings.brightwall_setup_screen_enabled) and GetGlobalAA().registrySettings.brightwall_setup_screen_enabled) or (type(m.sign) = "roAssociativeArray" and m.sign.isBrightWall) then
  
      m.brightWallConfiguratorServer = CreateObject("roHttpServer", { port: 8088 })
      m.brightWallConfiguratorServer.SetPort(m.msgPort)
      m.GetBrightWallConfiguratorPageAA = { HandleEvent: GetBrightWallConfiguratorPage, mVar: m }
  
      bwConfigCredentials = GetBwConfigCredentials()
  
      m.brightWallConfiguratorServer.AddGetFromEvent({ url_path: "/", user_data: m.GetBrightWallConfiguratorPageAA, passwords: bwConfigCredentials })
      m.brightWallConfiguratorPage$ = m.SetupDeviceWebPageServer(currentSyncSpec, m.brightWallConfiguratorServer, "brightWallConfiguratorSite-001", "index.html")
  
      m.AddBrightWallConfiguratorServerHandlers(m.brightWallConfiguratorServer)
  
    endif
  
    ' 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
      
      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-
        m.deviceWebPageFilePath$ = m.SetupDeviceWebPageServer(currentSyncSpec, m.sign.localServer, m.sign.customDeviceWebPage.name, m.sign.customDeviceWebPage.indexFileName)
        
      else
        
        m.deviceWebPageFilePath$ = GetPoolFilePath(m.assetPoolFiles, "Default_PresentationWebPage/_deviceWebPage.html")
        
      end if
      
    end if
  
    plugins = CreateObject("roArray", 1, true)
    ' plugin info should only get from current running presentation, not all presentations
    ' TODO: construct plugin payload once scripts are redesigned to load for each presentation
    if type(m.sign) = "roAssociativeArray" and type(currentSyncSpec) = "roSyncSpec" then
      ' Construct status payload to send to bootstrap
      scriptFiles = currentSyncSpec.FilterFiles("download", { group: "script" })
      scriptDownloadSection = scriptFiles.getFileList("download")
  
      for each scriptFile in scriptDownloadSection
        fileName = scriptFile.name
        if fileName <> "" and fileName <> "autoplugins.brs" and fileName <> "autorun.brs" then
          pluginFile = CreateObject("roAssociativeArray")
          pluginFile.AddReplace("fileName",fileName)
          pluginFile.AddReplace("fileSize",scriptFile.size)
          pluginFile.AddReplace("fileHash",scriptFile.hash)
          plugins.push(pluginFile)
        end if
      next
    end if
  
    m.ResendAutorunCapabilitiesToSupervisor(plugins, presentationName$)
    
    assetCollection = currentSyncSpec.GetAssets("download")
    m.view = CreateObject("roAssetCollectionView", m.assetPool, assetCollection)
  
    if type(m.nodeJsObjects) = "roArray" then
      for i% = 0 to m.nodeJsObjects.Count() - 1
        m.nodeJsObjects[i%] = invalid
      next
    endif
  
    m.nodeJsObjects = []
  
    ' Launch node apps
    for each nodeAppName in m.nodeApps
      nodeApp = m.nodeApps.Lookup(nodeAppName)
      syncSpecFileName$ = nodeApp.prefix$ + nodeApp.filePath$
      m.nodeJsObjects.push(CreateObject("roNodeJs", m.view.getpath() + nodeApp.prefix$ + nodeApp.filePath$, {}))
    next
  
    ' Notify controlling devices we have started playback
    m.SendUDPNotification("startPlayback")
    
  end sub
  
  
  Function GetBwConfigCredentials() as object
  
    bwConfigUserName$ = GetGlobalAA().registrySettings.bwConfigUserName
    bwConfigPassword$ = GetGlobalAA().registrySettings.bwConfigPassword
    
    if (len(bwConfigUserName$) + len(bwConfigPassword$)) > 0 then
      bwConfigCredentials = { }
      bwConfigCredentials.AddReplace(bwConfigUserName$, bwConfigPassword$)
    else
      bwConfigCredentials = invalid
    end if
  
    return bwConfigCredentials
  
  end function
  
  
  Sub SetSignType(autoplayPath$ as string)
  
    registrySection = GetGlobalAA().registrySection
    registrySettings = GetGlobalAA().registrySettings
  
    brightAuthor = GetAutoplay(autoplayPath$)
    if type(brightAuthor.signData) = "roArray" then
      meta = brightAuthor.signData[0].meta
      signType = meta.signType
    else
      meta = brightAuthor.meta
      signType = "Standard"
    endif
  
    if signType = "BrightWall" then
  
      registrySettings.isBrightWall = true
      registrySettings.signType = "BrightWall"
  
      ' compare to existing registry settings - if identical, do nothing
      if lcase(registrySection.Read("signType")) = "brightwall" then
          
        if BrightWallConfigChanged(registrySection, meta) then
  
          ' use case: user published BrightWall, prior publish was for a BrightWall but the BrightWall configuration has changed
          ' reset BrightWall settings and force user to go through Configurator.
  
          registrySettings.brightwall_num_rows% = meta.brightWallConfiguration.rowCount
          registrySection.write("brightwall_num_rows", StripLeadingSpaces(stri(meta.brightWallConfiguration.rowCount)))
  
          registrySettings.brightwall_num_columns%  = meta.brightWallConfiguration.columnCount
          registrySection.write("brightwall_num_columns", StripLeadingSpaces(stri(meta.brightWallConfiguration.columnCount)))
  
          registrySettings.brightwall_row_index% = -1
          registrySection.write("brightwall_row_index", "-1")
  
          registrySettings.brightwall_column_index% = -1
          registrySection.write("brightwall_column_index", "-1")
  
          registrySettings.brightwall_setup_screen_enabled = true
          registrySection.write("brightwall_setup_screen_enabled", "true")
  
          registrySettings.brightwall_device_setup_active_screen = "ConfigureScreen"
          registrySection.write("brightwall_device_setup_active_screen", "ConfigureScreen")
  
          registrySettings.sync_master = false
          registrySection.write("sync_master", "false")
          
        else
  
          ' load all BrightWall registry settings
          registrySettings.brightwall_setup_screen_enabled = GetBoolFromString(registrySection.Read("brightwall_setup_screen_enabled"), false)
          registrySettings.brightwall_num_rows% = int(val(registrySection.Read("brightwall_num_rows")))
          registrySettings.brightwall_num_columns% = int(val(registrySection.Read("brightwall_num_columns")))
          registrySettings.brightwall_row_index% = int(val(registrySection.Read("brightwall_row_index")))
          registrySettings.brightwall_column_index% = int(val(registrySection.Read("brightwall_column_index")))
          
          registrySettings.sync_master = GetBoolFromString(registrySection.Read("sync_master"), false)
  
        endif
  
      else  ' registrySection !== "BrightWall" but signType === "BrightWall"
  
        ' set the Brightwall registry settings to what's in the presentation; clear other settings; prepares unit to go into configurator
  
        registrySection.write("signType", "brightWall")
  
        registrySettings.brightwall_num_rows% = meta.brightWallConfiguration.rowCount
        registrySection.write("brightwall_num_rows", StripLeadingSpaces(stri(meta.brightWallConfiguration.rowCount)))
  
        registrySettings.brightwall_num_columns%  = meta.brightWallConfiguration.columnCount
        registrySection.write("brightwall_num_columns", StripLeadingSpaces(stri(meta.brightWallConfiguration.columnCount)))
  
        registrySettings.brightwall_row_index% = -1
        registrySection.write("brightwall_row_index", "-1")
  
        registrySettings.brightwall_column_index% = -1
        registrySection.write("brightwall_column_index", "-1")
  
        registrySettings.brightwall_setup_screen_enabled = true
        registrySection.write("brightwall_setup_screen_enabled", "true")
  
        registrySettings.brightwall_device_setup_active_screen = "ConfigureScreen"
        registrySection.write("brightwall_device_setup_active_screen", "ConfigureScreen")
  
        registrySettings.sync_master = false
        registrySection.write("sync_master", "false")
        
      endif
    
      registrySettings.brightwall_bezel_left = GetDoubleFromRegistry(registrySection, "brightwall_bezel_left")
      registrySettings.brightwall_bezel_right = GetDoubleFromRegistry(registrySection, "brightwall_bezel_right")
      registrySettings.brightwall_bezel_top = GetDoubleFromRegistry(registrySection, "brightwall_bezel_top")
      registrySettings.brightwall_bezel_bottom = GetDoubleFromRegistry(registrySection, "brightwall_bezel_bottom")
      registrySettings.brightwall_bezel_screen_width = GetDoubleFromRegistry(registrySection, "brightwall_bezel_screen_width")
      registrySettings.brightwall_bezel_screen_height = GetDoubleFromRegistry(registrySection, "brightwall_bezel_screen_height")
  
    else  ' signType !== "BrightWall"
  
      '  Reset several BrightWall related registrySettings properties
      registrySettings.isBrightWall = false
      registrySettings.signType = "Standard"
      registrySettings.brightwall_setup_screen_enabled = false
      registrySettings.sync_master = false
      
      registrySection.write("signType", "Standard")
  
    endif
  
  end sub
  
  
  Function GetDoubleFromRegistry(registrySection as object, registryKey as string) as double
  
    registrySetting = registrySection.Read(registryKey)
    if registrySetting = "" then
      registryValue = 0
    else
      registryValue = val(registrySetting)
    endif
  
    return registryValue
  
  end function
  
  
  Function BrightWallConfigChanged(registrySection as object, meta as object) as boolean
  
    if meta.brightWallConfiguration.rowCount <> int(val(registrySection.Read("brightwall_num_rows"))) then
      return true
    endif
  
    if meta.brightWallConfiguration.columnCount <> int(val(registrySection.Read("brightwall_num_columns"))) then
      return true
    endif
  
    return false
  
  end function
  
  
  Function SetupDeviceWebPageServer(currentSyncSpec as object, server as object, webPageName as string, webPageFileName as string) as string
  
    deviceWebPageFilePath$ = ""
  
    downloadFiles = currentSyncSpec.GetFileList("download")
  
    for each downloadFile in downloadFiles
      fileName$ = downloadFile.name
      ' the main web page is
      ' <site name>/<custom device web page file name>
      name$ = webPageName
      indexFileName$ = webPageFileName
      indexPoolPath$ = name$ + "/" + indexFileName$
      
      if fileName$ = indexPoolPath$ then
        
        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$)
              server.AddGetFromFile({ url_path: url$, filename: filePath$, content_type: contentType$ })
            end if
          end if
        end if
      end if
      
    next
  
    return deviceWebPageFilePath$
  
  end function
  
  
  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" or mimeType = "audio/ac3" or mimeType = "audio/eac3" 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"
    else if ext = "ac3"
      return "audio/ac3"
    else if ext = "eac3"
      return "audio/eac3"
      
      ' 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, videoConnector$ as string)
    
    userVariables = m.currentUserVariables
    suffix$ = "$"
    if videoConnector$ <> "" then suffix$ = "_" + videoConnector$ + "$"
    
    for each userVariableKey in userVariables
      userVariable = userVariables.Lookup(userVariableKey)
      if userVariable.systemVariable$ = "EdidMonitorSerialNumber" then
        m.UpdateVariable(userVariable, m.sysInfo["edidMonitorSerialNumber"+suffix$], postMsg)
      else if userVariable.systemVariable$ = "EdidYearOfManufacture" then
        m.UpdateVariable(userVariable, m.sysInfo["edidYearOfManufacture"+suffix$], postMsg)
      else if userVariable.systemVariable$ = "EdidMonitorName" then
        m.UpdateVariable(userVariable, m.sysInfo["edidMonitorName"+suffix$], postMsg)
      else if userVariable.systemVariable$ = "EdidManufacturer" then
        m.UpdateVariable(userVariable, m.sysInfo["edidManufacturer"+suffix$], postMsg)
      else if userVariable.systemVariable$ = "EdidUnspecifiedText" then
        m.UpdateVariable(userVariable, m.sysInfo["edidUnspecifiedText"+suffix$], postMsg)
      else if userVariable.systemVariable$ = "EdidSerialNumber" then
        m.UpdateVariable(userVariable, m.sysInfo["edidSerialNumber"+suffix$], postMsg)
      else if userVariable.systemVariable$ = "EdidManufacturerProductCode" then
        m.UpdateVariable(userVariable, m.sysInfo["edidManufacturerProductCode"+suffix$], postMsg)
      else if userVariable.systemVariable$ = "EdidWeekOfManufacture" then
        m.UpdateVariable(userVariable, m.sysInfo["edidWeekOfManufacture"+suffix$], 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
        m.UpdateVariable(userVariable, m.sysInfo.ipAddressWired$, postMsg)
      else if userVariable.systemVariable$ = "IpAddressWireless" then
        m.UpdateVariable(userVariable, 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
      
    for i% = 0 to webSocketEventBodyProperties.Count() - 1
      
      webSocketEventPropertyAA = webSocketEventBodyProperties[i%]
      eventBodyPropertyName = webSocketEventPropertyAa.name
      
      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, wssEventParameter as object, wssEventTransitionEventSpec as object, specifiedEventPropertyName as string) as object
    
    paramAttrs = { }
    paramAttrs.propertyNameBreadcrumbs = []
    
    ' if there is no parameter and everything to this point matched, then this is a match
    if type(wssEventParameter) <> "roAssociativeArray" then
      paramAttrs.matchFound = true
      return paramAttrs
    end if
    
    matchedPropertyName$ = ""
    propertyName = wssEventParameter.parameterName
    if propertyName = specifiedEventPropertyName then
      matchedPropertyName$ = propertyName
    else
      paramAttrs.matchFound = false
      return paramAttrs
    endif
  
    'properties = wssEventProperties.Lookup(matchedPropertyName$)
    
    'propertyName$ = properties.PropertyName
    'uniqueName$ = properties.UniqueName
    
    paramAttrs.matchFound = MatchToUniqueParameterName(wssCommunicationSpecEvent, wssEventParameter.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$ = wssEventParameter.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(wssEventParameter.parameterValue)
      
    end if
    
    return paramAttrs
    
  end function
  
  
  Function MatchWssEvent(wssCommunicationSpecEvent as object, webSocketEvent as object, wssEventParameter as object, wssEventTransitionEventSpec as object, specifiedEventPropertyName as string) as object
    
    autoplayEvent = { }
    
    ' endpoint for this event
    resource = webSocketEvent.header.resource
    
    paramAttrs = GetMatchedParameter(webSocketEvent, wssCommunicationSpecEvent, wssEventParameter, 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(wss as object) as object
    
    ' TODO - should this log file always be enabled?
    getGlobalAA().eddieDumpFile = CreateObject("roAppendFile", "eddie.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
      validDateTime$ = helper_ValidateInvalidPrint(dateTime$)
      m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_INVALID_DATE_TIME_SPEC, validDateTime$)
      m.bsp.diagnostics.PrintDebug("### Invalid roDateTime transformation : " + validDateTime$)
      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
          
          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
      
      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
      
      createDatagramReceiverObj = false
      
      if state.type$ = "mediaList" then
        createDatagramReceiverObj = m.CheckForSyncEventInEventList(state.transitionToNextEventList, createDatagramReceiverObj)
        createDatagramReceiverObj = m.CheckForSyncEventInEventList(state.transitionToPreviousEventList, createDatagramReceiverObj)
      endif
  
      if type(state.udpEvents) = "roAssociativeArray" or type(state.synchronizeEvents) = "roAssociativeArray" or createDatagramReceiverObj then
        
        m.bsp.CreateDatagramReceiver(m.bsp.udpReceivePort)      
  
        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.IsSyncMaster or m.IsSyncSlave) then
      aa = { }
      aa.Domain = m.id$
      m.syncManager = invalid
      m.syncManager = CreateObject("roSyncManager", aa)
      m.bsp.diagnostics.PrintDebug("@@@ roSyncManager created. Value of ptp domain:" + aa.Domain)
      m.syncManager.SetMasterMode(m.IsSyncMaster)
      
      if m.IsSyncSlave then
        m.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
  
  
  Function CheckForSyncEventInEventList(eventList as object, createDatagramReceiverObj as boolean) as boolean
    for each event in eventList
      if event.eventName = "synchronize" then
        m.IsSyncSlave = true
        createDatagramReceiverObj = true
      endif
    next
    return createDatagramReceiverObj
  end function
  
  
  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.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 not getGlobalAA().settings.lwsEnableUpdateNotifications 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 CreateDatagramReceiver(udpReceivePort)
  
    createDatagramReceiverObj = false
    
    if type(m.udpReceiver) <> "roDatagramReceiver" then
      createDatagramReceiverObj = true
    else
      if type(m.existingUdpReceivePort) = "roInt" and m.existingUdpReceivePort <> m.udpReceivePort then
        createDatagramReceiverObj = true
      end if
    end if
  
    if createDatagramReceiverObj then
      m.udpReceiver = CreateObject("roDatagramReceiver", udpReceivePort)
      ' Set user data to distinguish between presentation udp messages and bootstrap udp messages
      m.udpReceiver.SetUserData("receiver")
      m.udpReceiver.SetPort(m.msgPort)
      m.existingUdpReceivePort = m.udpReceivePort
    end if
      
  end sub
  
  
  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
  
    gaa = GetGlobalAA()
  
    usbConnectorNameToUsbSpec = gaa.usbConnectorNameToUsbSpec
    ' BCN-11195
    ' If Bose serial device is defined in presentation but not connected when creating serial object
    ' bypass this serial deivce port creation by returning true
    ' This check is agnostic to the successor conditions
    if not IsSerialPortNumberOfTypeString(port$) and m.sign.boseProductsByConnector <> invalid and m.sign.boseProductsByConnector[port$] <> invalid and not usbConnectorNameToUsbSpec.DoesExist(port$) then
      di = CreateObject("roDeviceInfo")
      connectedUSBDevices = di.GetUSBTopology({ array : true })
  
      boseProduct = m.sign.boseProductsByConnector[port$]
      GetConnectedUSBDeviceName(m.bsp, di.GetModel(), connectedUSBDevices, port$, boseProduct.usbInternalHub$)
      if not usbConnectorNameToUsbSpec.DoesExist(port$) then
        ' Force the retry operation to not occur since the serial device is not connected
        m.diagnostics.PrintDebug("Attempt to open serial port " + port$ + " failed because device is not connected")
        return true
      end if
    end if
  
    if IsUsbPort(port$) then
  
      usbSpec = usbConnectorNameToUsbSpec.Lookup(port$)
      hidOutputSpec = usbSpec.hidOutputSpec
  
      usbHIDPortConfiguration = gaa.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 if IsSerialPortNumberOfTypeString(port$) then ' validation for port integers represented as strings, e.g., "0", "1", etc
  
      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 m.diagnostics.PrintDebug("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
    else
  
      m.diagnostics.PrintDebug("Error with serial port value validation: " + port$)
      return false    
    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
  
  
  ' Check whether input value is a integer represented as a string type
  Function IsSerialPortNumberOfTypeString(inputVariable as string) as boolean
  
    if not IsString(inputVariable) then return false
    if (inputVariable = "0" or inputVariable = "1" or inputVariable = "2" or inputVariable = "3" or inputVariable = "4" or inputVariable = "5" or inputVariable = "6" or inputVariable = "7") then
      return true
    end if
  
    return false
  
  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 IsBoolean(inputVariable as object) as boolean
  
    if type(inputVariable) = "roBoolean" or type(inputVariable) = "boolean" then return true
    return false
  
  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
  
  
  ' This function is similar to lodash get, given an object and the path, trying to get
  ' the field with type check. If type check failed or cannot find the field, the default
  ' will be returned.
  ' @params
  ' startObj: the object to start the search
  ' path$: the path to look down in startObj
  ' targetType$: the type that returned result from object should match
  ' theDefault: the default value returns if nothing is found
  Function getVarFromObj(startObj as object, path$ as string, targetType$ as string, theDefault)
    ' split path$ by dot to prepare for looking in nested object
    regex = CreateObject("roRegEx", "\.", "i")
    pathArray = regex.Split(path$)
  
    ' if cannot get the correct path, return the original object, or the default depends on the type check
    if type(pathArray) <> "roList" or pathArray.Count() = 0 then 
      if type(startObj) = targetType$ then
        return startObj
      else
        return theDefault
      end if
    end if
  
    targetObj = startObj
  
    ' looping down the object to find the field
    for i% = 0 to pathArray.Count() - 1
      targetObj = targetObj[pathArray[i%]]
      
      if i% = pathArray.Count() - 1 then
        ' if it's the last field, check type of the result and return depend on the type check
        if type(targetObj) = targetType$ then
          return targetObj
        else
          return theDefault
        end if
      else
        ' if has not reached the last field, the type has to be object to continue the search
        if type(targetObj) <> "roAssociativeArray" then return theDefault
      end if
    next
  end function
  
  
  ' Helper function to get the value of the text parameter from a parameters object.
  ' Should only be used with object that has GetCurrentParameterValue() function.
  Function getTextParameterFallbackToEmpty(parameters as object, key as string) as string
    
    parameter = parameters[key]
    returnVal$ = ""
    if type(parameter) = "roAssociativeArray" and IsString(parameter.GetCurrentParameterValue()) then
      returnVal$ = parameter.GetCurrentParameterValue()
    end if
  
    return returnVal$
  
  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
    bsp.videoModePlugins = []
    for each videoModePluginSpec in meta.videoModePlugins
      videoModePlugin = jsonParseVideoModePlugin(videoModePluginSpec)
      bsp.videoModePlugins.push(videoModePlugin)
    next
  
    'script plugins
    bsp.scriptPlugins = []
    for each scriptPluginSpec in meta.scriptPlugins
      scriptPlugin = jsonParseScriptPlugin(scriptPluginSpec)
      bsp.scriptPlugins.push(scriptPlugin)
    next
  
    Sign.screens = BrightAuthor.screens
    
    videoMode = GetVideoMode()
    if type(videoMode) = "roVideoMode" then
      ' check if player can be rotated by screen
      if CanRotateByScreen(Sign, videoMode) then
        screenModes = videoMode.GetScreenModes()
        screensInUse = {}
  
        hasScreenEnabled = false
        ' loop through the 2D array to configure video mode of each screen
        for each screenRow in Sign.screens
          if type(screenRow) = "roArray" then
            for each screen in screenRow            
              ' screen is a valid object
              if type(screen) = "roAssociativeArray" and type(screen.dimensions) = "roAssociativeArray" then
                name$ = screen.videoConnectorName
                ' Update the corresponding settings to the OS video output array
                videoOutputOSIndex% = GetVideoOutputIndexFromOS(name$, screenModes)
                
                ' non-negative index means the connector name can be found in OS array
                if videoOutputOSIndex% >= 0 then
                  videoMode$ = screen.videoMode
  
                  forceResolution = screen.forceResolution
                  if forceResolution = invalid or forceResolution = false then
                    videoMode$ = videoMode$ + ":preferred"
                  end if
                  
                  dolbyVisionEnabled = screen.dolbyVisionEnabled
                  if dolbyVisionEnabled = true then
                    videoMode$ = videoMode$ + ":dbv"
                  end if
                  
                  fullResGraphicsEnabled = screen.fullResGraphicsEnabled
                  if fullResGraphicsEnabled = true then
                    videoMode$ = videoMode$ + ":fullres"
                  end if
                  
                  tenBitColorEnabled = screen.tenBitColorEnabled
                  if tenBitColorEnabled = true then
                    videoMode$ = videoMode$ + ":10bit"
                  end if
  
                  screenModes[videoOutputOSIndex%].video_mode = videoMode$
                  ' transform has options normal, 90, 180 or 270
                  orientation$ = lcase(screen.monitorOrientation)
                  if orientation$ = "portraitbottomleft" then
                    screenModes[videoOutputOSIndex%].transform = "90"
                  else if orientation$ = "portraitbottomright" then
                    screenModes[videoOutputOSIndex%].transform = "270"
                  else
                    screenModes[videoOutputOSIndex%].transform = "normal"
                  end if
  
                  displayLeftX = screen.dimensions.x
                  displayTopY = screen.dimensions.y
                  
                  ' negative display_x and display_y is okay for OS to handle
                  screenModes[videoOutputOSIndex%].display_x = displayLeftX
                  screenModes[videoOutputOSIndex%].display_y = displayTopY
  
                  screenModes[videoOutputOSIndex%].enabled = true
                  screensInUse.AddReplace(stri(videoOutputOSIndex%), screen.name)
  
                  hasScreenEnabled = true
                end if
              end if
            next
          end if
        next
  
        ' only make updates when at least one screen is enabled
        if hasScreenEnabled = true then
          ' disable any HDMI ports that wasn't mentioned in the array
          for i% = 0 to screenModes.Count() - 1
            if (not screensInUse.DoesExist(stri(i%))) then screenModes[i%].enabled = false
          next
        end if
  
        ok = videoMode.SetScreenModes(screenModes)
        ' error handling in case it fails
        if ok = 0 then
          errMsg$ = "Error: Can't set VIDEOMODE screen array. Resetting to 1920x1080x60i"
          bsp.diagnostics.PrintDebug(errMsg$)
          bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_SET_VIDEO_MODE, errMsg$)
          videoMode.SetMode("1920x1080x60i")
        end if
  
        ' we do not support video wall and multiple screens at the same time
        Sign.isBrightWall = false
  
      else
        ' players do not support multi screens
        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
          
          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
  
        registrySettings = GetGlobalAA().registrySettings
  
        Sign.isBrightWall = registrySettings.isBrightWall
        if Sign.isBrightWall then
          Sign.brightwall_num_rows% = registrySettings.brightwall_num_rows%
          Sign.brightwall_num_columns% = registrySettings.brightwall_num_columns%
          Sign.brightWallRowPosition% = registrySettings.brightwall_row_index%
          Sign.brightWallColumnPosition% = registrySettings.brightwall_column_index%
  
          Sign.bezelWidthInPixels = 0
          Sign.bezelHeightInPixels = 0
  
          videoMode = GetVideoMode()
          if type(videoMode) = "roVideoMode" then
  
            screenWidth = videoMode.GetResX()
            screenHeight = videoMode.GetResY()
  
            if (registrySettings.brightwall_bezel_screen_width > 0) then
              Sign.bezelWidthInPixels = int(((registrySettings.brightwall_bezel_left + registrySettings.brightwall_bezel_right) / registrySettings.brightwall_bezel_screen_width) * screenWidth)
            else
              Sign.bezelWidthInPixels = 0
            endif
  
            if (registrySettings.brightwall_bezel_screen_height >0) then
              Sign.bezelHeightInPixels = int(((registrySettings.brightwall_bezel_top + registrySettings.brightwall_bezel_bottom) / registrySettings.brightwall_bezel_screen_height) * screenHeight)
            else
              Sign.bezelHeightInPixels = 0
            endif
  
          endif
        endif
  
        videoMode = invalid
      end if
  
    endif
  
    Sign.monitorOrientation = meta.monitorOrientation
  
    Sign.deviceWebPageDisplay$ = meta.deviceWebPageDisplay
    Sign.customDeviceWebPage = meta.customDeviceWebPage
  
    Sign.enableSettingsHandler = meta.enableSettingsHandler
  
    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
  
    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
      
      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$, systemVariable$)
        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
  
      videoConnector$ = getVarFromObj(userVariableSpec, "videoConnector$", "roString", "")
      if videoConnector$ <> "" then userVariable.videoConnector$ = videoConnector$
      
    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?
  
    ' 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, meta)
  
    ' 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.ptpDomain$ = meta.ptpDomain$
  
    if (Sign.enableEnhancedSynchronization) then
  
      rebootRequired = false
      
      ' check the domain value in the registry. if it does not exist or is different, set it and reboot.
      ptpDomainInRegistry$ = gaa.registrySection.Read("ptp_domain")
      if ptpDomainInRegistry$ <> Sign.ptpDomain$ then
        gaa.registrySection.Write("ptp_domain", Sign.ptpDomain$)
        bsp.diagnostics.PrintDebug("@@@ PTP domain value written to registry:" + Sign.ptpDomain$)
        rebootRequired = true
      end if
      
      if rebootRequired then
        gaa.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.hdmi1MinVolume% = meta.hdmi1MinVolume
    Sign.hdmi1MaxVolume% = meta.hdmi1MaxVolume
    Sign.hdmi2MinVolume% = meta.hdmi2MinVolume
    Sign.hdmi2MaxVolume% = meta.hdmi2MaxVolume
    Sign.hdmi3MinVolume% = meta.hdmi3MinVolume
    Sign.hdmi3MaxVolume% = meta.hdmi3MaxVolume
    Sign.hdmi4MinVolume% = meta.hdmi4MinVolume
    Sign.hdmi4MaxVolume% = meta.hdmi4MaxVolume
    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 nodeAppDescription in meta.nodeAppDescriptions
      nodeApp = newNodeApp(bsp, nodeAppDescription)
      bsp.nodeApps.AddReplace(nodeApp.name$, nodeApp)
    next
  
    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 CanUseScreenModes(sign as object, videoMode as object) as boolean
    if type(videoMode) <> "roVideoMode" then videoMode = GetVideoMode()
  
    ' GetScreenModes is a function introduced in Series 5 players. 
    ' Therefore we need to check if it's supported before use to avoid errors in lower series.
    if type(videoMode) = "roVideoMode" and findMemberFunction(videoMode, "GetScreenModes") <> invalid then
      screenModes = videoMode.GetScreenModes()
      if type(screenModes) = "roArray" and screenModes.Count() > 0 and type(sign.screens) = "roArray" and sign.screens.Count() > 0 then return true
    end if
    return false
  end function
  
  
  ' CanRotateByScreen and CanUseScreenModes return the same results.
  ' Split in two functions to make it more sense with where they are called.
  Function CanRotateByScreen(sign as object, videoMode as object) as boolean
    return CanUseScreenModes(sign, videoMode)
  end function
  
  
  ' HasMultiScreenOutputs will return a subset of CanUseScreenModes where the OS supports more than 1 video output
  Function HasMultiScreenOutputs(sign as object) as boolean
    videoMode = GetVideoMode()
    return CanUseScreenModes(sign, videoMode) and (videoMode.GetScreenModes().Count() > 1)
  end function
  
  
  Function GetVideoOutputIndexFromOS(name as string, screenModes as object) as integer
    for i% = 0 to screenModes.Count() - 1
      if screenModes[i%].name = name then return i%
    next
    return -1
  end function
  
  
  Function BuildBoseProductsByConnector(bsp as object, meta 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$)
      
      if type(meta.wssDeviceSpec) = "roAssociativeArray" then
        boseProduct.wssCommunicationSpec = ParseBoseWssCommunicationSpec(meta.wssDeviceSpec)
      endif
  
      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$
    
    ' Bose special - BCN-9689
    if m.bsp.sysinfo.deviceModel$ = "AU325" then
      print "**--**"
      print m.name$
      print m.currentValue$
    endif
  
    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
  
  
  Sub SetCurrentValueIfNewer(timestamp as integer, value as object, postMsg as boolean)
  
    if m.lastModifiedTime > timestamp then
      return
    endif
  
    m.SetCurrentValue(value, postMsg)
  
    m.lastModifiedTime = timestamp
  
  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, systemVariable$ as string) as object
    
    userVariable = { }
    userVariable.GetCurrentValue = GetCurrentUserVariableValue
    userVariable.SetCurrentValue = SetCurrentUserVariableValue
    userVariable.SetCurrentValueIfNewer = SetCurrentValueIfNewer
    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
    userVariable.systemVariable$ = systemVariable$
    userVariable.lastModifiedTime = 0
  
    return userVariable
    
  end function
  
  
  ' BSP User Variable functions
  
  Sub UpdateVariable(userVariable as object, value as object, postMsg as boolean)
    userVariable.SetCurrentValue(value, postMsg)
    userVariableUpdates = {}
    userVariableUpdates[userVariable.name$] = value
    m.BroadcastUserVariableUpdatesToBrightWall(userVariableUpdates)
  end sub
  
  
  ' Function that updates variables on slave in Brightwall. Variable payload was sent in SyncManager payload
  ' Variables are only updated if the timestamp in the payload is more recent than the last modified time of the variable
  Sub UpdateUserVariablesFromBrightWallPayload(payloadTimestamp as string, userVariablesPayload as object)
  
    registrySettings = GetGlobalAA().registrySettings
    if not registrySettings.isBrightWall or registrySettings.sync_master then
      return
    endif
  
    dt = CreateObject("roDateTime")
    ok = dt.FromIsoString(payloadTimestamp)
    payloadTimeSinceEpoch = dt.ToSecondsSinceEpoch()
  
    variableUpdated = false
  
    for each variableName in userVariablesPayload
  
      newVariableValue$ = userVariablesPayload.Lookup(variableName)
  
      userVariable = m.GetUserVariable(variableName)
      if type(userVariable) = "roAssociativeArray" then
        currentVariableValue$ = userVariable.GetCurrentValue()      
        if (newVariableValue$ <> currentVariableValue$) then
          variableUpdated = true
          userVariable.SetCurrentValueIfNewer(payloadTimeSinceEpoch, newVariableValue$, false)
        endif
      else
        ' this can occur if the slave receives a sync event from the master after the slave has changed to a new presentation
        ' one scenario where this occurs is publishing using lfn where the slave is updated before the master
        m.diagnostics.PrintDebug("User variable " + variableName + " not found.")
        m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND, variableName)
      end if
  
    next
  
    if variableUpdated then
      userVariablesChanged = { }
      userVariablesChanged["EventType"] = "USER_VARIABLES_UPDATED"
      m.msgPort.PostMessage(userVariablesChanged)
    
      ' Notify controlling devices to refresh
      m.SendUDPNotification("refresh")
    endif
  
  end sub
  
  
  Sub BroadcastUserVariableUpdatesToBrightWall(updatedUserVariables as object)
    
    ' only broadcast if this device is acting as a BrightWall master
    registrySettings = GetGlobalAA().registrySettings
    
    if registrySettings.isBrightWall and registrySettings.sync_master then
  
      timestamp = m.GetCurrentTimeAsIsoString()
  
      payload = {}
      payload.type = "userVariableUpdates"
      payload.timestamp = timestamp
      payload.userVariables = updatedUserVariables
  
      payload$ = FormatJson(payload)
  
      m.bwUdpSender.Send(payload$)
  
    endif
  
  end sub
  
  
  Function GetUserVariablesAsPayload() as object
  
    userVariablesPayload = {}
  
    userVariables = m.currentUserVariables
    for each userVariableName in userVariables
      userVariable = userVariables.Lookup(userVariableName)
      userVariableValue = userVariable.GetCurrentValue()
      userVariablesPayload.AddReplace(userVariableName, userVariableValue)
    next
  
    return userVariablesPayload
  
  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
  
  
  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)
  
    suffix$ = "$"
    videoConnector$ = getVarFromObj(userVariable, "videoConnector$", "roString", "")
    if videoConnector$ <> "" then suffix$ = "_" + videoConnector$ + "$"
  
    if userVariable.systemVariable$ = "SerialNumber" then
      m.UpdateVariable(userVariable, m.sysInfo.deviceUniqueID$, false)
    else if userVariable.systemVariable$ = "IPAddressWired" then
      m.UpdateVariable(userVariable, m.sysInfo.ipAddressWired$, false)
    else if userVariable.systemVariable$ = "IPAddressWireless" then
      m.UpdateVariable(userVariable, m.sysInfo.ipAddressWireless$, false)
    else if userVariable.systemVariable$ = "FirmwareVersion" then
      m.UpdateVariable(userVariable, m.sysInfo.deviceFWVersion$, false)
    else if userVariable.systemVariable$ = "ScriptVersion" then
      m.UpdateVariable(userVariable, m.sysInfo.autorunVersion$, false)
    else if userVariable.systemVariable$ = "EdidMonitorSerialNumber" then
      m.UpdateVariable(userVariable, m.sysInfo["edidMonitorSerialNumber"+suffix$], false)
    else if userVariable.systemVariable$ = "EdidYearOfManufacture" then
      m.UpdateVariable(userVariable, m.sysInfo["edidYearOfManufacture"+suffix$], false)
    else if userVariable.systemVariable$ = "EdidMonitorName" then
      m.UpdateVariable(userVariable, m.sysInfo["edidMonitorName"+suffix$], false)
    else if userVariable.systemVariable$ = "EdidManufacturer" then
      m.UpdateVariable(userVariable, m.sysInfo["edidManufacturer"+suffix$], false)
    else if userVariable.systemVariable$ = "EdidUnspecifiedText" then
      m.UpdateVariable(userVariable, m.sysInfo["edidUnspecifiedText"+suffix$], false)
    else if userVariable.systemVariable$ = "EdidSerialNumber" then
      m.UpdateVariable(userVariable, m.sysInfo["edidSerialNumber"+suffix$], false)
    else if userVariable.systemVariable$ = "EdidManufacturerProductCode" then
      m.UpdateVariable(userVariable, m.sysInfo["edidManufacturerProductCode"+suffix$], false)
    else if userVariable.systemVariable$ = "EdidWeekOfManufacture" then
      m.UpdateVariable(userVariable, m.sysInfo["edidWeekOfManufacture"+suffix$], false)
    else if userVariable.systemVariable$ = "ActivePresentation" then
      if IsString(m.activePresentation$) then
        m.UpdateVariable(userVariable, m.activePresentation$, false)
      else
        m.UpdateVariable(userVariable, "", false)
      end if
    else if userVariable.systemVariable$ = "BrightAuthorVersion" then
      m.UpdateVariable(userVariable, m.sysInfo.baconVersion$, false)
    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 object) as string
    
    if IsBoolean(val) then
      if val then
        return "yes"
      endif
    else if IsInteger(val) then
      if val = 1 then
        return "yes"
      endif
    endif
  
    return "no"
    
  end function
  
  
  Function IsTruthy(value)
    valueType = type(value)
    if valueType = "Boolean" or valueType = "roBoolean" then
      if value = true or value = True then
        return true
      else if value = false or value = False then
        return false
      end if
    else if valueType = "String" or valueType = "roString" then
      if value = "true" or value = "True" or value = "1" or value = "on" or value = "On" or value = "yes" or value = "Yes" then
        return true
      else if value = "false" or value = "False" or value = "0" or value = "off" or value = "Off" or value = "no" or value = "No" then
        return false
      end if
    else if valueType = "Integer" or valueType = "roInteger" then
      if value = 1 then
        return true
      else if value = 0 then
        return false
      end if
    end if
    return invalid
  end function
  
  
  Function GetValidBool(value, defaultValue as boolean) as boolean
    returnValue = IsTruthy(value)
    if returnValue = invalid then
      returnValue = defaultValue
    endif
    return returnValue
  end function
  
  
  Function GetBoolFromString(value, defaultValue as boolean) as boolean
  
    if IsString(value) and value = "" then
      return defaultValue
    endif
  
    val = IsTruthy(value)
    if val = invalid then
      return defaultValue
    else
      return val
    endif
    
  end function
  
  
  Function GetStringFromBool(boolValue as boolean) as string
  
    if boolValue then
      return "true"
    else
      return "false"
    endif
  
  end function
  
  
  Function GetIntFromString(str$ as string) as integer
    if IsString(str$) and str$ <> "" then
      return int(val(str$))
    else
      return 0
    end if
  end function
  
  
  Function GetValidInt(val, defaultValue as Integer) as Integer
    if IsInteger(val) then
      return val
    else
      return defaultValue
    end if
  end function
  
  
  Function GetValidString(val, defaultValue as string) as String
    if IsString(val) then
      return val
    else
      return defaultValue
    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.CheckForSyncEventInEventList = CheckForSyncEventInEventList
  
    zoneHSM.InitializeZoneCommon = InitializeZoneCommon
    zoneHSM.InitializeBrightWallZoneCommon = InitializeBrightWallZoneCommon
    zoneHSM.GetBrightWallRectangle = GetBrightWallRectangle
    
    zoneHSM.IsBrightWallMaster = IsBrightWallMaster
    zoneHSM.IsBrightWallSlave = IsBrightWallSlave
  
    zoneHSM.language$ = globalVariables.language$
  
    zoneHSM.IsSyncMaster = false  
    zoneHSM.IsSyncSlave = false
  
    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$ = "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$ = userEventData.port
  
      wssEvents = bsState.wssEvents
      if type(wssEvents[port$]) <> "roAssociativeArray" then
        wssEvents[port$] = { }
      end if
      
      wssEventData = transitionspec.userevent.data
      wssEventId = wssEventData.wssEventId
      wssEventName = wssEventData.wssEventName
      wssEventParameter = wssEventData.wssEventParameter
  
      wssPortEvents = wssEvents[port$]
      
      if type(wssPortEvents[wssEventId]) <> "roAssociativeArray" then
        wssPortEvents[wssEventId] = { }
      end if
      
      wssPortEvents[wssEventId].wssEventId = wssEventId
      wssPortEvents[wssEventId].wssEventName = wssEventName
      wssPortEvents[wssEventId].wssEventParameter = wssEventParameter
      
      if type(wssPortEvents[wssEventId].wssEventTransitionEventSpecs) <> "roAssociativeArray" then
        wssPortEvents[wssEventId].wssEventTransitionEventSpecs = { }
      end if
      
      wssEventTransitionEventSpecs = wssPortEvents[wssEventId].wssEventTransitionEventSpecs
  
      if type(wssEventParameter) = "roAssociativeArray" then
        propertyName = wssEventParameter.parameterName
        if not wssEventTransitionEventSpecs.DoesExist(propertyName) then
          wssEventTransitionEventSpecs.AddReplace(propertyName, { })
        end if
        wssEventTransitionSpecByPropertyName = wssEventTransitionEventSpecs.Lookup(propertyName)
        keyPropertyValue = wssEventParameter.parameterValue
        wssEventTransitionSpecByPropertyName.AddReplace(keyPropertyValue, transition)
      endif
  
      if wssEventTransitionEventSpecs.IsEmpty() then
        wssEventTransitionSpec = { }
        wssEventTransitionSpec.transition = transition
        wssEventTransitionSpec.wssEventData = wssEventData
        wssPortEvents[wssEventId] = 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$ = "multiAssetVideo" 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
      
      zoneHSM.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.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 = { }
    
    ' 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.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, sign, stateDescription.mediaListItem, zoneHSM, state, item)
      state.type$ = "mediaList"
      
    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"
      
    else if type(stateDescription.multiAssetItem) = "roAssociativeArray" then
      
      newMultiAssetPlaylistItem(bsp, stateDescription.multiAssetItem, state)
  
      if state = invalid then
        return state
      endif
      
    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.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.GetBrightWallVideoPlayerRectangle = GetBrightWallVideoPlayerRectangle
    state.PlayVideo = PlayVideo
    state.PostPlayVideo = PostPlayVideo
    state.ConfigureBPButtons = ConfigureBPButtons
    state.ConfigureGPIOButtons = ConfigureGPIOButtons
  
    state.IsSubscribedToVideoEndEvent = IsSubscribedToVideoEndEvent
  
    newMediaPlaylistItem(bsp, playlistItemDescription, state, playlistItemBS)
    
    playlistItemBS.automaticallyLoop = playlistItemDescription.automaticallyLoop
    
  end sub
  
  
  Function IsSubscribedToVideoEndEvent()
  
    if type(m.videoEndEvent) = "roAssociativeArray" then
      return true
    else
      return false
    endif
    
  end function
  
  
  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
    state.IsSubscribedToVideoEndEvent = IsSubscribedToVideoEndEvent
    
  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
    state.IsSubscribedToVideoEndEvent = IsSubscribedToVideoEndEvent
    state.GetBrightWallVideoPlayerRectangle = GetBrightWallVideoPlayerRectangle
    
  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
  
    templateItem.videoConnector$ = getVarFromObj(templateItemDescription, "data.videoConnector", "String", "")
    
    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
  
    templateItem.videoConnector$ = getVarFromObj(templateItemDescription, "data.videoConnector", "String", "")
    
    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.GetBrightWallVideoPlayerRectangle = GetBrightWallVideoPlayerRectangle
    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 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.GetBrightWallVideoPlayerRectangle = GetBrightWallVideoPlayerRectangle
    state.PlayVideo = PlayVideo
    state.PostPlayVideo = PostPlayVideo
    state.SetVideoTimeCodeEvents = SetVideoTimeCodeEvents
    state.SendMrssSynchronizeRuntimeCommand = SendMrssSynchronizeRuntimeCommand
    
    state.GetAnyMediaRSSTransition = GetAnyMediaRSSTransition
    state.IsSubscribedToVideoEndEvent = IsSubscribedToSignChannelEndEvent
    
  end sub
  
  
  Function IsSubscribedToSignChannelEndEvent()
  
    if type(m.signChannelEndEvent) = "roAssociativeArray" then
      return true
    else
      return false
    endif
    
  end function
  
  
  Sub newMultiAssetPlaylistItem(bsp as object, playlistItemDescription as object, state as object)
  
    registrySettings = GetGlobalAA().registrySettings
  
    if registrySettings.isBrightWall then
      contentItems = playlistItemDescription.contentItems
      for each contentItem in contentItems
        if registrySettings.brightwall_row_index% = contentItem.row and registrySettings.brightwall_column_index% = contentItem.column then
          item = {}
          newVideoPlaylistItem(bsp, contentItem.contentItem, state, item)
          state.videoItem = item
          state.type$ = "multiAssetVideo"
          return
        endif
      next
    endif
  
    bsp.diagnostics.PrintDebug("newMultiAssetPlaylistItem - no matching row or column - indicates UI/publishing issue")
    
  end sub
  
  
  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.GetBrightWallVideoPlayerRectangle = GetBrightWallVideoPlayerRectangle
    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
    state.IsSubscribedToVideoEndEvent = IsSubscribedToVideoEndEvent
    
  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
    state.IsSubscribedToVideoEndEvent = IsSubscribedToVideoEndEvent
    
  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
    state.IsSubscribedToVideoEndEvent = IsSubscribedToVideoEndEvent
    
  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
    state.IsSubscribedToVideoEndEvent = IsSubscribedToVideoEndEvent
    
  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 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["hdmi1"]
    hdmi1$ = ""
    if type(parameter) = "roAssociativeArray" and IsString(parameter.GetCurrentParameterValue()) then
      hdmi1$ = parameter.GetCurrentParameterValue()
    end if
  
    parameter = parameters["hdmi2"]
    hdmi2$ = ""
    if type(parameter) = "roAssociativeArray" and IsString(parameter.GetCurrentParameterValue()) then
      hdmi2$ = parameter.GetCurrentParameterValue()
    end if
  
    parameter = parameters["hdmi3"]
    hdmi3$ = ""
    if type(parameter) = "roAssociativeArray" and IsString(parameter.GetCurrentParameterValue()) then
      hdmi3$ = parameter.GetCurrentParameterValue()
    end if
  
    parameter = parameters["hdmi4"]
    hdmi4$ = ""
    if type(parameter) = "roAssociativeArray" and IsString(parameter.GetCurrentParameterValue()) then
      hdmi4$ = parameter.GetCurrentParameterValue()
    end if
    
    parameter = parameters["spdif"]
    spdif$ = parameter.GetCurrentParameterValue()
  
    hasMultiScreenOutputsBool = HasMultiScreenOutputs(m.Sign)
  
    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")
  
    if hasMultiScreenOutputsBool = true then
      hdmi1AudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector("HDMI:1"))
      hdmi2AudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector("HDMI:2"))
      hdmi3AudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector("HDMI:3"))
      hdmi4AudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector("HDMI:4"))
    else
      hdmiAudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector("HDMI"))
    end if
  
    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 hasMultiScreenOutputsBool = true then
      AddAudioOutputByType(hdmi1$, hdmi1AudioOutput, compressed, pcm)
      AddAudioOutputByType(hdmi2$, hdmi2AudioOutput, compressed, pcm)
      AddAudioOutputByType(hdmi3$, hdmi3AudioOutput, compressed, pcm)
      AddAudioOutputByType(hdmi4$, hdmi4AudioOutput, compressed, pcm)
    else
      AddAudioOutputByType(hdmi$, hdmiAudioOutput, compressed, pcm)
    end if
  
    AddAudioOutputByType(spdif$, spdifAudioOutput, compressed, pcm)
  
    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" or category = "NET" 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" or ucase(category) = "NET")  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("HDMI:1")
    m.UnmuteAudioConnector("HDMI:2")
    m.UnmuteAudioConnector("HDMI:3")
    m.UnmuteAudioConnector("HDMI:4")
    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, "hdmi:1", "HDMI:1")
    m.MuteAudioOutput(muteOn, parameters, "hdmi:2", "HDMI:2")
    m.MuteAudioOutput(muteOn, parameters, "hdmi:3", "HDMI:3")
    m.MuteAudioOutput(muteOn, parameters, "hdmi:4", "HDMI:4")
    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" or lcase(connector$) = "analog1" 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$) = "hdmi1" then
      m.hdmi1Volume% = ExecuteChangeConnectorVolume("HDMI:1", volume%, m.sign.hdmi1MinVolume%, m.sign.hdmi1MaxVolume%)
    else if lcase(connector$) = "hdmi2" then
      m.hdmi2Volume% = ExecuteChangeConnectorVolume("HDMI:2", volume%, m.sign.hdmi2MinVolume%, m.sign.hdmi2MaxVolume%)
    else if lcase(connector$) = "hdmi3" then
      m.hdmi3Volume% = ExecuteChangeConnectorVolume("HDMI:3", volume%, m.sign.hdmi3MinVolume%, m.sign.hdmi3MaxVolume%)
    else if lcase(connector$) = "hdmi4" then
      m.hdmi4Volume% = ExecuteChangeConnectorVolume("HDMI:4", volume%, m.sign.hdmi4MinVolume%, m.sign.hdmi4MaxVolume%)
    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$) = "hdmi1" then
      m.hdmi1Volume% = ExecuteChangeConnectorVolume("HDMI:1", m.hdmiVolume% + volumeDelta%, m.sign.hdmi1MinVolume%, m.sign.hdmi1MaxVolume%)
    else if lcase(connector$) = "hdmi2" then
      m.hdmi2Volume% = ExecuteChangeConnectorVolume("HDMI:2", m.hdmiVolume% + volumeDelta%, m.sign.hdmi2MinVolume%, m.sign.hdmi2MaxVolume%)
    else if lcase(connector$) = "hdmi3" then
      m.hdmi3Volume% = ExecuteChangeConnectorVolume("HDMI:3", m.hdmiVolume% + volumeDelta%, m.sign.hdmi3MinVolume%, m.sign.hdmi3MaxVolume%)
    else if lcase(connector$) = "hdmi4" then
      m.hdmi4Volume% = ExecuteChangeConnectorVolume("HDMI:4", m.hdmiVolume% + volumeDelta%, m.sign.hdmi4MinVolume%, m.sign.hdmi4MaxVolume%)
    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
    
    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
  
  
  ' Send the CEC display on command through the specified video connector
  Sub CecDisplayOn(parameters as object)
  
    videoConnector$ = getTextParameterFallbackToEmpty(parameters, "videoConnector")
    m.SendCecCommand("400D", true, videoConnector$)
    
  end sub
  
  
  ' Send the CEC display off command through the specified video connector
  Sub CecDisplayOff(parameters as object)
    
    videoConnector$ = getTextParameterFallbackToEmpty(parameters, "videoConnector")
    m.SendCecCommand("4036", true, videoConnector$)
    
  end sub
  
  
  ' Send the CEC set source command through the specified video connector
  Sub CecSetSourceToBrightSign(parameters as object)
    
    videoConnector$ = getTextParameterFallbackToEmpty(parameters, "videoConnector")
    m.SendCecCommand("4F821000", false, videoConnector$)
    
  end sub
  
  
  ' Helper function of ExecuteCecPhilipsSetVolumeCommand
  Sub CecPhilipsSetVolume(volume% as integer, videoConnector$ as string)
    
    b = CreateObject("roByteArray")
    b[0] = volume%
    volumeAsAscii$ = b.ToHexString()
    b = invalid
    setVolume$ = "40A0000C3022" + volumeAsAscii$
    SendCecCommand(setVolume$, true, videoConnector$)
    
  end sub
  
  ' Helper function to send the CEC command
  Sub SendCecCommand(cecCommand$ as string, cecSubstituteSourceAddress as boolean, videoConnector$ as string)
    
    if videoConnector$ <> "" then
      cec = CreateObject("roCecInterface", videoConnector$)
    else
      cec = CreateObject("roCecInterface")
    end if
  
    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$
    
    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.GetZoneInScreensCount = GetZoneInScreensCount
    zoneHSM.ZoneOverlapsCurrentScreen = ZoneOverlapsCurrentScreen
  
    zoneHSM.SetSyncInfo = SetSyncInfo
  
    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$ = ""
    
    ' byte arrays to store stream byte input
    zoneHSM.serialStreamInputBuffers = CreateObject("roArray", 8, true)
    for i% = 0 to 7
      zoneHSM.serialStreamInputBuffers[i%] = CreateObject("roByteArray")
    next
    
    if not m.bsp.sign.isBrightWall then
      zoneHSM.rectangle = CreateObject("roRectangle", zoneHSM.x%, zoneHSM.y%, zoneHSM.width%, zoneHSM.height%)
    else
      m.InitializeBrightWallZoneCommon()
    endif
    
  end sub
  
  
  Function BuildUserData(userDataType as string, id as string) as object
    userData = {}
    userData.type = userDataType
    userData.id = id
    return userData
  end function
  
  
  Function UserDataMatches(userData as object, userDataType as string, id as string) as boolean
  
    if type(userData) <> "roAssociativeArray" then
      return false
    endif
  
    if not IsString(userData.type) or userData.type <> userDataType then
      return false
    endif
  
    if not IsString(userData.id) or userData.id <> id then
      return false
    endif
  
    return true
  
  end function
  
  
  ' m is the zone
  Function GetBrightWallRectangle(rectangleWidth as integer, rectangleHeight as integer) as object
  
    videoModeWidth = m.bsp.videoMode.getVideoResX()
    videoModeHeight = m.bsp.videoMode.getVideoResY()
  
    zoneCenterX = m.x% + (m.width% / 2)
    rectangleX = zoneCenterX - (rectangleWidth / 2)
  
    zoneCenterY = m.y% + (m.height% / 2)
    rectangleY = zoneCenterY - (rectangleHeight / 2)
  
    ' The following are the indices of the screens that user has specified for the left and right edges of the zone.
    ' Therefore, this calculation does not include the bezel width. The bezel width is then taken into account when determining the player's rectangle
    if (rectangleX < 0) then
      rectangleStartingColumnIndex = 0
    else
      rectangleStartingColumnIndex = int(rectangleX / videoModeWidth)
    endif
    rectangleEndingColumnIndex = int( (int(rectangleX + rectangleWidth) - 1) / videoModeWidth)
  
    rectangleWidth = rectangleWidth + (rectangleEndingColumnIndex - rectangleStartingColumnIndex) * m.bsp.sign.bezelWidthInPixels
    x% = rectangleX - (m.bsp.sign.brightWallColumnPosition% - rectangleStartingColumnIndex) * (videoModeWidth + m.bsp.sign.bezelWidthInPixels)
  
    if (rectangleY < 0) then
      rectangleStartingRowIndex = 0
    else
      rectangleStartingRowIndex = int(rectangleY / videoModeHeight)
    endif
    rectangleEndingRowIndex = int( (int(rectangleY + rectangleHeight) - 1) / videoModeHeight)
  
    rectangleHeight = rectangleHeight + (rectangleEndingRowIndex - rectangleStartingRowIndex) * m.bsp.sign.bezelHeightInPixels
    y% = rectangleY - (m.bsp.sign.brightWallRowPosition% - rectangleStartingRowIndex) * (videoModeHeight + m.bsp.sign.bezelHeightInPixels)
  
    bwRectangle = CreateObject("roRectangle", x%, y%, rectangleWidth, rectangleHeight)
    
    return bwRectangle
  
  end function
  
  
  Sub InitializeBrightWallZoneCommon()
  
    if m.type$ <> "Ticker" then   ' Rectangle for BrightWall ticker zones delegated to TickerZoneConstructor
  
      videoModeWidth = m.bsp.videoMode.getVideoResX()
      videoModeHeight = m.bsp.videoMode.getVideoResY()
  
      x% = m.x% - (m.bsp.sign.brightWallColumnPosition% * (videoModeWidth + m.bsp.sign.bezelWidthInPixels))
      y% = m.y% - (m.bsp.sign.brightWallRowPosition% * (videoModeHeight + m.bsp.sign.bezelHeightInPixels))
      
      m.rectangle = m.GetBrightWallRectangle(m.width%, m.height%)
    
    endif
  
  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$ = "BTW" 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
  
  
  ' m is the state
  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, payload$)
    
    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
      bsp.UpdateVariable(m.variableToAssignFromWildcard, input$, true)
    end if
    
  end sub
  
  
  Sub AssignEventInputToUserVariable(bsp as object, input$ as string)
    
    if type(m.variableToAssignFromInput) = "roAssociativeArray" then
      
      bsp.UpdateVariable(m.variableToAssignFromInput, 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
              bsp.UpdateVariable(variableToAssign, 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.RemoteDownEventHandler = RemoteDownEventHandler
    state.WssEventHandler = WssEventHandler
    state.MediaListSyncManagerEventHandler = MediaListSyncManagerEventHandler
  end sub
  
  
  Function SerialStreamLineEventHandler(event as object, stateData as object) as string
  
    serialEvent$ = event.GetString()
  
    port$ = event.GetUserData()
  
    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 WssEventHandler(event as object, stateData as object, pluginMessage$ as string) as string
  
    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
        
        ' eventFid$ = "USB " + webSocketEvent.fid
  
        for each usbConnector in m.bsp.sign.boseProductsByConnector
          boseProduct = m.bsp.sign.boseProductsByConnector[usbConnector]
          if type(boseProduct.usbSpec) = "roAssociativeArray" then
            outputSpec = boseProduct.usbSpec.hidOutputSpec
            fid$ = mid(outputSpec, 5)
  '                port$ = boseProduct.port$
  '                fid$ = port$ + "." + boseProduct.usbNetInterfaceIndex$
            if webSocketEvent.fid = fid$ then
              rivieraPort$ = usbConnector
              exit for
            endif
          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)
          
            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.wssEventParameter, 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 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 UserDataMatches(event.GetUserData(), "zoneTimer", m.id$) 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) = "roStreamEndEvent" then
  
      port$ = event.GetUserData()
      
      m.bsp.diagnostics.PrintDebug("roStreamEndEvent received on port " + port$)
      m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "streamEnd", port$, "0")
      m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_STREAM_END, port$)
      
      if not m.bsp.pluginProcessesStreamEndEvent then
  
        m.bsp.diagnostics.PrintDebug("process roStreamEndEvent")
  
        outputOnly = false
        
        if type(m.bsp.serial) = "roAssociativeArray" and m.bsp.serial.DoesExist(port$) then
          m.bsp.serial[port$] = invalid
        end if
        
        if type(m.bsp.serialOutputOnlySpec) = "roAssociativeArray" and m.bsp.serialOutputOnlySpec.DoesExist(port$) then
          outputOnly = m.bsp.serialOutputOnlySpec[port$]
        end if
        
        m.bsp.ScheduleRetryCreateSerial(port$, outputOnly)
      
      else
  
        m.bsp.diagnostics.PrintDebug("ignore roStreamEndEvent")
  
      endif
        
    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$ = ""
  
      synchronizePayload$ = event.GetId()
      synchronizePayload = ParseJson(synchronizePayload$)
  
      timestamp = synchronizePayload.timestamp
  
      if synchronizePayload.syncPayloadType = "syncBasic" then
  
        synchronizeEvent$ = synchronizePayload.synchronizeKeyword
  
      else if synchronizePayload.syncPayloadType = "mrssSynchronizeRuntimeCommand" then
  
        ' scenario: current state is not MRSS; a transition into the state is necessary to catch up to the master
        targetStateName$ = synchronizePayload.targetStateName
        nextState = m.stateMachine.stateTable[targetStateName$]
        nextState.brightWallSlaveStateDisplayItem = synchronizePayload.displayItem
        nextState.brightWallSlaveStateFilePath = synchronizePayload.filePath
  
        m.stateMachine.syncInfo = { }
        m.stateMachine.syncInfo.SyncDomain = event.GetDomain()
        m.stateMachine.syncInfo.SyncId = event.GetId()
        m.stateMachine.syncInfo.SyncIsoTimestamp = event.GetIsoTimestamp()
  
        return m.ExecuteTransition(m.synchronizeEvents[targetStateName$], stateData, targetStateName$)
      
      else if synchronizePayload.syncPayloadType = "synchronizePlayFile" then
  
        synchronizeEvent$ = synchronizePayload.synchronizeKeyword
        userVariablesPayload = synchronizePayload.userVariables
        m.bsp.UpdateUserVariablesFromBrightWallPayload(synchronizePayload.timestamp, userVariablesPayload)
  
      else if synchronizePayload.syncPayloadType = "synchronizeMediaListFirstTimeEntry" or synchronizePayload.syncPayloadType = "synchronizeMediaListEntry" or synchronizePayload.syncPayloadType = "synchronizeMediaListForward" or synchronizePayload.syncPayloadType = "synchronizeMediaListBackward" then
  
        retVal = m.MediaListSyncManagerEventHandler(event, stateData, synchronizePayload)
        if retVal <> invalid then
          return retVal
        endif
  
      else if synchronizePayload.syncPayloadType = "syncBwBasic" then
          synchronizeEvent$ = synchronizePayload.synchronizeKeyword
          userVariablesPayload = synchronizePayload.userVariables
          m.bsp.UpdateUserVariablesFromBrightWallPayload(synchronizePayload.timestamp, userVariablesPayload)
      endif
  
      m.bsp.diagnostics.PrintTimestamp()
      m.bsp.diagnostics.PrintDebug("Synchronize Event " + synchronizeEvent$)
  
      synchronizeEvents = m.synchronizeEvents
  
      if type(synchronizeEvents) = "roAssociativeArray" then
  
        if type(synchronizeEvents[synchronizeEvent$]) = "roAssociativeArray" or type(synchronizeEvents["<any>"]) = "roAssociativeArray" or type(synchronizeEvents["(.*)"]) = "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 if type(synchronizeEvents["(.*)"]) = "roAssociativeArray"
            return m.ExecuteTransition(m.synchronizeEvents["(.*)"], stateData, synchronizeEvent$)
          else
            return m.ExecuteTransition(m.synchronizeEvents["<any>"], stateData, synchronizeEvent$)
          end if
        end if
        
      end if
  
    ' Check user data to distinguish between presentation udp messages and bootstrap udp messages
    else if type(event) = "roDatagramEvent" and IsString(event.getUserData()) 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
            
            return m.WssEventHandler(event, stateData, pluginMessage$)
            
          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
  
  
  Function MediaListSyncManagerEventHandler(event as object, stateData as object, synchronizePayload as object) as object
  
    synchronizeEvent$ = synchronizePayload.targetStateName
  
    synchronizeEvents = m.synchronizeEvents
  
    if type(synchronizeEvents) = "roAssociativeArray" then
      if type(synchronizeEvents[synchronizeEvent$]) = "roAssociativeArray" or type(synchronizeEvents["<any>"]) = "roAssociativeArray" or type(synchronizeEvents["(.*)"]) = "roAssociativeArray"then
  
        userVariablesPayload = synchronizePayload.userVariables
        m.bsp.UpdateUserVariablesFromBrightWallPayload(synchronizePayload.timestamp, userVariablesPayload)
  
        nextState = m.stateMachine.stateTable[synchronizeEvent$]
        if synchronizePayload.syncPayloadType = "synchronizeMediaListEntry" then
          ' this code is executed in the following circumstances
          ' each entry into the state other than firstTimeEntry, including
          '   normal entry (presentation transitions into the state via normal events)
          '       why does this require different code?
          '   slave restart using the following steps
          '     master & slave startup together
          '       run preso
          '       enter media list
          '     slave goes down
          '     master exits media list
          '     slave comes back up
          '     reenter media list
          ' results of test where the entry code in the media list state does not special case the fact that it is a slave
          ' fails in the following case
          '   media list is playing on master & slave
          '   stop slave
          '   advance to long video on master
          '   restart slave; it goes to first item in the list.
          ' without the special code, playbackIndex is 0 and stays at zero
          ' stop
          nextState.brightWallSlaveMediaListIndex = nextState.GetMediaListInitialPlaybackIndex()
        else if synchronizePayload.syncPayloadType = "synchronizeMediaListFirstTimeEntry" then
          ' this is the case where the master rebooted after the slave had already entered the media list - reset the index to be identical to the master's index
          nextState.brightWallSlaveMediaListIndex = nextState.GetMediaListFirstTimeThroughPlaybackIndex()
        else
          nextState.brightWallSlaveMediaListIndex = synchronizePayload.playbackIndex ' synchronizeMediaListForward, synchronizeMediaListBackward
        endif
  
        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")
        
        return m.ExecuteTransition(m.synchronizeEvents[synchronizeEvent$], stateData, synchronizeEvent$)
      endif
    endif
  
    return invalid
  
  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%)
      
      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)
      else
        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)
      userData = BuildUserData("zoneTimer", m.stateMachine.id$)
      timer.SetUserData(userData)
      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" 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 lcase(portraitSpec$) = "portrait" or lcase(portraitSpec$) = "portraitbottomleft" then
      return true
    end if
    
    return false
    
  end function
  
  
  Function IsPortraitBottomRight(portraitSpec$) as boolean
    
    if lcase(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 CanRotateByScreen(m.bsp.sign, {}) then
        ' no need to rotate per zone if already rotated by screen
      else 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.hdmi1Output$ = zoneDescription.hdmi1Output$
    zoneHSM.hdmi2Output$ = zoneDescription.hdmi2Output$
    zoneHSM.hdmi3Output$ = zoneDescription.hdmi3Output$
    zoneHSM.hdmi4Output$ = zoneDescription.hdmi4Output$
    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.hasMultiScreenOutputs = zoneDescription.hasMultiScreenOutputs
    
    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")
  
    if m.hasMultiScreenOutputs = true then
      hdmi1AudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector("HDMI:1"))
      hdmi2AudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector("HDMI:2"))
      hdmi3AudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector("HDMI:3"))
      hdmi4AudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector("HDMI:4"))
    else
      hdmiAudioOutput = CreateObject("roAudioOutput", GetAudioOutputConnector("HDMI"))
    end if
  
    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 m.hasMultiScreenOutputs = true then
      AddAudioOutputByType(m.hdmi1Output$, hdmi1AudioOutput, compressed, pcm)
      AddAudioOutputByType(m.hdmi2Output$, hdmi2AudioOutput, compressed, pcm)
      AddAudioOutputByType(m.hdmi3Output$, hdmi3AudioOutput, compressed, pcm)
      AddAudioOutputByType(m.hdmi4Output$, hdmi4AudioOutput, compressed, pcm)
    else
      AddAudioOutputByType(m.hdmiOutput$, hdmiAudioOutput, compressed, pcm)
    end if
  
    AddAudioOutputByType(m.spdifOutput$, spdifAudioOutput, compressed, pcm)
    
    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 AddAudioOutputByType(outputType$ as string, output as object, compressed as object, pcm as object)
    if outputType$ = "" or type(output) <> "roAudioOutput" then return
  
    if lcase(outputType$) = "passthrough" then
      compressed.push(output)
    else if lcase(outputType$) <> "none" then
      pcm.push(output)
    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
    ' TEDTODOREPLAY - I think that the videoPlayer's userData is always overwritten when videoplayback is launched.
    videoPlayer.SetUserData(BuildUserData("zoneVideoPlayer", zoneHSM.id$))
    if CanRotateByScreen(m.bsp.sign, {}) then
      ' no need to rotate per zone if already rotated by screen
    else 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()
          if m.mediaType$ = "video" then
            aa = { }
            aa["url"] = url$
            
            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
  
            if m.bsp.sign.isBrightWall then
              m.stateMachine.rectangle = m.stateMachine.GetBrightWallRectangle(m.stateMachine.width%, m.stateMachine.height%)
              m.stateMachine.videoPlayer.SetViewMode(0)
              m.stateMachine.videoPlayer.SetRectangle(m.stateMachine.rectangle)
            endif
  
            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
            aa = { }
            aa["url"] = url$
            ok = audioPlayer.PlayFile(aa)
            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
          aa = GetBinding("contentDownloadEnabled", 0)
          binding = aa.network_interface
          m.bsp.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for mjpegMimeStream is ", 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 CanRotateByScreen(m.bsp.sign, {}) then
            ' no need to rotate per zone if already rotated by screen
          else 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.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 UserDataMatches(event.GetUserData(), "zoneVideoPlayer", m.id$) 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
        
        ' if this is a BrightWall presentation and this is the master, send a sync command and play the media
        if m.stateMachine.IsBrightWallMaster() then
  
          ' don't loop if "Hold last frame.... is specified"      
          if (type(m.videoItem.automaticallyLoop) <> "roBoolean" or m.videoItem.automaticallyLoop) then
  
            payload = {}
            payload.syncPayloadType = "syncBwBasic"
            payload.userVariables = m.stateMachine.bsp.GetUserVariablesAsPayload()
            payload.synchronizeKeyword = m.id$
            payload.timestamp = m.stateMachine.bsp.GetCurrentTimeAsIsoString()
  
            payload$ = FormatJson(payload)
            
            syncManagerEvent = m.stateMachine.SyncManager.Synchronize(payload$, 300)
            m.bsp.diagnostics.PrintDebug("@@@ Created syncManagerEvent with sync payload: " + payload$)
  
            m.stateMachine.syncInfo = { }
            m.stateMachine.syncInfo.SyncDomain = syncManagerEvent.GetDomain()
            m.stateMachine.syncInfo.SyncId = syncManagerEvent.GetId()
            m.stateMachine.syncInfo.SyncIsoTimestamp = syncManagerEvent.GetIsoTimestamp()
  
            m.LaunchVideo("video")
  
            return "HANDLED"
  
          endif
        endif
  
        PostMediaEndEvent(m.bsp.msgPort)
        
      else if event.GetInt() = VIDEO_TIME_CODE then
        videoTimeCodeIndex$ = str(event.GetData())
        m.bsp.diagnostics.PrintDebug("Video TimeCode Event " + videoTimeCodeIndex$)
        if type(m.videoTimeCodeEvents) = "roAssociativeArray" then
          videoTimeCodeEvent = m.videoTimeCodeEvents[videoTimeCodeIndex$]
          if type(videoTimeCodeEvent) = "roAssociativeArray" then
            m.bsp.ExecuteTransitionCommands(m.stateMachine, videoTimeCodeEvent, "")
            m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "1")
            return "HANDLED"
          end if
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "0")
        end if
      end if
  
    else if type(event) = "roSyncManagerEvent" then
  
      if m.stateMachine.IsBrightWallSlave() then
        synchronizePayload$ = event.GetId()
        if IsString(synchronizePayload$) then
          synchronizePayload = ParseJson(synchronizePayload$)
          if type(synchronizePayload) = "roAssociativeArray" and IsString(synchronizePayload.syncPayloadType) then
            if synchronizePayload.syncPayloadType = "syncBwBasic" then
              synchronizeEvent$ = synchronizePayload.synchronizeKeyword
              if synchronizeEvent$ = m.id$ then
                ' Loop the current video
                m.stateMachine.syncInfo = CreateObject("roAssociativeArray")
                m.stateMachine.syncInfo.SyncDomain = event.GetDomain()
                m.stateMachine.syncInfo.SyncId = event.GetId()
                m.stateMachine.syncInfo.SyncIsoTimestamp = event.GetIsoTimestamp()
                m.LaunchVideo("video")
                return "HANDLED"
              endif
            endif
          endif
        endif
      endif
  
      return m.MediaItemEventHandler(event, stateData)
  
    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 CanRotateByScreen(m.bsp.sign, {}) then
        ' no need to rotate per zone if already rotated by screen
      else 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
                if type(m.videoEndEvent) = "roAssociativeArray" then
                  transition = m.videoEndEvent
                  if transition.targetMediaState$ = "" and (transition.remainOnCurrentStateActions = "stop" or transition.remainOnCurrentStateActions = "stopclear") then
                    m.videoItem.automaticallyLoop = false
                  endif
                endif
              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
        ' if this is a BrightWall presentation and this is the master, send a sync command and play the media
        if m.stateMachine.IsBrightWallMaster() then
          m.bsp.ExecuteSynchronizePlayFile(m.stateMachine, m.id$, m.payload$)
          m.LaunchVideo("playFile")
          return "HANDLED"
        endif
  
        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
  
    else if type(event) = "roSyncManagerEvent" then
  
      synchronizeEvent$ = ""
  
      synchronizePayload$ = event.GetId()
      synchronizePayload = ParseJson(synchronizePayload$)
  
      timestamp = synchronizePayload.timestamp
  
      if synchronizePayload.syncPayloadType = "synchronizePlayFile" then
  
        if type(m.videoItem) = "roAssociativeArray" then
  
          userVariablesPayload = synchronizePayload.userVariables
          m.bsp.UpdateUserVariablesFromBrightWallPayload(synchronizePayload.timestamp, userVariablesPayload)
  
          m.stateMachine.syncInfo = { }
          m.stateMachine.syncInfo.SyncDomain = event.GetDomain()
          m.stateMachine.syncInfo.SyncId = event.GetId()
          m.stateMachine.syncInfo.SyncIsoTimestamp = event.GetIsoTimestamp()
  
          m.LaunchVideo("video")
  
          return "HANDLED"
  
        endif
  
      endif
  
    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 = "udp" and type(event) = "roDatagramEvent" then
        udpEvent$ = event.GetString()
  
        m.bsp.diagnostics.PrintDebug("UDP Event " + udpEvent$)
  
        udpEventSpec = navigationEvent.eventData.data
        if (udpEvent$ = udpEventSpec or udpEventSpec = "(.*)") then
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "udp", udpEvent$, "1")
          return true
        else
          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
                  m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "udp", udpEvent$, "1")
                return true
              endif
            endif
          endif
        endif
  
      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
        
      else if navigationEvent.eventName = "synchronize" and type(event) = "roSyncManagerEvent" then
  
        synchronizePayload$ = event.GetId()
        synchronizePayload = ParseJson(synchronizePayload$)
  
        timestamp = synchronizePayload.timestamp
  
        synchronizeEvent$ = synchronizePayload.synchronizeKeyword
        ' TEDTODOBW - should these be removed or reinstated?
        'userVariablesPayload = synchronizePayload.userVariables
        'm.bsp.UpdateUserVariablesFromBrightWallPayload(synchronizePayload.timestamp, userVariablesPayload)
  
        m.bsp.diagnostics.PrintDebug("Synchronize Event " + synchronizeEvent$)
  
        if navigationEvent.eventData.data = synchronizeEvent$ then
          
          m.stateMachine.syncInfo = CreateObject("roAssociativeArray")
          m.stateMachine.syncInfo.SyncDomain = event.GetDomain()
          m.stateMachine.syncInfo.SyncId = event.GetId()
          m.stateMachine.syncInfo.SyncIsoTimestamp = event.GetIsoTimestamp()
        
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "enhancedSynchronize", synchronizeEvent$, "1")
          return true
        endif
  
      end if
      
    next
    
    return invalid
    
  end function
  
  
  Function HandleIntraStateEvent(event as object, navigationEventList as object) as boolean
    
    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
      if m.playbackIndices.count() = 0 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
      else
        m.bsp.diagnostics.PrintDebug("LaunchMediaListPlaybackItem failed - invalid item index " + stri(m.playbackIndex%) + " " + stri(m.playbackIndices.count()))
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_EMPTY_MEDIA_PLAYLIST, "invalid item index")
        m.playbackIndex% = 0
      end if
    endif
    
    ' 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
  
  
  ' videoWidth, videoHeight are the dimensions of the video (as determined by the probe code) if it's a video file,
  ' or from hdmi input info if it's live video
  
  ' given the dimensions of a source video (file or hdmi in), return the rectangle (x, y, width, height) to use
  ' when creating a roVideoPlayer given the selected viewMode.
  ' this function is required as the OS doesn't support viewMode for BrightWalls
  ' the callers of this function override what was calculated in InitializeZoneCommon
  Function GetBrightWallVideoPlayerRectangle(videoWidth as integer, videoHeight as integer) as object
  
    videoModeWidth = m.bsp.videoMode.getVideoResX()
    videoModeHeight = m.bsp.videoMode.getVideoResY()
  
    if IsPortraitBottomLeft(m.bsp.sign.monitorOrientation) then
      tmp = videoModeWidth
      videoModeWidth = videoModeHeight
      videoModeHeight = tmp
    else if IsPortraitBottomRight(m.bsp.sign.monitorOrientation) then
      tmp = videoModeWidth
      videoModeWidth = videoModeHeight
      videoModeHeight = tmp
    end if
  
    if m.type$ = "multiAssetVideo" then
  
      ' x, y, width, height must be known / set by the user. no scaling or transforming is done by the script
  
      initialColumn = int(m.stateMachine.x% / videoModeWidth)
      lastColumn = int((m.stateMachine.x% + m.stateMachine.width% - 1) / videoModeWidth)
  
      initialRow = int(m.stateMachine.y% / videoModeHeight)
      lastRow = int((m.stateMachine.y% + m.stateMachine.height% - 1) / videoModeHeight)
  
      if m.bsp.sign.brightWallColumnPosition% = initialColumn then
        x% = m.stateMachine.x%
      else
        x% = 0
      endif
  
      if m.bsp.sign.brightWallRowPosition% = initialRow then
        y% = m.stateMachine.y%
      else
        y% = 0
      endif
  
      rectangleWidth = videoWidth
      rectangleHeight = videoHeight
  
      ' No bezel compensation for multi asset widgets.
      return CreateObject("roRectangle", x%, y%, rectangleWidth, rectangleHeight)
  
    else
  
      ' aspect ratios of video and zone
      videoAspectRatio = videoHeight / videoWidth
  
      ' calculate width if limited by zone height
      widthIfHeightLimited = m.stateMachine.height% / videoAspectRatio
  
      ' calculate height if limited by zone width
      heightIfWidthLimited = m.stateMachine.width% * videoAspectRatio
  
      if m.stateMachine.viewMode% = 0 then
  
        ' scale to fit
        rectangleWidth = m.stateMachine.width%
        rectangleHeight = m.stateMachine.height%
  
      else if m.stateMachine.viewMode% = 1 then
        
        ' letterboxed and centered
        if heightIfWidthLimited > m.stateMachine.height% then
          rectangleWidth = widthIfHeightLimited
          rectangleHeight = m.stateMachine.height%
        else
          rectangleWidth = m.stateMachine.width%
          rectangleHeight = heightIfWidthLimited
        endif
  
      else if m.stateMachine.viewMode% = 2 then
  
        ' FillScreenAndCentered
        widthScalingFactor = m.stateMachine.width% / videoWidth
        heightScalingFactor = m.stateMachine.height% / videoHeight
  
        if widthScalingFactor > heightScalingFactor then
          rectangleWidth = m.stateMachine.width%
          rectangleHeight = m.stateMachine.height% * widthScalingFactor
        else
          rectangleWidth = m.stateMachine.width% * heightScalingFactor
          rectangleHeight = m.stateMachine.height%
        endif
  
      endif
  
      return m.stateMachine.GetBrightWallRectangle(rectangleWidth, rectangleHeight)
  
    endif
  
  end function
  
  
  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$ = "AlwaysLoop"
  
    if (type(m.videoItem.automaticallyLoop) = "roBoolean") and (not m.videoItem.automaticallyLoop) then
      holdLastFrameSpecified = true
    else
      holdLastFrameSpecified = false
    endif
  
    if disableLoopMode or m.IsSubscribedToVideoEndEvent() or holdLastFrameSpecified or type(m.synchronizeEvents) = "roAssociativeArray" or type(m.internalSynchronizeEvents) = "roAssociativeArray" then loopMode$ = "NoLoop"
    ' Support seamless looping during synchronization - overwrite loopMode as necessary
    if holdLastFrameSpecified and (type(m.synchronizeEvents) = "roAssociativeArray" or type(m.internalSynchronizeEvents) = "roAssociativeArray") then loopMode$ = "SeamlessLoopOrNotAtAll"
    
    if m.bsp.sign.isBrightWall then
      if m.stateMachine.IsBrightWallMaster() then
        if m.IsSubscribedToVideoEndEvent() or (type(m.videoItem.automaticallyLoop) = "roBoolean" and (not m.videoItem.automaticallyLoop)) then
          loopMode$ = "NoLoop"
        else
          loopMode$ = "SeamlessLoopOrNotAtAll"
        endif
      else
        if type(m.videoItem.automaticallyLoop) = "roBoolean" and m.videoItem.automaticallyLoop then
          loopMode$ = "SeamlessLoopOrNotAtAll"
        else
          loopMode$ = "NoLoop"
        endif
      endif
    endif
  
    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
    
    if m.bsp.sign.isBrightWall then
  
    ' probe and retrieve information about the target video
      videoFileMetadata = m.stateMachine.videoPlayer.ProbeFile(filePath$)
      if type(videoFileMetadata) = "roAssociativeArray" then
        videoWidth = videoFileMetadata.videoWidth
        videoHeight = videoFileMetadata.videoHeight
        m.stateMachine.rectangle = m.GetBrightWallVideoPlayerRectangle(videoWidth, videoHeight)
  
        m.stateMachine.videoPlayer.SetViewMode(0)
        m.stateMachine.videoPlayer.SetRectangle(m.stateMachine.rectangle)
      else
        m.bsp.diagnostics.PrintDebug("Error probing Brightwall video file in PlayVideo: " + filePath$)
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
        videoPlaybackFailure = { }
        videoPlaybackFailure["EventType"] = "VideoPlaybackFailureEvent"
        m.stateMachine.msgPort.PostMessage(videoPlaybackFailure)
      endif
  
    endif
  
    ' determine whether or not a preload has been performed
    preloaded = false
    if type(m.stateMachine.preloadState) = "roAssociativeArray" then
      if m.stateMachine.preloadedStateName$ = m.name$ then
        preloaded = true
      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 = " + 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)   
      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
  
        ' TEDTODOREPLAY - here, the id is the state, not the zone.
      m.stateMachine.videoPlayer.setUserData(BuildUserData("zoneVideoPlayer", m.id$))
  
      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
    
    player.setUserData(m.id$)
  
    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)
          
        end if
        
        if m.stateMachine.mosaicDecoderName <> "" then
          aa.Decoder = m.stateMachine.mosaicDecoderName
        end if
        
        if m.bsp.sign.isBrightWall then
          hdmiInputStatus = m.bsp.videoMode.GetHdmiInputStatus()
          if type(hdmiInputStatus) = "roAssociativeArray" then
            videoWidth = hdmiInputStatus.width
            videoHeight = hdmiInputStatus.height
            m.stateMachine.rectangle = m.GetBrightWallVideoPlayerRectangle(videoWidth, videoHeight)
            m.stateMachine.videoPlayer.SetViewMode(0)
            m.stateMachine.videoPlayer.SetRectangle(m.stateMachine.rectangle)
          endif
        endif
  
        ' TEDTODOREPLAY - here, the id is the state, not the zone.
        m.stateMachine.videoPlayer.setUserData(BuildUserData("zoneVideoPlayer", m.id$))
  
        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, aa as object)
    if type(aa) = "roAssociativeArray" then
      isLocalHtmlWidget = false
      if htmlWidget = invalid then
        r = CreateObject("roRectangle", 0, 0, 0, 0)
        h = CreateObject("roHtmlWidget", r)
        isLocalHtmlWidget = true
      else
        h = htmlWidget
      end if
      if type(h) = "roHtmlWidget" then
        ua$ = h.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)
          aa.user_agent = ua$
        end if
        if isLocalHtmlWidget then
          h = invalid
        end if
      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 LoadBrightWallDeviceSetup()
  
    videoMode = GetVideoMode()
    videoMode.SetMode("1920x1080x60p")
  
    if m.bsp = invalid then
      bsp = m
    else
      bsp = m.bsp
    endif
  
    bsp.view = invalid    
    bsp.brightWallDeviceSetupWidget = invalid
  
    syncSpec = GetActiveSyncSpec()
  
    assetCollection = syncSpec.GetAssets("download")
  
    aa = { }
    aa.nodejs_enabled = true
    aa.inspector_server = { port: 2999 }  
    aa.port = bsp.msgPort
    aa.javascript_enabled = true
    aa.focus_enabled = true
    aa.fonts = []
    aa.brightsign_js_objects_enabled = true
    aa.mouse_enabled = false
  
    security = { }
    security.websecurity = false
    security.camera_enabled = false
    security.insecure_https_enabled = false
    aa.security_params = security
  
    ' prefix$ = "brightWallDeviceSetupSite"
    prefix$ = "brightWallDeviceSetupSite-001/"
  
    asset = { }
    asset.pool = bsp.assetPool
    asset.collection = assetCollection
    asset.pool_prefix = prefix$
    asset.uri_prefix = "/" + asset.pool_prefix + "/"
    
    aa.assets = []
    aa.assets.push(asset)
  
    aa.url = GetInterstitialHtmlUrl(prefix$, "index.html")
  
    r = CreateObject("roRectangle", 0, 0, 1920, 1080)
  
    bsp.brightWallDeviceSetupWidget = CreateObject("roHtmlWidget", r, aa)
    bsp.brightWallDeviceSetupWidget.Show()
  
  end sub
  
  
  Sub LoadHtmlWidgetStaticInitialization()
    
    syncSpec = GetActiveSyncSpec()
    activeSyncSpecSettings = GetActiveSyncSpecSettings()
  
    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 CanRotateByScreen(m.bsp.sign, {}) then
      ' no need to rotate per zone if already rotated by screen
    else 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$
    
    SetUserAgentForHtmlWidget(m.bsp, m.stateMachine.loadingHtmlWidget, aa)
  
    limitStorageSpace = activeSyncSpecSettings.limitStorageSpace
  
    if limitStorageSpace then
      storageSpaceLimits = GetStorageSpaceLimits(activeSyncSpecSettings)
      aa.storage_path = "browser_storage"
      aa.storage_quota = GetRequestedBrowserStorageSpace(storageSpaceLimits)
    endif
  
    if isBoolean(m.enableFileURLSharedStorage) then
      aa.force_shared_storage = m.enableFileURLSharedStorage
    else
      aa.force_shared_storage = true
    endif
  
    if isBoolean(m.enableHtmlURLSharedStorage) then
      aa.force_unshared_storage = not m.enableHtmlURLSharedStorage
    else
      aa.force_unshared_storage = false
    endif
  
    m.stateMachine.loadingHtmlWidgetParams = aa
  
    if m.bsp.sign.isBrightWall then
      m.stateMachine.rectangle = m.stateMachine.GetBrightWallRectangle(m.stateMachine.width%, m.stateMachine.height%)
    endif
  
    m.stateMachine.loadingHtmlWidget = CreateObject("roHtmlWidget", m.stateMachine.rectangle, aa)
    
  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")
        userdata = m.stateMachine.loadingHtmlWidget.GetUserData()
        m.stateMachine.loadingHtmlWidget = CreateObject("roHtmlWidget", m.stateMachine.rectangle, m.stateMachine.loadingHtmlWidgetParams)
        m.stateMachine.loadingHtmlWidget.SetUserData(userdata)
        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
  
  
  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
      ' case 1: if system variable is directly created in the template
      if templateItem.type$ = "systemVariableTemplateItem" then
        return true
      ' case 2: if system variable is added as user variable and referenced in the template
      else if templateItem.type$ = "userVariableTemplateItem" then
        if getVarFromObj(templateItem, "userVariable.systemVariable$", "String", "") <> "" then return true
      end if
    next
  
    return false
    
  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
        
        videoConnector$ = getVarFromObj(templateItem, "videoConnector$", "roString", "")
        suffix$ = "$"
        if videoConnector$ <> "" then suffix$ = "_" + videoConnector$ + "$"
        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$ = "EdidMonitorSerialNumber" then
          text["text"] = m.bsp.sysInfo["edidMonitorSerialNumber"+suffix$]
        else if templateItem.systemVariableType$ = "EdidYearOfManufacture" then
          text["text"] = m.bsp.sysInfo["edidYearOfManufacture"+suffix$]
        else if templateItem.systemVariableType$ = "EdidMonitorName" then
          text["text"] = m.bsp.sysInfo["edidMonitorName"+suffix$]
        else if templateItem.systemVariableType$ = "EdidManufacturer" then
          text["text"] = m.bsp.sysInfo["edidManufacturer"+suffix$]
        else if templateItem.systemVariableType$ = "EdidUnspecifiedText" then
          text["text"] = m.bsp.sysInfo["edidUnspecifiedText"+suffix$]
        else if templateItem.systemVariableType$ = "EdidSerialNumber" then
          text["text"] = m.bsp.sysInfo["edidSerialNumber"+suffix$]
        else if templateItem.systemVariableType$ = "EdidManufacturerProductCode" then
          text["text"] = m.bsp.sysInfo["edidManufacturerProductCode"+suffix$]
        else if templateItem.systemVariableType$ = "EdidWeekOfManufacture" then
          text["text"] = m.bsp.sysInfo["edidWeekOfManufacture"+suffix$]
        else if templateItem.systemVariableType$ = "ActivePresentation" then
          if IsString(m.bsp.activePresentation$) then
            text["text"] = m.bsp.activePresentation$
          else
            text["text"] = ""
          end if
        else if templateItem.systemVariableType$ = "BrightAuthorVersion" then
          text["text"] = m.bsp.sysInfo.baconVersion$
        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      
  
          if m.bsp.sign.isBrightWall then
            r = m.stateMachine.GetBrightWallRectangle(m.stateMachine.width%, m.stateMachine.height%)
          else
            r = CreateObject("roRectangle", m.stateMachine.x%, m.stateMachine.y%, m.stateMachine.width%, m.stateMachine.height%)
          endif
  
          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
  
    userData = m.stateMachine.videoPlayer.getUserData()
    if userData = m.id$ then
      m.GetMRSSTemplateItem()
      return "HANDLED"
    endif
  
    return m.MediaItemEventHandler(event, stateData)
  
  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
    
    isBrightWallSlaveUnit = false
    if m.stateMachine.IsBrightWallSlave() then
      isBrightWallSlaveUnit = true
    endif
  
    if type(event) = "roAssociativeArray" then ' internal message event
  
      if IsString(event["EventType"]) then
  
        if event["EventType"] = "ENTRY_SIGNAL" then
          m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
  
          ' 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
          
          ' BrightWall slave entry
          if isBrightWallSlaveUnit then
            if type(m.brightWallSlaveStateDisplayItem) = "roAssociativeArray" and IsString(m.brightWallSlaveStateFilePath) then
              ' check for existence of file
              file = CreateObject("roReadFile", m.brightWallSlaveStateFilePath)
              if type(file) = "roReadFile" then
                m.DisplayMRSSItem(m.brightWallSlaveStateDisplayItem, m.brightWallSlaveStateFilePath)
              endif
            endif
          endif
  
          m.currentFeed = invalid
          m.pendingFeed = invalid
        
          ' see if the designated feed has already been downloaded (doesn't imply content exists)
          if type(m.liveDataFeed) <> "roAssociativeArray" then
            stop
          endif
  
          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" and not isBrightWallSlaveUnit 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 and not isBrightWallSlaveUnit 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 and not isBrightWallSlaveUnit 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(event) = "roSyncManagerEvent" then
  
      if isBrightWallSlaveUnit then
  
        synchronizePayload$ = event.GetId()
        synchronizePayload = ParseJson(synchronizePayload$)
  
        timestamp = synchronizePayload.timestamp
  
        if type(synchronizePayload) = "roAssociativeArray" and synchronizePayload.syncPayloadType = "mrssSynchronizeRuntimeCommand" then
  
          if IsString(synchronizePayload.filePath) and type(synchronizePayload.displayItem) = "roAssociativeArray" then
  
            ' check for existence of file
            file = CreateObject("roReadFile", synchronizePayload.filePath)
            if type(file) = "roReadFile" then
              m.stateMachine.syncInfo = { }
              m.stateMachine.syncInfo.SyncDomain = event.GetDomain()
              m.stateMachine.syncInfo.SyncId = event.GetId()
              m.stateMachine.syncInfo.SyncIsoTimestamp = event.GetIsoTimestamp()
              m.DisplayMRSSItem(synchronizePayload.displayItem, synchronizePayload.filePath)
            endif
  
          endif
  
        else
  
          return m.MediaItemEventHandler(event, stateData)
  
        endif
  
      endif
  
    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 m.stateMachine.IsBrightWallMaster() then
      m.SendMrssSynchronizeRuntimeCommand(displayItem, filePath$)
    endif
  
    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)
      
      aa = { }
      
      if m.bsp.sign.htmlEnableJavascriptConsole then
        aa.inspector_server = { port: 2999 }
      end if
      
      if CanRotateByScreen(m.bsp.sign, {}) then
        ' no need to rotate per zone if already rotated by screen
      else 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
      
      aa.url = filePath$
      'print "Loading MRSS HTML path: ";filePath$
      ' ?? call LaunchTimer here to handle load timeout ??
  
      SetUserAgentForHtmlWidget(m.bsp, m.stateMachine.loadingHtmlWidget, aa)
  
      m.stateMachine.loadingHtmlWidget = CreateObject("roHtmlWidget", m.stateMachine.rectangle, aa)
      
      userData = { }
      userData.stateId$ = m.id$
      m.stateMachine.loadingHtmlWidget.SetUserData(userData)
      
      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 CanRotateByScreen(m.bsp.sign, {}) then
      ' no need to rotate per zone if already rotated by screen
    else 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
  
    activeSyncSpec = GetActiveSyncSpec()
    assetCollection = activeSyncSpec.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 CanRotateByScreen(m.bsp.sign, {}) then
      ' no need to rotate per zone if already rotated by screen
    else 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
  
  
  Function ZoneOverlapsScreen(minZoneX as integer, minZoneY as integer, zoneWidth as integer, zoneHeight as integer, minScreenX as integer, minScreenY as integer, screenWidth as integer, screenHeight as integer) as boolean
  
    maxZoneX = minZoneX + zoneWidth - 1
    maxScreenX = minScreenX + screenWidth - 1
    maxZoneY = minZoneY + zoneHeight - 1
    maxScreenY = minScreenY + screenHeight - 1
    zoneLeftOfScreen = maxZoneX < minScreenX
    zoneRightOfScreen = minZoneX > maxScreenX
    zoneAboveScreen = minZoneY > maxScreenY
    zoneBelowScreen = maxZoneY < minScreenY
  
    return not(zoneLeftOfScreen or zoneRightOfScreen or zoneAboveScreen or zoneBelowScreen)
  
  end function
  
  
  Function ZoneOverlapsCurrentScreen() as boolean
  
    videoModeWidth = m.bsp.videoMode.getVideoResX()
    videoModeHeight = m.bsp.videoMode.getVideoResY()
  
    columnIndex = m.bsp.sign.brightWallColumnPosition%
    rowIndex = m.bsp.sign.brightWallRowPosition%
  
    screenX = columnIndex * videoModeWidth
    screenY = rowIndex * videoModeHeight
  
    return ZoneOverlapsScreen(m.x%, m.y%, m.width%, m.height%, screenX, screenY, videoModeWidth, videoModeHeight)
  
  end function
  
  
  Function GetZoneInScreensCount()
  
    videoModeWidth = m.bsp.videoMode.getVideoResX()
    videoModeHeight = m.bsp.videoMode.getVideoResY()
  
    rowCount = m.bsp.sign.brightwall_num_rows%
    columnCount = m.bsp.sign.brightwall_num_columns%
  
    zoneInScreensCount = 0
    for rowIndex = 0 to rowCount - 1
      for columnIndex = 0 to columnCount - 1
        screenX = columnIndex * videoModeWidth
        screenY = rowIndex * videoModeHeight
        if ZoneOverlapsScreen(m.x%, m.y%, m.width%, m.height%, screenX, screenY, videoModeWidth, videoModeHeight) then
          zoneInScreensCount = zoneInScreensCount + 1
        endif
      next
    next
  
    return zoneInScreensCount
  
  end function
  
  
  ' if isBrightWall, see if ticker intersects multiple screens.
  ' ticker location
  '   m.x%, m.y%, m.width%, m.height%
  ' screen dimensions
  '   videoModeWidth, videoModeHeight
  ' screen location
  '   ((m.bsp.sign.brightWallColumnPosition% - 1) * videoModeWidth)
  '   ((m.bsp.sign.brightWallRowPosition% - 1) * videoModeHeight)
  Sub TickerZoneConstructor()
  
    zoneHSM = m
  
    zoneHSM.isMultiScreen = false
    zoneHSM.isTickerMaster = false
  
    m.InitializeZoneCommon(m.bsp.msgPort)
  
    a = { }
    a["PauseTime"] = zoneHSM.delay%
    a["Rotation"] = zoneHSM.rotation%
    a["Alignment"] = zoneHSM.alignment%
  
    if m.bsp.sign.isBrightWall then
  
      videoModeWidth = m.bsp.videoMode.getVideoResX()
      videoModeHeight = m.bsp.videoMode.getVideoResY()
  
      if m.GetZoneInScreensCount() > 1 and m.ZoneOverlapsCurrentScreen() then   ' Multiscreen
        
        zoneSpecX = zoneHSM.x%
        zoneSpecY = zoneHSM.y%
        totalTickerWidth = zoneHSM.width%
        zoneSpecHeight = zoneHSM.height%
  
        initialColumn = int(zoneSpecX / videoModeWidth)
        lastColumn = int((zoneSpecX + totalTickerWidth) / videoModeWidth)
  
        initialRow = int(zoneSpecY / videoModeHeight)
        lastRow = int((zoneHSM.y% + zoneHSM.height%) / videoModeWidth)
        cumulativeZoneHeightsOfSlaveScreens = (videoModeHeight - zoneHSM.y%) + ((m.bsp.sign.brightWallRowPosition% - 1) * videoModeHeight)
  
        if m.bsp.sign.brightWallColumnPosition% = initialColumn then
          ' left most device in wall
          tickerOffset = 0        
          zoneHSM.width% = videoModeWidth - zoneHSM.x%
        else if m.bsp.sign.brightWallColumnPosition% <> lastColumn then
          ' device that's not in the first or last column
          tickerOffset = ((m.bsp.sign.brightWallColumnPosition% - 1) * videoModeWidth) + (videoModeWidth - zoneHSM.x%)
          zoneHSM.x% = 0
          zoneHSM.width% = videoModeWidth
        else ' if (m.bsp.sign.brightWallColumnPosition% = lastColumn) then => right most device in wall
          tickerOffset = ((m.bsp.sign.brightWallColumnPosition% - 1) * videoModeWidth) + (videoModeWidth - zoneHSM.x%)
          zoneHSM.x% = 0
          zoneHSM.width% = totalTickerWidth - tickerOffset
          zoneHSM.isTickerMaster = true
        endif
  
        ' Apply bezel compensation
        tickerOffset = tickerOffset + (m.bsp.sign.brightWallColumnPosition% - initialColumn) * m.bsp.sign.bezelWidthInPixels
        totalTickerWidth = totalTickerWidth + (lastColumn - initialColumn) * m.bsp.sign.bezelWidthInPixels
  
        ' TEDTODOBW - tickers that cross vertical screens do NOT work. They cause a failure in the autorun.
        if m.bsp.sign.brightWallRowPosition% = initialRow then
          y% = zoneHSM.y% MOD videoModeHeight
          ' hack
          if (zoneHSM.y% + zoneHSM.height%) = videoModeHeight then
            height% = videoModeHeight - y%
          else
            height% = (zoneHSM.y% + zoneHSM.height%) MOD videoModeHeight - y%
          endif
          zoneHSM.y% = y%
          zoneHSM.height% = height%
        else if m.bsp.sign.brightWallRowPosition% = lastRow then
          zoneHSM.y% = 0
          zoneHSM.height% = zoneSpecHeight - cumulativeZoneHeightsOfSlaveScreens
        else
          ' screen that's not in the first or last row
          zoneHSM.y% = 0
          zoneHSM.height% = videoModeHeight
        endif
  
        zoneHSM.isMultiScreen = true
  
        zoneHSM.rectangle = createObject("roRectangle", zoneHSM.x%, zoneHSM.y%, zoneHSM.width%, zoneHSM.height%)
        
        ' Hardcode 3 as it is the only value that works for multiscreen
        textWidget = CreateObject("roTextWidget", zoneHSM.rectangle, zoneHSM.numberOfLines%, 3, a)
  
        if type(textWidget) = "roTextWidget" then
  
          ok = textWidget.SetMultiscreen(tickerOffset, totalTickerWidth, "224.0.126.10", 1539)
  
          if not ok then
            errorData$ = stri(tickerOffset) + " " + stri(totalTickerWidth)
            m.bsp.diagnostics.PrintDebug("### textWidget.SetMultiscreen returned false: " + errorData$)
            m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_MULTISCREEN_TICKER_FAILURE, errorData$)
          endif
  
        endif
  
      else ' BrightWall but not multiscreen
  
        ' update x, y rectangle coordinates to render all of ticker within a single zone
        zoneHSM.x% = zoneHSM.x% - (m.bsp.sign.brightWallColumnPosition% * videoModeWidth)
        zoneHSM.y% = zoneHSM.y% - (m.bsp.sign.brightWallRowPosition% * videoModeHeight)
        zoneHSM.rectangle = CreateObject("roRectangle", zoneHSM.x%, zoneHSM.y%, zoneHSM.width%, zoneHSM.height%)        
  
        textWidget = CreateObject("roTextWidget", zoneHSM.rectangle, zoneHSM.numberOfLines%, zoneHSM.scrollingMethod%, a)
  
      endif
  
    else
  
        textWidget = CreateObject("roTextWidget", zoneHSM.rectangle, zoneHSM.numberOfLines%, zoneHSM.scrollingMethod%, a)
  
    endif
  
    zoneHSM.widget = textWidget
    
    if type(textWidget) = "roTextWidget" then
  
      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
    
    endif
  
    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()
  
    if m.stateMachine.widget <> invalid then
  
      if not(m.bsp.sign.isBrightWall and m.stateMachine.isMultiScreen and not m.stateMachine.isTickerMaster) then
  
        ' 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
      
      endif
  
      if m.stateMachine.isVisible then m.stateMachine.widget.Show()
    
    endif
  
  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.ProcessSupervisorCheckForContentMessage = ProcessSupervisorCheckForContentMessage
    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" and not GetGlobalAA().registrySettings.brightwall_setup_screen_enabled 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(GetActiveSettings().deviceScreenShotsInterval, 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 GetActiveSettings().deviceScreenShotsEnabled 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
          
          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
  
          ' if BrightWall device setup is active, restart it properly
          if IsBoolean(GetGlobalAA().registrySettings.brightwall_setup_screen_enabled) and GetGlobalAA().registrySettings.brightwall_setup_screen_enabled then
            m.bsp.sign = invalid
            RebootDeviceBrightWall(m.bsp)
            return "HANDLED"
          endif
  
          ' new content was downloaded from the network
          m.bsp.diagnostics.PrintDebug("STPlayerEventHandler - CONTENT_UPDATED")
  
          syncSpecSpec = GetSyncSpec()
          if type(syncSpecSpec) <> "roAssociativeArray" then
            stop
          endif
          currentSyncSpec = syncSpecSpec.syncSpec
          currentSyncSpecFileName = syncSpecSpec.fileName
  
          if currentSyncSpec.ReadFromFile(currentSyncSpecFileName) then
            m.bsp.assetCollection = currentSyncSpec.GetAssets("download")
            m.bsp.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.assetPool, m.bsp.assetCollection)
            UpdateSyncSpecAndSettings(currentSyncSpec, currentSyncSpecFileName, syncSpecSpec.syncSpecType)        
          end if
          
          m.bsp.Restart("")
          
          activeScheduledPresentation = m.bsp.schedule.activeScheduledEvent
          if type(activeScheduledPresentation) = "roAssociativeArray" and type(m.bsp.sign) = "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
        
        TakeSnapshot(m.bsp.systemTime, presentationName$)
        
        m.bsp.remoteSnapshotTimer.SetElapsed(GetActiveSettings().deviceScreenShotsInterval, 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
      
      eventString$ = ""
      if findMemberFunction(event, "GetString") <> invalid then eventString$ = event.GetString()
  
      if eventString$ = "" then
        edid = m.bsp.videoMode.GetEdidIdentity(true)
      else
        edid = m.bsp.videoMode.GetEdidIdentity(eventString$)
      end if
      
      UpdateEdidValues(edid, m.bsp.sysInfo, eventString$)
      edid = invalid
      
      m.bsp.UpdateEdidUserVariables(true, eventString$)
      
      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
      
      m.bsp.diagnostics.PrintDebug("### 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"
        
      end if
      
    else if type(event) = "roControlCloudMessageEvent" then
      
      if IsString(event.getUserData()) and event.GetUserData() = "bootstrap" then
        m.bsp.diagnostics.PrintDebug("supervisor / bootstrap roControlCloudMessageEvent received")
        ccloudData = event.GetData()
        if IsString(ccloudData) then
          payload = ParseJson(ccloudData)
          m.ProcessSupervisorCheckForContentMessage(payload)
          return "HANDLED"
        end if
      end if
  
    else if type(event) = "roDatagramEvent"
  
      userData = event.GetUserData()
  
      if IsString(userData) and userData = "bootstrap" then
        m.bsp.diagnostics.PrintDebug("supervisor / bootstrap roDatagramEvent received")
        payload = ParseJson(event.GetString())
        m.ProcessSupervisorCheckForContentMessage(payload)
        return "HANDLED"
      endif
  
      if IsString(userData) and userData = "brightWall" then
        m.bsp.ProcessBrightWallUdpMessage(event.GetString())
      endif
  
    end if
  
    stateData.nextState = m.superState
    return "SUPER"
  
  end function
  
  
  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$ = GetGlobalAA().settings.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$
    ' TEDTODO-Subban: can I eliminate all references to m.stateMachine.currentSync?
    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"] = GetGlobalAA().settings.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
  
  
  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 lcase(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]
                  m.bsp.UpdateVariable(userVariable, 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())
  
        if m.bsp.textDataFeedsNumRetries% >= m.bsp.textMaxRetries% then
          
          globalAA = GetGlobalAA()
          if not globalAA.networkInterfacePriorityLists.DoesExist("textFeedsDownloadEnabled") then
            ' TODO - unexpected error
            stop
          endif
  
          networkInterfacePriorityList = globalAA.networkInterfacePriorityLists.Lookup("textFeedsDownloadEnabled")
          m.bsp.textDataFeedsBindingPriorityIndex = m.bsp.textDataFeedsBindingPriorityIndex + 1
          if m.bsp.textDataFeedsBindingPriorityIndex >= networkInterfacePriorityList.count() then
            ' all network interfaces failed
            m.bsp.diagnostics.PrintDebug("### text data feed content download failed on all network interfaces")
            m.bsp.textDataFeedsBindingPriorityIndex = 0
          else
            ' try next network interface
            m.bsp.diagnostics.PrintDebug("### text data feed content download failed. Try next network interface")
          endif
  
        else
          m.bsp.textDataFeedsNumRetries% = m.bsp.textDataFeedsNumRetries% + 1
          m.bsp.diagnostics.PrintDebug("### retry text data feed content download")
        endif
  
  
        ' 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%)
  
      return "HANDLED"
  
    end if
    
  stateData.nextState = m.superState
  return "SUPER"
    
  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
  
  
  ' TEDTODO - see references to syncSpec - no work done here yet.
  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.json"
        syncSpecFilePath$ = m.stateMachine.storagePath$ + syncSpecFile$
        if not FileExists(syncSpecFilePath$) then
          performingFWUpdate = true
          syncSpecFile$ = "/fwUpdate/fw-sync.json"
          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$ = GetGlobalAA().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.json")
          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.json.")
            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.json then do cleanup
      if not m.stateMachine.newSync.WriteToFile("local-sync.json") 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.json"
    if FileExists(syncSpecFilePath$) then
      return true
    end if
    
    syncSpecFilePath$ = storagePath$ + "/fwUpdate/fw-sync.json"
    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$)
    
    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)
    
    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
    
    aa = GetBinding("textFeedsDownloadEnabled", m.textDataFeedsBindingPriorityIndex)
    binding = aa.network_interface
    m.textDataFeedsBindingPriorityIndex = aa.priorityIndex
  
    m.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for RetrieveLiveDataFeed is ", 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.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.UpdateRemoteSnapshotSettingsFromSyncSpec = UpdateRemoteSnapshotSettingsFromSyncSpec
    
    NetworkingStateMachine.ResetDownloadTimerToDoRetry = ResetDownloadTimerToDoRetry
  
    NetworkingStateMachine.retryInterval% = 60
    NetworkingStateMachine.numRetries% = 0
    NetworkingStateMachine.maxRetries% = 3
  
    NetworkingStateMachine.networkingBindingPriorityIndex = 0
  
    NetworkingStateMachine.logFileUploadsBindingPriorityIndex = 0
    NetworkingStateMachine.logFileUploadsNumRetries% = 0
    NetworkingStateMachine.logFileUploadsMaxRetries% = 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.ProcessSupervisorCheckForUpdateScheduleMessage = ProcessSupervisorCheckForUpdateScheduleMessage
    NetworkingStateMachine.stWaitForTimeout.superState = NetworkingStateMachine.stNetworkScheduler
    
    NetworkingStateMachine.stRetrievingSyncList = NetworkingStateMachine.newHState(bsp, "RetrievingSyncList")
    NetworkingStateMachine.stRetrievingSyncList.StartSync = StartSync
    NetworkingStateMachine.stRetrievingSyncList.SyncSpecXferEvent = SyncSpecXferEvent
    NetworkingStateMachine.stRetrievingSyncList.GetSyncSpecChangeType = GetSyncSpecChangeType
    NetworkingStateMachine.stRetrievingSyncList.HandleSyncSpecUnchanged = HandleSyncSpecUnchanged
    NetworkingStateMachine.stRetrievingSyncList.HStateEventHandler = STRetrievingSyncListEventHandler
    NetworkingStateMachine.stRetrievingSyncList.superState = NetworkingStateMachine.stNetworkScheduler
    NetworkingStateMachine.stRetrievingSyncList.ConfigureNetwork = ConfigureNetwork
    NetworkingStateMachine.stRetrievingSyncList.UpdateRegistrySetting = UpdateRegistrySetting
    NetworkingStateMachine.stRetrievingSyncList.UpdateBoolRegistrySetting = UpdateBoolRegistrySetting
    ' TEDTODO - rename these functions
    NetworkingStateMachine.stRetrievingSyncList.UpdateSettingsFromSyncSpec = UpdateSettingsFromSyncSpec
    NetworkingStateMachine.stRetrievingSyncList.ProcessSyncSpecSettingsUpdates0 = ProcessSyncSpecSettingsUpdates0
    NetworkingStateMachine.stRetrievingSyncList.ProcessSyncSpecSettingsUpdates2 = ProcessSyncSpecSettingsUpdates2
    NetworkingStateMachine.stRetrievingSyncList.ProcessSyncSpecSettingsUpdates3 = ProcessSyncSpecSettingsUpdates3
  
    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
    
    activeSyncSpec = GetActiveSyncSpec()
    activeSettings = GetActiveSettings()  
    activeSyncSpecSettings = GetActiveSyncSpecSettings()
  
    ' 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$ = GetGlobalAA().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 = GetActiveSyncSpec()
  
    base$ = activeSyncSpecSettings.base
    nextURL = GetURL(base$, activeSyncSpecSettings.next)
    m.eventURL = GetURL(base$, activeSyncSpecSettings.event)
    m.deviceDownloadProgressURL = GetURL(base$, activeSyncSpecSettings.devicedownloadprogress)
    m.deviceDownloadURL = GetURL(base$, activeSyncSpecSettings.devicedownload)
    m.trafficDownloadURL = GetURL(base$, activeSyncSpecSettings.trafficdownload)
    m.deviceErrorURL = GetURL(base$, activeSyncSpecSettings.deviceerror)
    m.uploadLogFileURL$ = GetURL(base$, activeSyncSpecSettings.uploadlogs)
    
    timezone = activeSettings.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 '
    ' TEDTODO - okay that accessToken doesn't exist for sfn?
    m.accessToken$ = activeSyncSpecSettings.accessToken
    user$ = activeSyncSpecSettings.user
    password$ = activeSyncSpecSettings.password
    if user$ <> "" or password$ <> "" then
      m.setUserAndPassword = true
      m.enableBasicAuthentication = activeSyncSpecSettings.enableBasicAuthentication
    else
      m.setUserAndPassword = false
      m.enableBasicAuthentication = false
    end if
    
    inheritNetworkProperties = activeSettings.inheritNetworkProperties
  
    ' get net connect parameters, setup timer, and rate limits
    timeBetweenNetConnects$ = activeSyncSpecSettings.timeBetweenNetConnects
    contentDownloadsRestricted = activeSettings.contentDownloadsRestricted
    contentDownloadRangeStart = activeSettings.contentDownloadRangeStart
    contentDownloadRangeLength = activeSettings.contentDownloadRangeLength
    
    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 then
      m.diagnostics.PrintDebug("### Content downloads are restricted to the time from " + stri(contentDownloadRangeStart) + " for " + stri(contentDownloadRangeLength) + " minutes.")
    else
      m.diagnostics.PrintDebug("### Content downloads are unrestricted")
    end if
    
    ' program the rate limit for networking
    notInDownloadWindow = false
    if contentDownloadsRestricted then
      currentTime = m.systemTime.GetLocalDateTime()
      startOfRange% = GetActiveSettings().contentDownloadRangeStart
      endOfRange% = startOfRange% + GetActiveSettings().contentDownloadRangeLength 
      notInDownloadWindow = OutOfDownloadWindow(currentTime, startOfRange%, endOfRange%)
    end if
    
    '  remote snapshot values
    m.SetRemoteSnapshotUrls()
    m.pendingSnapshotsToUpload = { }
    
    ' diagnostic web server
    dwsRebootRequired = false
    if not GetGlobalAA().useSupervisorConfigSpec then  
      dwsRebootRequired = GetAndSaveDWSParams(activeSettings, GetGlobalAA().registrySettings)
    endif
  
    downloadRateLimits = GetDownloadRateLimits(activeSettings, not notInDownloadWindow)
    SetDownloadRateLimits(m.diagnostics, downloadRateLimits)
    
    m.bsp.obfuscatedEncryptionKey = ""
  
    if dwsRebootRequired then RebootSystem()
    
    return m.stRetrievingSyncList
    
  end function
  
  
  Sub SetRemoteSnapshotUrls()
    
    m.deviceScreenShotsTemporaryStorage$ = GetActiveSyncSpecSettings().deviceScreenShotsTemporaryStorage
    m.incomingDeviceScreenshotsQueue$ = GetActiveSyncSpecSettings().incomingDeviceScreenshotsQueue
    m.awsAccessKeyId$ = GetActiveSyncSpecSettings().awsAccessKeyId
    m.awsSecretAccessKey$ = GetActiveSyncSpecSettings().awsSecretAccessKey
    m.awsSessionToken$ = GetActiveSyncSpecSettings().awsSessionToken
    m.ParseAWSURLs()
    m.uploadSnapshotsURL$ = GetActiveSyncSpecSettings().uploadSnapshots
    
    m.uploadDeviceScreenshotHandlerAddress = GetActiveSyncSpecSettings().uploadDeviceScreenshotHandlerAddress
    m.securityToken = GetActiveSyncSpecSettings().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 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
  
  
  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
  
  
  Function CleanServerHeaders(serverMetadata as object) as object
  
    serverHeaders = GetServerMetadata(GetGlobalAA().registrySection, serverMetadata)
    if lcase(getGlobalAA().settings.setupType) = "sfn" then
      serverHeaders.delete("user") 
      serverHeaders.delete("password")
    endif
  
    return serverHeaders
  
  end function
  
  
  Sub AddUploadHeaders(url as object, contentDisposition$)
  
    serverHeaders = CleanServerHeaders(m.currentSync.GetMetadata("server"))
    url.SetHeaders(serverHeaders)
    
    ' 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")
    
    aa = GetBinding("contentDownloadEnabled", 0)
    binding = aa.network_interface
    m.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for UploadDeviceDownloadProgressItems is ", 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$)
    
    aa = GetBinding("contentDownloadEnabled", 0)
    binding = aa.network_interface
    m.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for UploadDeviceDownload is ", 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
  
    aa = GetBinding("logsUploadEnabled", m.logFileUploadsBindingPriorityIndex)
    binding = aa.network_interface
    m.logFileUploadsBindingPriorityIndex = aa.priorityIndex
  
    m.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for UploadLogFiles is ", 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
  
      m.logFileUploadsBindingPriorityIndex = 0  
      m.logFileUploadsNumRetries% = 0
  
    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))
      
      if m.logFileUploadsNumRetries% >= m.logFileUploadsMaxRetries% then
        
        globalAA = GetGlobalAA()
        if not globalAA.networkInterfacePriorityLists.DoesExist("logsUploadEnabled") then
          ' TODO - unexpected error
          stop
        endif
  
        networkInterfacePriorityList = globalAA.networkInterfacePriorityLists.Lookup("logsUploadEnabled")
        m.logFileUploadsBindingPriorityIndex = m.logFileUploadsBindingPriorityIndex + 1
        if m.logFileUploadsBindingPriorityIndex >= networkInterfacePriorityList.count() then
          ' all network interfaces failed
          m.diagnostics.PrintDebug("### mrss data feed content download failed on all network interfaces")
          m.logFileUploadsBindingPriorityIndex = 0
        else
          ' try next network interface
          m.diagnostics.PrintDebug("### mrss data feed content download failed. Try next network interface")
        endif
  
      else
        m.logFileUploadsNumRetries% = m.logFileUploadsNumRetries% + 1
        m.diagnostics.PrintDebug("### retry mrss data feed content download")
      endif
  
    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#
    
    aa = GetBinding("contentDownloadEnabled", 0)
    binding = aa.network_interface
    m.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for UploadTrafficDownload is ", 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
    
    m.pendingMRSSContentDownloaded# = 0
    m.lastMRSSContentDownloaded# = contentDownloaded#
    
    aa = GetBinding("mediaFeedsDownloadEnabled", 0)
    binding = aa.network_interface
    m.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for UploadMRSSTrafficDownload is ", 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(GetGlobalAA().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$)
    
    aa = GetBinding("contentDownloadEnabled", 0)
    binding = aa.network_interface
    m.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for UploadEvent is ", 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$)
    
    aa = GetBinding("contentDownloadEnabled", 0)
    binding = aa.network_interface
    m.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for UploadDeviceError is ", 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 GetActiveSettings().contentDownloadsRestricted then
          startOfRange% = GetActiveSettings().contentDownloadRangeStart
          endOfRange% = startOfRange% + GetActiveSettings().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
        
        downloadRateLimits = GetDownloadRateLimits(GetActiveSettings(), true)
        SetDownloadRateLimits(m.bsp.diagnostics, downloadRateLimits)
        
        ' start window end timer
        if GetActiveSettings().contentDownloadsRestricted then
          
          currentTime = m.stateMachine.systemTime.GetLocalDateTime()
          startOfRange% = GetActiveSettings().contentDownloadRangeStart
          endOfRange% = startOfRange% + GetActiveSettings().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
        downloadRateLimits = GetDownloadRateLimits(GetActiveSettings(), false)
        SetDownloadRateLimits(m.bsp.diagnostics, downloadRateLimits)
        
        ' start window start timer
        currentTime = m.stateMachine.systemTime.GetLocalDateTime()
        startOfRange% = GetActiveSettings().contentDownloadRangeStart
        endOfRange% = startOfRange% + GetActiveSettings().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
    
  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
  
      globalAA = GetGlobalAA()
  
      if not globalAA.networkInterfacePriorityLists.DoesExist("contentDownloadEnabled") then
        ' TODO - unexpected error
        stop
      endif
  
      networkInterfacePriorityList = globalAA.networkInterfacePriorityLists.Lookup("contentDownloadEnabled")
  
      m.networkingBindingPriorityIndex = m.networkingBindingPriorityIndex + 1
  
      if m.networkingBindingPriorityIndex >= networkInterfacePriorityList.count() then
  
        ' all network interfaces failed
        m.currentTimeBetweenNetConnects% = m.timeBetweenNetConnects%
        m.diagnostics.PrintDebug("### reset_download_timer_to_do_retry - wait " + stri(m.currentTimeBetweenNetConnects%) + " seconds.")
  
        m.networkingBindingPriorityIndex = 0
  
      else
  
        ' try next network interface
        m.currentTimeBetweenNetConnects% = m.retryInterval%
        m.diagnostics.PrintDebug("### reset_download_timer_to_do_retry - max retries attempted on current network interface - try next one.")
  
      endif
  
      m.numRetries% = 0
  
    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
  
    else if type(event) = "roControlCloudMessageEvent" and IsString(event.getUserData()) and event.GetUserData() = "bootstrap" then
      m.bsp.diagnostics.PrintDebug("supervisor / bootstrap roControlCloudMessageEvent received")
      ccloudData = event.GetData()
      if IsString(ccloudData) then
        payload=ParseJson(ccloudData)    
        action = m.ProcessSupervisorCheckForUpdateScheduleMessage(stateData, payload)
        if IsString(action) then
          return action
        endif
      endif
  
    ' Check user data to distinguish between presentation udp messages and bootstrap udp messages
    else if type(event) = "roDatagramEvent" and IsString(event.getUserData()) and event.GetUserData() = "bootstrap" then
      payload = ParseJson(event.GetString())
      action = m.ProcessSupervisorCheckForUpdateScheduleMessage(stateData, payload)
      if IsString(action) then
        return action
      endif
  
    ' 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
  
  
  Sub ProcessSupervisorCheckForContentMessage(payload as object)
  
    if type(payload) = "roAssociativeArray" and IsString(payload.message) then 
      if payload.message = "checkforcontent" and IsBoolean(payload.updateSettings) and payload.updateSettings then
        if GetGlobalAA().useSupervisorConfigSpec
          m.bsp.diagnostics.PrintDebug("supervisor updateSettings roControlCloudMessageEvent received")
          SetPendingSettings()
          UpdateSettingsFromConfig()
          SetActiveSettingsFromPendingSettings()
        else
          m.bsp.diagnostics.PrintDebug("ignore supervisor updateSettings roControlCloudMessageEvent received - settings handler disabled")
        endif
      else if payload.message = "cellular-status" then
        if isBoolean(payload.value) then
          globalAA = GetGlobalAA()
          globalAA.cellularModemActive = payload.value 
        endif
      endif
    endif
  end sub
  
  
  Function ProcessSupervisorCheckForUpdateScheduleMessage(stateData as object, payload as object) as object
  
    if type(payload) = "roAssociativeArray" then
      if IsString(payload.message) and payload.message = "checkforcontent" then
        m.bsp.diagnostics.PrintDebug("===Successfully read checkforcontent message.")
        if (not GetGlobalAA().useSupervisorConfigSpec) or (IsBoolean(payload.updateSchedule) and payload.updateSchedule) then
          stateData.nextState = m.stateMachine.stRetrievingSyncList
          return "TRANSITION"
        endif        
      end if
    endif
  
    return invalid
  
  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.
    
    activeSettings = GetActiveSettings()  
    activeSyncSpecSettings = GetActiveSyncSpecSettings()
    
    base$ = activeSyncSpecSettings.base
    nextURL = GetURL(base$, activeSyncSpecSettings.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(activeSyncSpecSettings.user, activeSyncSpecSettings.password)
    m.xfer.EnableUnsafeAuthentication(m.stateMachine.enableBasicAuthentication)
    m.xfer.SetMinimumTransferRate(10, 240)
    headers = CleanServerHeaders(m.stateMachine.currentSync.GetMetadata("server"))
    headers.group = GetGlobalAA().settings.group
    m.xfer.SetHeaders(headers)
  
    ' 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 for legacy BrightWall
    m.xfer.AddHeader("BrightWallName", "")
    m.xfer.AddHeader("BrightWallScreenNumber", "")
    
    aa = GetBinding("contentDownloadEnabled", m.stateMachine.networkingBindingPriorityIndex)
    binding = aa.network_interface
    m.stateMachine.networkingBindingPriorityIndex = aa.priorityIndex
  
    m.bsp.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for StartSync is ", binding))
    ok = m.xfer.BindToInterface(binding)
    if not ok then stop
    
    if not m.xfer.AsyncGetToObject("roSyncSpec") then stop
    
  end sub
  
  
  ' TEDTODO - accessToken - some places read from registry; other places from sync spec. is this correct? didn't exist in old autorun
  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 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("localToBSN-sync.json") 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
  
    rc = event.GetResponseCode()
  
    if rc = 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()
      WriteRegistrySetting("lastBSNConnectionTime", currentTime)
      GetGlobalAA().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
      
      if headers.DoesExist("last-modified") then
        lastModifiedHeader$ = headers["last-modified"]
        lastModifiedDt = ParseHTTPDateTime(lastModifiedHeader$)
      else
        lastModifiedDt = m.bsp.systemTime.GetUtcDateTime()
      endif
  
      lastModifiedTime$ = FormatDateTime(lastModifiedDt) + "Z"
  
      ' proceed with download, etc if content changed or metadata other than aws properties changed
      syncSpecChange = m.GetSyncSpecChangeType()
      if syncSpecChange = "noChange" or syncSpecChange = "unknownChange" then
        return m.stateMachine.stWaitForTimeout
      else if syncSpecChange = "awsPropertiesChanged" then
        activeSyncSpecSettings = GetActiveSyncSpecSettings()
        activeSyncSpecSettings.awsAccessKeyId = m.stateMachine.newSync.LookupMetadata("client", "awsAccessKeyId")
        activeSyncSpecSettings.awsSecretAccessKey = m.stateMachine.newSync.LookupMetadata("client", "awsSecretAccessKey")
        activeSyncSpecSettings.awsSessionToken = m.stateMachine.newSync.LookupMetadata("client", "awsSessionToken")
        m.stateMachine.UpdateRemoteSnapshotSettingsFromSyncSpec(m.stateMachine.newSync)
        return m.stateMachine.stWaitForTimeout
      endif
  
  
      m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_RECEIVED, "YES")
      
      m.stateMachine.assetCollection = m.stateMachine.newSync.GetAssets("download")
      m.stateMachine.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.assetPool, m.stateMachine.assetCollection)
      
      m.stateMachine.BuildFileDownloadList(m.stateMachine.newSync)
      m.stateMachine.UploadDeviceDownloadProgressFileList()
      m.stateMachine.FileListPendingUpload = false
      m.stateMachine.AddDeviceDownloadItem("SyncSpecDownloadStarted", "", "")
      
      m.stateMachine.contentDownloaded# = 0
      
      ' Use of syncSettings, pendingSyncSettings is made tricky due to the different combinations of
      '     bsn vs. sfn
      '       impacts whether a given parameter can be changed publish operation
      '       impacts whether a settings message would have been received (no for SFN, yes for BSN if Settings Supervisor (but
      '       in the current release, there are no settings that change on a BSN publish))
      '     Settings Supervisor vs Non Settings Supervisor
      
      ' only update settings if pre Settings supervisor or sfn
      if not GetGlobalAA().useSupervisorConfigSpec or lcase(GetGlobalAA().settings.setupType) = "sfn"
        SetPendingSyncSpecAndSettings(m.stateMachine.newSync)
        syncSettings = GetPendingSettings()  
        pendingSyncSpecSettings = GetPendingSyncSpecSettings()
        if not GetGlobalAA().useSupervisorConfigSpec then
          ' pre settings supervisor
          m.UpdateSettingsFromSyncSpec(syncSettings, pendingSyncSpecSettings)
        else
          ' sfn with settings supervisor
          SendUpdatedSettingsToSupervisor(lastModifiedTime$, syncSettings)
        end if
      else
        ' if Settings Supervisor and bsn, continue to use existing settings but use new sync spec
        SetPendingSyncSpec(m.stateMachine.newSync)
        syncSettings = GetActiveSettings()
        pendingSyncSpecSettings = GetPendingSyncSpecSettings()
      endif  
  
      ' Update the pool sizes based on the newly downloaded sync spec
      m.bsp.SetPoolSizes(pendingSyncSpecSettings)
  
      m.stateMachine.UpdateRemoteSnapshotSettingsFromSyncSpec(m.stateMachine.newSync)
  
      ' Only proceed with sync list download if the current time is within the range of allowed times for content downloads
      currentTime = m.stateMachine.systemTime.GetLocalDateTime()
      notInDownloadWindow = false
      if syncSettings.contentDownloadsRestricted then
        startOfRange% = syncSettings.contentDownloadRangeStart
        endOfRange% = startOfRange% + syncSettings.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.networkingBindingPriorityIndex = 0
          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())
  
          downloadRateLimits = GetDownloadRateLimits(syncSettings, not notInDownloadWindow)
          SetDownloadRateLimits(m.bsp.diagnostics, downloadRateLimits)
  
          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())
  
          downloadRateLimits = GetDownloadRateLimits(syncSettings, not notInDownloadWindow)
          SetDownloadRateLimits(m.bsp.diagnostics, downloadRateLimits)        
        
        end if
        
      else
        downloadRateLimits = GetDownloadRateLimits(syncSettings, not notInDownloadWindow)
        SetDownloadRateLimits(m.bsp.diagnostics, downloadRateLimits)        
      end if
      
      return m.stateMachine.stDownloadingSyncFiles
      
    else if rc = 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 rc <> 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(rc)
        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(rc))
      else
        print "received 503"
      end if
      
    end if
    
    return m.stateMachine.stWaitForTimeout
    
  end function
  
  
  Sub HandleSyncSpecUnchanged(debugMsg as string)
  
      m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SYNCSPEC_RECEIVED, "NO")
      m.bsp.diagnostics.PrintDebug(debugMsg)
      m.stateMachine.AddDeviceDownloadItem("SyncSpecUnchanged", "", "")
      m.stateMachine.newSync = invalid
      m.stateMachine.networkingBindingPriorityIndex = 0
      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
      
  end sub
  
  
  Function GetSyncSpecChangeType() as string
  
    if m.stateMachine.newSync.EqualTo(m.stateMachine.currentSync) then
  
      m.HandleSyncSpecUnchanged("GetSyncSpecChangeType ### Server has given us a spec that matches current-sync. Nothing more to do.")
      return "noChange"
    
    else if m.stateMachine.newSync.FilesEqualTo(m.stateMachine.currentSync) then
  
      ' no files have changed - only metadata has changed
      ' sync spec changes can mostly be ignored if the only changes are to the following 
      '     awsAccessKeyId
      '     awsSecretAccessKey
      '     awsSessionToken
  
      metaDataMatches = true
      awsPropertyChanged = false
  
      newClientMetadata = m.stateMachine.newSync.getMetadata("client")
      currentClientMetadata = m.stateMachine.currentSync.getMetadata("client")
  
      for each propertyName in currentClientMetadata
        
        if currentClientMetadata.DoesExist(propertyName) then
          currentPropertyValue = currentClientMetadata.Lookup(propertyName)
          if not newClientMetadata.DoesExist(propertyName) then
            metaDataMatches = false
            m.bsp.diagnostics.PrintDebug("GetSyncSpecChangeType " + propertyName + " exists in current sync spec but not new sync spec")
          else
            newPropertyValue = newClientMetadata.Lookup(propertyName)
            if type(currentPropertyValue) = type(newPropertyValue) then
              if newPropertyValue <> currentPropertyValue then
                if lcase(propertyName) = "awsaccesskeyid" or lcase(propertyName) = "awssecretaccesskey" or lcase(propertyName)= "awssessiontoken" then
                  m.bsp.diagnostics.PrintDebug("GetSyncSpecChangeType aws property updated")
                  awsPropertyChanged = true
                else
                  metaDataMatches = false
                  m.bsp.diagnostics.PrintDebug("GetSyncSpecChangeType sync spec change due to updated value for property " + propertyName)
                endif
              endif
            else
              metaDataMatches = false
              m.bsp.diagnostics.PrintDebug("GetSyncSpecChangeType " + propertyName + " types differ between old and new")
            endif
          endif
        endif
  
        if not metaDataMatches then
          exit for
        endif
  
      next
  
      if metaDataMatches then
        if awsPropertyChanged then
          return "awsPropertiesChanged"
        else
          ' TEDTODO - is this possible?
          m.HandleSyncSpecUnchanged("GetSyncSpecChangeType ### Server has given us a spec that meaningfully matches current-sync. Nothing more to do.")
          return "unknownChange"
        endif
      else
        return "metadataChanged"
      endif
  
    else
      return "fullChange"
    end if
    
  end function
  
  
  Function NetworkingIsActive()
    
    return m.networkingActive
    
  end function
  
  
  Function GetRateLimitValue(mode as string, rate as string) as integer
  
    if mode = "unlimited" then
      rate% = 0
    else if mode = "specified" then
      rate% = int(val(rate))
    else
      rate% = -1
    endif
  
    return rate%
  
  end function
  
  
  Function GetDownloadRateLimits(settings as object, inDownloadWindow as boolean) as object
  
    downloadRateLimits = []
  
    for each interface in settings.network.interfaces
  
      ' TEDTODO -currently only implemented for sync spec settings and likely not correctly.
      ' I'm not sure this is really true - test and either fix or remove comment.
      if settings.contentDownloadsRestricted then
        if inDownloadWindow then
          rateLimit% = interface.ratelimitrateinwindow%
        else
          rateLimit% = interface.ratelimitrateoutsidewindow%
        endif
      else
        rateLimit% = interface.rateLimitRateOutsideWindow%
      end if
  
      interfaceRateLimits = {}
      interfaceRateLimits.networkInterface = interface.networkInterface
      interfaceRateLimits.rateLimit% = rateLimit%
  
      downloadRateLimits.push(interfaceRateLimits)
    
    next
  
    return downloadRateLimits
  
  end function
  
  
  Sub SetDownloadRateLimits(diagnostics as object, downloadRateLimits as object)
  
    for each rateLimitSpec in downloadRateLimits
  
      nc = CreateObject("roNetworkConfiguration", rateLimitSpec.networkInterface)
      if type(nc) = "roNetworkConfiguration"
        diagnostics.PrintDebug("SetInboundShaperRate to " + stri(rateLimitSpec.rateLimit%))
        ok = nc.SetInboundShaperRate(rateLimitSpec.rateLimit%)
        if not ok then diagnostics.PrintDebug("Failure calling SetInboundShaperRate with parameter " + stri(rateLimitSpec.rateLimit%))
        ok = nc.Apply()
        if not ok then diagnostics.PrintDebug("Failure calling roNetworkConfiguration.Apply()")
      end if
  
    next
  
  end sub
  
  
  Function UpdateRegistrySetting(newValue$ as string, existingValue$ as string, registryKey$ as string) as string
    
    if lcase(newValue$) <> lcase(existingValue$) then
      WriteRegistrySetting(registryKey$, newValue$)
    end if
    
    return newValue$
    
  end function
  
  
  Sub UpdateBoolRegistrySetting(newValue as boolean, existingValue as boolean, registryKey$ as string)
    
    if newValue <> existingValue then
      WriteRegistrySetting(registryKey$, GetStringFromBool(newValue))
    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
  
    activeSyncSpecSettings = GetActiveSyncSpecSettings()
  
    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(activeSyncSpecSettings.user, activeSyncSpecSettings.password)
    m.stateMachine.assetFetcher.EnableUnsafeAuthentication(m.stateMachine.enableBasicAuthentication)
    m.stateMachine.assetFetcher.SetMinimumTransferRate(1000, 60)
    serverHeaders = CleanServerHeaders(m.stateMachine.newSync.GetMetadata("server"))
    m.stateMachine.assetFetcher.SetHeaders(serverHeaders)
    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)
  
    aa = GetBinding("contentDownloadEnabled", m.stateMachine.networkingBindingPriorityIndex)
    binding = aa.network_interface
    m.stateMachine.networkingBindingPriorityIndex = aa.priorityIndex
  
    m.bsp.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for assetFetcher is (StartSyncListDownload)", 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
    
    newSyncSettings = GetPendingSettings()  
    newSyncSpecSettings = GetPendingSyncSpecSettings()
  
    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 binding priority index and timeout period
      m.stateMachine.networkingBindingPriorityIndex = 0
      m.stateMachine.numRetries% = 0
      m.stateMachine.currentTimeBetweenNetConnects% = m.stateMachine.timeBetweenNetConnects%
      
      ' diagnostic web server - pre Settings Supervisor
      dwsRebootRequired = false
      if not GetGlobalAA().useSupervisorConfigSpec then
        dwsRebootRequired = GetAndSaveDWSParams(newSyncSettings, GetGlobalAA().registrySettings)
        m.stateMachine.stRetrievingSyncList.UpdateBoolRegistrySetting(newSyncSettings.dwsEnabled, GetGlobalAA().registrySettings.dwsEnabled, "dwse")
        m.stateMachine.stRetrievingSyncList.UpdateRegistrySetting(newSyncSettings.dwsPassword, GetGlobalAA().registrySettings.dwsPassword$, "dwsp")
      endif
  
      oldSyncSpecScriptsOnly = m.stateMachine.currentSync.FilterFiles("download", { group: "script" })
      newSyncSpecScriptsOnly = m.stateMachine.newSync.FilterFiles("download", { group: "script" })
      
      rebootRequired = false
      
      if not oldSyncSpecScriptsOnly.FilesEqualTo(newSyncSpecScriptsOnly) then
        
        realizer = CreateObject("roAssetRealizer", m.bsp.assetPool, "/")
        globalAA = GetGlobalAA()
        globalAA.bsp.msgPort.DeferWatchdog(120)
        event = realizer.Realize(newSyncSpecScriptsOnly)
        realizer = invalid
        
        if event.GetEvent() <> m.stateMachine.EVENT_REALIZE_SUCCESS then
          m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_REALIZE_FAILURE, stri(event.GetEvent()) + chr(9) + event.GetName() + chr(9) + event.GetFailureReason())
          m.bsp.diagnostics.PrintDebug("### Realize failed " + stri(event.GetEvent()) + chr(9) + event.GetName() + chr(9) + event.GetFailureReason())
          m.stateMachine.AddDeviceErrorItem("RealizeFailure", event.GetName(), event.GetFailureReason(), str(event.GetEvent()))
          
          m.stateMachine.newSync = invalid
          m.stateMachine.assetFetcher = invalid
          
          return m.stateMachine.stWaitForTimeout
        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 - pre Settings Supervisor
      if not GetGlobalAA().useSupervisorConfigSpec then
        timezone = newSyncSettings.timezone
        if timezone <> "" then
          m.stateMachine.systemTime.SetTimeZone(timezone)
        end if
      endif
  
      m.bsp.diagnostics.PrintTimestamp()
      m.bsp.diagnostics.PrintDebug("### DOWNLOAD COMPLETE")
      
      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
      
      ' dws - pre Settings Supervisor
      if not GetGlobalAA().useSupervisorConfigSpec then
        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
      endif
  
      m.stateMachine.assetCollection = m.stateMachine.newSync.GetAssets("download")
      m.stateMachine.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.assetPool, m.stateMachine.assetCollection)
      if type(m.stateMachine.assetPoolFiles) <> "roAssetPoolFiles" then stop
      
      globalAA = GetGlobalAA()
      globalAA.autoscheduleFilePath$ = GetPoolFilePath(m.stateMachine.assetPoolFiles, "autoschedule.json")
      if globalAA.autoscheduleFilePath$ = "" then stop
      
      globalAA.boseProductsFilePath$ = GetPoolFilePath(m.stateMachine.assetPoolFiles, "PartnerProducts.json")
      m.stateMachine.newSync = invalid
      m.stateMachine.assetFetcher = invalid
      
      m.stateMachine.currentSync = CreateObject("roSyncSpec")
      if type(m.stateMachine.currentSync) <> "roSyncSpec" then stop
      if not m.stateMachine.currentSync.ReadFromFile("current-sync.json") then stop
      
      ' settings supervisor
      '   for bsn, values are updated via message from supervisor (none are updated via Publish with 'current' bacon)
      '   for sfn, the user can make changes for
      '     contentDownloadsRestricted, loggingEnabled and remoteSnapshots when publishing
      '     these settings are updated in a different way
      '     'TEDTODO - verify that this works? I think it's a roundabout way.
      ' non settings supervisor
      '   update settings (only changes for sfn, see above)
      if not GetGlobalAA().useSupervisorConfigSpec then
  
        UpdateSyncSpecAndSettings(m.stateMachine.currentSync, "current-sync.json", "network")
  
        settings = GetGlobalAA().settings
  
      endif
  
      m.bsp.diagnostics.PrintTimestamp()
      m.bsp.diagnostics.PrintDebug("### return from HandleAssetFetcherEvent")
      
      debugOn = newSyncSpecSettings.enableSerialDebugging
      m.bsp.diagnostics.UpdateDebugOn(debugOn)
      
      systemLogDebugOn = newSyncSpecSettings.enableSystemLogDebugging
      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" then
  
        buttonPanelIndex$ = StripLeadingSpaces(stri(m.buttonPanelIndex%))
        id = "TouchBoard-" + buttonPanelIndex$ + "-GPIO"
  
        if UserDataMatches(event.GetUserData(), "bpEvent", id) 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
  
      end if
      
    else
      
      if type(event) = "roControlUp" then
  
        buttonPanelIndex$ = StripLeadingSpaces(stri(m.buttonPanelIndex%))
        id = "TouchBoard-" + buttonPanelIndex$ + "-GPIO"
  
        if UserDataMatches(event.GetUserData(), "bpEvent", id) 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"
  
        endif
    
      ' 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))
  
      globalAA = getGlobalAA()
  
      eventHandled = false
  
      if globalAA.recordEventsMode then   
        if IsEventRecordable(type(msg)) then
          m.RecordEvent(msg)
        endif
      else if globalAA.playbackEventsMode then
        eventHandled = m.PlaybackEvent(msg)
      endif
  
      if not eventHandled then
  
        if type(msg) = "roControlDown" and stri(msg.GetSourceIdentity()) = stri(m.svcPort.GetIdentity()) then
          if msg.GetInt() = 12 then
            stop
          end if
        end if
      
        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
        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
      
      endif
  
    end while
    
  end sub
  
  
  Function ExecuteSwitchPresentationCommand(presentationName$ as string) as boolean
    
    ' retrieve target presentation
    presentation = m.presentations.Lookup(presentationName$)
    
    if type(presentation) = "roAssociativeArray" then
      
      ' check for existence of target presentation - if it's not present, don't try to switch to it
      autoplayFileName$ = "autoplay-" + presentation.presentationName + ".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, payload$ as string) 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, payload$)
        
      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()
    
    specifiedPort$ = parameters["port"].getCurrentParameterValue()
    runtimePort$ = m.GetRuntimeUsbConnector(specifiedPort$)
    
    if not gaa.usbConnectorNameToUsbSpec.DoesExist(runtimePort$) then
      m.diagnostics.PrintDebug("ExecuteSendWssCommand: speaker not found at connector: " + runtimePort$)
      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
    
    speakerSpec$ = gaa.usbConnectorNameToUsbSpec[runtimePort$].hidOutputSpec
  
    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, speaker.fid, mid(speakerSpec$, 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: " + runtimePort$)
      return
    end if
    
    wssCommand = parameters.wssCommand
    wssCommandName$ = wssCommand.getCurrentParameterValue()
    
    ' command data from wssCommunicationSpec
    
    boseProduct = m.sign.boseProductsByConnector[specifiedPort$]
    
    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["message"]
    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["message"]
    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$ = "fastblink" then
          blinkRate% = BLINK_SPEED_FAST%
        else if blinkRate$ = "mediumblink" 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$ = "hardonoff" then
          fadeOut% = 0
        else
          fadeOut% = 1
          if transitionMode$ = "smoothfulloverlap" then
            transitionMode% = MARQUEE_TRANSITION_FULL%
          else if transitionMode$ = "smoothpartialoverlap"
            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
  
  ' m is bsp
  Function GetCurrentTimeAsIsoString() as string
    return m.systemTime.GetLocalDateTime().ToIsoString()
  end function
  
  
  Sub SetSyncInfo(syncManagerPayload$ as string)
  
    syncManagerEvent = m.SyncManager.Synchronize(syncManagerPayload$, 300)
  
    m.syncInfo = { }
    m.syncInfo.SyncDomain = syncManagerEvent.GetDomain()
    m.syncInfo.SyncId = syncManagerEvent.GetId()
    m.syncInfo.SyncIsoTimestamp = syncManagerEvent.GetIsoTimestamp()
  
  end sub
  
  
  Sub ExecuteSynchronizePlayFile(zoneHSM as object, targetStateName$ as string, synchronizeKeyword$ as string)
  
    data = {}
    data.syncPayloadType = "synchronizePlayFile"
    data.synchronizeKeyword = synchronizeKeyword$
    data.userVariables = m.GetUserVariablesAsPayload()
    data.timestamp = m.GetCurrentTimeAsIsoString()
    
    payload$ = FormatJson(data)
  
    m.diagnostics.PrintDebug("Send synchronize command " + payload$ + " from ExecuteSynchronizePlayFile using SyncManager.")
  
    zoneHSM.SetSyncInfo(payload$)  
  
  end sub
  
  
  Sub ExecuteSynchronizeMediaListHelper(zoneHSM as object, data as object)
  
    data.userVariables = m.GetUserVariablesAsPayload()
    data.timestamp = m.GetCurrentTimeAsIsoString()
    payload$ = FormatJson(data)
  
    m.diagnostics.PrintDebug("Send synchronize command " + payload$ + " from ExecuteSynchronizeMediaListHelper using SyncManager.")
    
    zoneHSM.SetSyncInfo(payload$)  
  
  end sub
  
  
  Sub ExecuteSynchronizeMediaListEntry(zoneHSM as object, targetStateName$ as string)
    mediaListState = zonehsm.statetable[targetstatename$]
    if type(mediaListState) = "roAssociativeArray" and IsString(mediaListState.type$) and mediaListState.type$ = "mediaList" then
      if IsBoolean(mediaListState.firstTimeThrough) and mediaListState.firstTimeThrough then
        data = { syncPayloadType: "synchronizeMediaListFirstTimeEntry", targetStateName: targetStateName$, synchronizeKeyword: targetStateName$ }
      else
        data = { syncPayloadType: "synchronizeMediaListEntry", targetStateName: targetStateName$, synchronizeKeyword: targetStateName$ }
      endif
    endif
    data.timestamp = m.GetCurrentTimeAsIsoString()
    m.ExecuteSynchronizeMediaListHelper(zoneHSM, data)
  end sub
  
  
  Sub ExecuteSynchronizeMediaListForward(zoneHSM as object, targetStateName$ as string)
    mediaListState = zoneHSM.stateTable[targetStateName$]
    data = { syncPayloadType: "synchronizeMediaListForward", targetStateName: targetStateName$, synchronizeKeyword: targetStateName$, playbackIndex: mediaListState.playbackIndex% }
    data.timestamp = m.GetCurrentTimeAsIsoString()
    m.ExecuteSynchronizeMediaListHelper(zoneHSM, data)
  end sub
  
  
  Sub ExecuteSynchronizeMediaListBackward(zoneHSM as object, targetStateName$ as string)
    mediaListState = zoneHSM.stateTable[targetStateName$]
    data = { syncPayloadType: "synchronizeMediaListBackward", targetStateName: targetStateName$, synchronizeKeyword: targetStateName$, playbackIndex: mediaListState.playbackIndex% }
    data.timestamp = m.GetCurrentTimeAsIsoString()
    m.ExecuteSynchronizeMediaListHelper(zoneHSM, data)
  end sub
  
  
  Sub SendMrssSynchronizeRuntimeCommand(displayItem as object, filePath$ as string)
  
    zoneHSM = m.stateMachine
  
    payload = { syncPayloadType: "mrssSynchronizeRuntimeCommand", targetStateName: m.id$, displayItem: displayItem, filePath: filePath$ }
    payload.timestamp = m.stateMachine.bsp.GetCurrentTimeAsIsoString()
    payload.userVariables = m.stateMachine.bsp.GetUserVariablesAsPayload()  
  
    payload$ = FormatJson(payload)
  
    m.bsp.diagnostics.PrintDebug("SendMrssSynchronizeRuntimeCommand: " + payload$)
    
    zoneHSM.SetSyncInfo(payload$)  
  
  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.PrintTimestamp()
      m.diagnostics.PrintDebug("Send synchronize command " + synchronizeKeyword$ + " using SyncManager.")
  
      payload = {}
      payload.synchronizeKeyword = synchronizeKeyword$
      payload.timestamp = m.GetCurrentTimeAsIsoString()
  
      if lcase(GetGlobalAA().registrySettings.signType) = "brightwall" then
        payload.syncPayloadType = "syncBwBasic"
        payload.userVariables = m.GetUserVariablesAsPayload()
      else
        payload.syncPayloadType = "syncBasic"
      endif
  
      payload$ = FormatJson(payload)
      syncManagerEvent = zoneHSM.SyncManager.Synchronize(payload$, 300)
      m.diagnostics.PrintDebug("@@@ Created syncManagerEvent with sync payload: " + payload$)
  
      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
  
      zone.rectangle = CreateObject("roRectangle", x%, y%, width%, height%)
  
      ' not yet supported for BrightWalls
      ' bsp = zoneHSM.bsp
      ' if bsp.sign.isBrightWall then
      '  videoModeWidth = bsp.videoMode.getVideoResX()
      '  videoModeHeight = bsp.videoMode.getVideoResY()
      '  x% = 0 - (bsp.sign.brightWallColumnPosition% * videoModeWidth) + x%
      '  y% = 0 - (bsp.sign.brightWallRowPosition% * videoModeHeight) + y%
      ' endif
      ' r = CreateObject("roRectangle", x%, y%, width%, height%)
  
      if type(zone.videoPlayer) = "roVideoPlayer" then
        zone.videoPlayer.SetRectangle(zone.rectangle)
      end if
      
      if type(zone.mjpegvideoPlayer) = "roVideoPlayer" then
        zone.mjpegVideoPlayer.SetRectangle(zone.rectangle)
      end if
      
      if type(zone.imagePlayer) = "roImageWidget" then
        zone.imagePlayer.SetRectangle(zone.rectangle)
      end if
      
      if type(zone.displayedHtmlWidget) = "roHtmlWidget" then
        zone.displayedHtmlWidget.SetRectangle(zone.rectangle)
      end if
      
      if type(zone.canvasWidget) = "roCanvasWidget" then
        zone.canvasWidget.SetRectangle(zone.rectangle)
      end if
      
      if type(zone.widget) = "roHtmlWidget" or type(zone.widget) = "roTextWidget" then
        ok = zone.widget.SetRectangle(zone.rectangle)
      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
        
        if type(zoneHSM.widget) <> "Invalid" 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
        
        if type(zoneHSM.widget) <> "Invalid" then zoneHSM.widget.Show()
        
      else if zoneHSM.type$ = "Ticker" then
        
        if type(zoneHSM.widget) <> "Invalid" 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
  
  
  ' Send the CEC send hex string command through the specified video connector
  Sub ExecuteCecSendStringCommand(parameters as object)
    
    cecCommandParameter = parameters["cecString"]
    cecCommand$ = cecCommandParameter.GetCurrentParameterValue()
  
    videoConnector$ = getTextParameterFallbackToEmpty(parameters, "videoConnector")
    
    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$ + " videoConnector " + videoConnector$)
    m.SendCecCommand(cecCommand$, cecSubstituteSourceAddress, videoConnector$)
    
  end sub
  
  
  ' Send the CEC philips set volume command through the specified video connector
  Sub ExecuteCecPhilipsSetVolumeCommand(parameters as object)
    volumeParameter = parameters["volume"]
    volume$ = volumeParameter.GetCurrentParameterValue()
  
    videoConnector$ = getTextParameterFallbackToEmpty(parameters, "videoConnector")
    
    m.diagnostics.PrintDebug("Set cec Philips volume to " + volume$ + " with connector " + videoConnector$)
    volume% = int(val(volume$))
    m.CecPhilipsSetVolume(volume%, videoConnector$)
  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
      m.UpdateVariable(userVariable, 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
  
  
  ' m is bsp
  Sub ExecuteCmd(zoneHSM as object, command$ as string, parameters as object, payload$ as string)
    
    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$ = "synchronizePlayFile" then
      targetStateName$ = parameters.message.getCurrentParameterValue()
      m.ExecuteSynchronizePlayFile(zoneHSM, targetStateName$, payload$)
    else if command$ = "synchronizeMediaListEntry" then
      targetStateName$ = parameters.message.getCurrentParameterValue()
      m.ExecuteSynchronizeMediaListEntry(zoneHSM, targetStateName$)
    else if command$ = "synchronizeMediaListForward" then
      targetStateName$ = parameters.message.getCurrentParameterValue()
      m.ExecuteSynchronizeMediaListForward(zoneHSM, targetStateName$)
    else if command$ = "synchronizeMediaListBackward" then
      targetStateName$ = parameters.message.getCurrentParameterValue()
      m.ExecuteSynchronizeMediaListBackward(zoneHSM, targetStateName$)
    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(parameters)
    else if command$ = "cecDisplayOff" then
      m.CecDisplayOff(parameters)
    else if command$ = "cecSetSourceToBrightSign" then
      m.CecSetSourceToBrightSign(parameters)
    else if command$ = "cecSendString" then
      m.ExecuteCecSendStringCommand(parameters)
    else if command$ = "cecPhilipsSetVolume" then
      m.ExecuteCecPhilipsSetVolumeCommand(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)
    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%
      m.UpdateVariable(userVariable, 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.GetLogFiles = GetLogFiles
    logging.FlushLogFile = FlushLogFile
    logging.UpdateLogCounter = UpdateLogCounter
    logging.PrepareAddVariableDataToLog = PrepareAddVariableDataToLog
    logging.AddVariableDataToLog = AddVariableDataToLog
    logging.CleanupPostAddVariableDataToLog = CleanupPostAddVariableDataToLog
    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
      WriteRegistrySetting("lc", logCounter$)
      GetGlobalAA().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$ = GetGlobalAA().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$ = GetGlobalAA().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"
      
      WriteRegistrySetting("ld", logDate$)
      GetGlobalAA().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$ + GetActiveSyncSpecSettings().account
        logFile.SendLine(header$)
        header$ = "Group" + t$ + GetActiveSettings().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", "*")
  
    if listOfPendingLogFiles.count() > 0 then
      openedDB = m.PrepareAddVariableDataToLog()
    endif
  
    for each file in listOfPendingLogFiles
  
      sourceFilePath$ = "currentLog/" + file
      
      m.AddVariableDataToLog(sourceFilePath$)
      
      destinationFilePath$ = "logs/" + file
      CopyFile(sourceFilePath$, destinationFilePath$)
      DeleteFile(sourceFilePath$)
    next
  
    if listOfPendingLogFiles.count() > 0 then
      m.CleanupPostAddVariableDataToLog(openedDB)
    endif
  
  end sub
  
  
  Function PrepareAddVariableDataToLog() as boolean
  
    openedDB = false
    
    if m.variableLoggingEnabled then
      openedDB = false
      if not m.bsp.variablesDBExists then
        m.bsp.ReadVariablesDB("")
        openedDB = true
      end if
    endif
  
    return openedDB
  
  end function
  
  
  Sub CleanupPostAddVariableDataToLog(openedDB as boolean)
  
    if openedDB then
      m.bsp.userVariablesDB = invalid
    end if
  
  end sub
  
  
  Sub AddVariableDataToLog(filePath$ as string)
  
    if m.variableLoggingEnabled then
      
      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()
        
      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
  
  
  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
  
  ' Transform invalid value to "invalid" for purpose of avoiding runtime errors when printing variable
  Function helper_ValidateInvalidPrint(varToValidate as object) as object
    
    if varToValidate = invalid then
      return "invalid"
    else
      return varToValidate
    end if
  
  end function
  
  '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
        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 */
  
  ' 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
    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
    end while
    m.activeState = path[0]
    
    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
  
  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_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_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_CONTROL_PORT_DISCONNECTED = "1304"
    diagnosticCodes.EVENT_MULTISCREEN_TICKER_FAILURE = "1305"
    
    return diagnosticCodes
    
  end function
  
  '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$)
    
    aa = GetBinding("mediaFeedsDownloadEnabled", m.bsp.mrssDataFeedsBindingPriorityIndex)
    binding = aa.network_interface
    m.bsp.mrssDataFeedsBindingPriorityIndex = aa.priorityIndex
    
    m.bsp.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for assetFetcher is (DownloadMRSSContent) ", 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$)
    
    aa = GetBinding("mediaFeedsDownloadEnabled", m.bsp.mrssDataFeedsBindingPriorityIndex)
    binding = aa.network_interface
    m.bsp.mrssDataFeedsBindingPriorityIndex = aa.priorityIndex
  
    m.bsp.diagnostics.PrintDebug(GetBindingDiagnostic("### Binding for Download Live Feed Content is ", 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
    
    m.bsp.diagnostics.PrintDebug("----------------------------- HandleLiveDataFeedContentDownloadAssetFetcherProgressEvent: " + str(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
  
      if m.bsp.mrssDataFeedsNumRetries% >= m.bsp.mrssMaxRetries% then
        
        globalAA = GetGlobalAA()
        if not globalAA.networkInterfacePriorityLists.DoesExist("mediaFeedsDownloadEnabled") then
          ' TODO - unexpected error
          stop
        endif
  
        networkInterfacePriorityList = globalAA.networkInterfacePriorityLists.Lookup("mediaFeedsDownloadEnabled")
        m.mrssDataFeedsBindingPriorityIndex = m.mrssDataFeedsBindingPriorityIndex + 1
        if m.mrssDataFeedsBindingPriorityIndex >= networkInterfacePriorityList.count() then
          ' all network interfaces failed
          m.diagnostics.PrintDebug("### mrss data feed content download failed on all network interfaces")
          m.mrssDataFeedsBindingPriorityIndex = 0
        else
          ' try next network interface
          m.diagnostics.PrintDebug("### mrss data feed content download failed. Try next network interface")
        endif
  
      else
        m.bsp.mrssDataFeedsNumRetries% = m.bsp.mrssDataFeedsNumRetries% + 1
        m.bsp.diagnostics.PrintDebug("### retry mrss data feed content download")
      endif
  
  
      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
  
        m.bsp.mrssDataFeedsBindingPriorityIndex = 0  
        m.bsp.mrssDataFeedsNumRetries% = 0
  
        ' 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
        
        m.bsp.mrssDataFeedsBindingPriorityIndex = 0  
        m.bsp.mrssDataFeedsNumRetries% = 0
  
        ' 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
      text$ = jsonItem.text
      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
  
  
  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) as integer
    
    version% = autoplay.version
    return version%
    
  end function
  
  
  Function GetBaconVersion(autoplay) as string
  
    if IsString(autoplay.BrightAuthorConnectedVersion) then
      return autoplay.BrightAuthorConnectedVersion
    end if
  
    return "unknown"
  
  end function
  
  
  Function GetSignDataFromAutoplay(BrightAuthor as object) as object
  
    registrySettings = GetGlobalAA().registrySettings
    if registrySettings.isBrightWall then
      if registrySettings.sync_master
        ' master
        signData = BrightAuthor.signData[0]
      else
        ' slave
        signData = BrightAuthor.signData[1]
      endif
    else
      
      if BrightAuthor.signData = invalid then
        signData = BrightAuthor
      else
        signData = BrightAuthor.signData[0]
      endif
  
    endif
  
    return signData
  
  end function
  
  
  Function ParseAutoplay(BrightAuthor as object, bsp as object) 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    
      assetCollection = GetActiveSyncSpec().GetAssets("download")
      apf = CreateObject("roAssetPoolFiles", m.assetPool, assetCollection)
      
      autoplayPoolFile$ = apf.GetPoolFilePath(autoplayFileName$)
      if autoplayPoolFile$ = "" then stop
      schedule.autoplayPoolFile$ = autoplayPoolFile$
      apf = 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
  
    ' recurrence enabled but no valid start date, play all day every day
    if scheduledEventBS.recurrence and scheduledEventBS.recurrenceStartDate = invalid then
      validDateTime$ = helper_ValidateInvalidPrint(dateTime$)
      m.bsp.diagnostics.PrintDebug("Recurrence start date is missing or unparseable '" + validDateTime$ + "'. Scheduled event will run all day every day and not recur.")
      m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_INVALID_DATE_TIME_SPEC, validDateTime$)
      scheduledEventBS.recurrence = false
      scheduledEventBS.allDayEveryDay = true
    end if 
    
    ' date time invalid, all day every day, do not recur
    if scheduledEventBS.dateTime = invalid then
      validDateTime$ = helper_ValidateInvalidPrint(dateTime$)
      m.bsp.diagnostics.PrintDebug("Date Time is missing or unparseable '" + validDateTime$ + "'. Scheduled event will run all day every day.")
      m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_INVALID_DATE_TIME_SPEC, validDateTime$)
      scheduledEventBS.allDayEveryDay = true
      scheduledEventBS.recurrence = false
    end if
    
    scheduledEventBS.recurrenceGoesForever = scheduledEventJSON.recurrenceGoesForever
    
    dateTime$ = scheduledEventJSON.recurrenceEndDate
    recurrenceEndDate = FixDateTime(dateTime$)
    if recurrenceEndDate <> invalid then 
      recurrenceEndDate.AddSeconds(60 * 60 * 24) ' adjust the recurrence end date to refer to the beginning of the next day
      scheduledEventBS.recurrenceEndDate = recurrenceEndDate
    else 
      ' no valid end date, assume the recurrence goes forever
      validDateTime$ = helper_ValidateInvalidPrint(dateTime$)
      m.bsp.diagnostics.PrintDebug("Could not parse recurrence end date '" + validDateTime$ + "'. Ignored.")
      m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_INVALID_DATE_TIME_SPEC, validDateTime$)
      scheduledEventBS.recurrenceGoesForever = true
    end if
    
    ' 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)
    
    signData = GetSignDataFromAutoplay(BrightAuthor)
  
    signMetadata = signData.meta
  
    if IsString(signMetadata.autoplayType) and signMetadata.autoplayType <> "" then
      meta.autoplayType = signMetadata.autoplayType
    else
      meta.autoplayType = "Standard"
    endif
  
    if IsBoolean(signMetadata.disableSettingsHandler) then
      meta.enableSettingsHandler = not signMetadata.disableSettingsHandler
    else
      meta.enableSettingsHandler = true
    endif
  
    meta.publishedModel = signMetadata.model
    meta.name = signMetadata.name
    meta.videoMode = signMetadata.videoMode
    meta.forceResolution = signMetadata.forceResolution
    meta.tenBitColorEnabled = signMetadata.tenBitColorEnabled
    
    meta.dolbyVisionEnabled = signMetadata.dolbyVisionEnabled
    meta.fullResGraphicsEnabled = signMetadata.fullResGraphicsEnabled
    meta.audioAutoLevel = signMetadata.audioAutoLevel
    
    meta.videoConnector = signMetadata.videoConnector
    meta.monitorOrientation = lcase(signMetadata.monitorOrientation)
    
    meta.deviceWebPageDisplay = signMetadata.deviceWebPageDisplay
    meta.customDeviceWebPage = signMetadata.customDeviceWebPage
    
    meta.alphabetizeVariableNames = signMetadata.alphabetizeVariableNames
    
    meta.htmlEnableJavascriptConsole = signMetadata.htmlEnableJavascriptConsole
    
    meta.backgroundScreenColor = GetColor(signMetadata.backgroundScreenColor)
    meta.dontChangePresentationUntilMediaEndEventReceived = signMetadata.delayScheduleChangeUntilMediaEndEvent
    meta.delayScheduleChangeUntilMediaEndEvent = signMetadata.delayScheduleChangeUntilMediaEndEvent
    meta.languageKey = signMetadata.languageKey 'BACONTODO - check case'
    
    meta.bp900AConfigureAutomatically = signMetadata.buttonPanels["bp900a"].configureAutomatically
    meta.bp900BConfigureAutomatically = signMetadata.buttonPanels["bp900b"].configureAutomatically
    meta.bp900CConfigureAutomatically = signMetadata.buttonPanels["bp900c"].configureAutomatically
    meta.bp900DConfigureAutomatically = signMetadata.buttonPanels["bp900d"].configureAutomatically
    meta.bp200AConfigureAutomatically = signMetadata.buttonPanels["bp200a"].configureAutomatically
    meta.bp200BConfigureAutomatically = signMetadata.buttonPanels["bp200b"].configureAutomatically
    meta.bp200CConfigureAutomatically = signMetadata.buttonPanels["bp200c"].configureAutomatically
    meta.bp200DConfigureAutomatically = signMetadata.buttonPanels["bp200d"].configureAutomatically
    
    meta.bp900AConfiguration% = signMetadata.buttonPanels["bp900a"].configuration
    meta.bp900BConfiguration% = signMetadata.buttonPanels["bp900b"].configuration
    meta.bp900CConfiguration% = signMetadata.buttonPanels["bp900c"].configuration
    meta.bp900DConfiguration% = signMetadata.buttonPanels["bp900d"].configuration
    meta.bp200AConfiguration% = signMetadata.buttonPanels["bp200a"].configuration
    meta.bp200BConfiguration% = signMetadata.buttonPanels["bp200b"].configuration
    meta.bp200CConfiguration% = signMetadata.buttonPanels["bp200c"].configuration
    meta.bp200DConfiguration% = signMetadata.buttonPanels["bp200d"].configuration
  
    ' remote configuration
    meta.irRemoteControl = signMetadata.irRemote.irRemoteControl
    meta.irInConfiguration = signMetadata.irRemote.irInConfiguration
    meta.irOutConfiguration = signMetadata.irRemote.irOutConfiguration
  
    ' serial ports'
    meta.serialPortConfigurations = CreateObject("roArray", 1, true)
    
    for each serialPortConfigurationSpec in signMetadata.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
    
    meta.videoModePlugins = []
    for each videoModePluginSpec in signMetadata.videoModePlugins
      meta.videoModePlugins.push(videoModePlugin)
    next
  
    meta.scriptPlugins = []
    for each scriptPlugin in signMetadata.scriptPlugins
      meta.scriptPlugins.push(scriptPlugin)
    next
  
    ' parse parser plugins
    meta.parserPlugins = CreateObject("roArray", 1, true)
    for each parserPluginSpec in signMetadata.parserPlugins
      parserPlugin = jsonParseParserPlugin(parserPluginSpec)
      meta.parserPlugins.push(parserPlugin)
    next
    
    ' first pass parse of user variables
    meta.userVariableSpecs = []
    
    userVariablesJson = signMetadata.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
      
      videoConnector$ = getVarFromObj(userVariableJson, "data.videoConnector", "String", "")
      if videoConnector$ <> "" userVariableSpec.videoConnector$ = videoConnector$
      
      meta.userVariableSpecs.push(userVariableSpec)
    next
    
    ' parse live data feeds
    meta.liveDataFeedDescriptions = []
    liveDataFeedsJson = signMetadata.liveDataFeeds
    for each liveDataFeedJson in liveDataFeedsJson
      liveDataFeedDescription = jsonParseLiveDataFeed(liveDataFeedJson)
      meta.liveDataFeedDescriptions.push(liveDataFeedDescription)
    next
    meta.resetVariablesOnPresentationStart = signMetadata.resetVariablesOnPresentationStart
  
    ' parse Node apps
    meta.nodeAppDescriptions = []
    for each nodeAppJson in signMetadata.nodeApps
      nodeAppDescription = jsonParseNodeApp(bsp, nodeAppJson)
      meta.nodeAppDescriptions.push(nodeAppDescription)
    next
  
    ' parse HTML sites
    meta.htmlSiteDescriptions = []
    for each htmlSiteJson in signMetadata.htmlSites
      htmlSiteDescription = jsonParseHtmlSite(bsp, htmlSiteJson)
      meta.htmlSiteDescriptions.push(htmlSiteDescription)
    next
    
    ' parse presentation identifiers
    meta.presentationIdentifiers = []
    if type(signMetadata.presentationIdentifiers) = "roArray" then
      for each presentationIdentifier in signMetadata.presentationIdentifiers
        meta.presentationIdentifiers.push(presentationIdentifier)
      next
    end if
    
    ' parse beacons
    meta.beacons = []
    
    ' get list of additional files to publish
    meta.additionalPublishedFiles = []
    if type(signMetadata.auxiliaryFiles) = "roArray" then
      for each auxiliaryFile in signMetadata.auxiliaryFiles
        meta.additionalPublishedFiles.push(auxiliaryFile)
      next
    end if
    
    meta.boseProducts = signMetadata.partnerProducts
    meta.wssDeviceSpec = signMetadata.wssDeviceSpec
  
    meta.udpReceivePort = signMetadata.udpReceiverPort
    meta.udpSendPort = signMetadata.udpDestinationPort
    meta.udpAddressType = signMetadata.udpDestinationAddressType 'BACONTODO - check case'
    
    if meta.udpAddressType = "" then meta.udpAddressType = "IPAddress"
    meta.udpAddress = signMetadata.udpDestinationAddress
    
    meta.enableEnhancedSynchronization = false
    meta.ptpDomain$ = "0"
    if type(signMetadata.enableEnhancedSynchronization) = "roAssociativeArray" then
      meta.enableEnhancedSynchronization = true
      meta.ptpDomain$ = StripLeadingSpaces(stri(signMetadata.enableEnhancedSynchronization.ptpDomain))
    else
    end if
    
    meta.flipCoordinates = signMetadata.flipCoordinates
    meta.touchCursorDisplayMode = lcase(signMetadata.touchCursorDisplayMode)
      
    meta.gpio = CreateObject("roArray", 8, true)
    for i% = 0 to 7
      meta.gpio[i%] = signMetadata.gpio[i%]
    next
    
    meta.audioConfiguration = signMetadata.audioConfiguration
    
    audioSignPropertyMap = signMetadata.audioSignPropertyMap
    meta.audio1MinVolume = audioSignPropertyMap["analog1"].min
    meta.audio1MaxVolume = audioSignPropertyMap["analog1"].max
    meta.hdmiMinVolume = audioSignPropertyMap["hdmi"].min
    meta.hdmiMaxVolume = audioSignPropertyMap["hdmi"].max
    meta.hdmi1MinVolume = getVarFromObj(audioSignPropertyMap, "hdmi1.min", "Integer", 0)
    meta.hdmi1MaxVolume = getVarFromObj(audioSignPropertyMap, "hdmi1.max", "Integer", 100)
    meta.hdmi2MinVolume = getVarFromObj(audioSignPropertyMap, "hdmi2.min", "Integer", 0)
    meta.hdmi2MaxVolume = getVarFromObj(audioSignPropertyMap, "hdmi2.max", "Integer", 100)
    meta.hdmi3MinVolume = getVarFromObj(audioSignPropertyMap, "hdmi3.min", "Integer", 0)
    meta.hdmi3MaxVolume = getVarFromObj(audioSignPropertyMap, "hdmi3.max", "Integer", 100)
    meta.hdmi4MinVolume = getVarFromObj(audioSignPropertyMap, "hdmi4.min", "Integer", 0)
    meta.hdmi4MaxVolume = getVarFromObj(audioSignPropertyMap, "hdmi4.max", "Integer", 100)
    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 = signMetadata.inactivityTimeout
    meta.inactivityTime = signMetadata.inactivityTime
    
    meta.graphicsZOrder = signMetadata.graphicsZOrder 'BACONTODO - check case'
    
    meta.isMosaic = false
    
  end sub
  
  
  Function jsonParseZones(BrightAuthor as object, Sign as object)
  
    signData = GetSignDataFromAutoplay(BrightAuthor)
    zoneList = signData.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.hdmi1Output$ = getVarFromObj(zoneSpec, "zoneSpecificParameters.hdmi1Output", "String", "")
      zoneDescription.hdmi2Output$ = getVarFromObj(zoneSpec, "zoneSpecificParameters.hdmi2Output", "String", "")
      zoneDescription.hdmi3Output$ = getVarFromObj(zoneSpec, "zoneSpecificParameters.hdmi3Output", "String", "")
      zoneDescription.hdmi4Output$ = getVarFromObj(zoneSpec, "zoneSpecificParameters.hdmi4Output", "String", "")
      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
      ' get if support multi screen to decide if we should use hdmi or hdmi-1 through hdmi-4
      zoneDescription.hasMultiScreenOutputs = HasMultiScreenOutputs(sign)
    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 jsonParseNodeApp(bsp as object, nodeAppJson as object) as object
  
    nodeAppDescription = { }
    nodeAppDescription.name$ = nodeAppJson.name
    nodeAppDescription.prefix$ = nodeAppJson.prefix
    nodeAppDescription.filePath$ = nodeAppJson.fileName
    
    return nodeAppDescription
    
  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)
    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.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"
    else if type(stateSpec.multiAssetItem) = "roAssociativeArray" then
      stateDescription.multiAssetItem = stateSpec.multiAssetItem
      stateDescription.type = "multiAsset"
    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
    
    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")
  
    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, sign 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, sign, playlistItemDescription.transitionToNextEventList)
    ConfigureMediaListStateIO(bsp, sign, 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.firstTimeThrough = true
  
    state.HStateEventHandler = STDisplayingMediaListItemEventHandler
    state.PopulateMediaListFromLiveDataFeed = PopulateMediaListFromLiveDataFeed
    state.GetMediaListFirstTimeThroughPlaybackIndex = GetMediaListFirstTimeThroughPlaybackIndex
    state.GetMediaListInitialPlaybackIndex = GetMediaListInitialPlaybackIndex
    state.ShuffleMediaListContent = ShuffleMediaListContent
    state.ConfigureIntraStateEventHandlersButton = ConfigureIntraStateEventHandlersButton
    state.PrePlayAudio = PrePlayAudio
    state.PlayAudio = PlayAudio
    state.PostPlayAudio = PostPlayAudio
    state.PlayMixerAudio = PlayMixerAudio
    state.GetBrightWallVideoPlayerRectangle = GetBrightWallVideoPlayerRectangle
    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
    
    state.IsSubscribedToVideoEndEvent = IsMediaListPlaylistItemSubscribedToVideoEndEvent
  
  end sub
  
  
  Function IsMediaListPlaylistItemSubscribedToVideoEndEvent()
  
    if type(m.videoEndEvent) = "roAssociativeArray" then
      return true
    endif
  
    if m.transitionToNextEventList.count() > 0 then
      for each transitionToNextEvent in m.transitionToNextEventList
        if transitionToNextEvent.eventName = "mediaEnd" then
          return true
        endif
      next
    endif
  
    if m.transitionToPreviousEventList.count() > 0 then
      for each transitionToPreviousEvent in m.transitionToPreviousEventList
        if transitionToPreviousEvent.eventName = "mediaEnd" then
          return true
        endif
      next
    endif
  
    return false
  
  end function
  
  
  Function GetMediaListFirstTimeThroughPlaybackIndex() as integer
  
    if m.specifiedStartIndex% >= m.numItems% then
      return 0
    else
      return m.specifiedStartIndex%
    end if
  
  end function
  
  
  Function GetMediaListInitialPlaybackIndex() as integer
  
    if m.numItems% > 0 and m.startIndex% >= m.numItems% then
      startIndex% = 0
    end if
  
    ' reset playback index if appropriate
    if m.playFromBeginning then
      playbackIndex% = m.startIndex%
    else
      playbackIndex% = m.playbackIndex%
    end if
  
    if m.numItems% > 0 then
          
      ' prevent start index from pointing beyond the number of items
      if m.playFromBeginning or playbackIndex% >= m.numItems% then
        if m.specifiedStartIndex% >= m.numItems% then
          startIndex% = 0
        else
          startIndex% = m.specifiedStartIndex%
        end if
        playbackIndex% = startIndex%
  
      end if
    
    endif
  
    return playbackIndex%
  
  end function
  
  
  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 m.stateMachine.IsBrightWallSlave() then
            m.playbackIndex% = m.brightWallSlaveMediaListIndex  ' is this the only special code? the rest is meant to be identical?
          endif
          
          ' 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 or m.playbackIndex% >= m.numItems% then
              if m.specifiedStartIndex% >= m.numItems% then
                m.startIndex% = 0
              else
                m.startIndex% = m.specifiedStartIndex%
              end if
              m.playbackIndex% = m.startIndex%
              if not m.playFromBeginning then ' BCN-9609
                m.bsp.diagnostics.PrintDebug("****** ------ STDisplayingMediaListItemEventHandler RESET PLAYBACK_INDEX")
              endif
            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
  
        ' if this is a BrightWall presentation and this is the master, send a sync command and play the media
        if m.stateMachine.IsBrightWallMaster() then
  
          ' revert index to the just played video
          m.playbackIndex% = m.playbackIndex% - 1
          if m.playbackIndex% < 0 then
            m.playbackIndex% = m.numItems% - 1
          end if
  
          data = { syncPayloadType: "synchronizeMediaListForward", targetStateName: m.id$, synchronizeKeyword: m.id$, playbackIndex: m.playbackIndex% }
          data.timestamp = m.bsp.GetCurrentTimeAsIsoString()
          m.bsp.ExecuteSynchronizeMediaListHelper(m.stateMachine, data)
  
          m.AdvanceMediaListPlayback(true, false)
  
          return "HANDLED"
        endif
  
        PostMediaEndEvent(m.bsp.msgPort)
  
      else if event.GetInt() = VIDEO_TIME_CODE then
        videoTimeCodeIndex$ = str(event.GetData())
        m.bsp.diagnostics.PrintDebug("Video TimeCode Event " + videoTimeCodeIndex$)
        if type(m.videoTimeCodeEvents) = "roAssociativeArray" then
          videoTimeCodeEvent = m.videoTimeCodeEvents[videoTimeCodeIndex$]
          if type(videoTimeCodeEvent) = "roAssociativeArray" then
            m.bsp.ExecuteTransitionCommands(m.stateMachine, videoTimeCodeEvent, "")
            m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "1")
            return "HANDLED"
          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
  
    else if type(event) = "roSyncManagerEvent" then
  
      synchronizePayload$ = event.GetId()
      synchronizePayload = ParseJson(synchronizePayload$)
  
      timestamp = synchronizePayload.timestamp
  
      if synchronizePayload.syncPayloadType = "synchronizeMediaListForward" or synchronizePayload.syncPayloadType = "synchronizeMediaListBackward" then
        
        m.playbackIndex% = synchronizePayload.playbackIndex    
  
        userVariablesPayload = synchronizePayload.userVariables
        m.bsp.UpdateUserVariablesFromBrightWallPayload(synchronizePayload.timestamp, userVariablesPayload)
  
        m.stateMachine.syncInfo = CreateObject("roAssociativeArray")
        m.stateMachine.syncInfo.SyncDomain = event.GetDomain()
        m.stateMachine.syncInfo.SyncId = event.GetId()
        m.stateMachine.syncInfo.SyncIsoTimestamp = event.GetIsoTimestamp()
  
        m.AdvanceMediaListPlayback(true, false)
        return "HANDLED"
      
      endif
        
    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, sign 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)
      else if event.name = "udp" then
        bsp.CreateDatagramReceiver(sign.udpReceivePort)
      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)
  
    settings = GetActiveSettings()
  
    if settings.deviceScreenShotsEnabled 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(settings.deviceScreenShotsOrientation) then
            out.AddReplace("snapshotOrientation", settings.deviceScreenShotsOrientation)
          else
            out.AddReplace("snapshotOrientation", "Landscape")
          end if
        end if
      end if
    end if
    
    out.AddReplace("unitName", settings.unitName)
    out.AddReplace("unitNamingMethod", settings.unitNamingMethod)
    out.AddReplace("unitDescription", settings.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
  
  
  ' not called from Bacon (or anything else) as of 2020/Jan 8
  Sub PopulateSnapshotConfigurationJson(mVar as object) as object
    stop  ' ensure failure so that code can be updated as needed  
    
    globalAA = GetGlobalAA()
    settings = GetActiveSettings()
  
    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 settings.deviceScreenShotsEnabled <> invalid then
      if settings.deviceScreenShotsEnabled 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 settings.deviceScreenShotsInterval <> invalid then
      out.AddReplace("interval", StripLeadingSpaces(stri(settings.deviceScreenShotsInterval))) ' 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 settings.deviceScreenShotsCountLimit <> invalid then
      out.AddReplace("maxImages", StripLeadingSpaces(stri(settings.deviceScreenShotsCountLimit))) ' 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.deviceScreenShotsQuality <> invalid then
      out.AddReplace("quality", StripLeadingSpaces(stri(settings.deviceScreenShotsQuality))) ' 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(settings.deviceScreenShotsOrientation) then
      out.AddReplace("orientation", settings.deviceScreenShotsOrientation) ' 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
  
  
  ' not called from Bacon (or anything else) as of 2020/Jan 8
  Function GetSnapshotConfigurationJson(userData as object, e as object)
    stop  ' ensure failure so that code can be updated as needed  
    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
  
  
  ' not called from Bacon (or anything else) as of 2020/Jan 8
  Sub PopulateSnapshotHistoryJson(mVar as object, startTimeSpecified as boolean, startTime as object) as object
    stop  ' ensure failure so that code can be updated as needed  
    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
  
  
  ' not called from Bacon (or anything else) as of 2020/Jan 8
  Function GetSnapshotHistoryJson(userData as object, e as object)
    stop  ' ensure failure so that code can be updated as needed  
    
    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
  
  
  ' not called from Bacon (or anything else) as of 2020/Jan 8
  Function GetSnapshotJson(userData as object, e as object)
    stop  ' ensure failure so that code can be updated as needed  
    
    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)
    
    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
  
  
  ' invoked on completion of LFN publish
  Sub PostSyncSpecJson(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 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
        mVar.diagnostics.PrintDebug("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
        registrySection = GetGlobalAA().registrySection
        registrySection.Delete("susse")
        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
    
    UpdateSyncSpecAndSettings(newSync, "local-sync.json", "local")        
  
    metadata = newSync.GetMetadata("client")
    if metadata.DoesExist("obfuscatedPassphrase") then
      obfuscatedPassphrase$ = GetActiveSettings().obfuscatedPassphrase
      deviceCustomization = CreateObject("roDeviceCustomization")
      deviceCustomization.StoreObfuscatedEncryptionKey("AesCtrHmac", obfuscatedPassphrase$)
      mVar.contentEncrypted = true
    end if
    
    mVar.SetPoolSizes(GetActiveSyncSpecSettings())
    
    ' at the moment, there are no settings that get communicated to the supervisor as a result of
    ' an lfn publish, so no need to send an update to the supervisor
    ' lastModifiedTime$ = newSync.LookupMetadata("client", "lastModifiedTime") 
  
    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
  
  
  'region SyncSpecAndSettings
  ' TEDTODO - compare against algorithm used by supervisor
  Function GetSyncSpec() as object
  
    syncSpecSpec = {}
    syncSpecSpec.syncSpec = CreateObject("roSyncSpec")
  
    syncSpecFileNames = ["current-sync.json", "local-sync.json", "localToBSN-sync.json"]
    syncSpecTypes = ["network", "local", "localToBsn"]
  
    for i = 0 to syncSpecFileNames.Count() - 1
      if syncSpecSpec.syncSpec.ReadFromFile(syncSpecFileNames[i]) then
        syncSpecSpec.fileName = syncSpecFileNames[i]
        syncSpecSpec.syncSpecType = syncSpecTypes[i]
        return syncSpecSpec
      endif
    next
  
    return invalid
  
  end function
  
  
  Sub InitSupervisorSupport()
  
    globalAA = GetGlobalAA()
  
    CreateControlCloudInterface()
  
    globalAA.supervisorSupport = GetSupervisorSupport()
    globalAA.supervisorSupportsConfigureNetwork = GetSupervisorSupportsConfigureNetwork()
    globalAA.supervisorVersion = GetSupervisorVersion()  
  
    ' until an autoplay is encountered that indicates that the supervisor should manage settings,
    ' don't have the supervisor manage settings.
    globalAA.supervisorEnableSettingsHandler = false
  
    SendAutorunCapabilitiesToSupervisorViaIpc([], "")
  
    globalAA.cellularModemActive = GetIsCellularModemActive()
  
    globalAA.useSupervisorConfigSpec = AutorunConfigEndpointExists()
  
    registrySection = globalAA.registrySection
    globalAA.debug_settings = GetBoolFromString(registrySection.Read("debug_settings"), false)
  
  end sub
  
  
  Sub InitializeSyncSpecAndSettings()
    
    globalAA = GetGlobalAA()
  
    syncSpecSpec = GetSyncSpec()
    if type(syncSpecSpec) <> "roAssociativeArray" then
      stop
    endif
  
    globalAA.syncSpec = syncSpecSpec.syncSpec
    globalAA.syncSpecFileName = syncSpecSpec.fileName
    globalAA.syncSpecType = syncSpecSpec.syncSpecType
  
    if globalAA.useSupervisorConfigSpec then
      globalAA.settings = GetConfigJsonSettings()
    else
      globalAA.settings = LoadSettingsFromLegacySyncSpec(syncSpecSpec.syncSpec)
      MergeLegacyRegistrySettingsIntoGlobalSettings(globalAA.settings)
      globalAA.supervisorSupportsUsbNetworkInterfaces = false
    endif
    globalAA.syncSpecSettings = LoadSyncSpecSettings(globalAA.syncSpec)
    BuildNetworkInterfacePriorityLists()
  
    globalAA.pendingSyncSpec = invalid
    globalAA.pendingSyncSpecSettings = invalid
  
    ' The circumstances when the updated settings should be sent to the supervisor.
    ' Standalone Publish - yes, if
    '     there's no config.json file
    '     there's a config.json file but its timestamp is older than the timestamp found in local-sync.json
    
    setupType = lcase(globalAA.settings.setupType)
  
    if globalAA.useSupervisorConfigSpec and globalAA.syncSpecType = "local" and (setupType = "standalone" or setupType = "") then
            
      ' get timestamps of config file and local-sync.json
      lastModifiedTimeSyncSpec = globalAA.syncSpec.LookupMetadata("client","lastModifiedTime")
      lastModifiedTimeConfig = globalAA.settings.lastModifiedTime
  
      if lastModifiedTimeConfig > lastModifiedTimeSyncSpec then
        ' settings have been loaded (in GetConfigJsonSettings) - nothing else needs to be done
        ' TEDTODO-Subban: globalAA.bsp doesn't exist yet.
        '        globalAA.bsp.diagnostics.PrintDebug("### Use settings from config. Config lastModifiedTime: " + lastModifiedTimeConfig + "SyncSpec lastModifiedTime" + lastModifiedTimeSyncSpec)
        '      print "### Use settings from config. Config lastModifiedTime: " + lastModifiedTimeConfig + ", SyncSpec lastModifiedTime: " + lastModifiedTimeSyncSpec
        return
      endif
  
      if lcase(setupType) = "standalone" or setupType = "" then
  
        ' retrieve settings from sync spec and registry
        settings = LoadSettingsFromLegacySyncSpec(syncSpecSpec.syncSpec)
        MergeLegacyRegistrySettingsIntoGlobalSettings(settings)
        globalAA = GetGlobalAA()
        AddRegistrySettingsNotInStandaloneSyncSpecIntoGlobalSettings(globalAA.registrySection, settings)
  
        globalAA.settings = settings
        BuildNetworkInterfacePriorityLists()
  
        lastModifiedTime = globalAA.syncSpec.LookupMetadata("client","lastModifiedTime") + "Z"
  
        SendUpdatedSettingsToSupervisor(lastModifiedTime, settings)
        'TEDTODO-Subban - get settings from supervisor after setting them?
        ' globalAA.settings = GetConfigJsonSettings()
  
      endif
      
    endif
  
  end sub
  
  
  Sub SetPendingSettings()
    globalAA = GetGlobalAA()
    globalAA.pendingSettings = GetConfigJsonSettings()
  end sub
  
  
  Sub SetPendingSyncSpec(pendingSyncSpec as object)
    globalAA = GetGlobalAA()
    globalAA.pendingSyncSpec = pendingSyncSpec
    globalAA.pendingSyncSpecSettings = LoadSyncSpecSettings(globalAA.pendingSyncSpec)
  end sub
  
  
  Sub SetPendingSyncSpecAndSettings(pendingSyncSpec as object)
  
    globalAA = GetGlobalAA()
  
    globalAA.pendingSyncSpec = pendingSyncSpec
  
    legacySyncSpecSettings = LoadSettingsFromLegacySyncSpec(pendingSyncSpec)
    MergeLegacyRegistrySettingsIntoGlobalSettings(legacySyncSpecSettings)
  
    if globalAA.useSupervisorConfigSpec then
  
      pendingSettings = GetConfigJsonSettings()
  
      ' if sfn, retrieve properties from sync spec that are now 'owned' by the supervisor
      if lcase(GetGlobalAA().settings.setupType) = "sfn" then
  
        pendingSettings.deviceScreenShotsEnabled = legacySyncSpecSettings.deviceScreenShotsEnabled
        pendingSettings.deviceScreenShotsInterval = legacySyncSpecSettings.deviceScreenShotsInterval
        pendingSettings.deviceScreenShotsCountLimit = legacySyncSpecSettings.deviceScreenShotsCountLimit
        pendingSettings.deviceScreenShotsQuality = legacySyncSpecSettings.deviceScreenShotsQuality
        pendingSettings.deviceScreenShotsOrientation = legacySyncSpecSettings.deviceScreenShotsOrientation
  
        pendingSettings.contentDownloadsRestricted = legacySyncSpecSettings.contentDownloadsRestricted
        pendingSettings.contentDownloadRangeStart = legacySyncSpecSettings.contentDownloadRangeStart
        pendingSettings.contentDownloadRangeLength = legacySyncSpecSettings.contentDownloadRangeLength
  
      	pendingSettings.playbackLoggingEnabled = legacySyncSpecSettings.playbackLoggingEnabled
      	pendingSettings.diagnosticLoggingEnabled = legacySyncSpecSettings.diagnosticLoggingEnabled
      	pendingSettings.eventLoggingEnabled = legacySyncSpecSettings.eventLoggingEnabled
      	pendingSettings.stateLoggingEnabled = legacySyncSpecSettings.stateLoggingEnabled
      	pendingSettings.variableLoggingEnabled = legacySyncSpecSettings.variableLoggingEnabled
  
      	pendingSettings.uploadLogFilesAtBoot = legacySyncSpecSettings.uploadLogFilesAtBoot
      	pendingSettings.uploadLogFilesAtSpecificTime = legacySyncSpecSettings.uploadLogFilesAtSpecificTime
      	pendingSettings.uploadLogFilesTime = legacySyncSpecSettings.uploadLogFilesTime
  
      endif
  
      globalAA.pendingSettings = pendingSettings
  
    else
  
      globalAA.pendingSettings = legacySyncSpecSettings  
    
    end if
    
    globalAA.pendingSyncSpecSettings = LoadSyncSpecSettings(globalAA.pendingSyncSpec)
  
  end sub
  
  
  Function GetConfigJsonSettings() as object
    settingsContainer = GetAutorunConfigFromSupervisor()
    return LoadSettingsFromConfig(settingsContainer)
  end function
  
  
  Sub UpdateSyncSpecAndSettings(syncSpec as object, syncSpecFileName as string, syncSpecType as string)
  
    globalAA = GetGlobalAA()
  
    globalAA.syncSpec = syncSpec
    globalAA.syncSpecFileName = syncSpecFileName
    globalAA.syncSpecType = syncSpecType
  
    if globalAA.useSupervisorConfigSpec then
      globalAA.settings = GetConfigJsonSettings()
    else
      globalAA.settings = LoadSettingsFromLegacySyncSpec(syncSpec)
      MergeLegacyRegistrySettingsIntoGlobalSettings(globalAA.settings)
    endif
    BuildNetworkInterfacePriorityLists()
  
    globalAA.syncSpecSettings = LoadSyncSpecSettings(globalAA.syncSpec)
  
  end sub
  
  
  Sub BuildNetworkInterfacePriorityLists()
    globalAA = GetGlobalAA()
    globalAA.networkInterfacePriorityLists = {}
    BuildContentXfersNetworkInterfacePriorityList(globalAA)
    BuildMrssContentXfersNetworkInterfacePriorityList(globalAA)
    BuildTextFeedXfersNetworkInterfacePriorityList(globalAA)
    BuildLogUploadsNetworkInterfacePriorityList(globalAA)
  end sub
  
  
  Sub BuildContentXfersNetworkInterfacePriorityList(globalAA as object)
    
    priorityList = []
    for each interface in globalAA.settings.network.interfaces
      if DataTypeEnabledForInterface(globalAA.settings, "contentDownloadEnabled", interface) then
        priorityList.push(interface)
      endif
    next
    
    globalAA.networkInterfacePriorityLists.AddReplace("contentDownloadEnabled", priorityList)
    ' globalAA.contentXfersNetworkInterfacePriorityList = priorityList
  
  end sub
  
  
  Sub BuildMrssContentXfersNetworkInterfacePriorityList(globalAA as object)
    
    priorityList = []
    for each interface in globalAA.settings.network.interfaces
      if DataTypeEnabledForInterface(globalAA.settings, "mediaFeedsDownloadEnabled", interface) then
        priorityList.push(interface)
      endif
    next
    
    globalAA.networkInterfacePriorityLists.AddReplace("mediaFeedsDownloadEnabled", priorityList)
    ' globalAA.contentXfersNetworkInterfacePriorityList = priorityList
  
  end sub
  
  
  Sub BuildTextFeedXfersNetworkInterfacePriorityList(globalAA as object)
    
    priorityList = []
    for each interface in globalAA.settings.network.interfaces
      if DataTypeEnabledForInterface(globalAA.settings, "textFeedsDownloadEnabled", interface) then
        priorityList.push(interface)
      endif
    next
    
    globalAA.networkInterfacePriorityLists.AddReplace("textFeedsDownloadEnabled", priorityList)
  
  end sub
  
  
  Sub BuildLogUploadsNetworkInterfacePriorityList(globalAA as object)
    
    priorityList = []
    for each interface in globalAA.settings.network.interfaces
      if DataTypeEnabledForInterface(globalAA.settings, "logsUploadEnabled", interface) then
        priorityList.push(interface)
      endif
    next
    
    globalAA.networkInterfacePriorityLists.AddReplace("logsUploadEnabled", priorityList)
  
  end sub
  
  
  Function DataTypeEnabledForInterface(settings as Object, dataType as string, interface as object) as boolean
    
    enabled = false
    if interface.DoesExist(dataType) then
      dataTypeEnabled = interface.LookupCi(dataType)
      if IsBoolean(dataTypeEnabled) and dataTypeEnabled then
        enabled = true
      endif
    endif
  
    return enabled
  
  end function
  
  
  Function GetActiveSyncSpec() as object
    return GetGlobalAA().syncSpec
  end function
  
  
  Function GetActiveSyncSpecType() as string
    return GetGlobalAA().syncSpecType
  end function
  
  
  Sub SetActiveSettingsFromPendingSettings()
    globalAA = GetGlobalAA()
    globalAA.settings = globalAA.pendingSettings
    BuildNetworkInterfacePriorityLists()
    globalAA.pendingSettings = invalid
  end sub
  
  
  Function GetActiveSettings() as object
    return GetGlobalAA().settings
  end function
  
  
  Function GetActiveSyncSpecSettings() as object
    return GetGlobalAA().syncSpecSettings
  end function
  
  
  Function GetPendingSettings() as object
    return GetGlobalAA().pendingSettings
  end function
  
  
  Function GetPendingSyncSpecSettings() as object
    return GetGlobalAA().pendingSyncSpecSettings
  end function
  
  
  
  Function GetSupervisorSupportsUsbNetworkInterfaces(config as object) as boolean
  
    if config.meta.DoesExist("client") and config.meta.client.DoesExist("bsnAPIVersion") then
      bsnAPIVersion = config.meta.client.bsnAPIVersion
      if IsString(bsnAPIVersion) and bsnAPIVersion <> "" and bsnAPIVersion <> "2019/03" then
        return true
      endif
    endif
  
    return false
  
  end function
  
  
  Function GetSetupTypeSetting(config as object, registrySection as object) as string
  
    if config.meta.DoesExist("client") and config.meta.client.DoesExist("setupType") and IsString(config.meta.client.setupType) then
      return config.meta.client.setupType
    else
      return registrySection.Read("sut")
    endif
  
  end function
  
  
  Function GetGroupSetting(config as object, registrySection as object) as string
  
    if config.meta.DoesExist("server") and config.meta.server.DoesExist("group") and IsString(config.meta.server.group) then
      return config.meta.server.group
    else
      return registrySection.Read("g")
    endif
  
  end function
  
  
  Function GetUnitNameSetting(config as object, registrySection as object) as string
  
    if config.meta.DoesExist("client") and config.meta.client.DoesExist("unitName") and IsString(config.meta.client.unitName) then
      return config.meta.client.unitName
    else
      return registrySection.Read("un")
    endif
  
  end function
  
  
  Function GetUnitDescriptionSetting(config as object, registrySection as object) as string
  
    if config.meta.DoesExist("client") and config.meta.client.DoesExist("unitDescription") and IsString(config.meta.client.unitDescription) then
      return config.meta.client.unitDescription
    else
      return registrySection.Read("ud")
    endif
  
  end function
  
  
  Function GetUnitNamingMethodSetting(config as object, registrySection as object) as string
  
    if config.meta.DoesExist("client") and config.meta.client.DoesExist("unitNamingMethod") and IsString(config.meta.client.unitNamingMethod) then
      return config.meta.client.unitNamingMethod
    else
      return registrySection.Read("unm")
    endif
  
  end function
  
  
  Function GetTimezoneSetting(config as object, registrySection as object) as string
    if config.meta.DoesExist("client") and config.meta.client.DoesExist("timeZone") and IsString(config.meta.client.timeZone) then
      return config.meta.client.timeZone
    else
      systemTime = CreateObject("roSystemTime")
      return systemTime.GetTimeZone()
    endif
  end function
  
  
  Function GetLwsEnableUpdateNotificationsSetting(config as object, registrySection as object) as boolean
  
    if config.meta.DoesExist("client") and config.meta.client.DoesExist("lwsEnableUpdateNotifications") and IsString(config.meta.client.lwsEnableUpdateNotifications) then
      return config.meta.client.lwsEnableUpdateNotifications
    else
      return GetBoolFromString(registrySection.Read("nlwseun"), true)
    endif
  
  end function
  
  
  Function GetLwsUserName(config as object, registrySection as object) as string
  
    if config.meta.DoesExist("client") and config.meta.client.DoesExist("lwsUserName") and IsString(config.meta.client.lwsUserName) then
      return config.meta.client.lwsUserName
    else
      return registrySection.Read("nlwsu")
    endif
  
  end function
  
  
  Function GetLwsPassword(config as object, registrySection as object) as string
  
    if config.meta.DoesExist("client") and config.meta.client.DoesExist("lwsPassword") and IsString(config.meta.client.lwsPassword) then
      return config.meta.client.lwsPassword
    else
      return registrySection.Read("nlwsp")
    endif
  
  end function
  
  
  Function GetLwsConfig(config as object, registrySection as object) as string
  
    if config.meta.DoesExist("client") and config.meta.client.DoesExist("lwsConfig") then
      settingLwsConfig = config.meta.client.lwsConfig
      if settingLwsConfig = "content" then
        lwsConfig = "c"
      else if settingLwsConfig = "status" then
        lwsConfig = "s"
      else
        lwsConfig = ""
      endif
      return lwsConfig
    else
      return registrySection.Read("nlws")
    endif
  
  end function
  
  
  Sub GetDeviceScreenShotsEnabled(config as object, registrySection as object) as boolean
  
    if config.meta.DoesExist("client") and config.meta.client.DoesExist("deviceScreenShotsEnabled") and IsBoolean(config.meta.client.deviceScreenShotsEnabled) then
  	  return config.meta.client.deviceScreenShotsEnabled
    else
      return GetBoolFromString(registrySection.Read("enableRemoteSnapshot"), false)
    endif
  
  end sub
  
  
  ' load string property, default to ""
  Function GetConfigStringDefaultToEmpty(val) as string
    if IsString(val) then
      return val
    else
      return ""
    endif    
  end function
  
  
  Function LoadSettingsFromConfig(config as object) as object
    
    globalAA = GetGlobalAA()
  
    globalAA.supervisorSupportsUsbNetworkInterfaces = GetSupervisorSupportsUsbNetworkInterfaces(config)
  
    registrySection = globalAA.registrySection
  
    settings = {}
  
    dt = registrySection.Read("last_modified_autorun_settings_at")
    if IsString(dt) and dt <> "" then 
      settings.lastModifiedTime = dt
    else
      ' new device; set last modified time in a way that always forces sending settings from supervisor for standalone units
      settings.lastModifiedTime = "2000-01-01T00:00:00.000"
    endif
  
    settings.group = GetGroupSetting(config, registrySection)
    settings.setupType = GetSetupTypeSetting(config, registrySection)
    settings.uploadUsage = GetConfigStringDefaultToEmpty(config.meta.client.uploadUsage)
    settings.nowPlaying = GetConfigStringDefaultToEmpty(config.meta.client.nowPlaying)
    settings.getFile = GetConfigStringDefaultToEmpty(config.meta.client.getFile)
  	settings.recoveryHandler = GetConfigStringDefaultToEmpty(config.meta.client.recoveryHandler)
  	settings.recoverySetup = GetConfigStringDefaultToEmpty(config.meta.client.recoverySetup)
  	settings.batteryCharger = GetConfigStringDefaultToEmpty(config.meta.client.batteryCharger)
  	settings.unitName = GetUnitNameSetting(config, registrySection)
  	settings.unitNamingMethod = GetUnitNamingMethodSetting(config, registrySection)
  	settings.unitDescription = GetUnitDescriptionSetting(config, registrySection)
  	settings.timeZone = GetTimezoneSetting(config, registrySection)
  	settings.contentDownloadsRestricted = GetValidBool(config.meta.client.contentDownloadsRestricted, false)
  	settings.contentDownloadRangeStart = GetValidInt(config.meta.client.contentDownloadRangeStart, 0)
  	settings.contentDownloadRangeLength = GetValidInt(config.meta.client.contentDownloadRangeLength, 0)
  
  	settings.lwsConfig = GetLwsConfig(config, registrySection)
  	settings.lwsEnableUpdateNotifications = GetLwsEnableUpdateNotificationsSetting(config, registrySection)
    settings.lwsUserName = GetLwsUserName(config, registrySection)
    settings.lwsPassword = GetLwsPassword(config, registrySection)
  	settings.playbackLoggingEnabled = GetValidBool(config.meta.client.playbackLoggingEnabled, false)
  	settings.eventLoggingEnabled = GetValidBool(config.meta.client.eventLoggingEnabled, false)
  	settings.stateLoggingEnabled = GetValidBool(config.meta.client.stateLoggingEnabled, false)
  	settings.diagnosticLoggingEnabled = GetValidBool(config.meta.client.diagnosticLoggingEnabled, false)
  	settings.variableLoggingEnabled = GetValidBool(config.meta.client.variableLoggingEnabled, false)
  	settings.uploadLogFilesAtBoot = GetValidBool(config.meta.client.uploadLogFilesAtBoot, false)
  	settings.uploadLogFilesAtSpecificTime = GetValidBool(config.meta.client.uploadLogFilesAtSpecificTime, false)
  	settings.uploadLogFilesTime = GetValidInt(config.meta.uploadLogFilesTime, 0)
  
  	settings.deviceScreenShotsEnabled = GetDeviceScreenShotsEnabled(config, registrySection)
  	settings.deviceScreenShotsInterval = GetValidInt(config.meta.client.deviceScreenShotsInterval, 300)
  	settings.deviceScreenShotsCountLimit = GetValidInt(config.meta.client.deviceScreenShotsCountLimit, 2)
  	settings.deviceScreenShotsQuality = GetValidInt(config.meta.client.deviceScreenShotsQuality, 50)
  	settings.deviceScreenShotsOrientation = GetValidString(config.meta.client.deviceScreenShotsOrientation, "Landscape")
  
  	settings.idleScreenColor = GetValidString(config.meta.client.idleScreenColor, "FF000000")
  
    settings.network = {}
  
    settings.network.interfaces = []
  
    if config.meta.DoesExist("network") and config.meta.network.DoesExist("interfaces") then
  
      for each interface in config.meta.network.interfaces
  
        interfaceSpec = {}
  
        interfaceSpec.networkInterface = interface.name
  
        interfaceSpec.rateLimitRateInWindow% = GetValidInt(interface.rateLimitInsideContentDownloadWindow, -1)
        interfaceSpec.rateLimitRateInitialDownloads% = GetValidInt(interface.rateLimitDuringInitialDownloads, -1)
        interfaceSpec.rateLimitRateOutsideWindow% = GetValidInt(interface.rateLimitOutsideContentDownloadWindow, -1)
  
        interfaceSpec.contentDownloadEnabled = GetValidBool(interface.contentDownloadEnabled, true)
        interfaceSpec.textFeedsDownloadEnabled = GetValidBool(interface.textFeedsDownloadEnabled, true)
        interfaceSpec.mediaFeedsDownloadEnabled = GetValidBool(interface.mediaFeedsDownloadEnabled, true)
        interfaceSpec.healthReportingEnabled = GetValidBool(interface.healthReportingEnabled, true)
        interfaceSpec.logsUploadEnabled = GetValidBool(interface.logsUploadEnabled, true)
  
        settings.network.interfaces.push(interfaceSpec)
      
      next
  
    endif
  
    return settings
  
  end function
  
  
  ' Note - this function is called when running with a supervisor that supports settings
  '     on boot for standalone publishes
  '     from SetPendingSyncSpecAndSettings
  ' In those cases, this function is setting properties inappropriately (e.g., network settings unused by the autorun)/
  ' It's not clear that it causes any issues however as these properties are never read / used in this case.
  Function LoadSettingsFromLegacySyncSpec(syncSpec as object) as object
  
    globalAA = GetGlobalAA()
  
    settings = {}
  
    settings.group = syncSpec.LookupMetadata("server", "group")
  
    ' the list of settings read here should be identical to those read in config.json
  
    ' TEDTODO - what about new supervisor, no setup / publish type === standalone
    if syncSpec.LookupMetadata("client", "deviceScreenShotsEnabled") <> "" then
      ' executed for old or new supervisors, for a publish type other than standalone
      settings.deviceScreenShotsEnabled = GetBoolFromString(syncSpec.LookupMetadata("client", "deviceScreenShotsEnabled"), false)
      settings.deviceScreenShotsCountLimit = GetIntFromString(syncSpec.LookupMetadata("client", "deviceScreenShotsCountLimit"))
      settings.deviceScreenShotsInterval = GetIntFromString(syncSpec.LookupMetadata("client", "deviceScreenShotsInterval"))
      settings.deviceScreenShotsQuality = GetIntFromString(syncSpec.LookupMetadata("client", "deviceScreenShotsQuality"))
      settings.deviceScreenShotsOrientation = syncSpec.LookupMetadata("client", "deviceScreenShotsOrientation")
    else if not globalAA.useSupervisorConfigSpec then
      ' executed for old supervisor, standalone
      registrySettings = globalAA.registrySettings
      settings.deviceScreenShotsEnabled = GetBoolFromString(registrySettings.deviceScreenShotsEnabled, false)
      settings.deviceScreenShotsInterval = int(val(registrySettings.deviceScreenShotsInterval))
      settings.deviceScreenShotsCountLimit = int(val(registrySettings.deviceScreenShotsCountLimit))
      settings.deviceScreenShotsQuality = int(val(registrySettings.deviceScreenShotsQuality))
      settings.deviceScreenShotsOrientation = registrySettings.deviceScreenShotsOrientation
    end if
  
    settings.diagnosticLoggingEnabled = syncSpecValueTrue(syncSpec.LookupMetadata("client", "diagnosticLoggingEnabled"))
    settings.eventLoggingEnabled = syncSpecValueTrue(syncSpec.LookupMetadata("client", "eventLoggingEnabled"))
    settings.playbackLoggingEnabled = syncSpecValueTrue(syncSpec.LookupMetadata("client", "playbackLoggingEnabled"))
    settings.stateLoggingEnabled = syncSpecValueTrue(syncSpec.LookupMetadata("client", "stateLoggingEnabled"))
    settings.variableLoggingEnabled = syncSpecValueTrue(syncSpec.LookupMetadata("client", "variableLoggingEnabled"))
    settings.uploadLogFilesAtBoot = syncSpecValueTrue(syncSpec.LookupMetadata("client", "uploadLogFilesAtBoot"))
    settings.uploadLogFilesAtSpecificTime = syncSpecValueTrue(syncSpec.LookupMetadata("client", "uploadLogFilesAtSpecificTime"))
    settings.uploadLogFilesTime = GetIntFromString(syncSpec.LookupMetadata("client", "uploadLogFilesTime"))
  
    settings.unitDescription = syncSpec.LookupMetadata("client", "unitDescription")
    settings.unitName = syncSpec.LookupMetadata("client", "unitName")
    settings.unitNamingMethod = syncSpec.LookupMetadata("client", "unitNamingMethod")
    settings.contentDownloadsRestricted = GetBoolFromString(syncSpec.LookupMetadata("client", "contentDownloadsRestricted"), false)
  	settings.contentDownloadRangeStart = GetIntFromString(syncSpec.LookupMetadata("client", "contentDownloadRangeStart"))
  	settings.contentDownloadRangeLength = GetIntFromString(syncSpec.LookupMetadata("client", "contentDownloadRangeLength"))
  
    setupType = lcase(globalAA.registrySection.read("sut"))
  
    if setupType <> "bsn" then
      
      settings.networkConnectionPriorityWired = 0
      if IsString(globalAA.registrySettings.wiredNetworkingParameters.networkConnectionPriority$) then
        ncp$ = globalAA.registrySettings.wiredNetworkingParameters.networkConnectionPriority$
        if len(ncp$) > 0 then
          settings.networkConnectionPriorityWired = int(val(ncp$))
        endif
      endif
  
      settings.networkConnectionPriorityWireless = 1
      if IsString(globalAA.registrySettings.wirelessNetworkingParameters.networkConnectionPriority$) then
        ncp$ = globalAA.registrySettings.wirelessNetworkingParameters.networkConnectionPriority$
        if len(ncp$) > 0 then
          settings.networkConnectionPriorityWireless = int(val(ncp$))
        endif
      endif
  
      settings.dwsEnabled = globalAA.registrySettings.dwsEnabled
      settings.dwsPassword = globalAA.registrySettings.dwsPassword$
      settings.idleScreenColor = globalAA.registrySettings.idleScreenColor$
      settings.timezone = ""  ' tells the rest of the system that the timeZone hasn't changed
      ' compatible with old code, but not necessarily right
      settings.useDHCP = ""
      settings.timeServer = GetGlobalAA().registrySettings.timeServer$
  
    else
  
      settings.networkConnectionPriorityWired = int(val(syncSpec.LookupMetadata("client", "networkConnectionPriorityWired")))
      settings.networkConnectionPriorityWireless = int(val(syncSpec.LookupMetadata("client", "networkConnectionPriorityWireless")))
    
      settings.dwsEnabled = syncSpecValueTrue(syncSpec.LookupMetadata("client", "dwsEnabled"))
      settings.dwsPassword = syncSpec.LookupMetadata("client", "dwsPassword")
      settings.idleScreenColor = syncSpec.LookupMetadata("client", "idleScreenColor")
      settings.timezone = syncSpec.LookupMetadata("client", "timezone")
      settings.useDHCP = syncSpec.LookupMetadata("client", "useDHCP")
      settings.timeServer = syncSpec.LookupMetadata("client", "timeServer")
  
    endif
    
    ' TEDTODO
    ' the following values are not in the sync spec for SFN. However, it appears as though these variables are not used in a meaningful way
    settings.proxy = syncSpec.LookupMetadata("client", "proxy")
    settings.networkHosts = syncSpec.LookupMetadata("client", "networkHosts")
    settings.passphrase = syncSpec.LookupMetadata("client", "passphrase")
    settings.ssid = syncSpec.LookupMetadata("client", "ssid")
  
    settings.network = {}
    settings.network.interfaces = []
  
    sysInfo = globalAA.sysInfo
    settings.useWireless = false
    if sysInfo.modelSupportsWifi then
      registrySettings = GetGlobalAA().registrySettings
      inheritNetworkProperties = registrySettings.inheritNetworkProperties    
      if inheritNetworkProperties = "yes" or setupType <> "bsn" then
        settings.useWireless = GetBoolFromString(registrySettings.useWireless$, false)
      else
        settings.useWireless = GetBoolFromString(syncSpec.LookupMetadata("client", "useWireless"), false)
      end if
    endif
  
    if settings.useWireless then
  
      wiredSettings = GetNetworkSettings(syncSpec, "_2", "Wired")
      wiredSettings.networkInterface = "eth0"
  
      wirelessSettings = GetNetworkSettings(syncSpec, "", "Wireless")
      wirelessSettings.networkInterface = "wlan0"
      
      networkConnectionPriorityWired% = settings.networkConnectionPriorityWired
      networkConnectionPriorityWireless% = settings.networkConnectionPriorityWireless
  
      if networkConnectionPriorityWired% = 0 then
        settings.network.interfaces.push(wiredSettings)
        settings.network.interfaces.push(wirelessSettings)
      else
        settings.network.interfaces.push(wirelessSettings)
        settings.network.interfaces.push(wiredSettings)
      endif
  
    else
  
      wiredSettings = GetNetworkSettings(syncSpec, "", "Wired")
      wiredSettings.networkInterface = "eth0"
      settings.network.interfaces.push(wiredSettings)
  
    endif
  
    return settings
  
  end function
  
  
  ' Load the settings that are in the sync spec for both Settings and Non Settings Supervisors
  Function LoadSyncSpecSettings(syncSpec) as object
  
    syncSpecSettings = {}
  
    syncSpecSettings.account = syncSpec.LookupMetadata("server", "account")
    
    ' for sfn
    syncSpecSettings.user = syncSpec.LookupMetadata("server", "user")
    syncSpecSettings.password = syncSpec.LookupMetadata("server", "password")
    syncSpecSettings.enableBasicAuthentication = GetBoolFromString(syncSpec.LookupMetadata("server", "enableBasicAuthentication"), false)
  
    ' for bsn
    syncSpecSettings.accessToken = syncSpec.LookupMetadata("server", "accessToken")
  
    syncSpecSettings.base = syncSpec.LookupMetadata("client", "base")
    syncSpecSettings.next = syncSpec.LookupMetadata("client", "next")
    syncSpecSettings.event = syncSpec.LookupMetadata("client", "event")
    syncSpecSettings.devicedownload = syncSpec.LookupMetadata("client", "devicedownload")
    syncSpecSettings.devicedownloadprogress = syncSpec.LookupMetadata("client", "devicedownloadprogress")
    syncSpecSettings.deviceerror = syncSpec.LookupMetadata("client", "deviceerror")
    syncSpecSettings.trafficdownload = syncSpec.LookupMetadata("client", "trafficdownload")
  
    clientMetadata = syncSpec.GetMetadata("client")
    syncSpecSettings.uploadlogs = clientMetadata.LookupCi("uploadLogs")
  
    syncSpecSettings.timeBetweenNetConnects = syncSpec.LookupMetadata("client", "timeBetweenNetConnects")
  
    syncSpecSettings.enableSerialDebugging = syncSpecValueTrue(syncSpec.LookupMetadata("client", "enableSerialDebugging"))
    syncSpecSettings.enableSystemLogDebugging = syncSpecValueTrue(syncSpec.LookupMetadata("client", "enableSystemLogDebugging"))
  
    syncSpecSettings.limitStorageSpace = syncSpecValueTrue(syncSpec.LookupMetadata("client", "limitStorageSpace"))
    syncSpecSettings.spaceLimitedByAbsoluteSize = syncSpecValueTrue(syncSpec.LookupMetadata("client", "spaceLimitedByAbsoluteSize"))
    syncSpecSettings.publishedDataSizeLimitMB = syncSpec.LookupMetadata("client", "publishedDataSizeLimitMB")
    syncSpecSettings.publishedDataSizeLimitPercentage = syncSpec.LookupMetadata("client", "publishedDataSizeLimitPercentage")
    syncSpecSettings.dynamicDataSizeLimitMB = syncSpec.LookupMetadata("client", "dynamicDataSizeLimitMB")
    syncSpecSettings.dynamicDataSizeLimitPercentage = syncSpec.LookupMetadata("client", "dynamicDataSizeLimitPercentage")
    syncSpecSettings.htmlDataSizeLimitPercentage = syncSpec.LookupMetadata("client", "htmlDataSizeLimitPercentage")
    syncSpecSettings.htmlDataSizeLimitMB = syncSpec.LookupMetadata("client", "htmlDataSizeLimitMB")
    syncSpecSettings.htmlLocalStorageSizeLimitPercentage = syncSpec.LookupMetadata("client", "htmlLocalStorageSizeLimitPercentage")
    syncSpecSettings.htmlLocalStorageSizeLimitMB = syncSpec.LookupMetadata("client", "htmlLocalStorageSizeLimitMB")
    syncSpecSettings.htmlIndexedDBSizeLimitPercentage = syncSpec.LookupMetadata("client", "htmlIndexedDBSizeLimitPercentage")
    syncSpecSettings.htmlIndexedDBSizeLimitMB = syncSpec.LookupMetadata("client", "htmlIndexedDBSizeLimitMB")
    syncSpecSettings.awsAccessKeyId = syncSpec.LookupMetadata("client", "awsAccessKeyId")
    syncSpecSettings.awsSecretAccessKey = syncSpec.LookupMetadata("client", "awsSecretAccessKey")
    syncSpecSettings.awsSessionToken = syncSpec.LookupMetadata("client", "awsSessionToken")
    syncSpecSettings.deviceScreenShotsTemporaryStorage = syncSpec.LookupMetadata("client", "deviceScreenShotsTemporaryStorage")
    syncSpecSettings.incomingDeviceScreenshotsQueue = syncSpec.LookupMetadata("client", "incomingDeviceScreenshotsQueue")
    syncSpecSettings.uploadSnapshots = syncSpec.LookupMetadata("client", "uploadSnapshots")
    syncSpecSettings.uploadDeviceScreenshotHandlerAddress = syncSpec.LookupMetadata("client", "uploadDeviceScreenshotHandlerAddress")
    syncSpecSettings.securityToken = syncSpec.LookupMetadata("client", "securityToken")
  
    syncSpecSettings.obfuscatedPassphrase = syncSpec.LookupMetadata("client", "obfuscatedPassphrase")
  
    return syncSpecSettings
  
  end function
  
  
  Function GetNetworkSettings(syncSpec as object, suffix as string, interfaceName as string) as object
  
    networkSettings = {}
    networkSettings.rateLimitRateInWindow% = GetRateLimitValue(syncSpec.LookupMetadata("client", "rateLimitModeInWindow" + suffix), syncSpec.LookupMetadata("client", "rateLimitRateInWindow" + suffix))
    networkSettings.rateLimitRateInitialDownloads% = GetRateLimitValue(syncSpec.LookupMetadata("client", "rateLimitModeInitialDownloads" + suffix), syncSpec.LookupMetadata("client", "rateLimitRateInitialDownloads" + suffix))
    networkSettings.rateLimitRateOutsideWindow% = GetRateLimitValue(syncSpec.LookupMetadata("client", "rateLimitModeOutsideWindow" + suffix), syncSpec.LookupMetadata("client", "rateLimitRateOutsideWindow" + suffix))
  
    networkSettings.contentDownloadEnabled = GetBoolFromString(syncSpec.LookupMetadata("client", "contentXfersEnabled" + interfaceName), true)
    networkSettings.textFeedsDownloadEnabled = GetBoolFromString(syncSpec.LookupMetadata("client", "textFeedsXfersEnabledWired" + interfaceName), true)
    networkSettings.mediaFeedsDownloadEnabled = GetBoolFromString(syncSpec.LookupMetadata("client", "mediaFeedsXfersEnabledWired" + interfaceName), true)
    networkSettings.healthReportingEnabled = GetBoolFromString(syncSpec.LookupMetadata("client", "healthXfersEnabledWired" + interfaceName), true)
    networkSettings.logsUploadEnabled = GetBoolFromString(syncSpec.LookupMetadata("client", "logUploadsXfersEnabledWired" + interfaceName), true)
  
    networkSettings.useDHCP = syncSpec.LookupMetadata("client", "useDHCP" + suffix)
    networkSettings.staticIPAddress = syncSpec.LookupMetadata("client", "staticIPAddress" + suffix)
    networkSettings.subnetMask = syncSpec.LookupMetadata("client", "subnetMask" + suffix)
    networkSettings.gateway = syncSpec.LookupMetadata("client", "gateway" + suffix)
    networkSettings.dns1 = syncSpec.LookupMetadata("client", "dns1" + suffix)
    networkSettings.dns2 = syncSpec.LookupMetadata("client", "dns2" + suffix)
    networkSettings.dns3 = syncSpec.LookupMetadata("client", "dns3" + suffix)
  
    return networkSettings
  
  end function
  
  
  Function GetWiredInterfaceIndex(settings as object) as integer
  
    if (settings.useWireless) then
  
      nc = CreateObject("roNetworkConfiguration", 1)
      if type(nc) = "roNetworkConfiguration" then
        currentConfig = nc.GetCurrentConfig()
        if type(currentConfig) = "roAssociativeArray" then
          if settings.networkConnectionPriorityWireless = 0 then
            return 1
          endif
        endif
      endif
  
    else
  
      return 0
  
    endif
  
  end function
  
  
  Sub GetDeviceSerialNumberJson(userData as object, e as object)
    
    mVar = userData.mVar
    
    respBody = { }
    respBody.AddReplace("serialNumber", mVar.sysInfo.deviceUniqueID$)
    
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(respBody))
    e.SendResponse(200)
    
  end sub
  
  Function GetWirelessInterfaceIndex(settings as object)
  
    if (settings.useWireless) then
  
      nc = CreateObject("roNetworkConfiguration", 1)
      if type(nc) = "roNetworkConfiguration" then
        currentConfig = nc.GetCurrentConfig()
        if type(currentConfig) = "roAssociativeArray" then
          if settings.networkConnectionPriorityWireless = 0 then
            return 0
          else
            return 1
          endif
        endif
      endif
  
    endif
  
    return invalid
  
  end function
  
  'region remoteSnapshots
  
  Sub UpdateRemoteSnapshotSettingsFromSyncSpec(syncSpec as object)
  
    settings = GetActiveSettings()
    registrySection = GetGlobalAA().registrySection
  
    ' remember current settings
    currentDeviceScreenShotsEnabled = settings.deviceScreenShotsEnabled
  
    m.SetRemoteSnapshotUrls()
    
    if not GetGlobalAA().useSupervisorConfigSpec then
  
      registrySection.Write("enableRemoteSnapshot", syncSpec.LookupMetadata("client", "deviceScreenShotsEnabled"))
      settings.deviceScreenShotsEnabled = GetBoolFromString(registrySection.Read("enableRemoteSnapshot"), false)
  
      registrySection.Write("remoteSnapshotInterval", syncSpec.LookupMetadata("client", "deviceScreenShotsInterval"))
      settings.deviceScreenShotsInterval = GetIntFromString(registrySection.Read("remoteSnapshotInterval"))
  
      registrySection.Write("remoteSnapshotMaxImages", syncSpec.LookupMetadata("client", "deviceScreenShotsCountLimit"))
      settings.deviceScreenShotsCountLimit = GetIntFromString(registrySection.Read("remoteSnapshotMaxImages"))
      
      registrySection.Write("remoteSnapshotJpegQualityLevel", syncSpec.LookupMetadata("client", "deviceScreenShotsQuality"))
      settings.deviceScreenShotsQuality = GetIntFromString(registrySection.Read("remoteSnapshotJpegQualityLevel"))
      
      orientationFromSyncSpec = syncSpec.LookupMetadata("client", "deviceScreenShotsOrientation")
      if IsNonEmptyString(orientationFromSyncSpec) then
        registrySection.Write("remoteSnapshotOrientation", orientationFromSyncSpec)
        settings.deviceScreenShotsOrientation = orientationFromSyncSpec
      else
        registrySection.Write("remoteSnapshotOrientation", "Landscape")
        settings.deviceScreenShotsOrientation = "Landscape"
      end if
  
    end if
    
    if currentDeviceScreenShotsEnabled <> settings.deviceScreenShotsEnabled then
      if settings.deviceScreenShotsEnabled then ' remote snapshots were off; turn them on
        m.bsp.remoteSnapshotTimer = CreateObject("roTimer")
        m.bsp.remoteSnapshotTimer.SetPort(m.bsp.msgPort)
        m.bsp.remoteSnapshotTimer.SetElapsed(settings.deviceScreenShotsInterval, 0)
        m.bsp.remoteSnapshotTimer.Start()
      else ' remote snapshots were on; turn them off
        if type(m.bsp.remoteSnapshotTimer) = "roTimer" then
          m.bsp.remoteSnapshotTimer.Stop()
          m.bsp.remoteSnapshotTimer = invalid
        end if
      end if
    end if
  
  end sub
  
  
  Sub UpdateRemoteSnapshotSettingsFromNewSettings(newSettings as object)
  
    globalAA = GetGlobalAA()
    bsp = globalAA.bsp
    settings = GetActiveSettings()
  
    currentDeviceScreenShotsEnabled = settings.deviceScreenShotsEnabled
  
    ' update global settings
    settings.deviceScreenShotsEnabled = newSettings.deviceScreenShotsEnabled
    settings.deviceScreenShotsInterval = newSettings.deviceScreenShotsInterval
    settings.deviceScreenShotsCountLimit = newSettings.deviceScreenShotsCountLimit
    settings.deviceScreenShotsQuality = newSettings.deviceScreenShotsQuality
    settings.deviceScreenShotsOrientation = newSettings.deviceScreenShotsOrientation
  
    if currentDeviceScreenShotsEnabled <> settings.deviceScreenShotsEnabled then
      if settings.deviceScreenShotsEnabled then ' remote snapshots were off; turn them on
        bsp.remoteSnapshotTimer = CreateObject("roTimer")
        bsp.remoteSnapshotTimer.SetPort(bsp.msgPort)
        bsp.remoteSnapshotTimer.SetElapsed(settings.deviceScreenShotsInterval, 0)
        bsp.remoteSnapshotTimer.Start()
      else ' remote snapshots were on; turn them off
        if type(bsp.remoteSnapshotTimer) = "roTimer" then
          bsp.remoteSnapshotTimer.Stop()
          bsp.remoteSnapshotTimer = invalid
        end if
      end if
    end if
  
    if settings.deviceScreenShotsEnabled then     
      DeleteExcessSnapshots()
    endif
  
  end sub
  
  
    ' invoked when changing snapshot properties on BrightSign app
  Function SetSnapshotConfiguration(userData as object, e as object) as object
  
    globalAA = GetGlobalAA()
  
    mVar = userData.mVar
    
    mVar.diagnostics.PrintDebug("### SetSnapshotConfiguration")
    mVar.logging.WriteDiagnosticLogEntry(mVar.diagnosticCodes.EVENT_SET_SNAPSHOT_CONFIGURATION, "")
    
    e.AddResponseHeader("Content-type", "text/plain")
    
    args = e.GetFormData()
  
    if globalAA.useSupervisorConfigSpec then
  
      systemTime = CreateObject("roSystemTime")
      dt = systemTime.GetUtcDateTime()
      lastModifiedTime = FormatDateTime(dt) + "Z"
    
      supervisorSettings = {}
  
      if args.enableRemoteSnapshot <> invalid then
        supervisorSettings.AddReplace("deviceScreenShotsEnabled", GetBoolFromString(args.enableRemoteSnapshot, false))
      end if
      if args.remoteSnapshotInterval <> invalid then
        supervisorSettings.AddReplace("deviceScreenShotsInterval", int(val(args.remoteSnapshotInterval)))
      end if
      if args.remoteSnapshotMaxImages <> invalid then
        supervisorSettings.AddReplace("deviceScreenShotsCountLimit", int(val(args.remoteSnapshotMaxImages)))
      end if
      if args.remoteSnapshotJpegQualityLevel <> invalid then
        supervisorSettings.AddReplace("deviceScreenShotsQuality", int(val(args.remoteSnapshotJpegQualityLevel)))
      end if
  
      ' check both remoteSnapshotOrientation (new name) and remoteSnapshotDisplayPortrait (obsolete name still used by BrightSignApp)
      supervisorSettings.AddReplace("deviceScreenShotsOrientation", "Landscape")
      if IsNonEmptyString(args.remoteSnapshotOrientation) then
        supervisorSettings.AddReplace("deviceScreenShotsOrientation", args.remoteSnapshotOrientation)
      else if IsNonEmptyString(args.remoteSnapshotDisplayPortrait) then
        if lcase(args.remoteSnapshotDisplayPortrait) = "true" then
          supervisorSettings.AddReplace("deviceScreenShotsOrientation", "PortraitBottomLeft")
        end if
      end if
  
      SendUpdatedSettingsToSupervisor(lastModifiedTime, supervisorSettings)
  
    else
  
      settings = GetActiveSettings()
      registrySection = globalAA.registrySection
  
      currentDeviceScreenShotsEnabled = settings.deviceScreenShotsEnabled
      currentDeviceScreenShotsInterval = settings.deviceScreenShotsInterval
  
      ' set global values from form data
      args = e.GetFormData()
      if args.enableRemoteSnapshot <> invalid then
        settings.deviceScreenShotsEnabled = GetBoolFromString(args.enableRemoteSnapshot, false)
        registrySection.Write("enableRemoteSnapshot", GetYesNoFromBool(settings.deviceScreenShotsEnabled))
      end if
      if args.remoteSnapshotInterval <> invalid then
        settings.deviceScreenShotsInterval = int(val(args.remoteSnapshotInterval))
        registrySection.Write("remoteSnapshotInterval", stri(settings.deviceScreenShotsInterval))
      end if
      if args.remoteSnapshotMaxImages <> invalid then
        settings.deviceScreenShotsCountLimit = int(val(args.remoteSnapshotMaxImages))
        registrySection.Write("remoteSnapshotMaxImages", stri(settings.deviceScreenShotsCountLimit))
      end if
      if args.remoteSnapshotJpegQualityLevel <> invalid then
        settings.deviceScreenShotsQuality = int(val(args.remoteSnapshotJpegQualityLevel))
        registrySection.Write("remoteSnapshotJpegQualityLevel", stri(settings.deviceScreenShotsQuality))
      end if
      settings.deviceScreenShotsOrientation = "Landscape"
      if IsNonEmptyString(args.remoteSnapshotOrientation) then
        settings.deviceScreenShotsOrientation = args.remoteSnapshotOrientation
        registrySection.Write("remoteSnapshotOrientation", settings.deviceScreenShotsOrientation)
      else if IsNonEmptyString(args.remoteSnapshotDisplayPortrait) then
        if lcase(args.remoteSnapshotDisplayPortrait) = "true" then
          settings.deviceScreenShotsOrientation = "PortraitBottomLeft"
        end if
        registrySection.Write("remoteSnapshotOrientation", settings.deviceScreenShotsOrientation)
      end if
  
      if settings.deviceScreenShotsEnabled then
        DeleteExcessSnapshots()
        if not currentDeviceScreenShotsEnabled or settings.deviceScreenShotsInterval <> currentDeviceScreenShotsInterval then      
          mVar.InitiateRemoteSnapshotTimer()
        end if
      else if currentDeviceScreenShotsEnabled then
        mVar.RemoveRemoteSnapshotTimer()
      end if
  
    endif
  
    e.SetResponseBodyString("OK")
    e.SendResponse(200)
    
  end function
  
  
  Sub DeleteExcessSnapshots()
    
    globalAA = GetGlobalAA()
  
    while globalAA.listOfSnapshotFiles.Count() >= GetActiveSettings().deviceScreenShotsCountLimit 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()
    settings = GetActiveSettings()
  
    ' before taking snapshot, delete the oldest if necessary
    DeleteExcessSnapshots()
    
    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)
  
    ' rotation depend on screen orientation
    rotationDegree% = 0
    isBSPObject = (type(globalAA.bsp) = "roAssociativeArray")
    isBSPSignObject = (isBSPObject) and (type(globalAA.bsp.sign) = "roAssociativeArray")
  
    if (isBSPSignObject and HasMultiScreenOutputs(globalAA.bsp.sign)) then
      ' No need to rotate if it's multi screen mode because the rotation is done in video mode per screen
    else
      if IsNonEmptyString(settings.deviceScreenShotsOrientation) then
        orientation$ = settings.deviceScreenShotsOrientation
        if IsPortraitBottomLeft(lcase(orientation$)) then
          rotationDegree% = 90
        else if IsPortraitBottomRight(lcase(orientation$)) then
          rotationDegree% = 270
        end if
      end if
    end if
  
    ' snapshot resolution
    width = videoMode.GetResX()
    height = videoMode.GetResY()
  
    ' if original resolution higher than 4k(3840x2160) pixels, downsample to 4k
    ' 1.calculate the num of pixels(p1) in 4k image and the current image(p2)
    fourKPixels% = 3840 * 2160
    imagePixels% = width * height
    if fourKPixels% < imagePixels% then
      ' 2.ratio = square root of (p2/p1) = width or height ratio
      ratio = sqr(imagePixels% / fourKPixels%)
      ' 3.final width or height = original width or height / ratio
      width = int(width / ratio)
      height = int(height / ratio)
    end if
    
    aa = { }
    aa.filename = "snapshots/" + dateTime$ + ".jpg"
    aa.Width = width
    aa.Height = height
    aa.filetype = "JPEG"
    aa.quality = settings.deviceScreenShotsQuality
    aa.rotation = rotationDegree%
    aa.Async = 0
    
    if IsString(activePresentation$) then
      aa.description = activePresentation$
    end if
    
    if isBSPObject then
      globalAA.bsp.diagnostics.PrintDebug("------------------------------------------- SCREENSHOT " + aa.filename + "-------------------------------------")
    end if
    ok = videoMode.Screenshot(aa)
    if not ok then
      if isBSPObject 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 isBSPObject then
        snapshotCaptured = { }
        snapshotCaptured["EventType"] = "SNAPSHOT_CAPTURED"
        snapshotCaptured["SnapshotName"] = dateTime$ + ".jpg"
        globalAA.bsp.msgPort.PostMessage(snapshotCaptured)
      end if
      
    end if
    
    videoMode = invalid
    
  end sub
  
  
  'endregion
  
  
  'region settingsInConfig
  
  Sub UpdateSettingsFromConfig()
  
    newSettings = GetPendingSettings()
    bsp = GetGlobalAA().bsp
  
    ' deviceScreenShots, etc.
    UpdateRemoteSnapshotSettingsFromNewSettings(newSettings)
  
    ' ****loggingEnabled
    playbackLoggingEnabled = newSettings.playbackLoggingEnabled
    eventLoggingEnabled = newSettings.eventLoggingEnabled
    diagnosticLoggingEnabled = newSettings.diagnosticLoggingEnabled
    stateLoggingEnabled = newSettings.stateLoggingEnabled
    variableLoggingEnabled = newSettings.variableLoggingEnabled
    uploadLogFilesAtBoot = newSettings.uploadLogFilesAtBoot
    uploadLogFilesAtSpecificTime = newSettings.uploadLogFilesAtSpecificTime
    uploadLogFilesTime% = newSettings.uploadLogFilesTime
    bsp.logging.ReinitializeLogging(playbackLoggingEnabled, eventLoggingEnabled, stateLoggingEnabled, diagnosticLoggingEnabled, variableLoggingEnabled, uploadLogFilesAtBoot, uploadLogFilesAtSpecificTime, uploadLogFilesTime%)
  
    ' contentDownloadsRestricted
    contentDownloadsRestricted = newSettings.contentDownloadsRestricted
    notInDownloadWindow = false
    if contentDownloadsRestricted then
      currentTime = bsp.systemTime.GetLocalDateTime()
      startOfRange% = newSettings.contentDownloadRangeStart
      endOfRange% = startOfRange% + newSettings.contentDownloadRangeLength 
      notInDownloadWindow = OutOfDownloadWindow(currentTime, startOfRange%, endOfRange%)
    end if
  
    ' rate limits
    downloadRateLimits = GetDownloadRateLimits(newSettings, notInDownloadWindow)
    SetDownloadRateLimits(bsp.diagnostics, downloadRateLimits)
  
  end sub
  
  'endregion
  
  'region supervisor
  
  Function GetSupervisorSupport()
  
    supervisorSupport = invalid
  
    localBaseUrl = "http://localhost"
    supervisorApiV1Route = "/api/v1"
  
    supervisorSupportUrlXfer = CreateObject("roUrlTransfer")
    supervisorSupportUrlXfer$ = localBaseUrl + supervisorApiV1Route + "/system"
    supervisorSupportUrlXfer.SetUrl(supervisorSupportUrlXfer$)
    supervisorSupportUrlXfer.SetTimeout(15000)
    supervisorSupportUrlXfer.SetProxyBypass(["127.0.0.1", "localhost"])
  
    supervisorSupportStr = supervisorSupportUrlXfer.GetToString()
    if IsString(supervisorSupportStr) and len(supervisorSupportStr) > 0 then
      supervisorSupport = ParseJson(supervisorSupportStr)
      if type(supervisorSupport) = "roAssociativeArray" and type(supervisorSupport.data) = "roAssociativeArray" then
        if type(supervisorSupport.data.result) = "roAssociativeArray" then
          supervisorSupport = supervisorSupport.data.result
        endif
      endif
    endif
  
    return supervisorSupport
  
  end function
  
  
  Function GetSupervisorSupportsConfigureNetwork() as boolean
  
    supervisorSupport = GetGlobalAA().supervisorSupport
    if type(supervisorSupport) <> "roAssociativeArray" then
      return false
    endif
  
    url = CreateObject("roUrlTransfer")
    bootstrapRestBase = "http://127.0.0.1"
    registrationRequestUrl = bootstrapRestBase + "/api/v1/system/supervisor/registration"
  
    url.SetUrl(registrationRequestUrl)
    url.SetProxyBypass(["127.0.0.1", "localhost"]) ' BCN-8732
  
    result = url.GetToString()
    if result <> "" then
      payload = ParseJson(result)
      if type(payload) = "roAssociativeArray" and type(payload.data) = "roAssociativeArray" and type(payload.data.result) = "roAssociativeArray" then
        if type(payload.data.result.setupSupport) = "roAssociativeArray" and IsBoolean(payload.data.result.setupSupport.network) and payload.data.result.setupSupport.network then
          return true
        endif
      endif
    endif
  
    return false
  
  end function
  
  
  Function GetSupervisorVersion() as string
  
    supervisorSupport = GetGlobalAA().supervisorSupport
  
    if type(supervisorSupport) <> "roAssociativeArray" then
      supervisorVersion = "preS2P"
    else
      if type(supervisorSupport.supportAutorunNetwork) = "roAssociativeArray" then
        supervisorVersion = "s3P"
      else
        autorunConfigEndpointFound = AutorunConfigEndpointExists()
        if not autorunConfigEndpointFound then
          supervisorVersion = "preS2P"
        else
          supervisorVersion = "s2P"
        endif
      endif
    endif
  
    return supervisorVersion
  
  end function
  
  
  Function GetEnableSettingsHandler() as boolean
  
    supervisorSupport = GetGlobalAA().supervisorSupport
    supervisorVersion = GetGlobalAA().supervisorVersion
  
    if supervisorVersion = "s3P" or supervisorVersion = "s2P" then
      if IsBoolean(supervisorSupport.settingsHandler.enabled) and supervisorSupport.settingsHandler.enabled then
        ' TEDTODO - issue if the user publishes a presentation without ever having run setup
        return true
      else
        return false
      endif
    else if supervisorVersion = "preS2P" then
      return false
    else
      ' TODO - unexpected error
      stop
      return false
    endif
  
  end function
  
  
  Function GetIsCellularModemActive() as boolean
  
    cellularModemActive = false
  
    supervisorSupport = GetGlobalAA().supervisorSupport
    
    if type(supervisorSupport) = "roAssociativeArray" then
      cellularModemActive = supervisorSupport.LookupCi("cellularModemActive")
      if cellularModemActive = invalid then
        cellularModemActive = false
      endif
    endif
  
    return cellularModemActive
  
  end function
  
  
  Sub CreateAutorunConfigMsgPort()
  
    globalAA = GetGlobalAA()
    
    if type(globalAA.autorunConfigMsgPort) <> "roMessagePort" then
      globalAA.autorunConfigMsgPort = CreateObject("roMessagePort")
      if type(globalAA.autorunConfigMsgPort) <> "roMessagePort" then
        stop
      endif
    endif
  
  end sub
  
  
  Function InvokeAutorunConfigGet() as object
  
    globalAA = GetGlobalAA()
  
    ' GET http://[device-ip]/api/v1/system/supervisor/autorun-config
  
    localBaseUrl = "http://localhost"
    supervisorApiV1Route = "/api/v1"
  
    autorunConfigUrlXfer = CreateObject("roUrlTransfer")
    autorunConfigUrlXfer$ = localBaseUrl + supervisorApiV1Route + "/system/supervisor/autorun-config"
    autorunConfigUrlXfer.SetUrl(autorunConfigUrlXfer$)
    autorunConfigUrlXfer.SetPort(globalAA.autorunConfigMsgPort)
    autorunConfigUrlXfer.SetProxyBypass(["127.0.0.1", "localhost"])
    ok = autorunConfigUrlXfer.AsyncGetToString()
    if not ok then
      stop
    endif
  
    return autorunConfigUrlXfer
  
  end function
  
  
  Function GetAutorunConfigEvent() as object
  
    while true
      event = wait(5000, GetGlobalAA().autorunConfigMsgPort)
      if type(event) = "roUrlEvent" then
        return event
      endif
    endwhile
  
  end function
  
  
  Function AutorunConfigEndpointExists() as boolean
    
    globalAA = GetGlobalAA()
    
    CreateAutorunConfigMsgPort()
  
    autorunConfigUrlXfer = InvokeAutorunConfigGet()
  
    maxRetryCount = 5
    currentRetryAttempt = 0
  
    while currentRetryAttempt < maxRetryCount
      event = GetAutorunConfigEvent()
      if event.getResponseCode() = 200 then
        return true
      else if event.getResponseCode() = 404 then
        return false
      endif
  
      sleep(1000)
      
      ' retry
      ok = autorunConfigUrlXfer.AsyncGetToString()
      if not ok then
        stop
      endif
      
      currentRetryAttempt = currentRetryAttempt + 1
  
    end while
  
    return false
  
  end function
  
  
  Function GetAutorunConfigFromSupervisor()
  
    globalAA = GetGlobalAA()
    
    CreateAutorunConfigMsgPort()
  
    autorunConfigUrlXfer = InvokeAutorunConfigGet()
  
    while true
      
      event = GetAutorunConfigEvent()
  
      if event.getResponseCode() = 200 then
  
        autorunConfigStr = event.GetString()
  
        if IsBoolean(globalAA.debug_settings) and globalAA.debug_settings then
          WriteConfigToStorage(autorunConfigStr)
        endif
  
        autorunConfig = ParseJSON(autorunConfigStr)
  
        if type(autorunConfig) = "roAssociativeArray" and type(autorunConfig.data) = "roAssociativeArray" and type(autorunConfig.data.result) = "roAssociativeArray" and type(autorunConfig.data.result.settings) = "roAssociativeArray" then
          return autorunConfig.data.result.settings
        endif
  
      endif
  
      sleep(1000)
      
      ' retry
      ok = autorunConfigUrlXfer.AsyncGetToString()
      if not ok then
        stop
      endif
    
    end while
  
  end function
  
  
  ' this function is not called by the autorun, but can be accessed from within the debugger as follows
  ' config = GetAutorunConfigFromSupervisorSync()
  Function GetAutorunConfigFromSupervisorSync()
  
    localBaseUrl = "http://localhost"
    supervisorApiV1Route = "/api/v1"
  
    autorunConfigUrlXfer = CreateObject("roUrlTransfer")
    autorunConfigUrlXfer$ = localBaseUrl + supervisorApiV1Route + "/system/supervisor/autorun-config"
    autorunConfigUrlXfer.SetUrl(autorunConfigUrlXfer$)
    autorunConfigUrlXfer.SetProxyBypass(["127.0.0.1", "localhost"])
    autorunConfigStr = autorunConfigUrlXfer.GetToString()
  
    systemTime = createObject("roSystemTime")
    dateTime = systemTime.GetLocalDateTime()
    isoDateTime = GetISODateTimeString(dateTime)
    ok = WriteAsciiFile("config" + isoDateTime + ".json", autorunConfigStr)
  
    autorunConfig = ParseJSON(autorunConfigStr)
    return autorunConfig.data.result.payload
  
  end function
  
  
  Sub CreateControlCloudInterface()
  
    globalAA = GetGlobalAA()
    ' If we have a new enough system, use the control cloud access method
    globalAA.ccloud = CreateObject("roControlCloud")
    if globalAA.ccloud <> invalid then
      globalAA.ccloud.SetPort(globalAA.msgPort)
      globalAA.ccloud.SetUserData("bootstrap")
      ' send an empty message to indicate we'll use roControlCloud
      globalAA.ccloud.SendMessage({})
    endif
  
  end sub
  
  
  Sub SendAutorunCapabilitiesToSupervisorViaPost(plugins as object, presentationName as string)
  
    localBaseUrl = "http://localhost"
    supervisorApiV1Route = "/api/v1"
  
    supervisorCapabilitiesUrlXfer = CreateObject("roUrlTransfer")
    supervisorCapabilitiesUrlXfer$ = localBaseUrl + supervisorApiV1Route + "/system/supervisor/capabilities"
    supervisorCapabilitiesUrlXfer.SetUrl(supervisorCapabilitiesUrlXfer$)
    supervisorCapabilitiesUrlXfer.SetTimeout(15000)
    supervisorCapabilitiesUrlXfer.SetProxyBypass(["127.0.0.1", "localhost"])
    supervisorCapabilitiesUrlXfer.addHeader("Content-type", "application/json")
  
    reqBody = BuildCapabilitiesBody(plugins, presentationName)
  
    stringifiedJson = FormatJson(reqBody)
  
    aa = {}
    aa.method = "POST"
    aa.request_body_string = stringifiedJson
    aa.response_body_string = true
  
    postEvent = supervisorCapabilitiesUrlXfer.SyncMethod(aa)
  
    ' for s3P, check the response
    if GetGlobalAA().supervisorVersion = "s3P" then
      rc = postEvent.getResponseCode()
      if rc = 200 then
        result = postEvent.getString()
        resultBody = ParseJSON(result)
        if type(resultBody) = "roAssociativeArray" and type(resultBody.data) = "roAssociativeArray" and type(resultBody.data.result) = "roAssociativeArray" and IsBoolean(resultBody.data.result.success) and resultBody.data.result.success then
          ok = true
        else
          ok = false
        endif
      else
        ok = false
      endif
    endif
  
  end sub
  
  
  Sub SendAutorunCapabilitiesToSupervisorViaUdp(plugins as object, presentationName as string)
  
    if m.dgSocket <> invalid then
  
      script = ConstructAutorunScriptObject(plugins)
      presentation = ConstructAutorunPresentationObject(presentationName)
  
      body = {}
      body.AddReplace("script", script)
      if type(presentation) = "roAssociativeArray" then
        body.AddReplace("presentation", presentation)
      endif
  
      payload = {}
      payload.AddReplace("route", "/v1/script-status")
      payload.AddReplace("body", body)
      
      jsonArray = {}
      jsonArray.AddReplace("payload", payload)
  
      jsonString = FormatJson(jsonArray)
  
      result = m.dgSocket.SendTo("127.0.0.1", GetGlobalAA().registrySettings.wsUdpSocketPort%, jsonString)
      
    end if
  
  end sub
  
  
  Sub SendAutorunCapabilitiesToSupervisorViaIpc(plugins as object, presentationName as string)
  
    globalAA = GetGlobalAA()
  
    if globalAA.ccloud <> invalid then
  
      ' TEDTODO - globalAA.ccloud <> invalid for the BCN-10310 scenario - is that a problem?
  
      script = ConstructAutorunScriptObject(plugins)
      presentation = ConstructAutorunPresentationObject(presentationName)
  
      body = {}
      body.AddReplace("script", script)
      if type(presentation) = "roAssociativeArray" then
        body.AddReplace("presentation", presentation)
      endif
  
      payload = {}
      payload.AddReplace("route", "/v1/script-status")
      payload.AddReplace("body", body)
      
      jsonArray = {}
      jsonArray.AddReplace("payload", payload)
  
      globalAA.ccloud.SendMessage(jsonArray)
  
    endif
  
  end sub
  
  
  Sub ResendAutorunCapabilitiesToSupervisor(plugins as object, presentationName as string)
  
    supervisorVersion = GetGlobalAA().supervisorVersion
    if supervisorVersion = "s2P" or supervisorVersion = "s3P" then
      SendAutorunCapabilitiesToSupervisorViaPost(plugins, presentationName)
    else
      m.SendAutorunCapabilitiesToSupervisorViaUdp(plugins, presentationName)
    endif
  
  end sub
  
  
  Function BuildCapabilitiesBody(plugins as object, presentationName as string) as object
  
    settings = ConstructSettingsApplied()
    script = ConstructAutorunScriptObject(plugins)
    presentation = ConstructAutorunPresentationObject(presentationName)
  
    body = {}
    body.AddReplace("settings", settings)
    body.AddReplace("script", script)
    if type(presentation) = "roAssociativeArray" then
      body.AddReplace("presentation", presentation)
    endif
  
    return body
  
  end function
  
  
  Function ConstructSettingsApplied() as object
  
    settingsApplied = ["data-types", "content-downloads", "screenshots", "rate-limits", "log-uploads", "logging-enabled"]
  
    settings = {}
    settings.AddReplace("min-supervisor-version", "1.2.82")
    settings.AddReplace("autorun-applied", settingsApplied)
    settings.AddReplace("enabled", GetGlobalAA().supervisorEnableSettingsHandler)
  
    return settings
  
  end function
  
  
  Function ConstructAutorunScriptObject(plugins as object) as object
  
    sysInfo = GetGlobalAA().sysInfo
  
    script = CreateObject("roAssociativeArray")
    scriptType$ = "Autorun"
    version$ = sysInfo.autorunVersion$
    ' autorun version use the same logic as firmware version
    ' reuse the function CompareFirmwareVersions
    autorunVsCustomAutorunVersion% = CompareFirmwareVersions(sysInfo.autorunVersion$, sysInfo.customAutorunVersion$)
    if autorunVsCustomAutorunVersion% <= 0 then
      scriptType$ = "Custom"
      version$ = sysInfo.customAutorunVersion$
    end if
    
    script.AddReplace("type", scriptType$)
    script.AddReplace("version", version$)
  
    ' Constructing jsonArray.payload.body.script.plugins
    if type(plugins) = "roArray" and plugins.count() > 0 then
      script.AddReplace("plugins", plugins)
    end if
  
    return script
  
  end function
  
  
  Function ConstructAutorunPresentationObject(presentationName as string) as object
  
    presentation = invalid
  
    if presentationName <> invalid and presentationName <> "" then
      presentation = {}
      sign = GetGlobalAA().bsp.sign
      if type(sign) = "roAssociativeArray" and sign.isBrightWall then
        presentation.AddReplace("type", "BrightWall")
      else
        presentation.AddReplace("type", "Regular")
      endif
      presentation.AddReplace("name", presentationName)
    end if
  
    return presentation
  
  end function
  
  
  Sub WriteConfigToStorage(autorunConfigStr as string)
  
    systemTime = createObject("roSystemTime")
    dateTime = systemTime.GetLocalDateTime()
    isoDateTime = GetISODateTimeString(dateTime)
    ok = WriteAsciiFile("config" + isoDateTime + ".json", autorunConfigStr)
  
  end sub
  
  
  ' Autorun notifies the supervisor when it receives a new sync spec
  Sub SendUpdatedSettingsToSupervisor(lastModifiedTimestamp as string, settings as object)
  
    globalAA = GetGlobalAA()
    if not globalAA.useSupervisorConfigSpec then
      return
    endif
  
    ' PUT /api/v1/system/supervisor/autorunSettingsUpdate
    ' https://jira.brightsign.biz/browse/BCN-8534
  
    ' https://docs.brightsign.biz/display/DOC/roUrlTransfer#roUrlTransfer-PutFromString(aAsString)AsInteger
    ' PutFromString(a As String) As Integer
    ' Uses the HTTP PUT method to write the supplied string to the current URL and return the response code.
    ' Any response body is discarded; use roUrlTransfer.SyncMethod to retrieve the response body.
  
    ' https://docs.brightsign.biz/display/BSV61/6.1-Global+Functions#id-6.1GlobalFunctions-FormatJson(jsonAsroAssociativeArray,flagsAsInteger)AsString
    ' FormatJson(json As roAssociativeArray, flags As Integer) As String
  
    supervisorSettingsUpdateHandlerUrl = CreateObject("roUrlTransfer")
    supervisorSettingsUpdateHandlerUrl.SetUrl("http://localhost/api/v1/system/supervisor/autorun-settings-update")
    supervisorSettingsUpdateHandlerUrl.addHeader("Content-type", "application/json")
    supervisorSettingsUpdateHandlerUrl.SetTimeout(15000)
    supervisorSettingsUpdateHandlerUrl.SetProxyBypass(["127.0.0.1", "localhost"])
    
    supervisorSettings = PopulateSupervisorSettings(settings)
    reqBody = {}
    reqBody.SetModeCaseSensitive()
    reqBody.AddReplace("settings", supervisorSettings)
    reqBody.AddReplace("lastModifiedTimeStamp", lastModifiedTimeStamp)
    
    stringifiedJson = FormatJson(reqBody)
  
    rc = supervisorSettingsUpdateHandlerUrl.PutFromString(stringifiedJson)
    if rc <> 200 then
      ' TODO - unexpected error
      stop
    endif
  
  end sub
  
  
  Sub PopulateSupervisorSettings(settings as object) as object
  
    supervisorSettings = {}
  
    PopulateSettingIfItExists(supervisorSettings, settings, "playbackLoggingEnabled")
    PopulateSettingIfItExists(supervisorSettings, settings, "eventLoggingEnabled")
    PopulateSettingIfItExists(supervisorSettings, settings, "diagnosticLoggingEnabled")
    PopulateSettingIfItExists(supervisorSettings, settings, "stateLoggingEnabled")
    PopulateSettingIfItExists(supervisorSettings, settings, "variableLoggingEnabled")
    
    PopulateSettingIfItExists(supervisorSettings, settings, "uploadLogFilesAtBoot")
    PopulateSettingIfItExists(supervisorSettings, settings, "uploadLogFilesAtSpecificTime")
    PopulateSettingIfItExists(supervisorSettings, settings, "uploadLogFilesTime")
    
    PopulateSettingIfItExists(supervisorSettings, settings, "contentDownloadsRestricted")
    PopulateSettingIfItExists(supervisorSettings, settings, "contentDownloadRangeStart")
    PopulateSettingIfItExists(supervisorSettings, settings, "contentDownloadRangeLength")
  
    PopulateSettingIfItExists(supervisorSettings, settings, "deviceScreenShotsEnabled")
    PopulateSettingIfItExists(supervisorSettings, settings, "deviceScreenShotsInterval")
    PopulateSettingIfItExists(supervisorSettings, settings, "deviceScreenShotsCountLimit")
    PopulateSettingIfItExists(supervisorSettings, settings, "deviceScreenShotsQuality")
    PopulateSettingIfItExists(supervisorSettings, settings, "deviceScreenShotsOrientation")
  
    return supervisorSettings
  
  end sub
  
  
  Sub PopulateSettingIfItExists(newSettings as object, existingSettings as object, propertyName as string)
    if existingSettings.DoesExist(propertyName) then
      newSettings.AddReplace(propertyName, existingSettings[propertyName])
    endif
  end sub
  
  'end region
  
  
  'region Non Settings Supervisor specific code
  
  Sub ReadLegacyRegistrySettings(registrySection as object, registrySettings as object)
  
    registrySettings.inheritNetworkProperties = registrySection.Read("inp")
  
    registrySettings.useProxy = registrySection.Read("up")
    if registrySettings.useProxy = "yes" then
      registrySettings.proxy$ = registrySection.Read("ps")
      registrySettings.networkHosts$ = registrySection.Read("bph")
    else
      registrySettings.proxy$ = ""
      registrySettings.networkHosts$ = ""
    end if
    
    registrySettings.useWireless$ = registrySection.Read("wifi")
    registrySettings.ssid$ = registrySection.Read("ss")
    registrySettings.passphrase$ = registrySection.Read("pp")
    registrySettings.timeServer$ = registrySection.Read("ts")
    
    registrySettings.wiredNetworkingParameters = { }
    registrySettings.wiredNetworkingParameters.networkConnectionPriority$ = registrySection.Read("ncp")
    
    registrySettings.wirelessNetworkingParameters = { }
    registrySettings.wirelessNetworkingParameters.networkConnectionPriority$ = registrySection.Read("ncp2")
    
    if registrySettings.useWireless$ = "yes" then
      registrySettings.wirelessNetworkingParameters.useDHCP$ = registrySection.Read("dhcp")
      registrySettings.wirelessNetworkingParameters.staticIPAddress$ = registrySection.Read("sip")
      registrySettings.wirelessNetworkingParameters.subnetMask$ = registrySection.Read("sm")
      registrySettings.wirelessNetworkingParameters.gateway$ = registrySection.Read("gw")
      registrySettings.wirelessNetworkingParameters.dns1$ = registrySection.Read("d1")
      registrySettings.wirelessNetworkingParameters.dns2$ = registrySection.Read("d2")
      registrySettings.wirelessNetworkingParameters.dns3$ = registrySection.Read("d3")
      
      registrySettings.wiredNetworkingParameters.useDHCP$ = registrySection.Read("dhcp2")
      registrySettings.wiredNetworkingParameters.staticIPAddress$ = registrySection.Read("sip2")
      registrySettings.wiredNetworkingParameters.subnetMask$ = registrySection.Read("sm2")
      registrySettings.wiredNetworkingParameters.gateway$ = registrySection.Read("gw2")
      registrySettings.wiredNetworkingParameters.dns1$ = registrySection.Read("d12")
      registrySettings.wiredNetworkingParameters.dns2$ = registrySection.Read("d22")
      registrySettings.wiredNetworkingParameters.dns3$ = registrySection.Read("d32")
    else
      registrySettings.wiredNetworkingParameters.useDHCP$ = registrySection.Read("dhcp")
      registrySettings.wiredNetworkingParameters.staticIPAddress$ = registrySection.Read("sip")
      registrySettings.wiredNetworkingParameters.subnetMask$ = registrySection.Read("sm")
      registrySettings.wiredNetworkingParameters.gateway$ = registrySection.Read("gw")
      registrySettings.wiredNetworkingParameters.dns1$ = registrySection.Read("d1")
      registrySettings.wiredNetworkingParameters.dns2$ = registrySection.Read("d2")
      registrySettings.wiredNetworkingParameters.dns3$ = registrySection.Read("d3")
    end if
  
    registrySettings.contentXfersEnabledWired$ = registrySection.Read("cwr")
    registrySettings.textFeedsXfersEnabledWired$ = registrySection.Read("twr")
    registrySettings.healthXfersEnabledWired$ = registrySection.Read("hwr")
    registrySettings.mediaFeedsXfersEnabledWired$ = registrySection.Read("mwr")
    registrySettings.logUploadsXfersEnabledWired$ = registrySection.Read("lwr")
    
    registrySettings.contentXfersEnabledWireless$ = registrySection.Read("cwf")
    registrySettings.textFeedsXfersEnabledWireless$ = registrySection.Read("twf")
    registrySettings.healthXfersEnabledWireless$ = registrySection.Read("hwf")
    registrySettings.mediaFeedsXfersEnabledWireless$ = registrySection.Read("mwf")
    registrySettings.logUploadsXfersEnabledWireless$ = registrySection.Read("lwf")
  
    registrySettings.dwsEnabled = GetBoolFromString(registrySection.Read("dwse"), false)
    registrySettings.dwsPassword$ = registrySection.Read("dwsp")
  
    registrySettings.deviceScreenShotsEnabled = registrySection.Read("enableRemoteSnapshot")
    registrySettings.deviceScreenShotsInterval = registrySection.Read("remoteSnapshotInterval")
    registrySettings.deviceScreenShotsCountLimit = registrySection.Read("remoteSnapshotMaxImages")
    registrySettings.deviceScreenShotsQuality = registrySection.Read("remoteSnapshotJpegQualityLevel")
    registrySettings.deviceScreenShotsOrientation = registrySection.Read("remoteSnapshotOrientation")
  
  end sub
  
  
  Sub UpdateSettingsFromSyncSpec(newSettings as object, newSyncSpecSettings as object)
      
    m.ProcessSyncSpecSettingsUpdates0(newSettings)
  
    ' parameters are not set in SFN case, so don't change them here
    if lcase(GetGlobalAA().settings.setupType) <> "sfn" then
      m.ProcessSyncSpecSettingsUpdates2(newSettings)
    endif
  
    m.ProcessSyncSpecSettingsUpdates3(newSettings, newSyncSpecSettings)
  
  end sub
  
  
  Sub ProcessSyncSpecSettingsUpdates0(newSettings as object)
  
    ' Retrieve network connection priorities from the sync spec
    networkConnectionPriorityWired% = newSettings.networkConnectionPriorityWired
    nc = CreateObject("roNetworkConfiguration", 0)
    if type(nc) = "roNetworkConfiguration" then
      nc.SetRoutingMetric(networkConnectionPriorityWired%)
      nc.Apply()
      nc = invalid
    end if
  
    networkConnectionPriorityWireless% = newSettings.networkConnectionPriorityWireless
    nc = CreateObject("roNetworkConfiguration", 1)
    if type(nc) = "roNetworkConfiguration" then
      nc.SetRoutingMetric(networkConnectionPriorityWireless%)
      nc.Apply()
      nc = invalid
    end if
  
    ' Retrieve logging information from the sync spec
    playbackLoggingEnabled = newSettings.playbackLoggingEnabled
    eventLoggingEnabled = newSettings.eventLoggingEnabled
    diagnosticLoggingEnabled = newSettings.diagnosticLoggingEnabled
    stateLoggingEnabled = newSettings.stateLoggingEnabled
    variableLoggingEnabled = newSettings.variableLoggingEnabled
    uploadLogFilesAtBoot = newSettings.uploadLogFilesAtBoot
    uploadLogFilesAtSpecificTime = newSettings.uploadLogFilesAtSpecificTime
    uploadLogFilesTime% = newSettings.uploadLogFilesTime
  
    m.stateMachine.logging.ReinitializeLogging(playbackLoggingEnabled, eventLoggingEnabled, stateLoggingEnabled, diagnosticLoggingEnabled, variableLoggingEnabled, uploadLogFilesAtBoot, uploadLogFilesAtSpecificTime, uploadLogFilesTime%)
      
  end sub
  
  
  ' Update settings on bsn sync spec update. These are the settings that are not updated on sfn sync spec updates.
  Sub ProcessSyncSpecSettingsUpdates2(newSettings as object)
  
    settings = GetActiveSettings()
  
    unitNameFromRegistry$ = settings.unitName
    unitNamingMethodFromRegistry$ = settings.unitNamingMethod
    unitDescriptionFromRegistry$ = settings.unitDescription
    
    unitName$ = newSettings.unitName
    unitNamingMethod$ = newSettings.unitNamingMethod
    unitDescription$ = newSettings.unitDescription
    
    if unitName$ <> unitNameFromRegistry$ then
      WriteRegistrySetting("un", unitName$)
      settings.unitName = unitName$
    end if
    
    if unitNamingMethod$ <> unitNamingMethodFromRegistry$ then
      WriteRegistrySetting("unm", unitNamingMethod$)
      settings.unitNamingMethod = unitNamingMethod$
    end if
    
    if unitDescription$ <> unitDescriptionFromRegistry$ then
      WriteRegistrySetting("ud", unitDescription$)
      settings.unitDescription = unitDescription$
    end if
  
    proxyFromSyncSpec$ = newSettings.proxy
    if proxyFromSyncSpec$ <> "" then
      useProxy$ = "yes"
    else
      useProxy$ = "no"
    end if
    useProxy$ = m.UpdateRegistrySetting(useProxy$, GetGlobalAA().registrySettings.useProxy, "up")
    
    proxySpec$ = m.UpdateRegistrySetting(newSettings.proxy, GetGlobalAA().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(newSettings.networkHosts, GetGlobalAA().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 sub
  
  
  Sub ProcessSyncSpecSettingsUpdates3(newSettings as object, newSyncSpecSettings as object)
  
    ' Retrieve latest network configuration information from sync spec
  
    timeServer$ = newSettings.timeServer
  
    useWireless = newSettings.useWireless
    if not m.stateMachine.modelSupportsWifi then useWireless = false
    
    ' TEDTODO - parameters are not accurate for SFN, but AFAIK, it has no negative impact
    if useWireless then
      ssid$ = newSettings.ssid
      passphrase$ = newSettings.passphrase
    end if
    
    wiredInterfaceIndex = GetWiredInterfaceIndex(newSettings)
    wiredInterface = newSettings.network.interfaces[wiredInterfaceIndex]
  
    wirelessInterface = invalid
    wirelessInterfaceIndex = GetWirelessInterfaceIndex(newSettings)
    if wirelessInterfaceIndex <> invalid then
      wirelessInterface = newSettings.network.interfaces[wirelessInterfaceIndex]
    endif
  
    ' don't update settings if SFN publish type
    if lcase(GetGlobalAA().settings.setupType) <> "sfn" then
      
      wiredNetworkingParameters = { }
      wiredNetworkingParameters.networkConfigurationIndex% = 0
      wiredNetworkingParameters.networkConnectionPriority = newSettings.networkConnectionPriorityWired
      
      if useWireless and wirelessInterface <> invalid then
        
        wirelessNetworkingParameters = { }
        wirelessNetworkingParameters.networkConfigurationIndex% = 1
        wirelessNetworkingParameters.networkConnectionPriority = newSettings.networkConnectionPriorityWireless
        
        wirelessNetworkingParameters.useDHCP$ = wirelessInterface.useDHCP
        if wirelessInterface.useDHCP = "no" then
          wirelessNetworkingParameters.staticIPAddress$ = wirelessInterface.staticIPAddress
          wirelessNetworkingParameters.subnetMask$ = wirelessInterface.subnetMask
          wirelessNetworkingParameters.gateway$ = wirelessInterface.gateway
          wirelessNetworkingParameters.dns1$ = wirelessInterface.dns1
          wirelessNetworkingParameters.dns2$ = wirelessInterface.dns2
          wirelessNetworkingParameters.dns3$ = wirelessInterface.dns3
        end if
  
        wirelessNetworkingParameters.useWireless = true
        wirelessNetworkingParameters.ssid$ = ssid$
        wirelessNetworkingParameters.passphrase$ = passphrase$
        wirelessNetworkingParameters.timeServer$ = timeServer$
        
        m.ConfigureNetwork(wirelessNetworkingParameters, GetGlobalAA().registrySettings.wirelessNetworkingParameters, "")
        
        wiredNetworkingParameters.useDHCP$ = wiredInterface.useDHCP
        if wiredNetworkingParameters.useDHCP$ = "no" then
          wiredNetworkingParameters.staticIPAddress$ = wiredInterface.staticIPAddress
          wiredNetworkingParameters.subnetMask$ = wiredInterface.subnetMask
          wiredNetworkingParameters.gateway$ = wiredInterface.gateway
          wiredNetworkingParameters.dns1$ = wiredInterface.dns1
          wiredNetworkingParameters.dns2$ = wiredInterface.dns2
          wiredNetworkingParameters.dns3$ = wiredInterface.dns3
        end if
        wiredNetworkingParameters.useWireless = false
        wiredNetworkingParameters.timeServer$ = timeServer$
        
        m.ConfigureNetwork(wiredNetworkingParameters, GetGlobalAA().registrySettings.wiredNetworkingParameters, "2")
        
      else
  
        wiredNetworkingParameters.useDHCP$ = wiredInterface.useDHCP
        if wiredNetworkingParameters.useDHCP$ = "no" then
          wiredNetworkingParameters.staticIPAddress$ = wiredInterface.staticIPAddress
          wiredNetworkingParameters.subnetMask$ = wiredInterface.subnetMask
          wiredNetworkingParameters.gateway$ = wiredInterface.gateway
          wiredNetworkingParameters.dns1$ = wiredInterface.dns1
          wiredNetworkingParameters.dns2$ = wiredInterface.dns2
          wiredNetworkingParameters.dns3$ = wiredInterface.dns3
        end if
        wiredNetworkingParameters.useWireless = false
        wiredNetworkingParameters.timeServer$ = timeServer$
        
        m.ConfigureNetwork(wiredNetworkingParameters, GetGlobalAA().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$ = GetGlobalAA().registrySettings.useWireless$
    ' TEDTODO - SFN: correct the first time; broken afterwards; probably due to bug below
    ssidFromRegistry$ = GetGlobalAA().registrySettings.ssid$
    passphraseFromRegistry$ = GetGlobalAA().registrySettings.passphrase$
    
    useWireless$ = GetYesNoFromBool(useWireless)
    if useWirelessFromRegistry$ <> useWireless$ then
      WriteRegistrySetting("wifi", useWireless$)
      GetGlobalAA().registrySettings.useWireless$ = useWireless$
    end if
  
    if useWireless then
      if ssidFromRegistry$ <> ssid$ then
        ' TEDTODO - SFN. this is messed up; it writes an empty string to the registry.
        WriteRegistrySetting("ss", ssid$)
        GetGlobalAA().registrySettings.ssid$ = ssid$
      end if
      
      if passphraseFromRegistry$ <> passphrase$ then
        WriteRegistrySetting("pp", passphrase$)
        GetGlobalAA().registrySettings.passphrase$ = passphrase$
      end if
    end if
    
    timeServerFromRegistry$ = GetGlobalAA().registrySettings.timeServer$
    if timeServerFromRegistry$ <> timeServer$ then
      WriteRegistrySetting("ts", timeServer$)
      GetGlobalAA().registrySettings.timeServer$ = timeServer$
    end if
  
    ' Retrieve latest net connect spec information from sync spec
    timeBetweenNetConnects$ = newSyncSpecSettings.timeBetweenNetConnects
    
    if timeBetweenNetConnects$ <> "" then
      
      ' 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 newSettings.contentDownloadsRestricted then
      m.bsp.diagnostics.PrintDebug("### Content downloads are restricted to the time from " + stri(newSettings.contentDownloadRangeStart) + " for " + stri(newSettings.contentDownloadRangeLength) + " minutes.")
    else
      m.bsp.diagnostics.PrintDebug("### Content downloads are unrestricted")
    end if
      
  end sub
  
  
  Sub ConfigureNetwork(networkingParameters as object, registryNetworkingParameters as object, registryKeySuffix$ as string)
    
    nc = CreateObject("roNetworkConfiguration", networkingParameters.networkConfigurationIndex%)
    if type(nc) = "roNetworkConfiguration" then
      
      if networkingParameters.useDHCP$ = "no" then
        
        nc.SetIP4Address(networkingParameters.staticIPAddress$)
        nc.SetIP4Netmask(networkingParameters.subnetMask$)
        nc.SetIP4Gateway(networkingParameters.gateway$)
        
        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(networkingParameters.networkConnectionPriority)
      
      if networkingParameters.useWireless then
        nc.SetWiFiESSID(networkingParameters.ssid$)
        nc.SetObfuscatedWifiPassphrase(networkingParameters.passphrase$)
      end if
      
      newSettings = GetPendingSettings()  
      
      timeServer$ = newSettings.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 int(val(networkConnectionPriorityFromRegistry$)) <> networkingParameters.networkConnectionPriority then
          networkConnectionPriority$ = StripLeadingSpaces(stri(networkingParameters.networkConnectionPriority))
          WriteRegistrySetting("ncp" + registryKeySuffix$, networkConnectionPriority$)
          registryNetworkingParameters.networkConnectionPriority$ = networkConnectionPriority$
        end if
        
        if networkingParameters.useDHCP$ = "no" then
          
          if registryNetworkingParameters.useDHCP$ <> "no" then
            WriteRegistrySetting("dhcp" + registryKeySuffix$, "no")
            registryNetworkingParameters.useDHCP$ = "no"
          end if
          
          staticIPAddressFromRegistry$ = registryNetworkingParameters.staticIPAddress$
          subnetMaskFromRegistry$ = registryNetworkingParameters.subnetMask$
          gatewayFromRegistry$ = registryNetworkingParameters.gateway$
          dns1FromRegistry$ = registryNetworkingParameters.dns1$
          dns2FromRegistry$ = registryNetworkingParameters.dns2$
          dns3FromRegistry$ = registryNetworkingParameters.dns3$
          
          if staticIPAddressFromRegistry$ <> networkingParameters.staticIPAddress$ then
            WriteRegistrySetting("sip" + registryKeySuffix$, networkingParameters.staticIPAddress$)
            registryNetworkingParameters.staticIPAddress$ = networkingParameters.staticIPAddress$
          end if
          
          if subnetMaskFromRegistry$ <> networkingParameters.subnetMask$ then
            WriteRegistrySetting("sm" + registryKeySuffix$, networkingParameters.subnetMask$)
            registryNetworkingParameters.subnetMask$ = networkingParameters.subnetMask$
          end if
          
          if gatewayFromRegistry$ <> networkingParameters.gateway$ then
            WriteRegistrySetting("gw" + registryKeySuffix$, networkingParameters.gateway$)
            registryNetworkingParameters.gateway$ = networkingParameters.gateway$
          end if
          
          if dns1FromRegistry$ <> networkingParameters.dns1$ then
            WriteRegistrySetting("d1" + registryKeySuffix$, networkingParameters.dns1$)
            registryNetworkingParameters.dns1$ = networkingParameters.dns1$
          end if
          
          if dns2FromRegistry$ <> networkingParameters.dns2$ then
            WriteRegistrySetting("d2" + registryKeySuffix$, networkingParameters.dns2$)
            registryNetworkingParameters.dns2$ = networkingParameters.dns2$
          end if
          
          if dns3FromRegistry$ <> networkingParameters.dns3$ then
            WriteRegistrySetting("d3" + registryKeySuffix$, networkingParameters.dns3$)
            registryNetworkingParameters.dns3$ = networkingParameters.dns3$
          end if
          
        else
          
          if registryNetworkingParameters.useDHCP$ <> "yes" then
            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 GetBsnce() as boolean
  
    supervisorRegistrySection = CreateObject("roRegistrySection", "!supervisor.brightsignnetwork.com")
    if type(supervisorRegistrySection) <> "roRegistrySection" then
      print "Error: Unable to create supervisorRegistrySection roRegistrySection": stop
    end if
    bsnce = supervisorRegistrySection.Read("bsnce")
    if bsnce = "false" then
      return false
    else
      return true
    endif
  
  end function
  
  
  Function GetAndSaveDwsParams(settings as object, registrySettings as object)
  
    dwsEnabled = settings.dwsEnabled
    bsnce = GetBsnce()
  
    if not (dwsEnabled or bsnce) then
      enableDws = false
    else
      enableDws = true
    endif
  
    dwsAA = {}
  
    if enableDws then
      dwsPassword$ = settings.dwsPassword
      dwsAA["port"] = "80"
      dwsAA["password"] = dwsPassword$
    else
      dwsAA["port"] = 0
    endif
  
    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
  
    return false
  
  end function
  
  
  'end region
  
  'region BrightWall
  
  Function GetConfig(bsp) as object
  
    globalAA = GetGlobalAA()
  
    config = {}
    settings = GetGlobalAA().settings
  
    config.AddReplace("brightSignAttributes", {})
    config.brightSignAttributes.AddReplace("serialNumber", bsp.sysInfo.deviceUniqueID$)
    config.brightSignAttributes.AddReplace("autorunVersion", bsp.sysInfo.deviceUniqueID$)
    config.brightSignAttributes.AddReplace("deviceFWVersion", bsp.sysInfo.deviceFWVersion$)
    config.brightSignAttributes.AddReplace("deviceModel", bsp.sysInfo.deviceModel$)
    config.brightSignAttributes.AddReplace("deviceFamily", bsp.sysInfo.deviceFamily$)
    config.brightSignAttributes.AddReplace("unitName", settings.unitName)
    config.brightSignAttributes.AddReplace("unitNamingMethod", settings.unitNamingMethod)
    config.brightSignAttributes.AddReplace("unitDescription", settings.unitDescription)
    if IsString(bsp.activePresentation$) then
      config.brightSignAttributes.AddReplace("activePresentationName", bsp.activePresentation$)
    else  
      config.brightSignAttributes.AddReplace("activePresentationName", "")
    end if
   
    for each networkInterfaceAA in globalAA.settings.network.interfaces
  
      networkInterfaceId = networkInterfaceAA.networkInterface
  
      nc = CreateObject("roNetworkConfiguration", networkInterfaceId)
      currentConfig = nc.GetCurrentConfig()
      hostName = nc.GetHostName()
  
      networkInterface = {}
      networkInterface.AddReplace("id", networkInterfaceId)
      networkInterface.AddReplace("currentConfig", currentConfig)
      networkInterface.AddReplace("hostName", hostName)
    ' brightsign-<serial>.local
    
      networkInterfaces = {}
      networkInterfaces.AddReplace(networkInterfaceId, networkInterface)
  
    next
  
    config.brightSignAttributes.AddReplace("networkInterfaces", networkInterfaces)
    config.brightSignAttributes.AddReplace("isBrightWall", globalAA.registrySettings.isBrightWall)
    
    config.brightSignAttributes.AddReplace("isMaster", false)
    config.brightSignAttributes.AddReplace("rowIndex", -1)
    config.brightSignAttributes.AddReplace("columnIndex", -1)
    config.brightSignAttributes.AddReplace("brightwall_bezel_left", 0)
    config.brightSignAttributes.AddReplace("brightwall_bezel_right", 0)
    config.brightSignAttributes.AddReplace("brightwall_bezel_top", 0)
    config.brightSignAttributes.AddReplace("brightwall_bezel_bottom", 0)
    config.brightSignAttributes.AddReplace("brightwall_bezel_screen_width", 0)
    config.brightSignAttributes.AddReplace("brightwall_bezel_screen_height", 0)
  
    if (config.brightSignAttributes.isBrightWall) then
  
      if IsBoolean(globalAA.registrySettings.sync_master) and GetValidBool(globalAA.registrySection.Read("sync_master"), false) then
        config.brightSignAttributes.AddReplace("isMaster", true)
      endif
  
      if len(globalAA.registrySection.Read("brightwall_row_index")) > 0 then
        config.brightSignAttributes.AddReplace("rowIndex", int(val(globalAA.registrySection.Read("brightwall_row_index"))))
      endif
  
      if len(globalAA.registrySection.Read("brightwall_column_index")) > 0 then
        config.brightSignAttributes.AddReplace("columnIndex", int(val(globalAA.registrySection.Read("brightwall_column_index"))))
      endif
  
      if len(globalAA.registrySection.Read("brightwall_bezel_left")) > 0 then
        config.brightSignAttributes.AddReplace("brightwall_bezel_left", val(globalAA.registrySection.Read("brightwall_bezel_left")))
      endif
  
      if len(globalAA.registrySection.Read("brightwall_bezel_right")) > 0 then
        config.brightSignAttributes.AddReplace("brightwall_bezel_right", val(globalAA.registrySection.Read("brightwall_bezel_right")))
      endif
  
      if len(globalAA.registrySection.Read("brightwall_bezel_top")) > 0 then
        config.brightSignAttributes.AddReplace("brightwall_bezel_top", val(globalAA.registrySection.Read("brightwall_bezel_top")))
      endif
  
      if len(globalAA.registrySection.Read("brightwall_bezel_bottom")) > 0 then
        config.brightSignAttributes.AddReplace("brightwall_bezel_bottom", val(globalAA.registrySection.Read("brightwall_bezel_bottom")))
      endif
  
      if len(globalAA.registrySection.Read("brightwall_bezel_screen_width")) > 0 then
        config.brightSignAttributes.AddReplace("brightwall_bezel_screen_width", val(globalAA.registrySection.Read("brightwall_bezel_screen_width")))
      endif
  
      if len(globalAA.registrySection.Read("brightwall_bezel_screen_height")) > 0 then
        config.brightSignAttributes.AddReplace("brightwall_bezel_screen_height", val(globalAA.registrySection.Read("brightwall_bezel_screen_height")))
      endif
  
    endif
  
  
    config.AddReplace("brightWallAttributes", {})
  
    if (config.brightSignAttributes.isBrightWall) then
  
      if len(globalAA.registrySection.Read("brightwall_num_rows")) > 0 then
        config.brightWallAttributes.AddReplace("numRows", int(val(globalAA.registrySection.Read("brightwall_num_rows"))))
      else
        config.brightWallAttributes.AddReplace("numRows", -1)
      endif
  
      if len(globalAA.registrySection.Read("brightwall_num_columns")) > 0 then
        config.brightWallAttributes.AddReplace("numColumns", int(val(globalAA.registrySection.Read("brightwall_num_columns"))))
      else
        config.brightWallAttributes.AddReplace("numColumns", -1)
      endif
  
      if IsBoolean(globalAA.registrySettings.brightwall_setup_screen_enabled) and GetValidBool(globalAA.registrySection.Read("brightwall_setup_screen_enabled"), false) then
        config.brightWallAttributes.AddReplace("brightwall_setup_screen_enabled", true)
      else
        config.brightWallAttributes.AddReplace("brightwall_setup_screen_enabled", false)
      endif
  
      if len(globalAA.registrySection.Read("brightwall_device_setup_active_screen")) > 0 then
        config.brightWallAttributes.AddReplace("brightwall_device_setup_active_screen", globalAA.registrySection.Read("brightwall_device_setup_active_screen"))
      else
        config.brightWallAttributes.AddReplace("brightwall_device_setup_active_screen", -1)
      endif
  
    endif
  
    ' send activePresentationName to brightWallDeviceSetup
    if type(bsp.brightWallDeviceSetupWidget) = "roHtmlWidget" then
      activePresentationNameMessage = { command: "setActivePresentationName", data: bsp.activePresentation$ }
      bsp.brightWallDeviceSetupWidget.PostJSMessage(activePresentationNameMessage)
    endif
  
    return config
  
  end Function
  
  
  Sub GetBrightWallConfiguration(userData as object, e as object)
  
    mVar = userData.mVar
  
    config = GetConfig(mVar)
  
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(config))
    e.SendResponse(200)
  
    if type(mVar.brightWallDeviceSetupWidget) = "roHtmlWidget" then
      setIsBrightWallConfiguratorHostMessage = { command: "setIsBrightWallConfiguratorHost", data: false }
      mVar.brightWallDeviceSetupWidget.PostJSMessage(setIsBrightWallConfiguratorHostMessage)
    endif
  
  end sub
  
  
  ' invoked by BrightWallConfigurator periodically (every 10 seconds or so)
  Sub GetBrightWallDeviceList(userData as object, e as object)
  
    mVar = userData.mVar
  
    ' broadcast UDP message to local devices - local BrightWall devices will respond with their configuration
    payload = {}
    payload.type = "getIsBrightWallDevice"
  
    ' tell BrightWall devices what screen state should be displayed.
    if IsBoolean(GetGlobalAA().registrySettings.brightwall_setup_screen_enabled) and GetGlobalAA().registrySettings.brightwall_setup_screen_enabled then
      payload.brightWallHostConfigScreen = "brightWallDeviceSetup"
    else
      payload.brightWallHostConfigScreen = "presentationPlayback"
    endif
  
    payload$ = FormatJson(payload)
    mVar.bwUdpSender.Send(payload$)
  
    m.isBrightWallConfigHost = true
  
    brightSignDevicesInWall = []
    for each serialNumber in mVar.brightSignDevicesInWall
      if mVar.brightSignDevicesInWall.DoesExist(serialNumber) then
        brightSignDeviceInWallConfig = mVar.brightSignDevicesInWall.Lookup(serialNumber)
        brightSignDevicesInWall.push(brightSignDeviceInWallConfig)
      endif
    next
  
    brightSignDevicesInWallList = {}
    brightSignDevicesInWallList.AddReplace("brightSignDevicesInWallList", brightSignDevicesInWall)
  
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(brightSignDevicesInWallList))
    e.SendResponse(200)
  
    if type(mVar.brightWallDeviceSetupWidget) = "roHtmlWidget" then
  
      setIsBrightWallConfiguratorHostMessage = { command: "setIsBrightWallConfiguratorHost", data: true }
      mVar.brightWallDeviceSetupWidget.PostJSMessage(setIsBrightWallConfiguratorHostMessage)
  
      setBrightWallDevicesInWallMessage = { command: "setBrightSignDevicesInWall", data: FormatJson(brightSignDevicesInWallList) }
      mVar.brightWallDeviceSetupWidget.PostJSMessage(setBrightWallDevicesInWallMessage)
  
    endif
  
  end sub
  
  
  Sub GetIsBrightWallHandler(userData as object, e as object)
  
    mVar = userData.mVar
  
    respBody = {}
    respBody.isBrightWall = true
  
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(respBody))
    e.SendResponse(200)
  
  end sub
  
  
  Function GetHostIPAddress() as string
    
    nc = CreateObject("roNetworkConfiguration", 0)
    currentConfig = nc.GetCurrentConfig()
    ipAddress = currentConfig.ip4_address
    return ipAddress
  
  end Function
  
  
  Sub SetBrightWallPosition(userData as object, e as object)
  
    mVar = userData.mVar
  
    mVar.diagnostics.PrintTimestamp()
  
    targetIpAddress = e.GetRequestParam("ipAddress")  ' target ip address
    rowIndex = e.GetRequestParam("rowIndex")
    columnIndex = e.GetRequestParam("columnIndex")
  
    setBrightWallPositionPayload = {}
    setBrightWallPositionPayload.type = "SetBrightWallPosition"
    setBrightWallPositionPayload.targetIpAddress = targetIpAddress
    setBrightWallPositionPayload.rowIndex = rowIndex
    setBrightWallPositionPayload.columnIndex = columnIndex
    setBrightWallPositionPayload$ = FormatJson(setBrightWallPositionPayload)
    mVar.bwUdpSender.Send(setBrightWallPositionPayload$)
  
    resp = {}
    resp.AddReplace("success", true)
  
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(resp))
    e.SendResponse(200)
  
  end sub
  
  
  Sub SetDeviceBrightWallPosition(mVar as object, rowIndex as string, columnIndex as string)
  
    rowIndex% = int(val(rowIndex))
    columnIndex% = int(val(columnIndex))
  
    globalAA = GetGlobalAA()
    globalAA.registrySettings.brightwall_row_index% = rowIndex%
    globalAA.registrySettings.brightwall_column_index% = columnIndex%
  
    globalAA.registrySection.Write("brightwall_row_index", rowIndex)
    globalAA.registrySection.Write("brightwall_column_index", columnIndex)
    globalAA.registrySection.Flush()
  
    if type(mVar.brightWallDeviceSetupWidget) = "roHtmlWidget" then
      brightWallDeviceSetupMessage = { command: "setBrightWallPosition", rowIndex: rowIndex%, columnIndex: columnIndex% }
      mVar.brightWallDeviceSetupWidget.PostJSMessage(brightWallDeviceSetupMessage)
    endif
  
  end sub
  
  
  Sub SetBrightWallIsMaster(userData as object, e as object)
  
    mVar = userData.mVar
  
    mVar.diagnostics.PrintTimestamp()
  
    targetIpAddress = e.GetRequestParam("ipAddress")
    isMaster = e.GetRequestParam("isMaster")
  
    setIsMasterPayload = {}
    setIsMasterPayload.type = "SetIsMaster"
    setIsMasterPayload.targetIpAddress = targetIpAddress
    setIsMasterPayload.isMaster = isMaster
    setIsMasterPayload$ = FormatJson(setIsMasterPayload)
    mVar.bwUdpSender.Send(setIsMasterPayload$)
  
    resp = {}
    resp.AddReplace("success", true)
  
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(resp))
  
    mVar.diagnostics.PrintTimestamp()
  
    e.SendResponse(200)
  
  end sub
  
  
  Sub SetDeviceBrightWallIsMaster(mVar as object, isMaster as string)
  
    mVar.diagnostics.PrintTimestamp()
  
    isMasterBool = GetValidBool(isMaster, false)
  
    globalAA = GetGlobalAA()
    globalAA.registrySettings.sync_master = isMasterBool
    globalAA.registrySection.Write("sync_master", isMaster)
    globalAA.registrySection.Flush()
  
    if type(mVar.brightWallDeviceSetupWidget) = "roHtmlWidget" then
      brightWallDeviceSetupMessage = { command: "setBrightWallIsMaster", isMaster: isMasterBool }
      mVar.diagnostics.PrintTimestamp()
      mVar.brightWallDeviceSetupWidget.PostJSMessage(brightWallDeviceSetupMessage)
      mVar.diagnostics.PrintTimestamp()
    endif
  
  end sub
  
  
  Sub ExitConfigurator(userData as object, e as object)
  
    mVar = userData.mVar
  
    payload = {}
    payload.type = "exitConfigurator"
    payload$ = FormatJson(payload)
    mVar.bwUdpSender.Send(payload$)
  
    resp = {}
    resp.AddReplace("success", true)
  
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(resp))
    e.SendResponse(200)
  
  end sub
  
  
  Sub ExitDeviceConfigurator()
  
    globalAA = GetGlobalAA()
  
    globalAA.bsp.diagnostics.PrintTimestamp()
  
    globalAA.registrySettings.brightwall_setup_screen_enabled = false
    globalAA.registrySection.Write("brightwall_setup_screen_enabled", "0")
    globalAA.registrySection.Flush()
  
  end sub
  
  
  Sub ReenterConfigurator(userData as object, e as object)
  
    mVar = userData.mVar
    
    payload = {}
    payload.type = "reenterConfigurator"
    payload$ = FormatJson(payload)
    mVar.bwUdpSender.Send(payload$)
  
    resp = {}
    resp.AddReplace("success", true)
  
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(resp))
    e.SendResponse(200)
  
  end sub
  
  
  Sub ReenterDeviceConfigurator()
  
    globalAA = GetGlobalAA()
  
    globalAA.bsp.diagnostics.PrintTimestamp()
  
    globalAA.registrySettings.brightwall_setup_screen_enabled = true
    globalAA.registrySection.Write("brightwall_setup_screen_enabled", "1")
  
    globalAA.registrySettings.brightwall_device_setup_active_screen = "ConfigureScreen"
    globalAA.registrySection.Write("brightwall_device_setup_active_screen", "ConfigureScreen")
  
    globalAA.registrySection.Flush()
  
  end sub
  
  
  Sub ReenterDeviceSetup(userData as object, e as object)
  
    mVar = userData.mVar
    
    payload = {}
    payload.type = "reenterDeviceSetup"
    payload$ = FormatJson(payload)
    mVar.bwUdpSender.Send(payload$)
  
    resp = {}
    resp.AddReplace("success", true)
  
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(resp))
    e.SendResponse(200)
  
  end sub
  
  
  Sub ReenterDeviceDeviceSetup()
  
    globalAA = GetGlobalAA()
  
    globalAA.bsp.diagnostics.PrintTimestamp()
  
    if not globalAA.registrySettings.brightwall_setup_screen_enabled or globalAA.registrySection.Read("brightwall_setup_screen_enabled") <> "1" then
    
      globalAA.registrySettings.brightwall_setup_screen_enabled = true
      globalAA.registrySection.Write("brightwall_setup_screen_enabled", "1")
  
      globalAA.registrySettings.brightwall_device_setup_active_screen = "ConfigureScreen"
      globalAA.registrySection.Write("brightwall_device_setup_active_screen", "ConfigureScreen")
  
      globalAA.registrySection.Flush()
  
      RebootDeviceBrightWall(globalAA.bsp)
    
    endif
  
  end sub
  
  
  Sub LaunchAlignmentTool(userData as object, e as object)
  
    mVar = userData.mVar
    
    payload = {}
    payload.type = "launchAlignmentTool"
    payload$ = FormatJson(payload)
    mVar.bwUdpSender.Send(payload$)
  
    resp = {}
    resp.AddReplace("success", true)
  
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(resp))
    e.SendResponse(200)
  
  end sub
  
  
  Sub LaunchDeviceAlignmentTool()
  
    globalAA = GetGlobalAA()
  
    globalAA.bsp.diagnostics.PrintTimestamp()
  
    globalAA.registrySettings.brightwall_setup_screen_enabled = true
    globalAA.registrySection.write("brightwall_setup_screen_enabled", "true")
    globalAA.registrySettings.brightwall_device_setup_active_screen = "AlignScreen"
    globalAA.registrySection.Write("brightwall_device_setup_active_screen", "AlignScreen")
    globalAA.registrySection.Flush()
    val$ = globalAA.registrySection.Read("brightwall_device_setup_active_screen")
  
  end sub
  
  
  Sub ExitAlignmentTool(userData as object, e as object)
  
    mVar = userData.mVar
    
    payload = {}
    payload.type = "exitAlignmentTool"
    payload$ = FormatJson(payload)
    mVar.bwUdpSender.Send(payload$)
    
    resp = {}
    resp.AddReplace("success", true)
  
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(resp))
    e.SendResponse(200)
  
  end sub
  
  
  Sub ExitDeviceAlignmentTool()
  
    globalAA = GetGlobalAA()
    globalAA.bsp.diagnostics.PrintTimestamp()
  
    globalAA.registrySettings.brightwall_device_setup_active_screen = "ConfigureScreen"
    globalAA.registrySection.Write("brightwall_device_setup_active_screen", "ConfigureScreen")
    globalAA.registrySection.Flush()
  
  end sub
  
  
  Sub RebootBrightWall(userData as object, e as object)
  
    mVar = userData.mVar
    
    payload = {}
    payload.type = "rebootBrightWall"
    payload$ = FormatJson(payload)
    mVar.bwUdpSender.Send(payload$)
  
    resp = {}
    resp.AddReplace("success", true)
  
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(resp))
    e.SendResponse(200)
  
  end sub
  
  
  Sub RebootDeviceBrightWall(mVar as object)
  
    if type(mVar.view) = "roAssetCollectionView" then
  
      savedAssetCollectionView = mVar.view
  
      if mVar.brightWallDeviceSetupWidget <> invalid
        mVar.brightWallDeviceSetupWidget.Hide()
        mVar.brightWallDeviceSetupWidget = invalid
      endif
    
      if type(mVar.sign) = "roAssociativeArray" and type(mVar.sign.zonesHSM) = "roArray" then
        for each zoneHSM in mVar.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
  
      if type(mVar.brightWallConfiguratorServer) = "roHttpServer" then
        mVar.brightWallConfiguratorServer = invalid
      endif
  
      mVar.sign = invalid
      RunGarbageCollector()
  
      mVar.view = invalid    
    
    endif
  
    RestartScript()
    
  end sub
  
  
  Sub SetBezelDimensions(userData as object, e as object)
  
    mVar = userData.mVar
  
    mVar.diagnostics.PrintTimestamp()
  
    targetIpAddress = e.GetRequestParam("ipAddress")  ' target ip address
    brightwall_bezel_left = e.GetRequestParam("brightwall_bezel_left")
    brightwall_bezel_right = e.GetRequestParam("brightwall_bezel_right")
    brightwall_bezel_top = e.GetRequestParam("brightwall_bezel_top")
    brightwall_bezel_bottom = e.GetRequestParam("brightwall_bezel_bottom")
    brightwall_bezel_screen_width = e.GetRequestParam("brightwall_bezel_screen_width")
    brightwall_bezel_screen_height = e.GetRequestParam("brightwall_bezel_screen_height")
  
    setBezelDimensionsPayload = {}
    setBezelDimensionsPayload.type = "SetBezelDimensions"
    setBezelDimensionsPayload.targetIpAddress = targetIpAddress
    setBezelDimensionsPayload.brightwall_bezel_left = brightwall_bezel_left
    setBezelDimensionsPayload.brightwall_bezel_right = brightwall_bezel_right
    setBezelDimensionsPayload.brightwall_bezel_top = brightwall_bezel_top
    setBezelDimensionsPayload.brightwall_bezel_bottom = brightwall_bezel_bottom
    setBezelDimensionsPayload.brightwall_bezel_screen_width = brightwall_bezel_screen_width
    setBezelDimensionsPayload.brightwall_bezel_screen_height = brightwall_bezel_screen_height
    setBezelDimensionsPayload$ = FormatJson(setBezelDimensionsPayload)
    mVar.bwUdpSender.Send(setBezelDimensionsPayload$)
  
    resp = {}
    resp.AddReplace("success", true)
  
    e.AddResponseHeader("Content-type", "application/json")
    e.SetResponseBodyString(FormatJson(resp))
    e.SendResponse(200)
  
  end sub
  
  
  Sub SetDeviceBezelDimensions(mVar as object, brightwall_bezel_left as string, brightwall_bezel_right as string, brightwall_bezel_top as string, brightwall_bezel_bottom as string, brightwall_bezel_screen_width as string, brightwall_bezel_screen_height as string)
  
    globalAA = GetGlobalAA()
    globalAA.registrySettings.brightwall_bezel_left = val(brightwall_bezel_left)
    globalAA.registrySettings.brightwall_bezel_right = val(brightwall_bezel_right)
    globalAA.registrySettings.brightwall_bezel_top = val(brightwall_bezel_top)
    globalAA.registrySettings.brightwall_bezel_bottom = val(brightwall_bezel_bottom)
    globalAA.registrySettings.brightwall_bezel_screen_width = val(brightwall_bezel_screen_width)
    globalAA.registrySettings.brightwall_bezel_screen_height = val(brightwall_bezel_screen_height)
    globalAA.registrySection.Write("brightwall_bezel_left", brightwall_bezel_left)
    globalAA.registrySection.Write("brightwall_bezel_right", brightwall_bezel_right)
    globalAA.registrySection.Write("brightwall_bezel_top", brightwall_bezel_top)
    globalAA.registrySection.Write("brightwall_bezel_bottom", brightwall_bezel_bottom)
    globalAA.registrySection.Write("brightwall_bezel_screen_width", brightwall_bezel_screen_width)
    globalAA.registrySection.Write("brightwall_bezel_screen_height", brightwall_bezel_screen_height)
    globalAA.registrySection.Flush()
  
    if type(mVar.brightWallDeviceSetupWidget) = "roHtmlWidget" then
      bezelDimensions = {}
      bezelDimensions.brightwall_bezel_left = brightwall_bezel_left
      bezelDimensions.brightwall_bezel_right = brightwall_bezel_right
      bezelDimensions.brightwall_bezel_top = brightwall_bezel_top
      bezelDimensions.brightwall_bezel_bottom = brightwall_bezel_bottom
      bezelDimensions.brightwall_bezel_screen_width = brightwall_bezel_screen_width
      bezelDimensions.brightwall_bezel_screen_height = brightwall_bezel_screen_height
      setBezelDimensionsMessage = { command: "setBezelDimensions", data: FormatJson(bezelDimensions) }
      mVar.brightWallDeviceSetupWidget.PostJSMessage(setBezelDimensionsMessage)
    endif
  
  end sub
  
  
  Function IsBrightWallMaster() as boolean
  
    if m.bsp.sign.isBrightWall and m.isSyncMaster then
      return true
    else
      return false
    endif
  
  end function
  
  
  Function IsBrightWallSlave() as boolean
  
    if m.bsp.sign.isBrightWall and not m.isSyncMaster then
      return true
    else
      return false
    endif
  
  end function
  
  
  ' m is bsp
  Sub ProcessBrightWallUdpMessage(udpMessagePayload$ as string)
  
    udpMessagePayload = ParseJson(udpMessagePayload$)
  
    if udpMessagePayload.type = "getIsBrightWallDevice" then
  
      udpCommandPayload = {}
      udpCommandPayload.type = "BrightWallDeviceCheckin"
      udpCommandPayload.serialNumber = m.sysInfo.deviceUniqueid$
      udpCommandPayload.config = GetConfig(m)
      udpCommandPayload$ = FormatJson(udpCommandPayload)
      m.bwUdpSender.Send(udpCommandPayload$)
  
      ' if this is a Brightwall device and is a non config host - go into same screen as config host
      globalAA = GetGlobalAA()
      if globalAA.registrySettings.isBrightWall then
        if not IsBoolean(m.isBrightWallConfigHost) or not m.isBrightWallConfigHost then
  
          reenterBrightWallConfigurator = false
          if globalAA.registrySettings.brightwall_row_index% < 0 or globalAA.registrySettings.brightwall_column_index% < 0 then
            reenterBrightWallConfigurator = true
          else if udpMessagePayload.brightWallHostConfigScreen = "brightWallDeviceSetup" then
            reenterBrightWallConfigurator = true
          endif
  
          if reenterBrightWallConfigurator then
            if type(m.brightWallDeviceSetupWidget) <> "roHtmlWidget" then
              ReenterDeviceConfigurator()
              RebootDeviceBrightWall(m)
            endif
          else
            if type(m.brightWallDeviceSetupWidget) = "roHtmlWidget" then
              globalAA.registrySettings.brightwall_setup_screen_enabled = false
              globalAA.registrySection.Write("brightwall_setup_screen_enabled", "0")
              globalAA.registrySection.Flush()
              RebootDeviceBrightWall(m)
            endif
          endif
  
        endif
      endif
  
    else if udpMessagePayload.type = "rebootBrightWall" then
      RebootDeviceBrightWall(m)
    else if udpMessagePayload.type = "exitConfigurator" then
      ExitDeviceConfigurator()
    else if udpMessagePayload.type = "reenterConfigurator" then
      ReenterDeviceConfigurator()
    else if udpMessagePayload.type = "reenterDeviceSetup" then
      ReenterDeviceDeviceSetup()
    else if udpMessagePayload.type = "launchAlignmentTool" then
      LaunchDeviceAlignmentTool()
    else if udpMessagePayload.type = "exitAlignmentTool" then
      ExitDeviceAlignmentTool()
    else if udpMessagePayload.type = "setIsMasterAck" then
      m.diagnostics.PrintTimestamp()
    else if udpMessagePayload.type = "setBrightWallPositionAck" then
      m.diagnostics.PrintTimestamp()
    else if udpMessagePayload.type = "setBezelDimensionsAck" then
      m.diagnostics.PrintTimestamp()
    else if udpMessagePayload.type = "BrightWallDeviceCheckin" then
      serialNumber = udpMessagePayload.serialNumber
      config = udpMessagePayload.config
      m.brightSignDevicesInWall.AddReplace(serialNumber, config)  
    else if udpMessagePayload.type = "SetIsMaster" then
      targetIpAddress = udpMessagePayload.targetIpAddress
      hostIpAddress = GetHostIPAddress()
      if targetIpAddress = hostIpAddress then
        isMaster = udpMessagePayload.isMaster
        m.diagnostics.PrintTimestamp()
        SetDeviceBrightWallIsMaster(m, isMaster)
      endif
  
      payload = {}
      payload.type = "setIsMasterAck"
      payload$ = FormatJson(payload)
      m.bwUdpSender.Send(payload$)
  
    else if udpMessagePayload.type = "SetBrightWallPosition" then
      targetIpAddress = udpMessagePayload.targetIpAddress
      hostIpAddress = GetHostIPAddress()
      if targetIpAddress = hostIpAddress then
        rowIndex = udpMessagePayload.rowIndex
        columnIndex = udpMessagePayload.columnIndex
        m.diagnostics.PrintTimestamp()
        SetDeviceBrightWallPosition(m, rowIndex, columnIndex)
      endif
  
      payload = {}
      payload.type = "setBrightWallPositionAck"
      payload$ = FormatJson(payload)
      m.bwUdpSender.Send(payload$)
  
    else if udpMessagePayload.type = "SetBezelDimensions" then
      targetIpAddress = udpMessagePayload.targetIpAddress
      hostIpAddress = GetHostIPAddress()
      if targetIpAddress = hostIpAddress then
        brightwall_bezel_left = udpMessagePayload.brightwall_bezel_left
        brightwall_bezel_right = udpMessagePayload.brightwall_bezel_right
        brightwall_bezel_top = udpMessagePayload.brightwall_bezel_top
        brightwall_bezel_bottom = udpMessagePayload.brightwall_bezel_bottom
        brightwall_bezel_screen_width = udpMessagePayload.brightwall_bezel_screen_width
        brightwall_bezel_screen_height = udpMessagePayload.brightwall_bezel_screen_height
        m.diagnostics.PrintTimestamp()
        SetDeviceBezelDimensions(m, brightwall_bezel_left, brightwall_bezel_right, brightwall_bezel_top, brightwall_bezel_bottom, brightwall_bezel_screen_width, brightwall_bezel_screen_height)
      endif
  
      payload = {}
      payload.type = "setBezelDimensionsAck"
      payload$ = FormatJson(payload)
      m.bwUdpSender.Send(payload$)
  
    else if udpMessagePayload.type = "userVariableUpdates" then
      m.UpdateUserVariablesFromBrightWallPayload(udpMessagePayload.timestamp, udpMessagePayload.userVariables)
    
    endif
  
  end sub
  
  
  'region Record / Playback
  
  ' TEDTODOREPLAY - refactor
  Sub InitializeRecordPlayback()
  
    globalAA = getGlobalAA()
  
    globalAA.recordEventsMode = GetBoolFromString(globalAA.registrySection.Read("recordEvents"), false)
    globalAA.playbackEventsMode = GetBoolFromString(globalAA.registrySection.Read("playbackEvents"), false)
  
    if globalAA.recordEventsMode then
  
      ' file where event files are recorded
      globalAA.recordedEventsFile = CreateObject("roCreateFile", "recordedEvents.txt")
      
      st = CreateObject("roSystemTime")
      utcDateTime = st.GetUtcDateTime()
      autorunStartTime = utcDateTime.ToIsoString()
  
      recordedEventsFile = globalAA.recordedEventsFile
      recordedEventsFile.SendLine("AutorunStartTime=" + autorunStartTime)
  
    endif
  
    if globalAA.playbackEventsMode then
      
      recordedEventsFile = CreateObject("roReadFile", "recordedEvents.txt")
      if type(recordedEventsFile) <> "roReadFile" then
        stop
      endif
  
      autorunStartTimeLine = recordedEventsFile.ReadLine()
      autorunStartTimeRegex = CreateObject("roRegEx", "=", "i")
      autorunStartTimeTokens = autorunStartTimeRegex.split(autorunStartTimeLine)
      if autorunStartTimeTokens.Count() = 2 then
        globalAA.autorunStartTimestamp = autorunStartTimeTokens[1]       
      else
        stop
      endif
  
      playbackEvents = []
      globalAA.playbackEvents = playbackEvents
  
      regex = CreateObject("roRegEx", "!!!!", "i")
  
      atEof = false    
      while not atEof
  
        recordedEvent = recordedEventsFile.ReadLine()
        atEof = recordedEventsFile.AtEof()
        if not atEof then
          
          tokens = regex.split(recordedEvent)
  
          numTokens = tokens.count()
  
          if numTokens > 1 then
  
            if tokens[0].mid(0, 2) <> "T=" or tokens[1].mid(0, 2) <> "E=" then
              stop
            endif
  
            playbackEvent = {}
  
            eventTimestamp = tokens[0].mid(2)         
        
            playbackEvent.eventTimestamp = eventTimestamp
  
            if playbackEvents.Count() = 0 then
              lastEventTimestamp = globalAA.autorunStartTimestamp          
            else
              lastEventTimestamp = playbackEvents[playbackEvents.Count() - 1].eventTimestamp
            endif
  
            lastEventDateTime = CreateObject("roDateTime")
            lastEventDateTime.FromIsoString(lastEventTimestamp)
            lastEventSecondsSinceEpoch = lastEventDateTime.ToSecondsSinceEpoch()
            lastEventMsec = lastEventDateTime.GetMillisecond()
  
            currentEventTimestamp = eventTimestamp
            currentEventDateTime = CreateObject("roDateTime")
            currentEventDateTime.FromIsoString(currentEventTimestamp)
            currentEventSecondsSinceEpoch = currentEventDateTime.ToSecondsSinceEpoch()
            currentEventMsec = currentEventDateTime.GetMillisecond()
  
            secondsSinceLastEvent = currentEventSecondsSinceEpoch - lastEventSecondsSinceEpoch
            playbackEvent.timeSinceLastEvent = (secondsSinceLastEvent * 1000) + currentEventMsec - lastEventMsec
  
            playbackEvent.eventType = tokens[1].mid(2)
  
            if numTokens > 2 then
              if tokens[2].mid(0, 2) = "D=" then
                playbackEvent.eventData = tokens[2].mid(2)
              endif
            endif
  
            if numTokens > 3 then
              if tokens[3].mid(0, 2) = "U=" then
                playbackEvent.userData = ParseJson(tokens[3].mid(2))
              endif
            endif
  
            playbackEvents.push(playbackEvent)
  
          endif
  
        endif
  
      end while
  
      globalAA.recordedEventsFile = recordedEventsFile
  
    endif
  
  
  end sub
  
  
  Sub InitializePlaybackEventsTimer()
  
    globalAA = getGlobalAA()
  
    if globalAA.playbackEventsMode then
      globalAA.playbackEventIndex = 0
      globalAA.playbackEventsTimer = CreateObject("roTimer")
      globalAA.playbackEventsTimer.SetUserData(BuildUserData("playbackEventsTimer", ""))
      globalAA.playbackEventsTimer.SetPort(m.msgPort)
      globalAA.playbackEventsTimer.SetElapsed(0, globalAA.playbackEvents[0].timeSinceLastEvent)
      globalAA.playbackEventsTimer.Start()
    endif
  
  end sub
  
  
  Function GetControlEvent(eventType as string, userData as object, eventData as integer) as object
  
    ev = CreateObject(eventType)
    ev.SetUserData(userData)
    ev.SetInt(eventData)
    return ev
  
  end function
  
  
  Function IsEventRecordable(eventType as string) as boolean
  
    if eventType = "roControlDown" or eventType = "roControlUp" or eventType = "bp900AUserEvent" then 
      return true
    endif
    
    ' do not record
    '   roStorageAttached - ambiguous
    '   roVideoEvent
    '   roTimerEvent
    '   roHttpEvent - ambiguous?
    '   roControlCloudMessageEvent
  
    return false
  
  end function
  
  
  Sub RecordEvent(event as object)
  
    globalAA = getGlobalAA()
  
    utcDateTime = m.systemTime.GetUtcDateTime()
    eventDateTime = utcDateTime.ToIsoString()
  
    recordedEventsFile = globalAA.recordedEventsFile
  
    if type(event) = "roControlDown" then
      gpioNum = event.GetInt()
      ' TEDTODOREPLAY
      userData$ = FormatJson(event.getUserData())
      recordedEventsFile.SendLine("T=" + eventDateTime + "!!!!" + "E=roControlDown" + "!!!!" + "D=" + stri(gpioNum) + "!!!!" + "U=" + userData$)
    else if type(event) = "roControlUp" then
      gpioNum = event.GetInt()
      ' TEDTODOREPLAY
      userData$ = FormatJson(event.getUserData())
      recordedEventsFile.SendLine("T=" + eventDateTime + "!!!!" + "E=roControlUp" + "!!!!" + "D=" + stri(gpioNum) + "!!!!" + "U=" + userData$)
    else if type(event) = "bp900AUserEvent" then
      stop
    else if type(event) = "roStorageAttached" then
      storagePath$ = event.GetString()
      recordedEventsFile.SendLine("T=" + eventDateTime + "!!!!" + "E=roStorageAttached" + "!!!!" + "D=" + storagePath$)
    else if type(event) = "roHttpEvent" then
      recordedEventsFile.SendLine("T=" + eventDateTime + "!!!!" + "E=roHttpEvent")
    else if type(event) = "roControlCloudMessageEvent" then
      recordedEventsFile.SendLine("T=" + eventDateTime + "!!!!" + "E=roControlCloudMessageEvent")
    else
      stop
    endif
  
    recordedEventsFile.AsyncFlush()
  
  end sub
  
  
  Function ExecutePlaybackEvent(event) as boolean
  
    globalAA = getGlobalAA()
  
    eventHandled = false
  
    if type(event) = "roTimerEvent" and UserDataMatches(event.GetUserData(), "playbackEventsTimer", "") then
  
      playbackEvent = globalAA.playbackEvents[globalAA.playbackEventIndex]
      
      eventType = playbackEvent.eventType
      if playbackEvent.eventData <> invalid then
        eventData = playbackEvent.eventData
      endif
  
      if eventType = "roControlDown" or eventType = "roControlUp" then        
        ev = m.GetControlEvent(eventType, playbackEvent.userData, int(val(playbackEvent.eventData)))
        m.msgPort.PostMessage(ev)
        eventHandled = true
      endif
  
      globalAA.playbackEventIndex = globalAA.playbackEventIndex + 1
  
      if globalAA.playbackEventIndex < globalAA.playbackEvents.Count() then
        msecUntilNextEvent = globalAA.playbackEvents[globalAA.playbackEventIndex].timeSinceLastEvent
        globalAA.playbackEventsTimer.SetElapsed(0, msecUntilNextEvent)
        globalAA.playbackEventsTimer.Start()
      endif
      
    endif
  
    return eventHandled
  
  end function
  
  
  'endregion
  
  
  