Library "autoplugins.brs"

'region Main
Sub Main()
  
  autorunVersion$ = "10.0.111" 'Bacon in development
  customAutorunVersion$ = "10.0.0"
  
  ' create global registry section to be used throughout script
  globalAA = GetGlobalAA()
  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()
  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

  sysInfo["sessionGuid$"] = "" 

  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.brightWallName$ = registrySection.Read("brightWallName")
  registrySettings.brightWallScreenNumber$ = registrySection.Read("brightWallScreenNumber")
  
  registrySettings.lastBSNConnectionTime = registrySection.Read("lastBSNConnectionTime")
  
  'Read persistent beacon data
  registrySettings.beacon1 = registrySection.Read("beacon1")
  registrySettings.beacon2 = registrySection.Read("beacon2")

  'Open datagramSocket for autorun and bootstrap to communicate
  registrySettings.daUdpSocketPort$ = registrySection.Read("daUdpSocketPort")
  registrySettings.wsUdpSocketPort$ = registrySection.Read("wsUdpSocketPort")
  
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("TouchBoard-0-GPIO")
    BSP.bpInputPortIdentities[0] = stri(BSP.bpInputPorts[0].GetIdentity())
    BSP.bpInputPorts[0].SetPort(msgPort)
    properties = BSP.bpInputPorts[0].GetProperties()
    BSP.bpInputPortHardware[0] = properties.hardware
    BSP.bpInputPortConfigurations[0] = 0  
  end if
  
  BSP.bpInputPorts[1] = CreateObject("roControlPort", "TouchBoard-1-GPIO")
  if type(BSP.bpInputPorts[1]) = "roControlPort" then
    BSP.bpInputPorts[1].SetUserData("TouchBoard-1-GPIO")
    BSP.bpInputPortIdentities[1] = stri(BSP.bpInputPorts[1].GetIdentity())
    BSP.bpInputPorts[1].SetPort(msgPort)
    properties = BSP.bpInputPorts[1].GetProperties()
    BSP.bpInputPortHardware[1] = properties.hardware
    BSP.bpInputPortConfigurations[1] = 0
  end if
  
  BSP.bpInputPorts[2] = CreateObject("roControlPort", "TouchBoard-2-GPIO")
  if type(BSP.bpInputPorts[2]) = "roControlPort" then
    BSP.bpInputPorts[2].SetUserData("TouchBoard-2-GPIO")
    BSP.bpInputPortIdentities[2] = stri(BSP.bpInputPorts[2].GetIdentity())
    BSP.bpInputPorts[2].SetPort(msgPort)
    properties = BSP.bpInputPorts[2].GetProperties()
    BSP.bpInputPortHardware[2] = properties.hardware
    BSP.bpInputPortConfigurations[2] = 0
  end if
  
  BSP.bpInputPorts[3] = CreateObject("roControlPort", "TouchBoard-3-GPIO")
  if type(BSP.bpInputPorts[3]) = "roControlPort" then
    BSP.bpInputPorts[3].SetUserData("TouchBoard-3-GPIO")
    BSP.bpInputPortIdentities[3] = stri(BSP.bpInputPorts[3].GetIdentity())
    BSP.bpInputPorts[3].SetPort(msgPort)
    properties = BSP.bpInputPorts[3].GetProperties()
    BSP.bpInputPortHardware[3] = properties.hardware
    BSP.bpInputPortConfigurations[3] = 0
  end if
  
  BSP.sysInfo = sysInfo
  BSP.diagnosticCodes = diagnosticCodes
  
  BSP.diagnostics.SetSystemInfo(sysInfo, diagnosticCodes)
  BSP.logging.SetSystemInfo(sysInfo, diagnosticCodes)
  
  ' Create device specific User-Agent string
  BSP.userAgent$ = "BrightSign/" + sysInfo.deviceUniqueID$ + "/" + sysInfo.deviceFWVersion$ + " (" + sysInfo.deviceModel$ + ")"
  
  ' if the device is configured for local file networking with content transfers, require that the storage is writable
  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()
  
  ' Set up Bluetooth manager for players that support Bluetooth
  BSP.btManager = BSP.newBtManager()
  
  ' GPIO state machines and associated data structures
  dim gpioStateMachineRequired[8]
  dim gpioSM[8]
  
  BSP.gpioStateMachineRequired = gpioStateMachineRequired
  BSP.gpioSM = gpioSM
  
  ' BP state machines and associated data structures
  dim bpStateMachineRequired[4, 11]
  dim bpInputUsed[4, 11]
  dim bpOutputUsed[4, 11]
  dim bpSM[4, 11]
  
  BSP.bpStateMachineRequired = bpStateMachineRequired
  BSP.bpInputUsed = bpInputUsed
  BSP.bpOutputUsed = bpOutputUsed
  BSP.bpSM = bpSM
  
  ' 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.EventLoop()
  
end sub


Function IsControlPort(controlPort as object) as boolean
  return type(controlPort) = "roControlPort"
end function


Sub GetSupportedFeatures()
  
  ' NOTE - since minimum FW is required for bacon, all features that were supported in BA Classic are supported in this version of FW
  featureMinRevsFilePath$ = GetPoolFilePath(m.assetPoolFiles, "featureMinRevs.json")
  m.featureMinRevs = ParseFeatureMinRevs(featureMinRevsFilePath$)
  
  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.beaconsSupported = true
  m.btleSupported = true
  m.fullResolutionGraphicsSupported = true
  
end sub


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


Function ParseFeatureMinRevs(path$ as string)
  
  featureMinRevs = { }
  
  featureMinRevs$ = ReadAsciiFile(path$)
  
  if len(featureMinRevs$) > 0 then
    
    publishedFeatureMinRevs = ParseJson(featureMinRevs$)
    
    ' verify that this is a valid FeatureMinRevs Json file
    if type(publishedFeatureMinRevs.FeatureMinRevs) <> "roAssociativeArray" then print "Invalid FeatureMinRevs JSON file - name not FeatureMinRevs" : stop
    if not IsString(publishedFeatureMinRevs.FeatureMinRevs.version) then print "Invalid FeatureMinRevs JSON file - version not found" : stop
    
    for each featureAA in publishedFeatureMinRevs.FeatureMinRevs.features
      featureName$ = featureAA.name
      minRev$ = featureAA.minFWRev
      featureMinRevs.AddReplace(featureName$, minRev$)
    next
    
  end if
  
  return featureMinRevs
  
end function


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


  Sub DiagnoseAndRecoverWifiNetwork(event as object)
    ' Check if event is url event
    if type(event) <> "roUrlEvent" then return

    ' Create wifi interface if not exist
    if m.wifiNetworkConfiguration = invalid then
      m.wifiNetworkConfiguration = CreateObject("roNetworkConfiguration", 1)
    end if

    ' Return if failed to create wifi interface
    if type(m.wifiNetworkConfiguration) <> "roNetworkConfiguration" then return

    ' Check if OS has function to safely reassociate wifi
    if not IsBoolean(m.osSupportsReassociateWifi) then
      m.osSupportsReassociateWifi = (findMemberFunction(m.wifiNetworkConfiguration, "ReassociateWiFi") <> invalid)
    end if

    ' Return if OS doesn't support reassociate wifi
    if m.osSupportsReassociateWifi <> true then return

    if not IsBoolean(m.disableWifiAutoRecovery) then
      ' GetGlobalAA().registrySection is the "networking" registry section
      ' NOTE: If this registry key is changed, it will not take effect until autorun is restarted.
      registryKey = GetGlobalAA().registrySection.Read("disable_wifi_auto_recovery")
      m.disableWifiAutoRecovery = GetBoolFromString(registryKey, false)
    end if

    ' Return if disabled by registry key
    if m.disableWifiAutoRecovery = true then return

    currentConfig = m.wifiNetworkConfiguration.GetCurrentConfig()
    ' Exit earlier if wifi not confiugred
    if currentConfig.wifi_bssid = "" then return

    responseCode = event.GetResponseCode()
    if responseCode >= 0 then
      ' Clear failure count and return if there is a successful connection
      m.minusSixFailureCount = 0
      m.minusTwentyEightFailureCount = 0
      m.minusFiftySixFailureCount = 0
      return
    end if
    ' -6 is CURLE_COULDNT_RESOLVE_HOST
    if responseCode = -6 then
      m.minusSixFailureCount = m.minusSixFailureCount + 1
    end if
    ' -28 is CURLE_OPERATION_TIMEDOUT
    if responseCode = -28 then
      m.minusTwentyEightFailureCount = m.minusTwentyEightFailureCount + 1
    end if
    ' -56 is CURLE_RECV_ERROR
    if responseCode = -56 then
      m.minusFiftySixFailureCount = m.minusFiftySixFailureCount + 1
    end if

    ' Make sure at least 5 mins(300 in seconds) has passed since last network reset
    ' to leave some time for the network re-connection
    RECONNECT_SECONDS = 300
    RECONNECT_LIMIT = 5

    kick = false
    if (m.lastWifiReconnectTimestampInSeconds = 0) or (m.systemTime.GetLocalDateTime().ToSecondsSinceEpoch() - m.lastWifiReconnectTimestampInSeconds > RECONNECT_SECONDS) then
      ' Check failure count against kick-wifi threshold
      if (m.minusSixFailureCount > RECONNECT_LIMIT) or (m.minusTwentyEightFailureCount > RECONNECT_LIMIT) or (m.minusFiftySixFailureCount > RECONNECT_LIMIT) then
        kick = true 
      end if
    end if

    ip4_address = currentConfig.ip4_address
    ipInvalid = (currentConfig.link = true) and (ip4_address = Invalid or ip4_address = "" or instr(1, ip4_address, "169.254") = 1)

    ' Check and test for IP address
    if (ipInvalid or kick) then
      bad_bssid = currentConfig.wifi_bssid
      error_rates$ = "-6_count=" + stri(m.minusSixFailureCount) + ", -28_count=" + stri(m.minusTwentyEightFailureCount) + ", -56_count=" + stri(m.minusFiftySixFailureCount)

      m.PrintDebug("Recovering wifi: interface=" + bad_bssid + ", ip4_add=" + ip4_address)
      m.PrintDebug("Error counts: " + error_rates$)

      m.wifiNetworkConfiguration.ReassociateWiFi()
      m.lastWifiReconnectTimestampInSeconds = m.systemTime.GetLocalDateTime().ToSecondsSinceEpoch()
      ' We have reconnected, clear failure counts
      m.minusSixFailureCount = 0
      m.minusTwentyEightFailureCount = 0
      m.minusFiftySixFailureCount = 0
    end if
  end sub
    

  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.DeallocateNodeComponents = DeallocateNodeComponents
    
    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.ExecuteSendBMapCommand = ExecuteSendBMapCommand
    BSP.ExecuteSendBMapHexCommand = ExecuteSendBMapHexCommand
    BSP.ExecuteSwitchPresentationCommand = ExecuteSwitchPresentationCommand
    BSP.MatchWssEvent = MatchWssEvent
    BSP.ExecuteGpioOnCommand = ExecuteGpioOnCommand
    BSP.ExecuteGpioOffCommand = ExecuteGpioOffCommand
    BSP.ExecuteGpioSetStateCommand = ExecuteGpioSetStateCommand
    
    BSP.ExecuteSerialSendStringCommand = ExecuteSerialSendStringCommand
    BSP.ExecuteSendSerialBlockCommand = ExecuteSendSerialBlockCommand
    BSP.ExecuteSendSerialByteCommand = ExecuteSendSerialByteCommand
    BSP.ExecuteSendSerialBytesCommand = ExecuteSendSerialBytesCommand
    BSP.ExecuteSendUDPCommand = ExecuteSendUDPCommand
    BSP.ExecuteSendUDPBytesCommand = ExecuteSendUDPBytesCommand
    BSP.ExecuteSendProntoIRRemote = ExecuteSendProntoIRRemote
    BSP.ExecuteSendIRRemoteCommand = ExecuteSendIRRemoteCommand
    BSP.ExecuteSendBLC400OutputCommand = ExecuteSendBLC400OutputCommand
    BSP.ExecuteSendBPOutputCommand = ExecuteSendBPOutputCommand
    BSP.ExecuteSynchronizeCommand = ExecuteSynchronizeCommand
    BSP.ExecuteSendZoneMessageCommand = ExecuteSendZoneMessageCommand
    BSP.ExecuteSendPluginMessageCommand = ExecuteSendPluginMessageCommand
    BSP.ExecuteResizeZoneCommand = ExecuteResizeZoneCommand
    BSP.ExecuteHideZoneCommand = ExecuteHideZoneCommand
    BSP.ExecuteShowZoneCommand = ExecuteShowZoneCommand
    BSP.ExecutePauseZonePlaybackCommand = ExecutePauseZonePlaybackCommand
    BSP.ExecuteResumeZonePlaybackCommand = ExecuteResumeZonePlaybackCommand
    BSP.ExecuteInternalSynchronizeCommand = ExecuteInternalSynchronizeCommand
    BSP.ExecuteCecSendStringCommand = ExecuteCecSendStringCommand
    BSP.ExecuteCecPhilipsSetVolumeCommand = ExecuteCecPhilipsSetVolumeCommand
    BSP.ExecutePauseCommand = ExecutePauseCommand
    BSP.ExecuteSetVariableCommand = ExecuteSetVariableCommand
    BSP.ExecuteBeaconStartCommand = ExecuteBeaconStartCommand
    BSP.ExecuteBeaconStopCommand = ExecuteBeaconStopCommand
    
    BSP.ExecuteSetAllAudioOutputsCommand = ExecuteSetAllAudioOutputsCommand

    BSP.EventLoop = EventLoop
    
    BSP.SetAudioMode = SetAudioMode
    BSP.UnmuteAllAudio = UnmuteAllAudio
    BSP.UnmuteAudioConnector = UnmuteAudioConnector
    BSP.MuteAudioOutput = MuteAudioOutput
    BSP.MuteAudioOutputs = MuteAudioOutputs
    BSP.SetConnectorVolume = SetConnectorVolume
    BSP.ChangeConnectorVolume = ChangeConnectorVolume
    BSP.SetZoneVolume = SetZoneVolume
    BSP.ChangeZoneVolume = ChangeZoneVolume
    BSP.SetZoneChannelVolume = SetZoneChannelVolume
    BSP.ChangeZoneChannelVolume = ChangeZoneChannelVolume
    BSP.SetUSBAudioOutput = SetUSBAudioOutput
    
    BSP.SetAudioVolumeLimits = SetAudioVolumeLimits
    
    BSP.GetZone = GetZone
    BSP.GetVideoZone = GetVideoZone
    
    BSP.ChangeChannelVolumes = ChangeChannelVolumes
    BSP.SetChannelVolumes = SetChannelVolumes
    
    BSP.PauseVideo = PauseVideo
    BSP.ResumeVideo = ResumeVideo
    BSP.SetPowerSaveMode = SetPowerSaveMode
    
    BSP.CecDisplayOn = CecDisplayOn
    BSP.CecDisplayOff = CecDisplayOff
    BSP.CecSetSourceToBrightSign = CecSetSourceToBrightSign
    BSP.CecPhilipsSetVolume = CecPhilipsSetVolume
    BSP.SendCecCommand = SendCecCommand
    
    BSP.WaitForSyncResponse = WaitForSyncResponse
    
    BSP.GetAutoschedule = GetAutoschedule
    
    BSP.GetNonPrintableKeyboardCode = GetNonPrintableKeyboardCode
    BSP.InitializeNonPrintableKeyboardCodeList = InitializeNonPrintableKeyboardCodeList
    
    BSP.ConfigureIRRemote = ConfigureIRRemote

    BSP.ConfigureBPs = ConfigureBPs
    BSP.ConfigureBP = ConfigureBP
    BSP.ConfigureBPButton = ConfigureBPButton
    BSP.ConfigureBPInput = ConfigureBPInput
    
    BSP.ConfigureGPIOButton = ConfigureGPIOButton
    BSP.ConfigureGPIOInput = ConfigureGPIOInput
    
    BSP.GetID = GetID
    BSP.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.CreateBMapObjects = CreateBMapObjects
    BSP.CreateBMap = CreateBMap
    BSP.ScheduleRetryCreateBMap = ScheduleRetryCreateBMap
    BSP.RetryCreateBMap = RetryCreateBMap
    BSP.AttemptOpenBMap = AttemptOpenBMap

    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.ExportVariablesDBToAsciiFile = ExportVariablesDBToAsciiFile
    BSP.GetUserVariable = GetUserVariable
    BSP.DeleteVariable = DeleteVariable
    BSP.ResetVariables = ResetVariables
    BSP.ResetVariable = ResetVariable
    BSP.ChangeUserVariableValue = ChangeUserVariableValue
    BSP.AssignSystemVariablesToUserVariables = AssignSystemVariablesToUserVariables
    BSP.AssignSystemVariableToUserVariables = AssignSystemVariableToUserVariables
    BSP.GenerateSessionGuid = GenerateSessionGuid
    BSP.ClearSessionGuid = ClearSessionGuid
    
    BSP.UpdateDataFeed = UpdateDataFeed
    BSP.CreateUserVariablesFromDataFeed = CreateUserVariablesFromDataFeed
    
    BSP.CheckBLCsStatus = CheckBLCsStatus
    BSP.CheckBLCStatus = CheckBLCStatus
    
    BSP.UpdateIPAddressUserVariables = UpdateIPAddressUserVariables
    BSP.UpdateEdidUserVariables = UpdateEdidUserVariables
    
    BSP.GetAttachedFiles = GetAttachedFiles
    
    BSP.PostponeRestart = PostponeRestart
    BSP.ProcessMediaEndEvent = ProcessMediaEndEvent
    
    BSP.SetPoolSizes = SetPoolSizes
    
    BSP.InitiateRemoteSnapshotTimer = InitiateRemoteSnapshotTimer
    BSP.RemoveRemoteSnapshotTimer = RemoveRemoteSnapshotTimer
    
    BSP.NetworkingIsActive = NetworkingIsActive
    
    BSP.GetSupportedFeatures = GetSupportedFeatures
    
    BSP.encryptionByFile = { }
    BSP.SetPerFileEncryptionStatus = SetPerFileEncryptionStatus
    BSP.SetEncryptionAttributes = SetEncryptionAttributes
    
    BSP.QueueRetrieveLiveDataFeed = QueueRetrieveLiveDataFeed
    BSP.RetrieveLiveDataFeed = RetrieveLiveDataFeed
    BSP.RetrievePendingLiveDataFeed = RetrievePendingLiveDataFeed
    BSP.AdvanceToNextLiveDataFeedInQueue = AdvanceToNextLiveDataFeedInQueue
    BSP.RemoveFailedFeedFromQueue = RemoveFailedFeedFromQueue
    
    BSP.newBtManager = newBtManager
    
    BSP.GetRuntimeUsbConnector = GetRuntimeUsbConnector
    BSP.GetSpecifiedConnector = GetSpecifiedConnector

    ' json functions
    BSP.jsonAutoschedule = jsonAutoschedule

    BSP.ResendAutorunCapabilitiesToSupervisor = ResendAutorunCapabilitiesToSupervisor
    BSP.SendAutorunCapabilitiesToSupervisorViaUdp = SendAutorunCapabilitiesToSupervisorViaUdp
    
    return BSP
    
  end function
  
  'endregion
  
  '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
    
  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.beacons = { }
  m.scriptPlugins = []
  m.videoModePlugins = []
  m.parserPlugins = []
  m.additionalPublishedFiles = []
  
  m.bypassProxyHosts = []
  
  m.IsSyncMaster = false
  m.IsSyncSlave = false
  
  for n% = 0 to 3
    for i% = 0 to 10
      m.bpStateMachineRequired[n%, i%] = false
      m.bpInputUsed[n%, i%] = false
      m.bpOutputUsed[n%, i%] = false
    next
  next
  
  for i% = 0 to 7
    m.gpioStateMachineRequired[i%] = false
  next
  
  if presentationName$ = "" then
    
    autoscheduleFileContents$ = ReadAsciiFile(globalAA.autoscheduleFilePath$)
    
    if autoscheduleFileContents$ = "" then
      stop
    end if
    
    schedule = m.GetAutoschedule(globalAA.autoscheduleFilePath$)
    
    if type(schedule.activeScheduledEvent) = "roAssociativeArray" then
      autoplayPath$ = schedule.autoplayPoolFile$
    else
      autoplayPath$ = ""
    end if
    
    m.schedule = schedule
    
    if (autoplayPath$ <> "") then
      presentationName$ = schedule.activeScheduledEvent.presentationName$
    end if
    
  else
    
    autoplayFileName$ = GetAutoplayFileName(presentationName$)
    autoplayPath$ = m.assetPoolFiles.GetPoolFilePath(autoplayFileName$)
    m.activePresentation$ = presentationName$
    
  end if
  
  ' Clean up Node.js Enabled roHtmlWidget and Node.js Apps and their 'view' references
  m.DeallocateNodeComponents()
  
  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
  
  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

    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 })

    end if
    
  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

  ' 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-
      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$ = m.sign.customDeviceWebPage.name
        indexFileName$ = m.sign.customDeviceWebPage.indexFileName
        indexPoolPath$ = name$ + "/" + indexFileName$
        
        if fileName$ = indexPoolPath$ then
          
          m.deviceWebPageFilePath$ = GetPoolFilePath(m.assetPoolFiles, fileName$)
          
        else
          
          indexOfMarker% = instr(0, fileName$, name$)
          if indexOfMarker% > 0 then
            strippedFileName$ = mid(fileName$, indexOfMarker% + len(name$))
            ' other asset
            ext = GetFileExtension(strippedFileName$)
            if ext <> invalid then
              contentType$ = GetMimeTypeByExtension(ext)
              if contentType$ <> invalid then
                url$ = strippedFileName$
                filePath$ = GetPoolFilePath(m.assetPoolFiles, fileName$)
                m.sign.localServer.AddGetFromFile({ url_path: url$, filename: filePath$, content_type: contentType$ })
              end if
            end if
          end if
        end if
        
      next
      
    else
      
      m.deviceWebPageFilePath$ = GetPoolFilePath(m.assetPoolFiles, "Default_PresentationWebPage/_deviceWebPage.html")
      
    end if
    
  end if

  plugins = CreateObject("roArray", 1, true)
  ' plugun 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)
  
  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


' This function is responsible for deallocating Node.js Enabled roHtmlWidgets, Node.js Apps, and their roAssetCollectionView.
' The roHtmlWidgets and Node.js Apps are deallocated prior to deallocating their reference to roAssetCollectionView. 
' (hence the usage of 'components' in the function name referring to both "Node.js Enabled roHtmlWidgets" and Node.js Apps, 
' and their references to view)
Sub DeallocateNodeComponents()
  
  temporaryHtmlWidgetViews = []
  
  ' Deallocate each zone's and zone's state table's Node.js enabled roHtmlWidget references from prior presentation's scope
  if type(m.sign) = "roAssociativeArray" and type(m.sign.zonesHSM) = "roArray" then
    for each zoneHSM in m.sign.zonesHSM
      for each key in zoneHSM.stateTable
        if zoneHSM.stateTable[key].type$ = "html5" and zoneHSM.stateTable[key].isNodeServer then
          ' Save temporary view reference to invalidate roHtmlWidgets prior to invalidating their 'view'
          if zoneHSM.stateTable[key].view <> invalid then
            temporaryHtmlWidgetViews.push(zoneHSM.stateTable[key].view)          
          end if
        end if
      next
      
      ' Assume all loadingHtmlWidget and displayedhtmlwidget are Node.js enabled.
      ' There is no current method as of 11/4/22 indicating they are a Node.js enabled object.
      ' Deallocate loadingHtmlWidget which may be used for preparing next html widget to display
      zoneHSM.loadingHtmlWidget = InvalidateNodeHtmlWidget(zoneHSM.loadingHtmlWidget)
      zoneHSM.loadinghtmlwidgetparams = invalid
      
      ' Deallocate displayingHtmlWidget which may be currently displaying an html widget
      zoneHSM.displayedhtmlwidget = InvalidateNodeHtmlWidget(zoneHSM.displayedhtmlwidget)
      
      ' Deallocate each roHtmlWidget's view reference
      for i% = 0 to temporaryHtmlWidgetViews.Count() - 1
        temporaryHtmlWidgetViews[i%] = invalid
      next
      ' Empty out array for next zone
      temporaryHtmlWidgetViews = []
    next
  end if
  
  ' Deallocate nodeJsObject references and their 'view' from prior presentation running Node Apps
  if type(m.nodeJsObjects) = "roArray" then
    for i% = 0 to m.nodeJsObjects.Count() - 1
      m.nodeJsObjects[i%] = invalid
    next
    
    ' Each Node.js App references the base directory path of 'view', invalidating the global view 
    ' invalidates all Node.js App view references
    GetGlobalAA().bsp.view = invalid
  end if
  
end sub


Function InvalidateNodeHtmlWidget(htmlWidget as object) as object
  if type(htmlWidget) = "roHtmlWidget" then
    htmlWidget.Hide() ' Not necessary, call to avoid possibly visual artifacts
    htmlWidget = invalid
  end if
  return htmlWidget
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
      userVariable.SetCurrentValue(m.sysInfo["edidMonitorSerialNumber"+suffix$], postMsg)
    else if userVariable.systemVariable$ = "EdidYearOfManufacture" then
      userVariable.SetCurrentValue(m.sysInfo["edidYearOfManufacture"+suffix$], postMsg)
    else if userVariable.systemVariable$ = "EdidMonitorName" then
      userVariable.SetCurrentValue(m.sysInfo["edidMonitorName"+suffix$], postMsg)
    else if userVariable.systemVariable$ = "EdidManufacturer" then
      userVariable.SetCurrentValue(m.sysInfo["edidManufacturer"+suffix$], postMsg)
    else if userVariable.systemVariable$ = "EdidUnspecifiedText" then
      userVariable.SetCurrentValue(m.sysInfo["edidUnspecifiedText"+suffix$], postMsg)
    else if userVariable.systemVariable$ = "EdidSerialNumber" then
      userVariable.SetCurrentValue(m.sysInfo["edidSerialNumber"+suffix$], postMsg)
    else if userVariable.systemVariable$ = "EdidManufacturerProductCode" then
      userVariable.SetCurrentValue(m.sysInfo["edidManufacturerProductCode"+suffix$], postMsg)
    else if userVariable.systemVariable$ = "EdidWeekOfManufacture" then
      userVariable.SetCurrentValue(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
      userVariable.SetCurrentValue(m.sysInfo.ipAddressWired$, postMsg)
    else if userVariable.systemVariable$ = "IpAddressWireless" then
      userVariable.SetCurrentValue(m.sysInfo.ipAddressWireless$, postMsg)
    end if
  next
  
end sub
'endregion


'region Bose Products
Function ReadBoseProductsFile() as object

  boseProducts = { }

  globalAA = GetGlobalAA()
  boseProductsFileContents$ = ReadAsciiFile(globalAA.boseProductsFilePath$)
  if len(boseProductsFileContents$) = 0 then return boseProducts

  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()
    
  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
  boseProduct.bmapProtocol = boseProductJson.bmapProtocol

 ' 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 ParseBoseBmapCommunicationSpec(path$ as string) as object
  
  json$ = ReadAsciiFile(path$)
  bmap = ParseJson(json$)
  return bmap
  
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

      if type(state.mediaListEndEvent) = "roAssociativeArray" then
        m.CreateObjectsNeededForTransitionCommands(state.mediaListEndEvent)
      endif
    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.wssEvents) = "roAssociativeArray"
      m.CreateObjectsNeededForTransitionCommands(state.wssEvents)
    end if

    if type(state.signChannelEndEvent) = "roAssociativeArray"
      m.CreateObjectsNeededForTransitionCommands(state.signChannelEndEvent)
    end if
    
    if type(state.audioEndEvent) = "roAssociativeArray"
      m.CreateObjectsNeededForTransitionCommands(state.audioEndEvent)
    end if
    
    if type(state.keyboardEvents) = "roAssociativeArray" or type(state.usbStringEvents) = "roAssociativeArray" then
      
      if type(state.keyboardEvents) = "roAssociativeArray" then
        keyboardEvents = state.keyboardEvents
        for each keyboardEvent in state.keyboardEvents
          if type(keyboardEvents[keyboardEvent]) = "roAssociativeArray" then
            m.CreateObjectsNeededForTransitionCommands(keyboardEvents[keyboardEvent])
          end if
        next
      end if
      
      if type(state.usbStringEvents) = "roAssociativeArray"
        usbEvents = state.usbStringEvents
        for each usbEvent in state.usbEvents
          if type(usbEvents[usbEvent]) = "roAssociativeArray" then
            m.CreateObjectsNeededForTransitionCommands(usbEvents[usbEvent])
          end if
        next
      end if
      
    end if
    
    if type(state.remoteEvents) = "roAssociativeArray" then
      remoteEvents = state.remoteEvents
      for each remoteEvent in state.remoteEvents
        m.CreateObjectsNeededForTransitionCommands(remoteEvents[remoteEvent])
      next
    end if
    
    if (type(state.gpsEnterRegionEvents) = "roArray" and state.gpsEnterRegionEvents.Count() > 0) or (type(state.gpsExitRegionEvents) = "roArray" and state.gpsExitRegionEvents.Count() > 0) then
      m.CreateSerial(m.bsp, m.bsp.gpsPort$, false)
    end if
    
    serialEvents = state.serialEvents
    for each serialPort in serialEvents
      
      m.CreateSerial(m.bsp, serialPort, false)
      
      protocol$ = ""

      if IsUsbPort(serialPort) then

        usbSpec = GetGlobalAA().usbConnectorNameToUsbSpec.Lookup(serialPort)

        if GetGlobalAA().usbHIDPortConfigurations.DoesExist(serialPort) then
          usbHIDPortConfiguration = GetGlobalAA().usbHIDPortConfigurations[serialPort]
          protocol$ = usbHIDPortConfiguration.protocol$
        end if

      else
        port% = int(val(serialPort))
        serialPortConfiguration = m.bsp.sign.serialPortConfigurations[port%]
        protocol$ = serialPortConfiguration.protocol$
      end if
      
      if protocol$ = "Binary" then
        if type(serialEvents[serialPort]) = "roAssociativeArray" then
          if type(serialEvents[serialPort].streamInputTransitionSpecs) = "roArray" then
            for each streamInputTransitionSpec in serialEvents[serialPort].streamInputTransitionSpecs
              m.CreateObjectsNeededForTransitionCommands(streamInputTransitionSpec.transition)
            next
          end if
        end if
      else if protocol$ <> "" then
        for each serialEvent in serialEvents[serialPort]
          m.CreateObjectsNeededForTransitionCommands(serialEvents[serialPort][serialEvent])
        next
      end if
    next
    
    if type(state.zoneMessageEvents) = "roAssociativeArray" then
      
      for each zoneMessageEvent in state.zoneMessageEvents
        m.CreateObjectsNeededForTransitionCommands(state.zoneMessageEvents[zoneMessageEvent])
      next
      
    end if
    
    if type(state.pluginMessageEvents) = "roAssociativeArray" then
      
      for each pluginMessageEvent in state.pluginMessageEvents
        m.CreateObjectsNeededForTransitionCommands(state.pluginMessageEvents[pluginMessageEvent])
      next
      
    end if
    
    if type(state.internalSynchronizeEvents) = "roAssociativeArray" then
      
      for each internalSynchronizeEvent in state.internalSynchronizeEvents
        m.CreateObjectsNeededForTransitionCommands(state.internalSynchronizeEvents[internalSynchronizeEvent])
      next
      
    end if
    
    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

  mode = invalid
  if m.bsp.sign.enableEnhancedSynchronization and (m.bsp.IsSyncMaster or m.bsp.IsSyncSlave) then
    syncDomain = m.bsp.sign.ptpDomain$
    mode = "multi-player"
  else if not m.bsp.sign.enableEnhancedSynchronization and (m.bsp.IsSyncMaster) then
    ' Use the default sync manager domain
    syncDomain = "BS1"
    mode = "single-player"
  end if
  
  if ShouldResetSyncManager(m.bsp.syncManagerMode, mode) then
    aa = { }
    aa.Domain = syncDomain
    m.bsp.SyncManager = invalid
    m.bsp.SyncManager = CreateObject("roSyncManager", aa)
    m.bsp.diagnostics.PrintDebug("@@@ roSyncManager created. Value of sync domain:" + aa.Domain)
    m.bsp.SyncManager.SetMasterMode(m.bsp.IsSyncMaster)
    m.bsp.syncManagerMode = mode
    
    if m.bsp.IsSyncSlave then
      m.bsp.SyncManager.SetPort(m.bsp.msgPort)
      m.bsp.diagnostics.PrintDebug("@@@ Node is a slave")
    end if
    
    if m.bsp.setSyncDomainSupported then
      if m.bsp.videomode.SetSyncDomain(syncDomain) then
        m.bsp.diagnostics.PrintDebug("@@@ VSYNC Enabled on Domain @@@ : " + syncDomain)
      else
        m.bsp.diagnostics.PrintDebug("@@@ VSYNC failed to enable on Domain @@@ : " + syncDomain + " : " + m.bsp.videomode.GetFailureReason())
      end if
    end if
  end if

end sub


Function ShouldResetSyncManager(currentMode as Object, newMode as Object) as boolean
  ' Check the current set sync manager.
  ' If it's lower priority than what we want, then set the new sync manager.
  ' Priority: multi-player > single-player > none.

  ' No need to update if new mode is invalid
  if newMode = invalid then return false
  ' Do not overwrite a multi-player sync manager with lower priority
  if currentMode = "multi-player" and newMode = "single-player" then return false
  return true
end function


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.bsp.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.bsp.IsSyncMaster = true
  end if
  
end sub


Sub ConfigureIRRemote()

  irInConfiguration = m.sign.irInConfiguration
  irOutConfiguration = m.sign.irOutConfiguration

  irRemoteConfiguration = m.sign.irRemoteControl

  irConfig = {}

  irConfig.source = irInConfiguration.source

  irConfig.encodings = []
  irConfig.encodings[0] = irRemoteConfiguration.encoding
  
  m.irReceiver = CreateObject("roIRReceiver", irConfig)
  if type(m.irReceiver) = "roIRReceiver" then
    m.irReceiver.SetPort(m.msgPort)
  endif

End Sub


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


Sub ConfigureBP(buttonPanelIndex% as integer, touchBoardLED$ as string, touchBoardLEDSetup$ as string)
  
  if type(m.bpInputPorts[buttonPanelIndex%]) = "roControlPort" then
    if type(m.bpOutput[buttonPanelIndex%]) <> "roControlPort" then
      m.diagnostics.PrintDebug("Creating bpOutput")
      m.bpOutput[buttonPanelIndex%] = CreateObject("roControlPort", touchBoardLED$)
      if type(m.bpOutput[buttonPanelIndex%]) = "roControlPort" then
        m.bpOutput[buttonPanelIndex%].SetUserData(touchBoardLED$)
        m.bpOutputSetup[buttonPanelIndex%] = CreateObject("roControlPort", touchBoardLEDSetup$)
        if type(m.bpOutputSetup[buttonPanelIndex%]) = "roControlPort" then
          m.bpOutputSetup[buttonPanelIndex%].SetUserData(touchBoardLEDSetup$)
          m.bpOutputSetup[buttonPanelIndex%].SetOutputValue(0, 22)
        end if
      end if
    end if
  end if
end sub


Sub CreateUDPSender(bsp as object)
  
  createDatagramSender = false
  
  if type(bsp.udpSender) <> "roDatagramSender" then
    createDatagramSender = true
  else
    if (type(bsp.existingUdpAddressType$) = "roString" and bsp.existingUdpAddressType$ <> bsp.udpAddressType$) or (type(bsp.existingUdpAddress$) = "roString" and bsp.existingUdpAddress$ <> bsp.udpAddress$) or (type(bsp.existingUdpSendPort) = "roInt" and bsp.existingUdpSendPort <> bsp.udpSendPort) then
      createDatagramSender = true
    end if
  end if
  
  if createDatagramSender then
    bsp.diagnostics.PrintDebug("Creating roDatagramSender")
    bsp.udpSender = CreateObject("roDatagramSender")
    if bsp.udpAddressType$ = "LocalSubnet" then
      bsp.udpSender.SetDestination("BCAST-LOCAL-SUBNETS", bsp.udpSendPort)
    else if bsp.udpAddressType$ = "Ethernet" then
      bsp.udpSender.SetDestination("BCAST-SUBNET-0", bsp.udpSendPort)
    else if bsp.udpAddressType$ = "Wireless" then
      bsp.udpSender.SetDestination("BCAST-SUBNET-1", bsp.udpSendPort)
    else
      bsp.udpSender.SetDestination(bsp.udpAddress$, bsp.udpSendPort)
    end if
    
    bsp.existingUdpAddressType$ = bsp.udpAddressType$
    bsp.existingUdpAddress$ = bsp.udpAddress$
    bsp.existingUdpSendPort = bsp.udpSendPort
    
  end if
  
end sub


Sub SendUDPNotification(msg as string)

  if 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
  inputVariable = StripLeadingSpaces(inputVariable)
  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
  meta.videoModePlugins = []
  for each videoModePluginSpec in BrightAuthor.meta.videoModePlugins
    videoModePlugin = jsonParseVideoModePlugin(videoModePluginSpec)
    meta.videoModePlugins.push(videoModePlugin)
    bsp.videoModePlugins.push(videoModePlugin)
  next

  ' parse script plugins
  meta.scriptPlugins = []
  for each scriptPluginSpec in BrightAuthor.meta.scriptPlugins
    scriptPlugin = jsonParseScriptPlugin(scriptPluginSpec)
    ' BACON - is it necessary to store the script plugins both in meta and in bsp?
    meta.scriptPlugins.push(scriptPlugin)
    bsp.scriptPlugins.push(scriptPlugin)
  next

  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

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

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

      ' we do not support video wall and multiple screens at the same time
      Sign.isVideoWall = 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
        '   print "Video mode is ";Sign.videoMode$
        
        videoMode$ = Sign.videoMode$
        
        forceResolution = meta.forceResolution
        
        setPreferredResolution = false
        if not forceResolution then
          setPreferredResolution = true
          videoMode$ = videoMode$ + ":preferred"
        end if
        
        dolbyVisionEnabled = meta.dolbyVisionEnabled
        if dolbyVisionEnabled then
          videoMode$ = videoMode$ + ":dbv"
        end if
        
        fullResGraphicsEnabled = meta.fullResGraphicsEnabled
        if fullResGraphicsEnabled then
          videoMode$ = videoMode$ + ":fullres"
        end if
        
        tenBitColorEnabled = meta.tenBitColorEnabled
        if tenBitColorEnabled then
          videoMode$ = videoMode$ + ":10bit"
        end if
        
        ' allow videoModePlugin to set videoMode
        videoModeInputs = { }
        videoModeInputs.signVideoMode$ = Sign.videoMode$
        videoModeInputs.setPreferredResolution = setPreferredResolution
        videoModeInputs.fullResGraphicsEnabled = fullResGraphicsEnabled
        videoModeInputs.tenBitColorEnabled = tenBitColorEnabled
        videoModeInputs.dolbyVisionEnabled = dolbyVisionEnabled
        videoModeInputs.videoMode$ = videoMode$
      
        ' allow videoModePlugin to set single screen videoMode
        singleVideoMode = parseVideoModePlugin(videoModeInputs, bsp, videoMode$, {})
      
        ok = videoMode.SetMode(singleVideoMode)
        if ok = 0 then
          print "Error: Can't set VIDEOMODE to ::"; singleVideoMode; " resetting to 1920x1080x60i"
          ok = videoMode.SetMode("1920x1080x60i")
        endif
        
        if bsp.forceResolutionSupported then
          aa = videoMode.GetConfiguredMode()
          if aa <> invalid then
            bsp.configuredResX = aa.graphicsPlaneWidth
            bsp.configuredResY = aa.graphicsPlaneHeight
          else
            bsp.configuredResX = videoMode.GetResX()
            bsp.configuredResY = videoMode.GetResY()
          end if
          
          bsp.diagnostics.PrintDebug("Specified videoMode: " + singleVideoMode + ", actual videoMode: " + videoMode.GetMode())
          bsp.logging.WriteDiagnosticLogEntry(bsp.diagnosticCodes.EVENT_SET_VIDEO_MODE, "Specified videoMode: " + singleVideoMode + ", actual videoMode: " + videoMode.GetMode())
          
        else
          bsp.configuredResX = videoMode.GetResX()
          bsp.configuredResY = videoMode.GetResY()
        end if

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

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

      endif

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

      videoMode = invalid
    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 = { }
  GetGlobalAA().usbBMAPHIDPortConfigurations = { }

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

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

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

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

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

  for each serialPortConfigurationSpec in meta.serialPortConfigurations
    
    serialPortConfiguration = { }
    serialPortConfiguration.serialPortSpeed% = serialPortConfigurationSpec.serialPortSpeed%
    serialPortConfiguration.protocol$ = serialPortConfigurationSpec.protocol$
    serialPortConfiguration.sendEol$ = serialPortConfigurationSpec.sendEol$
    serialPortConfiguration.receiveEol$ = serialPortConfigurationSpec.receiveEol$
    serialPortConfiguration.invertSignals = serialPortConfigurationSpec.invertSignals
    
    serialPortConfiguration.serialPortMode$ = serialPortConfigurationSpec.serialPortMode
    
    port% = serialPortConfigurationSpec.port
    
    serialPortConfiguration.gps = serialPortConfigurationSpec.gps
    if serialPortConfigurationSpec.gps then
      bsp.gpsConfigured = true
      bsp.gpsPort$ = stri(port%)
    else
      serialPortConfiguration.gps = false
    end if
    
    Sign.serialPortConfigurations[port%] = serialPortConfiguration
  next

  ' parse parser plugins
  parserPluginsContainer = meta.parserPlugins
  for each parserPlugin in parserPluginsContainer
    bsp.parserPlugins.push(parserPlugin)
  next

  ' first pass parse of user variables

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

  ' BACONTODO - parseAutoplay may need this set earlier'
  bsp.privateDBSectionId% = bsp.GetDBSectionId(bsp.activePresentation$)
  if bsp.privateDBSectionId% < 0 then
    bsp.AddDBSection(bsp.activePresentation$)
    bsp.privateDBSectionId% = bsp.GetDBSectionId(bsp.activePresentation$)
  end if

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

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

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

  variablePosition% = 0

  userVariables = bsp.currentUserVariables

  for each userVariableSpec in meta.userVariableSpecs
    
    name$ = userVariableSpec.name
    defaultValue$ = userVariableSpec.defaultValue
    access$ = userVariableSpec.access
    
    if access$ = "Shared" then
      userVariableSpec.categoryId% = bsp.sharedBrightAuthorCategoryId%
    else
      userVariableSpec.categoryId% = bsp.privateBrightAuthorCategoryId%
    end if
    categoryId% = userVariableSpec.categoryId%
    
    systemVariable$ = userVariableSpec.systemVariable$
    url$ = userVariableSpec.url
    liveDataFeedId$ = userVariableSpec.liveDataFeedId
    
    if not userVariables.DoesExist(name$) then
      bsp.AddDBVariable(categoryId%, name$, defaultValue$, "", 0)
      userVariable = newUserVariable(bsp, name$, defaultValue$, defaultValue$, "", access$, 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?

  ' BACON - parse beacons

  ' get list of additional files to publish
  for each additionalPublishedFileName in meta.additionalPublishedFiles
    additionalPublishedFile = { }
    additionalPublishedFile.fileName$ = additionalPublishedFileName
    additionalPublishedFile.filePath$ = GetPoolFilePath(bsp.assetPoolFiles, additionalPublishedFileName)
    bsp.additionalPublishedFiles.push(additionalPublishedFile)
  next

  gaa = GetGlobalAA()

  ' parse boseProduct section - information about Bose products in use in this presentation
  Sign.boseProducts = meta.boseProducts

  Sign.boseProductsByConnector = Sign.BuildBoseProductsByConnector(bsp, 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.usbBMAPHIDPortConfigurations.DoesExist("usb700_1") then
      tmp = gaa.usbBMAPHIDPortConfigurations["usb700_1"]
      gaa.usbBMAPHIDPortConfigurations["usbTypeC"] = tmp
      gaa.usbBMAPHIDPortConfigurations.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.usbBMAPHIDPortConfigurations.DoesExist("usbTypeC") then
      tmp = gaa.usbBMAPHIDPortConfigurations["usbTypeC"]
      gaa.usbBMAPHIDPortConfigurations["usb700_1"] = tmp
      gaa.usbBMAPHIDPortConfigurations.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 = { }
  usbBMAPHIDPortConfigurations = { }

  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.usbBMAPHIDPortConfigurations.DoesExist(connector) then
        usbBMAPHIDPortConfiguration = gaa.usbBMAPHIDPortConfigurations.Lookup(connector)
        usbBMAPHIDPortConfigurations.AddReplace(connector, usbBMAPHIDPortConfiguration)
      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
  gaa.usbBMAPHIDPortConfigurations = usbBMAPHIDPortConfigurations

  if type(bsp.bmapByPort) <> "roAssociativeArray" then
    bsp.bmapByPort = {}
  endif

  ' Create bmap objects for the sign
  bsp.CreateBMapObjects(Sign)

  ' set default serial port speed, mode
  bsp.serialPortConfigurations = CreateObject("roArray", 8, true)
  for i% = 0 to 7
    bsp.serialPortConfigurations[i%] = meta.serialPortConfigurations[i%]
  next

  Sign.udpReceivePort = meta.udpReceivePort
  Sign.udpSendPort = meta.udpSendPort
  Sign.udpAddressType$ = meta.udpAddressType

  if Sign.udpAddressType$ = "" then Sign.udpAddressType$ = "IPAddress"
  Sign.udpAddress$ = meta.udpAddress

  ' synchronization section
  Sign.enableEnhancedSynchronization = meta.enableEnhancedSynchronization
  Sign.deviceIsSyncMaster = meta.deviceIsSyncMaster
  Sign.ptpDomain$ = meta.ptpDomain$

  if (Sign.enableEnhancedSynchronization) then
    if Sign.deviceIsSyncMaster then
      targetSyncMasterInRegistry = "1"
    else
      targetSyncMasterInRegistry = "0"
    end if
    
    rebootRequired = false
    ' check the sync master value in the registry. if it does not exist or is different, set it and reboot.
    
    ' check the domain value in the registry. if it does not exist or is different, set it and reboot.
    ptpDomainInRegistry$ = 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
    
    ' check the syncMaster value in the registry. if it does not exist or is different, set it and reboot.
    syncMasterInRegistry$ = gaa.registrySection.Read("sync_master")
    if syncMasterInRegistry$ <> targetSyncMasterInRegistry then
      gaa.registrySection.Write("sync_master", targetSyncMasterInRegistry)
      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

  ' BCN-14814: Update registry entry and reboot if needed
  chromiumPlaybackValue = meta.htmlEnableChromiumVideoPlayback
  if chromiumPlaybackValue <> invalid then
    rebootRequiredForChromium = false

    htmlRegistrySection = CreateObject("roRegistrySection", "html")
    if type(htmlRegistrySection) <> "roRegistrySection" then
      print "Error: Unable to create htmlRegistrySection roRegistrySection": stop
    end if

    useBsMediaPlayer = htmlRegistrySection.Read("use-brightsign-media-player")
    print " == useBsMediaPlayer from registry: ";useBsMediaPlayer

    if chromiumPlaybackValue = true and useBsMediaPlayer <> "0" then
      htmlRegistrySection.Write("use-brightsign-media-player", "0")
      rebootRequiredForChromium = true
    else if chromiumPlaybackValue = false and useBsMediaPlayer <> "1" then
      htmlRegistrySection.Write("use-brightsign-media-player", "1")
      rebootRequiredForChromium = true
    end if

    if rebootRequiredForChromium then
      htmlRegistrySection.Flush()
      RebootSystem()
    end if
  end if

  return Sign

end function


Sub CreateBMapObjects(sign)
  for each connector in sign.boseProductsByConnector
    m.CreateBMap(connector, sign)
  next
end sub


Function CreateBMap(connector, sign) as Object

  gaa = GetGlobalAA()

  boseProductInSign = sign.boseProductsByConnector[connector]

  if type(boseProductInSign.bmapCommunicationSpec) = "roAssociativeArray" then

    port$ = boseProductInSign.port$
    port$ = m.GetRuntimeUsbConnector(port$)

    m.diagnostics.PrintDebug("CreateBMap on port " + port$ + " invoked")

    ok = m.AttemptOpenBMap(port$)
    if not ok then
      m.ScheduleRetryCreateBMap(port$)
    end if
  
  endif

end function


Sub ScheduleRetryCreateBMap(port$ as string)

  m.diagnostics.PrintDebug("ScheduleRetryCreateBMap on port " + port$ + " invoked")

  if type(m.bmapPortsToRetry) <> "roAssociativeArray" then
    m.bmapPortsToRetry = { }
  end if
  
  if not m.bmapPortsToRetry.DoesExist(port$) then

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

    m.diagnostics.PrintDebug("ScheduleRetryCreateBMap on port " + port$ + " timer set")
    
  end if

end sub


Function RetryCreateBMap(port$ as string) as boolean

  m.diagnostics.PrintDebug("RetryCreateBMap on port " + port$ + " invoked")

  ok = m.AttemptOpenBMap(port$)

  if ok and m.bmapPortsToRetry.DoesExist(port$) then
    m.diagnostics.PrintDebug("RetryCreateBMap on port " + port$ + " succeeded, remove from bmapPortsToRetry")
    m.bmapPortsToRetry.Delete(port$)
  else
    m.diagnostics.PrintDebug("RetryCreateBMap on port " + port$ + " failed")
  end if
  
  return ok
  
end function


Function AttemptOpenBMap(port$ as string) as boolean

  gaa = GetGlobalAA()

  m.diagnostics.PrintDebug("AttemptOpenBMap invoked on port " + port$ + " invoked")

  if gaa.usbBMAPHIDPortConfigurations.DoesExist(port$) then
    m.diagnostics.PrintDebug("AttemptOpenBMap invoked on port " + port$ + ". Exists in usbBMAPHIDPortConfigurations")
    bmapHIDPortConfiguration = gaa.usbBMAPHIDPortConfigurations[port$]
    if bmapHIDPortConfiguration.bmapProtocol = "HID" then
      m.diagnostics.PrintDebug("AttemptOpenBMap invoked on port " + port$ + ". Protocol is HID")
      usbSpec = gaa.usbConnectorNameToUsbSpec[port$]
      boseUSBAudioDevice = usbSpec.audiooutputspec
      usbPort$ = usbSpec.hidoutputspec
      m.diagnostics.PrintDebug("Create roBMap on port " + port$ + " (usbPort " + usbPort$ + ")")
      bmap = CreateObject("roBmap", usbPort$)
      if type(bmap) = "roBmap" then
        m.diagnostics.PrintDebug("AttemptOpenBMap on port " + port$ + " succeeded")
        bmap.SetPort(m.msgPort)
        bmap.SetUserData(port$)
        m.bmapByPort.AddReplace(port$, bmap)

        ' post BMap connect event
        connectData = {}
        connectData.port = port$
        connectData.hidOutputSpec = usbPort$
        bmapConnectEvent = { }
        bmapConnectEvent["EventType"] = "BMAP_CONNECT_EVENT"
        bmapConnectEvent["EventData"] = connectData
        m.msgPort.PostMessage(bmapConnectEvent)

        return true
      else
        m.diagnostics.PrintDebug("AttemptOpenBMap on port " + port$ + "failed")
      endif
    end if
  else
    m.diagnostics.PrintDebug("AttemptOpenBMap invoked on port " + port$ + ". Does not exist in usbBMAPHIDPortConfigurations")
  end if

  return false

end function


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

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

  for each videoModePlugin in bsp.videoModePlugins

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

    retVal = Eval(setVideoModeFunction$)

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

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

End Function


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

    if IsString(meta.bmapSpecAssetName) then
      bmapCommunicationSpecFileName$ = meta.bmapSpecAssetName
      if bmapCommunicationSpecFileName$ <> "" then
        bmapCommunicationPath$ = GetPoolFilePath(bsp.assetPoolFiles, bmapCommunicationSpecFileName$)
        if bmapCommunicationPath$ <> "" then
          boseProduct.bmapCommunicationSpec = ParseBoseBmapCommunicationSpec(bmapCommunicationPath$)
        endif
      endif
    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.bmapProtocol = "HID" then
        usbBMAPHIDPortConfiguration = { }
        usbBMAPHIDPortConfiguration.bmapProtocol = "HID"
        usbBMAPHIDPortConfiguration.usbTapInterfaceIndex$ = boseProductSpec.usbTapInterfaceIndex$
        gaa.usbBMAPHIDPortConfigurations.AddReplace(boseProduct.port$, usbBMAPHIDPortConfiguration)
      end if

      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


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.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$
  
  return userVariable
  
end function


Function GetCategoryIdFromAccess(access$ as string) as integer
  
  if lcase(access$) = "shared" then
    categoryId% = m.sharedBrightAuthorCategoryId%
  else
    categoryId% = m.privateBrightAuthorCategoryId%
  end if
  
  return categoryId%
  
end function


Sub UpdateDBVariable(categoryId% as integer, name$ as string, currentValue$ as string)
  
  params = { cv_param: currentValue$, vn_param: name$, cri_param: categoryId% }
  
  m.userVariablesDB.RunBackground("UPDATE Variables2 SET CurrentValue=:cv_param WHERE VariableName=:vn_param AND CategoryReferenceId=:cri_param;", params)
  
end sub


Sub UpdateDBVariableDefaultValue(categoryId% as integer, name$ as string, defaultValue$ as string)
  
  params = { dv_param: defaultValue$, vn_param: name$, cri_param: categoryId% }
  
  m.userVariablesDB.RunBackground("UPDATE Variables2 SET DefaultValue=:dv_param WHERE VariableName=:vn_param AND CategoryReferenceId=:cri_param;", params)
  
end sub


Sub UpdateDBVariableMediaUrl(categoryId% as integer, name$ as string, mediaUrl$ as string)
  
  params = { mu_param: mediaUrl$, vn_param: name$, cri_param: categoryId% }
  
  m.userVariablesDB.RunBackground("UPDATE Variables2 SET MediaUrl=:mu_param WHERE VariableName=:vn_param AND CategoryReferenceId=:cri_param;", params)
  
end sub


Sub UpdateDBVariablePosition(categoryId% as integer, name$ as string, position% as integer)
  
  params = { p_param: position%, vn_param: name$, cri_param: categoryId% }
  
  m.userVariablesDB.RunBackground("UPDATE Variables2 SET Position=:p_param WHERE VariableName=:vn_param AND CategoryReferenceId=:cri_param;", params)
  
end sub


Sub AddDBVariable(categoryId% as integer, name$ as string, defaultValue$ as string, mediaUrl$ as string, position% as integer)
  
  insertSQL$ = "INSERT INTO Variables2 (CategoryReferenceId, VariableName, CurrentValue, DefaultValue, MediaUrl, Position) VALUES(?,?,?,?,?,?);"
  
  params = CreateObject("roArray", 6, false)
  params[0] = categoryId%
  params[1] = name$
  params[2] = defaultValue$
  params[3] = defaultValue$
  params[4] = mediaUrl$
  params[5] = position%
  
  m.ExecuteDBInsert(insertSQL$, params)
  
end sub


Sub AddDBSection(sectionName$ as string)
  
  insertSQL$ = "INSERT INTO Sections (SectionName) VALUES(:name_param);"
  
  params = { name_param: sectionName$ }
  
  m.ExecuteDBInsert(insertSQL$, params)
  
end sub


Sub DeleteDBVariable(categoryId% as integer, variableName$ as string)
  
  SQLITE_COMPLETE = 100
  
  params = { : uv_param: variableName$ }
  
  delete$ = "DELETE FROM Variables2 WHERE VariableName =:uv_param AND CategoryReferenceId = " + StripLeadingSpaces(stri(categoryId%)) + ";"
  
  deleteStatement = m.userVariablesDB.CreateStatement(delete$)
  
  if type(deleteStatement) <> "roSqliteStatement" then
    m.diagnostics.PrintDebug("DeleteStatement failure - " + delete$)
    stop
  end if
  
  bindResult = deleteStatement.BindByName(params)
  
  if not bindResult then
    m.diagnostics.PrintDebug("Bind failure")
    stop
  end if
  
  sqlResult = deleteStatement.Run()
  
  if sqlResult <> SQLITE_COMPLETE
    m.diagnostics.PrintDebug("sqlResult <> SQLITE_COMPLETE")
  end if
  
  deleteStatement.Finalise()
  
end sub


Sub AddDBCategory(sectionId% as integer, categoryName$ as string)
  
  insertSQL$ = "INSERT INTO Categories (SectionReferenceId, CategoryName) VALUES(?,?);"
  
  params = CreateObject("roArray", 2, false)
  params[0] = sectionId%
  params[1] = categoryName$
  
  m.ExecuteDBInsert(insertSQL$, params)
  
end sub


Sub ExecuteDBInsert(insert$ as string, params as object)
  
  SQLITE_COMPLETE = 100
  
  insertStatement = m.userVariablesDB.CreateStatement(insert$)
  
  if type(insertStatement) <> "roSqliteStatement" then
    m.diagnostics.PrintDebug("CreateStatement failure - " + insert$)
    stop
  end if
  
  if type(params) = "roArray" then
    bindResult = insertStatement.BindByOffset(params)
  else
    bindResult = insertStatement.BindByName(params)
  end if
  
  if not bindResult then
    m.diagnostics.PrintDebug("Bind failure")
    stop
  end if
  
  sqlResult = insertStatement.Run()
  
  if sqlResult <> SQLITE_COMPLETE
    m.diagnostics.PrintDebug("sqlResult <> SQLITE_COMPLETE")
  end if
  
  insertStatement.Finalise()
  
end sub


Sub ExecuteDBSelect(select$ as string, resultsCallback as object, selectData as object, params as object)

  SQLITE_ROWS = 102

  selectStmt = m.userVariablesDB.CreateStatement(select$)
  
  if type(selectStmt) <> "roSqliteStatement" then
    m.diagnostics.PrintDebug("CreateStatement failure - " + select$)
    stop
  end if
  
  bindResult = true
  if type(params) = "roArray" then
    bindResult = selectStmt.BindByOffset(params)
  else if type(params) = "roAssociativeArray" then
    bindResult = selectStmt.BindByName(params)
  end if
  
  if not bindResult then
    m.diagnostics.PrintDebug("Bind failure")
    stop
  end if
  
  sqlResult = selectStmt.Run()
  
  while sqlResult = SQLITE_ROWS
    
    resultsData = selectStmt.GetData()
    
    resultsCallback(resultsData, selectData)
    
    sqlResult = selectStmt.Run()
    
  end while
  
  selectStmt.Finalise()
  
end sub


Sub GetDBVersionCallback(resultsData as object, selectData as object)
  
  selectData.version$ = resultsData["Version"]
  
end sub


Function GetDBVersion() as string
  
  selectData = { }
  selectData.version$ = ""
  
  select$ = "SELECT SchemaVersion.Version FROM SchemaVersion;"
  m.ExecuteDBSelect(select$, GetDBVersionCallback, selectData, invalid)
  
  return selectData.version$
  
end function


Sub GetDBTableNamesCallback(resultsData as object, selectData as object)
  
  selectData.tableNames.AddReplace(resultsData["name"], true)
  
end sub


Function GetDBTableNames() as object
  
  selectData = { }
  selectData.tableNames = { }
  
  select$ = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
  m.ExecuteDBSelect(select$, GetDBTableNamesCallback, selectData, invalid)
  
  return selectData.tableNames
  
end function


Sub GetDBCategoryIdCallback(resultsData as object, selectData as object)
  
  selectData.categoryId% = resultsData["CategoryId"]
  
end sub


Function GetDBCategoryId(sectionId% as integer, categoryName$ as string) as object
  
  selectData = { }
  selectData.categoryId% = -1
  
  select$ = "SELECT CategoryId FROM Categories WHERE SectionReferenceId = " + StripLeadingSpaces(stri(sectionId%)) + " AND CategoryName='" + categoryName$ + "';"
  m.ExecuteDBSelect(select$, GetDBCategoryIdCallback, selectData, invalid)
  
  return selectData.categoryId%
  
end function


Sub GetDBSectionNamesCallback(resultsData as object, selectData as object)
  
  selectData.sections.push(resultsData["SectionName"])
  
end sub


Function GetDBSectionNames() as object
  
  selectData = { }
  selectData.sections = []
  
  select$ = "SELECT Sections.SectionName FROM Sections;"
  m.ExecuteDBSelect(select$, GetDBSectionNamesCallback, selectData, invalid)
  
  return selectData.sections
  
end function


Sub GetDBSectionIdCallback(resultsData as object, selectData as object)
  
  selectData.sectionId% = resultsData["SectionId"]
  
end sub


Function GetDBSectionId(sectionName$ as string) as object
  
  selectData = { }
  selectData.sectionId% = -1
  
  params = { sn_param: sectionName$ }
  select$ = "SELECT SectionId FROM Sections WHERE SectionName =:sn_param;"
  
  m.ExecuteDBSelect(select$, GetDBSectionIdCallback, selectData, params)
  
  return selectData.sectionId%
  
end function


Sub ReadSchema1TablesCallback(resultsData as object, selectData as object)
  
  sectionName$ = resultsData["SectionName"]
  if selectData.userVariableSets.DoesExist(sectionName$) then
    userVariables = selectData.userVariableSets.Lookup(sectionName$)
  else
    userVariables = []
    selectData.userVariableSets.AddReplace(sectionName$, userVariables)
  end if
  
  variableName$ = resultsData["VariableName"]
  currentValue$ = resultsData["CurrentValue"]
  defaultValue$ = resultsData["DefaultValue"]
  
  userVariable = newUserVariable(m, variableName$, currentValue$, defaultValue$, "", "", "")
  userVariables.push(userVariable)
  
end sub


Function ReadSchema1Tables() as object
  
  selectData = { }
  selectData.userVariableSets = { }
  
  select$ = "SELECT Sections.SectionName, Variables.VariableName, Variables.CurrentValue, Variables.DefaultValue FROM Variables INNER JOIN Sections ON Sections.SectionId = Variables.SectionReferenceId ORDER BY Sections.SectionName, Variables.VariableId;"
  m.ExecuteDBSelect(select$, ReadSchema1TablesCallback, selectData, invalid)
  
  return selectData.userVariableSets
  
end function


Sub GetUserVariablesGivenCategoryCallback(resultsData as object, selectData as object)
  
  variableName$ = resultsData["VariableName"]
  currentValue$ = resultsData["CurrentValue"]
  defaultValue$ = resultsData["DefaultValue"]
  mediaUrl$ = resultsData["MediaUrl"]
  
  ' access level isn't needed for this list
  userVariable = newUserVariable(m, variableName$, currentValue$, defaultValue$, mediaUrl$, "", "")
  selectData.userVariablesList.push(userVariable)
  
end sub


Function GetUserVariablesGivenCategory(sectionName$ as string, includeSharedSection as boolean, categoryName$ as string, sortByPosition as boolean) as object
  
  selectData = { }
  selectData.userVariablesList = []
  
  select$ = "SELECT Variables2.* FROM Variables2 INNER JOIN Sections ON Sections.SectionId=Categories.SectionReferenceId INNER JOIN Categories ON Categories.CategoryId=Variables2.CategoryReferenceId WHERE Categories.CategoryName='" + categoryName$ + "' AND ("
  
  if includeSharedSection then
    select$ = select$ + "Sections.SectionName='Shared' OR "
  end if
  
  select$ = select$ + "Sections.SectionName='" + sectionName$ + "')"
  
  if sortByPosition then
    select$ = select$ + " ORDER BY Variables2.Position"
  end if
  select$ = select$ + ";"
  
  m.ExecuteDBSelect(select$, GetUserVariablesGivenCategoryCallback, selectData, invalid)
  
  return selectData.userVariablesList
  
end function


Sub DoGetCategoriesCallback(resultsData as object, selectData as object)
  
  categoryName$ = resultsData["CategoryName"]
  if (categoryName$ <> "BrightAuthor") or (selectData.includeBrightAuthorCategory) then
    selectData.userVariableCategories.push(categoryName$)
  end if
  
end sub


Function DoGetCategories(sectionName$ as string, includeShared as boolean, includeBrightAuthorCategory) as object
  
  selectData = { }
  selectData.userVariableCategories = []
  selectData.includeBrightAuthorCategory = includeBrightAuthorCategory
  
  params = { : sn_param: sectionName$ }
  
  select$ = "SELECT CategoryName FROM Categories INNER JOIN Sections ON Sections.SectionId = Categories.SectionReferenceId WHERE "
  if includeShared then
    select$ = select$ + "Sections.SectionName = 'Shared' OR "
  end if
  
  select$ = select$ + "Sections.SectionName =:sn_param;"
  
  selectStmt = m.userVariablesDB.CreateStatement(select$)
  
  if type(selectStmt) <> "roSqliteStatement" then
    m.diagnostics.PrintDebug("CreateStatement failure - " + select$)
    stop
  end if
  
  bindResult = selectStmt.BindByName(params)
  
  if not bindResult then
    m.diagnostics.PrintDebug("Bind failure")
    stop
  end if
  
  m.ExecuteDBSelect(select$, DoGetCategoriesCallback, selectData, params)
  
  return selectData.userVariableCategories
  
end function


Sub ReadVariablesCallback(resultsData as object, selectData as object)
  
  sectionName$ = resultsData["SectionName"]
  
  if sectionName$ = selectData.presentationName$ or sectionName$ = "Shared" then
    
    variableName$ = resultsData["VariableName"]
    currentValue$ = resultsData["CurrentValue"]
    defaultValue$ = resultsData["DefaultValue"]
    mediaUrl$ = resultsData["MediaUrl"]
    
    if sectionName$ = "Shared" then
      access$ = "Shared"
    else
      access$ = "Private"
    end if
    
    userVariable = newUserVariable(selectData.bsp, variableName$, currentValue$, defaultValue$, mediaUrl$, access$, "")
    userVariable.position% = -1
    selectData.currentUserVariables.AddReplace(variableName$, userVariable)
    
  end if
  
end sub


Function ReadVariables(presentationName$ as string) as object
  
  selectData = { }
  selectData.bsp = m
  selectData.presentationName$ = presentationName$
  selectData.currentUserVariables = { }
  
  select$ = "SELECT Sections.SectionName, Variables2.VariableName, Variables2.CurrentValue, Variables2.DefaultValue, Variables2.MediaUrl FROM Variables2 INNER JOIN Sections ON Sections.SectionId=Categories.SectionReferenceId INNER JOIN Categories ON Categories.CategoryId=Variables2.CategoryReferenceId WHERE Categories.CategoryName='BrightAuthor' ORDER BY Sections.SectionName;"
  
  m.ExecuteDBSelect(select$, ReadVariablesCallback, selectData, invalid)
  
  return selectData.currentUserVariables
  
end function


Sub SetDBVersion(version$ as string)
  
  insertSQL$ = "INSERT INTO SchemaVersion (Version) VALUES(:version_param);"
  
  params = { version_param: version$ }
  
  m.ExecuteDBInsert(insertSQL$, params)
  
end sub


Sub UpdateDBVersion(version$ as string)
  
  params = { v_param: version$ }
  
  m.userVariablesDB.RunBackground("UPDATE SchemaVersion SET Version=:v_param;", params)
  
end sub


Sub CreateDBTable(statement$ as string)
  
  SQLITE_COMPLETE = 100
  
  createStmt = m.userVariablesDB.CreateStatement(statement$)
  
  if type(createStmt) <> "roSqliteStatement" then
    m.diagnostics.PrintDebug("CreateStatement failure - " + statement$)
    stop
  end if
  
  sqlResult = createStmt.Run()
  
  if sqlResult <> SQLITE_COMPLETE
    m.diagnostics.PrintDebug("sqlResult <> SQLITE_COMPLETE")
  end if
  
  createStmt.Finalise()
  
end sub


Sub DeleteDBTable(tableName$ as string)
  
  SQLITE_COMPLETE = 100
  
  deleteStmt = m.userVariablesDB.CreateStatement("DROP TABLE " + tableName$)
  
  if type(deleteStmt) <> "roSqliteStatement" then
    m.diagnostics.PrintDebug("CreateStatement failure - DeleteDBTable")
    stop
  end if
  
  sqlResult = deleteStmt.Run()
  
  if sqlResult <> SQLITE_COMPLETE
    m.diagnostics.PrintDebug("sqlResult <> SQLITE_COMPLETE")
  end if
  
  deleteStmt.Finalise()
  
end sub


Sub CreateSchema2Tables()
  
  m.CreateDBTable("CREATE TABLE Categories (CategoryId INTEGER PRIMARY KEY AUTOINCREMENT, SectionReferenceId INT, CategoryName TEXT);")
  
  m.CreateDBTable("CREATE TABLE Variables2 (VariableId INTEGER PRIMARY KEY AUTOINCREMENT, CategoryReferenceId INT, VariableName text, CurrentValue TEXT, DefaultValue TEXT, MediaUrl TEXT, Position INT);")
  
end sub


Sub DropSchema1Tables()
  
  m.DeleteDBTable("Variables")
  
end sub


Function DBTablesExist(existingTables, expectedTables) as boolean
  
  for each tableName in expectedTables
    if not existingTables.DoesExist(tableName) then
      m.diagnostics.PrintDebug("Table " + tableName + " does not exist in userVariables.db - reset db")
      return false
    end if
  end for
  
  return true
  
end function


Function DbIsValid() as boolean
  
  ' Check for validity of userVariables DB - not an exhaustive check, it merely checks to ensure that the appropriate
  ' tables are present
  '
  ' Check for existence of schema table. If it doesn't exist, db is invalid
  ' If it exists, check its value and base subsequent checks on the version
  ' Check to ensure that the expected tables exist
  
  tables = m.GetDBTableNames()
  
  ' common tables
  commonTables = ["SchemaVersion", "Sections"]
  schema1Tables = ["Variables"]
  schema2Tables = ["Variables2", "Categories"]
  
  if not m.DBTablesExist(tables, commonTables) then
    return false
  end if
  
  currentSchemaVersion$ = "2.0"
  existingSchemaVersion$ = m.GetDBVersion()
  
  ' check version of existing db schema to determine what tables to check for
  if existingSchemaVersion$ <> currentSchemaVersion$ then
    tablesExist = m.DBTablesExist(tables, schema1Tables)
  else
    tablesExist = m.DBTablesExist(tables, schema2Tables)
  end if
  
  return tablesExist
  
end function


Sub ReadVariablesDB(presentationName$ as string)
  
  SQLITE_ROWS = 102
  
  m.variablesDBExists = true
  
  m.dbSchemaVersion$ = "2.0"

  if type(m.userVariablesDB) <> "roSqliteDatabase" then
    
    m.userVariablesDB = CreateObject("roSqliteDatabase")
    m.userVariablesDB.SetPort(m.msgPort)
    
    m.diagnostics.PrintDebug("Open userVariables.db")
    
    ok = m.userVariablesDB.Open("userVariables.db")
    
    if ok then
      ok = m.DBIsValid()
    end if
    
    if ok then
      
      version$ = m.GetDBVersion()
      
      if version$ <> m.dbSchemaVersion$ then
        
        userVariableSets = m.ReadSchema1Tables()
        
        m.CreateSchema2Tables()
        
        ' store old data in new tables
        for each sectionName in userVariableSets
          sectionId% = m.GetDBSectionId(sectionName)
          m.AddDBCategory(sectionId%, "BrightAuthor")
          categoryId% = m.GetDBCategoryId(sectionId%, "BrightAuthor")
          userVariables = userVariableSets.Lookup(sectionName)
          position% = 0
          for each userVariable in userVariables
            m.AddDBVariable(categoryId%, userVariable.name$, userVariable.defaultValue$, "", position%)
            if userVariable.currentValue$ <> userVariable.defaultValue$ then
              m.UpdateDBVariable(categoryId%, userVariable.name$, userVariable.currentValue$)
            end if
            position% = position% + 1
          next
        next
        
        ' drop old tables
        m.DropSchema1Tables()
        
        ' update version
        m.UpdateDBVersion(m.dbSchemaVersion$)
        
      end if
      
    else
      
      ' in case there is a corrupt file
      m.diagnostics.PrintDebug("Unable to open userVariables.db, attempt to delete file then create db.")
      m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_DELETE_USER_VARIABLES_DB, "")
      ok = DeleteFile("userVariables.db")
      
      ok = m.userVariablesDB.Create("userVariables.db")
      if not ok then
        m.diagnostics.PrintDebug("Unable to create userVariables.db")
        return
      end if
      
      m.CreateDBTable("CREATE TABLE SchemaVersion (Version TEXT);")
      m.SetDBVersion(m.dbSchemaVersion$)
      
      m.CreateDBTable("CREATE TABLE Sections (SectionId INTEGER PRIMARY KEY AUTOINCREMENT, SectionName TEXT);")
      
      m.CreateSchema2Tables()
      
    end if
    
  end if
  
  ' get sections, variables for BrightAuthor category
  m.currentUserVariables = m.ReadVariables(presentationName$)
  
end sub


Function GetUserVariablesByCategoryList(categoryName$ as string) as object
  return m.GetUserVariablesGivenCategory(m.activePresentation$, true, categoryName$, true)
end function


Function GetDBCategoryNames(sectionName$ as string) as object
  return m.DoGetCategories(sectionName$, false, true)
end function


Function GetUserVariableCategoryList(sectionName$ as string) as object
  return m.DoGetCategories(sectionName$, true, false)
end function


Function GetOrderedVariables(sectionName$ as string) as object
  return m.GetUserVariablesGivenCategory(m.activePresentation$, true, "BrightAuthor", false)
end function


Sub ExportVariablesDBToAsciiFile(file as object)
  
  file.SendLine("Version" + chr(9) + m.dbSchemaVersion$)
  sectionNames = m.GetDBSectionNames()
  for each sectionName in sectionNames
    file.SendLine("Section" + chr(9) + sectionName)
    sectionId% = m.GetDBSectionId(sectionName)
    if sectionId% > 0 then
      categoryNames = m.GetDBCategoryNames(sectionName)
      for each categoryName in categoryNames
        file.SendLine("Category" + chr(9) + categoryName)
        userVariablesList = m.GetUserVariablesGivenCategory(sectionName, false, categoryName, false)
        for each userVariable in userVariablesList
          file.SendLine(userVariable.name$ + chr(9) + userVariable.currentValue$ + chr(9) + userVariable.defaultValue$)
        next
      next
    end if
  next
  
end sub


Function GetUserVariable(variableName$ as string) as object
  
  userVariable = invalid
  
  if m.currentUserVariables.DoesExist(variableName$) then
    userVariable = m.currentUserVariables.Lookup(variableName$)
  end if
  
  return userVariable
  
end function


Sub DeleteVariable(variableName$ as string)
  
  userVariables = m.currentUserVariables
  
  if userVariables.DoesExist(variableName$) then
    
    userVariable = userVariables.Lookup(variableName$)
    if lcase(userVariable.access$) = "shared" then
      categoryId% = m.sharedBrightAuthorCategoryId%
    else
      categoryId% = m.privateBrightAuthorCategoryId%
    end if
    
    m.DeleteDBVariable(categoryId%, variableName$)
    userVariables.Delete(variableName$)
    
  end if
  
  m.SendUDPNotification("refresh")
  
end sub


Sub ResetVariables()
  
  userVariables = m.currentUserVariables
  
  userVariableList = CreateObject("roList")
  for each variableName in userVariables
    userVariable = userVariables.Lookup(variableName)
    userVariableList.AddTail(userVariable)
  next
  
  for each userVariable in userVariableList
    userVariable.Reset(false)
  next
  
  userVariablesReset = { }
  userVariablesReset["EventType"] = "USER_VARIABLES_RESET"
  m.msgPort.PostMessage(userVariablesReset)
  
  m.SendUDPNotification("refresh")
  
end sub


Sub AssignSystemVariableToUserVariables(userVariable as object)

  suffix$ = "$"
  videoConnector$ = getVarFromObj(userVariable, "videoConnector$", "roString", "")
  if videoConnector$ <> "" then suffix$ = "_" + videoConnector$ + "$"

  if userVariable.systemVariable$ = "SerialNumber" then
    userVariable.SetCurrentValue(m.sysInfo.deviceUniqueID$, false)
  else if userVariable.systemVariable$ = "IPAddressWired" then
    userVariable.SetCurrentValue(m.sysInfo.ipAddressWired$, false)
  else if userVariable.systemVariable$ = "IPAddressWireless" then
    userVariable.SetCurrentValue(m.sysInfo.ipAddressWireless$, false)
  else if userVariable.systemVariable$ = "FirmwareVersion" then
    userVariable.SetCurrentValue(m.sysInfo.deviceFWVersion$, false)
  else if userVariable.systemVariable$ = "ScriptVersion" then
    userVariable.SetCurrentValue(m.sysInfo.autorunVersion$, false)
  else if userVariable.systemVariable$ = "EdidMonitorSerialNumber" then
    userVariable.SetCurrentValue(m.sysInfo["edidMonitorSerialNumber"+suffix$], false)
  else if userVariable.systemVariable$ = "EdidYearOfManufacture" then
    userVariable.SetCurrentValue(m.sysInfo["edidYearOfManufacture"+suffix$], false)
  else if userVariable.systemVariable$ = "EdidMonitorName" then
    userVariable.SetCurrentValue(m.sysInfo["edidMonitorName"+suffix$], false)
  else if userVariable.systemVariable$ = "EdidManufacturer" then
    userVariable.SetCurrentValue(m.sysInfo["edidManufacturer"+suffix$], false)
  else if userVariable.systemVariable$ = "EdidUnspecifiedText" then
    userVariable.SetCurrentValue(m.sysInfo["edidUnspecifiedText"+suffix$], false)
  else if userVariable.systemVariable$ = "EdidSerialNumber" then
    userVariable.SetCurrentValue(m.sysInfo["edidSerialNumber"+suffix$], false)
  else if userVariable.systemVariable$ = "EdidManufacturerProductCode" then
    userVariable.SetCurrentValue(m.sysInfo["edidManufacturerProductCode"+suffix$], false)
  else if userVariable.systemVariable$ = "EdidWeekOfManufacture" then
    userVariable.SetCurrentValue(m.sysInfo["edidWeekOfManufacture"+suffix$], false)
  else if userVariable.systemVariable$ = "ActivePresentation" then
    if IsString(m.activePresentation$) then
      userVariable.SetCurrentValue(m.activePresentation$, false)
    else
      userVariable.SetCurrentValue("", false)
    end if
  else if userVariable.systemVariable$ = "BrightAuthorVersion" then
    userVariable.SetCurrentValue(m.sysInfo.baconVersion$, false)
  else if userVariable.systemVariable$ = "PlayerModelNumber" then
    userVariable.SetCurrentValue(m.sysInfo.deviceModel$, 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


Sub GenerateSessionGuid()

  ' Generate uuidv1 from scratch
  ' Ref: https://datatracker.ietf.org/doc/html/rfc4122
  ' Ref: http://guid.one/guid/make

  ' Convert time stamp to bytes
  hexTime = GetHexTime()


  timeLow = Right(hexTime, 8)
  timeMid = Mid(hexTime, 4, 4)
  timeHiAndVersion = Left(hexTime, 4)

  ' Set the version. Take the 7th byte perform an and operation 
  ' with 0x0f followed by an or operation of 0x10.
  seventhByte = Mid(timeHiAndVersion, 0, 2)
  seventhByte = HexLogicOperator(seventhByte, "0f", "and")
  seventhByte = HexLogicOperator(seventhByte, "10", "or")
  timeHiAndVersion = seventhByte + Right(timeHiAndVersion, 2)

  clockSeq = GetClockSequence()

  ' Set the variant. Take the 9th byte perform an and operation 
  ' with 0x3f followed by an or operation of 0x80.
  ninthByte = Mid(clockSeq, 0, 2)
  ninthByte = HexLogicOperator(ninthByte, "3f", "and")
  ninthByte = HexLogicOperator(ninthByte, "80", "or")
  clockSeq = ninthByte + Right(clockSeq, 2)

  node = GetNode()

  uuid = lcase(timeLow + "-" + timeMid + "-" + timeHiAndVersion + "-" + clockSeq + "-" + node)
  m.sysInfo["sessionGuid$"] = uuid

end sub


Function HexLogicOperator(hex1 as String, hex2 as String, operator as String) as String
    
    if operator <> "and" and operator <> "or" then
        print "Invalid operator"
        return ""
    end if

    ' Convert the hex strings to decimal
    dec1 = HexToDec(hex1)
    dec2 = HexToDec(hex2)

    ' Perform the logical operation
    if operator = "and" then
        result = dec1 And dec2
    else
        result = dec1 Or dec2
    end if

    return DecToHex(result)

end function


Function HexToDec(hexString As String) As Integer
    
    hexString = UCase(hexString)

    decimal = 0
    length = Len(hexString)

    for i = 1 to length
        digit = Mid(hexString, i, 1)
        value = HexDigitToDec(digit)
        
        if value = -1 then
            ' Invalid hexadecimal digit
            return -1
        end if
        
        decimal = decimal * 16 + value
    next

    return decimal

end function


Function GetHexTime() as String

  systemTime = CreateObject("roSystemTime")
  currentTime = systemTime.GetUtcDateTime()

  unixSec = currentTime.ToSecondsSinceEpoch()
  unixMilisec = currentTime.GetMillisecond()
  
  hexSec = DecToHex(unixSec)
  hexMilisec = DecToHex(unixMilisec)

  ' Get the time between Epoch and 1582-10-15 00:00:00 UTC
  ' Seconds in decimal is 12219292800, hex is 2d8539c80
  hexSecondsBetweenEpochAnd1582 = "2d8539c80"

  hexTime = HexAddition(hexSecondsBetweenEpochAnd1582, hexSec)
  ' Convert to ms and add the milliseconds, multiple by 1000
  hexTime = HexMultiply(hexTime, "3e8")
  hexTime = HexAddition(hexTime, hexMilisec)

  ' Convert to 100-nanosecond, multiple by 10,000
  hexTime = HexMultiply(hexTime, "2710")
  
  ' Check the length of the hexTime, and pad/trim to 16 bytes
  return FixHexTimeLength(hexTime)

end function


Function FixHexTimeLength(hexTime as String) as String

    ' Check the length of the hexTime, and pad/trim to 16 bytes
    if Len(hexTime) > 16 then
        hexTime = Left(hexTime, 16)
    else if Len(hexTime) < 16 then
        hexTime = PadLeft(hexTime, 16)
    end if

    return hexTime

end function


Function DecToHex(decNum as Double) as String

    ' Divide the nubmer by 16 if it is greater than largest 32-bit integer
    if decNum > 2147483647 then
        decNum = decNum / 16
        hex = DecToHex(decNum)
        return HexMultiply(hex, "10")
    end if

    ' The function only supports positive integers or zero
    if decNum <= 0 then
        return "0"
    end if

    ' Convert a decimal number to hexadecimal
    hexNum = ""
    hexDigits = "0123456789ABCDEF"

    ' Perform division and append hex digits in reverse order
    while decNum > 0
        remainder = decNum Mod 16
        hexDigit = Mid(hexDigits, remainder + 1, 1)
        hexNum = hexDigit + hexNum
        decNum = int(decNum / 16)
    end while

    ' Return the hexadecimal result
    return hexNum

end function


Function HexAddition(hexNum1 as String, hexNum2 as String) as String

    ' Pad the shorter number with leading zeros
    maxLength = MaxInteger(Len(hexNum1), Len(hexNum2))
    hexNum1 = PadLeft(hexNum1, maxLength)
    hexNum2 = PadLeft(hexNum2, maxLength)

    ' Perform hexadecimal addition digit by digit
    carry = 0
    hexResult = ""

    for i = maxLength - 1 to 0 step -1
        digit1 = HexDigitToDec(Mid(hexNum1, i + 1, 1))
        digit2 = HexDigitToDec(Mid(hexNum2, i + 1, 1))

        sum = digit1 + digit2 + carry

        ' Calculate the carry and the result digit
        carry = Int(sum / 16)
        resultDigit = DecToHex(sum Mod 16)
        hexResult = resultDigit + hexResult
    end for

    ' Add the final carry if necessary
    if carry > 0 then
        hexResult = DecToHex(carry) + hexResult
    end if

    ' Return the hexadecimal result
    return hexResult

end function


Function MaxInteger(a as Integer, b as Integer) as Integer

    if (b > a) then
      return b
    else
      return a
    end if

end function


Function HexMultiply(hexNum1 as String, hexNum2 as String) as String

    hexNum1 = UCase(hexNum1)
    hexNum2 = UCase(hexNum2)

    product = []

    for i = Len(hexNum2) to 1 Step -1
        digit2 = HexDigitToDec(Mid(hexNum2, i, 1))
        carry = 0
        tempProduct = []

        for j = Len(hexNum1) to 1 Step -1
            digit1 = HexDigitToDec(Mid(hexNum1, j, 1))
            productDigit = digit1 * digit2 + carry
            remainder = productDigit Mod 16
            carry = Int(productDigit / 16)
            tempProduct.unshift(DecToHex(remainder))
        next

        if carry > 0 then
            tempProduct.unshift(DecToHex(carry))
        end if

        padding = PadLeft("", Len(hexNum2) - i)
        tempProduct.Push(padding)
        product.Push(ArrayToString(tempProduct))
    next

    result = "0"
    for each num In product
        result = HexAddition(result, num)
    next

    return result

end function


Function ArrayToString(arr as Object) as String

    ' Convert an array to a string
    str = ""
    for i = 0 to arr.Count() - 1
        str = str + arr[i]
    next
    if str = "" return "0"
    return str

end function


Function HexDigitToDec(hexDigit as String) as Integer

    ' Convert a single hexadecimal digit to decimal
    hexDigits = "0123456789ABCDEF"
    return InStr(hexDigits, UCase(hexDigit)) - 1

end function


Function PadLeft(str as String, length as Integer) as String

    ' Pad the left side of a string with leading zeros
    padding = String(length - Len(str), "0")
    return padding + str

end function


Function GetClockSequence() as String

    di = CreateObject("roDeviceInfo")
    return Left(di.GetDeviceUniqueId(), 4)

end function


Function GetNode() as String

    nc = CreateObject("roNetworkConfiguration", 0)
    address = nc.GetCurrentConfig().ethernet_mac
    address = ReplaceString(address, ":", "")
    return address

end function


Function ReplaceString (str as String, find as String, replace as String) as String
    
    regex = CreateObject("roRegEx", find, "i")
    return regex.ReplaceAll(str, replace)
    
end function


Sub ClearSessionGuid()

  m.sysInfo["sessionGuid$"] = ""

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.language$ = globalVariables.language$
  
  zoneHSM.playlist = newPlaylist(bsp, zoneHSM, sign, zoneDescription.playlist)
  
  zoneHSM.playbackActive = false
  return zoneHSM
  
end function


Function newPlaylist(bsp as object, zoneHSM as object, sign as object, playlistDescription as object) as object
  
  playlistBS = { }
  
  playlistBS.name$ = playlistDescription.name
  
  ' get states
  
  stateDescriptionList = playlistDescription.stateDescriptions
  if type(stateDescriptionList) <> "roArray" then print "Invalid autoplay file - state list not found" : stop
  
  initialMediaStateName = playlistDescription.initialMediaStateName
  
  if zoneHSM.type$ = "Ticker" then
    
    zoneHSM.rssDataFeedItems = []
    
    for each stateDescription in stateDescriptionList
      tickerItem = newTickerItem(bsp, zoneHSM, stateDescription)
      if tickerItem <> invalid then
        zoneHSM.rssDataFeedItems.push(tickerItem)
      end if
    next
    
  else
    
    zoneHSM.transitionDescriptions = []
    
    zoneHSM.stateTable = { }
    for each stateDescription in stateDescriptionList
      bsState = newState(bsp, zoneHSM, sign, stateDescription, invalid)
    next
    
    initialMediaStateName = playlistDescription.initialMediaStateName
    
    ' find the initial state for the playlist
    for each stateName in zoneHSM.stateTable
      bsState = zoneHSM.stateTable[stateName]
      if bsState.id$ = initialMediaStateName then
        playlistBS.firstState = GetInitialState(zoneHSM, bsState)
        exit for
      end if
    next
    
    ' find the initial states for each superstate
    allStates = CreateObject("roArray", 1, true)
    for each stateName in zoneHSM.stateTable
      allStates.push(stateName)
    next
    
    for each stateName in allStates
      bsState = zoneHSM.stateTable[stateName]
      if bsState.type$ = "superState" then
        initialStateName$ = bsState.initialStateName$
        for each innerStateName in zoneHSM.stateTable
          innerState = zoneHSM.stateTable[innerStateName]
          if innerState.id$ = initialStateName$ then
            bsState.firstState = GetInitialState(zoneHSM, innerState)
            exit for
          end if
        next
      end if
    next
    
    ' get transitions from top level states
    for each transitionDescription in playlistDescription.transitionDescriptions
      newTransition(bsp, zoneHSM, sign, transitionDescription)
    next
    
    ' get transitions from super states
    for each transitionDescription in zoneHSM.transitionDescriptions
      newTransition(bsp, zoneHSM, sign, transitionDescription)
    next
    
  end if
  
  return playlistBS
  
end function


Function GetInitialState(zoneHSM as object, state as object) as object
  
  if state.type$ <> "superState" then
    return state
  end if
  
  initialStateName$ = state.initialStateName$
  
  for each stateName in zoneHSM.stateTable
    state = zoneHSM.stateTable[stateName]
    if state.id$ = initialStateName$ then
      return GetInitialState(zoneHSM, state)
    end if
  next
  
end function


Function ConvertToByteArray(input$ as string) as object
  
  inputSpec = CreateObject("roByteArray")
  
  ' convert serial$ into byte array
  byteString$ = StripLeadingSpaces(input$)
  commaPosition = -1
  while commaPosition <> 0
    commaPosition = instr(1, byteString$, ",")
    if commaPosition = 0 then
      byteValue = val(byteString$)
    else
      byteValue = val(left(byteString$, commaPosition - 1))
    end if
    inputSpec.push(byteValue)
    byteString$ = mid(byteString$, commaPosition + 1)
  end while
  
  return inputSpec
  
end function


Function GetSimpleEventDataFromUserEvent(userEvent as object) as object
  return userEvent.data.data
end function


Sub newTransition(bsp as object, zoneHSM as object, sign as object, transitionSpec as object)

  stateTable = zoneHSM.stateTable
  
  sourceMediaState$ = transitionSpec.sourceMediaState
  
  ' given the sourceMediaState, find the associated bsState
  bsState = stateTable.Lookup(sourceMediaState$)
  
  userEventName$ = transitionSpec.userEvent.name
  userEventData = transitionSpec.userEvent.data
  
  nextMediaState$ = transitionSpec.targetMediaState
  
  transition = { }
  transition.AssignEventInputToUserVariable = AssignEventInputToUserVariable
  transition.AssignWildcardInputToUserVariable = AssignWildcardInputToUserVariable
  
  transition.targetMediaState$ = nextMediaState$
  transition.targetMediaStateIsPreviousState = transitionSpec.targetMediaStateIsPreviousState
  transition.remainOnCurrentStateActions = transitionSpec.remainOnCurrentStateActions
  
  ' if the transition points to a superstate, point it to the first state for the superstate instead
  if transition.targetMediaState$ <> "" then
    targetState = zoneHSM.stateTable[transition.targetMediaState$]
    if targetState.type$ = "superState" and targetState.firstState <> invalid then
      transition.targetMediaState$ = targetState.firstState.id$
    end if
  end if
  
  transition.assignInputToUserVariable = transitionSpec.assignInputToUserVariable
  if transition.assignInputToUserVariable then
    if IsString(transitionspec.variableToAssignFromInput$) then
      transition.variableToAssignFromInput$ = transitionSpec.variableToAssignFromInput$
      transition.variableToAssignFromInput = bsp.GetUserVariable(transition.variableToAssignFromInput$)
    else
      transition.variableToAssignFromInput = invalid
    end if
  else
    transition.variableToAssign$ = ""
  end if
  
  transition.assignWildcardToUserVariable = transitionSpec.assignWildcardToUserVariable
  if transition.assignWildcardToUserVariable then
    transition.variableToAssignFromWildcard = invalid
    if IsString(transitionspec.variableToAssign$) then
      variableToAssign$ = transitionSpec.variableToAssign$
      if variableToAssign$ <> "" then
        transition.variableToAssignFromWildcard = bsp.GetUserVariable(variableToAssign$)
        if transition.variableToAssignFromWildcard = invalid then
          bsp.diagnostics.PrintDebug("User variable " + variableToAssign$ + " not found.")
        end if
      end if
    end if
  end if
  
  if userEventName$ = "gpioUserEvent" then
    
    transition.buttonPanelIndex% = userEventData.buttonPanelIndex%
    transition.buttonNumber$ = userEventData.buttonNumber$
    transition.buttonDirection$ = userEventData.buttonDirection$
    
    transition.configuration$ = userEventData.configuration$
    
    if transition.configuration$ = "pressContinuous" then
      transition.initialHoldoff$ = userEventData.initialHoldoff$
      transition.repeatInterval$ = userEventData.repeatInterval$
    end if
    
    bsp.ConfigureGPIOInput(transition.buttonNumber$)
    
    if transition.buttonDirection$ = "down" then
      bsState.gpioEvents[transition.buttonNumber$] = transition
    else
      bsState.gpioUpEvents[transition.buttonNumber$] = transition
    end if
    
  else if userEventName$ = "bp900AUserEvent" or userEventName$ = "bp900BUserEvent" or userEventName$ = "bp900CUserEvent" or userEventName$ = "bp900DUserEvent" or userEventName$ = "bp200AUserEvent" or userEventName$ = "bp200BUserEvent" or userEventName$ = "bp200CUserEvent" or userEventName$ = "bp200DUserEvent" then
    
    transition.configuration$ = userEventData.configuration$
    transition.buttonPanelIndex% = userEventData.buttonPanelIndex%
    transition.buttonNumber$ = userEventData.buttonNumber$
    
    if transition.configuration$ = "pressContinuous" then
      transition.initialHoldoff$ = userEventData.initialHoldoff$
      transition.repeatInterval$ = userEventData.repeatInterval$
    end if
    
    bsp.ConfigureBPInput(transition.buttonPanelIndex%, transition.buttonNumber$)
    
    bpEvents = bsState.bpEvents
    currentBPEvent = bpEvents[transition.buttonPanelIndex%]
    currentBPEvent.AddReplace(transition.buttonNumber$, transition)
    
  else if userEventName$ = "gpsEvent" then
    eventDirection$ = userEventData.regionEventData.direction
    transition.radiusInFeet = userEventData.regionEventData.radiusInFeet
    transition.latitude = userEventData.regionEventData.latitude
    transition.latitudeInRadians = ConvertDecimalDegtoRad(transition.latitude)
    transition.longitude = userEventData.regionEventData.longitude
    transition.longitudeInRadians = ConvertDecimalDegtoRad(transition.longitude)
    
    if lcase(eventDirection$) = "enter" then
      bsState.gpsEnterRegionEvents.push(transition)
    else
      bsState.gpsExitRegionEvents.push(transition)
    end if
    
  else if userEventName$ = "serial" then
    
    userEventData = transitionSpec.userEvent.data
    
    port$ = StripLeadingSpaces(userEventData.port)
    port$ = m.bsp.GetRuntimeUsbConnector(port$)

    serial$ = userEventData.data

    if IsUsbPort(port$) then
      usbSpec = GetGlobalAA().usbConnectorNameToUsbSpec.Lookup(port$)
      if GetGlobalAA().usbHIDPortConfigurations.DoesExist(port$) then
        usbHIDPortConfiguration = GetGlobalAA().usbHIDPortConfigurations[port$]
        protocol$ = usbHIDPortConfiguration.protocol$
      else
        protocol$ = ""
      end if
      eventKey$ = port$
    else
      port% = int(val(port$))
      serialPortConfiguration = sign.serialPortConfigurations[port%]
      protocol$ = serialPortConfiguration.protocol$
      eventKey$ = port$
    end if
    
    serialEvents = bsState.serialEvents
    if type(serialEvents[eventKey$]) <> "roAssociativeArray" then
      serialEvents[eventKey$] = { }
    end if
    
    if protocol$ = "Binary" then
      if type(serialEvents[eventKey$].streamInputTransitionSpecs) <> "roArray" then
        serialEvents[eventKey$].streamInputTransitionSpecs = CreateObject("roArray", 1, true)
      end if
      
      streamInputTransitionSpec = { }
      streamInputTransitionSpec.transition = transition
      streamInputTransitionSpec.inputSpec = ConvertToByteArray(serial$)
      streamInputTransitionSpec.asciiSpec = serial$
      serialEvents[eventKey$].streamInputTransitionSpecs.push(streamInputTransitionSpec)
      
    else if protocol$ <> "" then
      serialPortEvents = serialEvents[eventKey$]
      serialPortEvents[serial$] = transition
    end if
    
  else if userEventName$ = "timeout" then
    
    bsState.mstimeoutValue% = userEventData.interval * 1000
    bsState.mstimeoutEvent = transition
    
  else if userEventName$ = "bmapHexEvent" then

    port$ = userEventData.port

    bmapHexString$ = userEventData.data

    bmapHexInputEvents = bsState.bmapHexInputEvents
    if type(bmapHexInputEvents[port$]) <> "roAssociativeArray" then
      bmapHexInputEvents[port$] = CreateObject("roAssociativeArray")
    end if

    bmapHexEventsForPort = bmapHexInputEvents[port$]
    bmapHexEventsForPort[bmapHexString$] = transition

  else if userEventName$ = "bmapEvent" then

    if type(bsState.bmapEvents) <> "roArray" then
      bsState.bmapEvents = []
    endif

    bmapEvent = {}
    bmapEvent.transition = transition

    port$ = userEventData.port
    bmapEvent.port = port$

    bmapEvent.functionBlockName = userEventData.functionBlock
    bmapEvent.functionName = userEventData.function
    bmapEvent.operatorName = userEventData.operator
    bmapEvent.fieldName = userEventData.field
    bmapEvent.bitfieldName = userEventData.bitfield
   
    bmapEvent.fieldValue = GetFieldValueAsStr(userEventData.data)

    bsState.bmapEvents.push(bmapEvent)

  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$ = "liveVideo" then
      
      bsState.videoEndEvent = transition
      
    else if bsState.type$ = "audio" then
      
      bsState.audioEndEvent = transition
      
    else if bsState.type$ = "mediaRSS" then
      
      bsState.signChannelEndEvent = transition
      
    else if bsState.type$ = "mediaList" then
      
      bsState.videoEndEvent = transition
      bsState.audioEndEvent = transition
      
    else if bsState.type$ = "playFile" then
      
      bsState.videoEndEvent = transition
      bsState.audioEndEvent = transition
      
    else if bsState.type$ = "stream" then
      
      bsState.videoEndEvent = transition
      bsState.audioEndEvent = transition
      
    else if bsState.type$ = "mjpeg" then
      
      bsState.videoEndEvent = transition
      
    else if bsState.type$ = "superState" then
      
      bsState.mediaEndEvent = transition
      
    end if
    
  else if userEventName$ = "mediaListEnd" then
    
    bsState.mediaListEndEvent = transition
    
  else if userEventName$ = "keyboard" then
    
    keyboardChar$ = userEventData.data
    
    if len(keyboardChar$) > 1 then
      keyboardChar$ = Lcase(keyboardChar$)
    end if
    
    if type(bsState.keyboardEvents) <> "roAssociativeArray" then
      bsState.keyboardEvents = { }
    end if
    
    bsState.keyboardEvents[keyboardChar$] = transition
    
  else if userEventName$ = "remote" then
  
    remote$ = ucase(userEventData.data)
    
    if type(bsState.remoteEvents) <> "roAssociativeArray" then
      bsState.remoteEvents = { }
    end if

    bsState.remoteEvents[remote$] = transition
    
  else if userEventName$ = "usb" then
    
    usbString$ = userEventData.data
    
    if type(bsState.usbStringEvents) <> "roAssociativeArray" then
      bsState.usbStringEvents = { }
    end if
    
    bsState.usbStringEvents[usbString$] = transition
    
  else if userEventName$ = "udp" then
    
    if type(bsState.udpEvents) <> "roAssociativeArray" then
      bsState.udpEvents = { }
    end if
    
    transition.udpLabel$ = userEventData.label
    transition.udpExport = userEventData.export
    
    bsState.udpEvents[userEventData.data] = transition
    
  else if userEventName$ = "synchronize" then
    
    bsp.IsSyncSlave = true
    
    synchronize$ = userEventData.data
    
    if type(bsState.synchronizeEvents) <> "roAssociativeArray" then
      bsState.synchronizeEvents = { }
    end if
    
    bsState.synchronizeEvents[synchronize$] = transition
    
  else if userEventName$ = "zoneMessage" then
    
    if type(bsState.zoneMessageEvents) <> "roAssociativeArray" then
      bsState.zoneMessageEvents = { }
    end if
    
    bsState.zoneMessageEvents[userEventData.data] = transition
    
  else if userEventName$ = "pluginMessageEvent" then
    
    pluginName$ = userEventData.name
    pluginMessage$ = userEventData.data
    ' unique key is concatenation of plugin name and plugin message
    key$ = pluginName$ + pluginMessage$
    
    if type(bsState.pluginMessageEvents) <> "roAssociativeArray" then
      bsState.pluginMessageEvents = { }
    end if
    
    bsState.pluginMessageEvents[key$] = transition
    
  else if userEventName$ = "internalSynchronize" then
    
    internalSynchronize$ = userEventData.data
    
    if type(bsState.internalSynchronizeEvents) <> "roAssociativeArray" then
      bsState.internalSynchronizeEvents = { }
    end if
    
    bsState.internalSynchronizeEvents[internalSynchronize$] = transition
    
  else if userEventName$ = "rectangularTouchEvent" then
    
    if type(bsState.touchEvents) <> "roAssociativeArray" then
      bsState.touchEvents = { }
    end if
    
    transition.x% = userEventData.x
    transition.y% = userEventData.y
    transition.width% = userEventData.width
    transition.height% = userEventData.height
    
    if sign.flipCoordinates then
      videoMode = CreateObject("roVideoMode")
      resX = videoMode.GetResX()
      resY = videoMode.GetResY()
      videoMode = invalid
      
      transition.x% = resX - (transition.x% + transition.width%)
      transition.y% = resY - (transition.y% + transition.height%)
    end if
    
    bsState.touchEvents[stri(sign.numTouchEvents%)] = transition
    sign.numTouchEvents% = sign.numTouchEvents% + 1
    
  else if userEventName$ = "audioTimeCodeEvent" then
    
    if type(bsState.audioTimeCodeEvents) <> "roAssociativeArray" then
      bsState.audioTimeCodeEvents = { }
    end if
    
    transition.timeInMS% = userEventData.eventTime
    bsState.audioTimeCodeEvents[stri(sign.numAudioTimeCodeEvents%)] = transition
    
    sign.numAudioTimeCodeEvents% = sign.numAudioTimeCodeEvents% + 1
    
  else if userEventName$ = "videoTimeCodeEvent" then
    
    if type(bsState.videoTimeCodeEvents) <> "roAssociativeArray" then
      bsState.videoTimeCodeEvents = { }
    end if
    
    transition.timeInMS% = userEventData.eventTime
    bsState.videoTimeCodeEvents[stri(sign.numVideoTimeCodeEvents%)] = transition
    
    sign.numVideoTimeCodeEvents% = sign.numVideoTimeCodeEvents% + 1
    
  end if
  
  
  ' get commands and conditional targets
  transition.conditionalTransitions = []
  for each conditionalTransitionSpec in transitionSpec.conditionalTransitions
    newConditionalTransition(bsp, zoneHSM, conditionalTransitionSpec, transition.conditionalTransitions)
  next
  
  transition.transitionCmds = []
  transitionCommands = transitionSpec.commands
  if transitionCommands.Count() > 0 then
    for each command in transitionCommands
      newCmd(bsp, command, transition.transitionCmds)
    next
  end if
  
  for each transitionCmd in transition.transitionCmds
    
    ' if the transition command is for an internal synchronize, add an event that the master will receive after it sends the preload command
    if transitionCmd.name$ = "internalSynchronize" then
      
      if type(transition.internalSynchronizeEventsMaster) <> "roAssociativeArray" then
        transition.internalSynchronizeEventsMaster = { }
      end if
      
      internalSynchronizeMasterTransition = { }
      internalSynchronizeMasterTransition.targetMediaState$ = nextMediaState$
      internalSynchronizeMasterTransition.targetMediaStateIsPreviousState = false
      
      '      transition.internalSynchronizeEventsMaster[transitionCmd.parameters["synchronizeKeyword"].GetCurrentParameterValue()] = internalSynchronizeMasterTransition
      transition.internalSynchronizeEventsMaster[transitionCmd.parameters["message"].GetCurrentParameterValue()] = internalSynchronizeMasterTransition
      
      ' modify this state's transition to not go to the next media state
      transition.targetMediaState$ = ""
      
    end if
  next
  
end sub


Function newConditionalTransition(bsp as object, zoneHSM as object, conditionalTransitionSpec as object, conditionalTransitions as object)
  
  userVariableName$ = conditionalTransitionSpec.variableName
  
  operator$ = conditionalTransitionSpec.compareOperator
  if operator$ = "" then operator$ = "EQ"
  
  targetMediaState$ = conditionalTransitionSpec.targetMediaState
  
  parameterValueSpec = jsonParseParameterValue(conditionalTransitionSpec.compareValue1)
  userVariableValue = newParameterValue(bsp, parameterValueSpec)
  
  parameterValueSpec2 = jsonParseParameterValue(conditionalTransitionSpec.compareValue2)
  userVariableValue2 = newParameterValue(bsp, parameterValueSpec2)
  
  userVariable = bsp.GetUserVariable(userVariableName$)
  if type(userVariable) = "roAssociativeArray" then
    conditionalTransition = { }
    conditionalTransition.userVariable = userVariable
    conditionalTransition.operator$ = operator$
    conditionalTransition.userVariableValue = userVariableValue
    conditionalTransition.userVariableValue2 = userVariableValue2
    
    conditionalTransition.targetMediaState$ = targetMediaState$
    if conditionalTransition.targetMediaState$ <> "" then
      targetState = zoneHSM.stateTable[conditionalTransition.targetMediaState$]
      if targetState.type$ = "superState" and targetState.firstState <> invalid then
        conditionalTransition.targetMediaState$ = targetState.firstState.id$
      end if
    end if
    
    targetAction = GetTargetActionFromEventAction(conditionalTransitionSpec.conditionalAction)
    conditionalTransition.remainOnCurrentStateActions = targetAction.remainOnCurrentStateActions
    conditionalTransition.targetMediaStateIsPreviousState = targetAction.targetMediaStateIsPreviousState
    
    conditionalTransition.transitionCmds = []
    for each command in conditionalTransitionSpec.commands
      newCmd(bsp, command, conditionalTransition.transitionCmds)
    next
    
    conditionalTransitions.push(conditionalTransition)
  else
    bsp.diagnostics.PrintDebug("User variable " + userVariableName$ + " not found.")
  end if
  
end function


Function GetTargetActionFromEventAction(eventAction as string) as object
  
  targetAction = { }
  
  if eventAction = "StopPlayback" then
    targetAction.targetMediaStateIsPreviousState = false
    targetAction.remainOnCurrentStateActions = "stop"
  else if eventAction = "StopPlaybackAndClearScreen" then
    targetAction.targetMediaStateIsPreviousState = false
    targetAction.remainOnCurrentStateActions = "stopclear"
  else if eventAction = "ReturnToPriorState" then
    targetAction.targetMediaStateIsPreviousState = true
    targetAction.remainOnCurrentStateActions = "none"
    '  if transitionSpec.eventAction = "None" then
  else
    targetAction.targetMediaStateIsPreviousState = false
    targetAction.remainOnCurrentStateActions = "none"
  end if
  
  return targetAction
  
end function


Function newTickerItem(bsp as object, zoneHSM as object, stateDescription as object) as object
  
  item = invalid
  
  if type(stateDescription.rssDataFeedPlaylistItem) = "roAssociativeArray" then
    item = newRSSDataFeedPlaylistItem(bsp, stateDescription)
  else if type(stateDescription.twitterItem) = "roAssociativeArray" then
    item = newTwitterPlaylistItem(bsp, stateDescription)
  else if type(stateDescription.textItem) = "roAssociativeArray" then
    item = newTextPlaylistItem(stateDescription)
  else if type(stateDescription.userVariableInTickerItem) = "roAssociativeArray"
    item = newUserVariablePlaylistItem(stateDescription, bsp, zoneHSM)
  end if
  
  return item
  
end function


Function newState(bsp as object, zoneHSM as object, sign as object, stateDescription as object, superState as object) as object

  ' get the name
  stateName$ = stateDescription.name
  
  state = zoneHSM.newHState(bsp, stateName$)

  state.name$ = stateName$
  
  if type(superState) = "roAssociativeArray" then
    state.superState = superState
  else
    state.superState = zoneHSM.stTop
  end if
  
  ' create data structures for arrays of specific events
  state.gpioEvents = { }
  state.gpioUpEvents = { }
  
  state.bpEvents = CreateObject("roArray", 3, true)
  state.bpEvents[0] = { }
  state.bpEvents[1] = { }
  state.bpEvents[2] = { }
  state.bpEvents[3] = { }
  
  state.serialEvents = { }
  state.gpsEnterRegionEvents = CreateObject("roArray", 1, true)
  state.gpsExitRegionEvents = CreateObject("roArray", 1, true)

  state.bmapHexInputEvents = {}

  ' get the item
  item = { }
  
  if type(stateDescription.imageItem) = "roAssociativeArray" then
    
    newImagePlaylistItem(bsp, stateDescription.imageItem, zoneHSM, state, item)
    state.imageItem = item
    state.type$ = "image"
    zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
    
  else if type(stateDescription.videoItem) = "roAssociativeArray" then
    
    newVideoPlaylistItem(bsp, stateDescription.videoItem, state, item)
    state.videoItem = item
    state.type$ = "video"
    ' TODO - Bacon
    '' any idea why the following was here? copy / paste error?'
    ''    zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
    
  else if type(stateDescription.audioItem) = "roAssociativeArray" then
    
    newAudioPlaylistItem(bsp, stateDescription.audioItem, state, item)
    state.audioItem = item
    state.type$ = "audio"
    
  else if type(stateDescription.liveVideoItem) = "roAssociativeArray" then
    
    newLiveVideoPlaylistItem(stateDescription.liveVideoItem, state)
    state.type$ = "liveVideo"
    
  else if type(stateDescription.eventHandlerItem) = "roAssociativeArray" then
    
    newEventHandlerPlaylistItem(stateDescription.eventHandlerItem, state)
    state.type$ = "eventHandler"
    
  else if type(stateDescription.eventHandler2Item) = "roAssociativeArray" then
    
    newEventHandlerPlaylistItem(stateDescription.eventHandler2Item, state)
    state.type$ = "eventHandler"
    
  else if type(stateDescription.templatePlaylistItem) = "roAssociativeArray" then
    
    newTemplatePlaylistItem(bsp, stateDescription.templatePlaylistItem, state)
    state.type$ = "template"
    
  else if type(stateDescription.mrssDataFeedPlaylistItem) = "roAssociativeArray" then
    
    ' require that the storage is writable
    if bsp.sysInfo.storageIsWriteProtected then DisplayStorageDeviceLockedMessage()
    
    newMRSSPlaylistItem(bsp, zoneHSM, stateDescription.mrssDataFeedPlaylistItem, state)
    
    state.type$ = "mediaRSS"
    
    if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
      zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
    end if
    
  else if type(stateDescription.localPlaylistItem) = "roAssociativeArray" then
    
    newLocalPlaylistItem(bsp, stateDescription.localPlaylistItem, state)
    state.type$ = "mediaRSS"
    
    if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
      zoneHSM.numImageItems% = zoneHSM.numImageItems% + 1
    end if
    
  else if type(stateDescription.mediaSuperState) = "roAssociativeArray" then
    
    state.HStateEventHandler = MediaItemEventHandler
    SetMediaItemEventHandlers(state)
    
    state.ExecuteTransition = ExecuteTransition
    state.MatchWssEvent = MatchWssEvent
    state.GetNextStateName = GetNextStateName
    state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
    
  else if type(stateDescription.backgroundImageItem) = "roAssociativeArray" then
    
    newBackgroundImagePlaylistItem(bsp, stateDescription.backgroundImageItem, state, item)
    state.backgroundImageItem = item
    
  else if type(stateDescription.mediaListItem) = "roAssociativeArray" then
    
    newMediaListPlaylistItem(bsp, 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"
    
  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
  
  state.processBMapMessageEvent = processBMapMessageEvent
  state.MatchBMapHexInputEvent = MatchBMapHexInputEvent
  state.MatchBMapEvent = MatchBMapEvent

  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
          ' Safe guarding backwards compatibility for 'setAllAudioOutputs'
          if bsCmd.name$ = "setAllAudioOutputs" and cmdParameterValueItem.type = "text" and cmdParameterValueItem.value = invalid then
            m.bsp.diagnostics.PrintDebug("### Assigning default value for 'setAllAudioOutputs' command's 'text' parameter 'value'")
            m.bsp.diagnostics.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_VARIABLE_REFERENCE_FAILURE, cmdParameterValueItem)
            cmdParameterValueItem.value = "none" ' default text value for 'setAllAudioOutputs' command
          end if

          pviSpecs.push(cmdParameterValueItem)
        next
        
        parameterValueSpec = jsonParseParameterValue(pviSpecs)
        parameterValue = newParameterValue(bsp, parameterValueSpec)
                
        bsCmd.parameters.AddReplace(cmdParameterName, parameterValue)
        
      next
      
    next
    
    cmds.push(bsCmd)
    
  next
  
end sub


Sub UpdateWidgetVisibility(showImage as boolean, hideImage as boolean, clearImage as boolean, showCanvas as boolean, hideCanvas as boolean, showHtml as boolean, hideHtml as boolean)
  
  if hideImage then
    if type(m.imagePlayer) = "roImageWidget" then
      m.imagePlayer.Hide()
    end if
  end if
  
  if clearImage then
    if type(m.imagePlayer) = "roImageWidget" then
      m.imagePlayer.StopDisplay()
    end if
  end if
  
  if hideCanvas then
    if type(m.canvasWidget) = "roCanvasWidget" then
      m.canvasWidget.Hide()
    end if
  end if
  
  if hideHtml then
    if type(m.displayedHtmlWidget) = "roHtmlWidget" then
      '			m.displayedHtmlWidget.Hide()
      m.loadingHtmlWidget = invalid
      m.displayedHtmlWidget = invalid
    end if
  end if
  
  if showImage then
    if type(m.imagePlayer) = "roImageWidget" and m.isVisible then
      m.imagePlayer.Show()
    end if
  end if
  
  if showCanvas then
    if type(m.canvasWidget) = "roCanvasWidget" and m.isVisible then
      m.canvasWidget.Show()
    end if
  end if
  
  if showHtml then
    if type(m.displayedHtmlWidget) = "roHtmlWidget" and m.isVisible then
      m.displayedHtmlWidget.Show()
    end if
  end if
  
end sub


Sub ShowImageWidget()
  
  m.UpdateWidgetVisibility(true, false, false, false, true, false, true)
  
  m.imageHidden = false
  m.canvasHidden = true
  m.htmlHidden = true
  
end sub


Sub ClearImagePlane()
  
  m.UpdateWidgetVisibility(false, false, true, false, true, false, true)
  
end sub


Sub ShowCanvasWidget()
  
  m.UpdateWidgetVisibility(false, true, false, true, false, false, true)
  
  m.imageHidden = true
  m.canvasHidden = false
  m.htmlHidden = true
  
end sub


Sub ShowHtmlWidget()
  
  m.UpdateWidgetVisibility(false, true, true, false, true, true, false)
  
  m.imageHidden = true
  m.canvasHidden = true
  m.htmlHidden = false
  
end sub


Sub LogPlayStart(itemType$ as string, fileName$ as string)
  
  if m.playbackActive then
    m.playbackEndTime$ = m.bsp.systemTime.GetLocalDateTime().GetString()
    m.bsp.logging.WritePlaybackLogEntry(m.name$, m.playbackStartTime$, m.playbackEndTime$, m.playbackItemType$, m.playbackFileName$)
  end if
  
  m.playbackActive = true
  m.playbackStartTime$ = m.bsp.systemTime.GetLocalDateTime().GetString()
  m.playbackItemType$ = itemType$
  m.playbackFileName$ = fileName$
  
end sub


Sub newMediaPlaylistItem(bsp as object, playlistItemDescription as object, state as object, playlistItemBS as object)
  
  playlistItemBS.fileName$ = playlistItemDescription.fileName
  playlistItemBS.userVariable = bsp.GetUserVariable(playlistItemBS.fileName$)
  if type(bsp.encryptionByFile) = "roAssociativeArray" then
    playlistItemBS.isEncrypted = bsp.encryptionByFile.DoesExist(playlistItemBS.fileName$)
  else
    playlistItemBS.isEncrypted = false
  end if
  
  SetMediaItemEventHandlers(state)
  state.ExecuteTransition = ExecuteTransition
  state.MatchWssEvent = MatchWssEvent
  state.GetNextStateName = GetNextStateName
  state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
  state.LaunchTimer = LaunchTimer
  state.PreloadItem = PreloadItem
  
end sub


Sub newImagePlaylistItem(bsp as object, playlistItemDescription as object, zoneHSM as object, state as object, playlistItemBS as object)
  
  newMediaPlaylistItem(bsp, playlistItemDescription, state, playlistItemBS)
  
  playlistItemBS.slideTransition% = GetSlideTransitionValue(playlistItemDescription.transitionEffect.transitionType)
  playlistItemBS.transitionDuration% = playlistItemDescription.transitionEffect.transitionDuration
  
  state.HStateEventHandler = STDisplayingImageEventHandler
  state.DisplayImage = DisplayImage
  state.PreDrawImage = PreDrawImage
  state.DrawImage = DrawImage
  state.PostDrawImage = PostDrawImage
  state.ClearVideo = ClearVideo
  state.ConfigureBPButtons = ConfigureBPButtons
  state.ConfigureGPIOButtons = ConfigureGPIOButtons
  
end sub


Function GetProbeData(assetPoolFiles as object, fileName$ as string) as object
  
  probe = invalid
  
  poolFileInfo = assetPoolFiles.GetPoolFileInfo(fileName$)
  if type(poolFileInfo) = "roAssociativeArray" then
    probe = poolFileInfo.Lookup("probe")
  end if
  
  return probe
  
end function


Sub newVideoPlaylistItem(bsp as object, playlistItemDescription as object, state as object, playlistItemBS as object)
  
  newMediaPlaylistItem(bsp, playlistItemDescription, state, playlistItemBS)
  
  playlistItemBS.automaticallyLoop = playlistItemDescription.automaticallyLoop
  
  playlistItemBS.probeData = GetProbeData(bsp.assetPoolFiles, playlistItemBS.fileName$)
  playlistItemBS.videoDisplayMode% = 0
  playlistItemBS.volume% = 100
  
  state.HStateEventHandler = STVideoPlayingEventHandler
  state.AddVideoTimeCodeEvent = AddVideoTimeCodeEvent
  state.SetVideoTimeCodeEvents = SetVideoTimeCodeEvents
  state.LaunchVideo = LaunchVideo
  state.PrePlayVideo = PrePlayVideo
  state.PlayVideo = PlayVideo
  state.PostPlayVideo = PostPlayVideo
  state.ConfigureBPButtons = ConfigureBPButtons
  state.ConfigureGPIOButtons = ConfigureGPIOButtons
  
  newMediaPlaylistItem(bsp, playlistItemDescription, state, playlistItemBS)
  
  playlistItemBS.automaticallyLoop = playlistItemDescription.automaticallyLoop
  
end sub


Sub newSuperStateItem(bsp as object, zoneHSM as object, sign as object, playlistItemDescription as object, state as object)
  
  state.initialStateName$ = playlistItemDescription.initialState
  
  for each subState in playlistItemDescription.states
    stateDescription = jsonParseState(subState)
    newState(bsp, zoneHSM, sign, stateDescription, state)
  next
  
  state.HStateEventHandler = STSuperStateEventHandler
  SetMediaItemEventHandlers(state)
  state.ExecuteTransition = ExecuteTransition
  state.MatchWssEvent = MatchWssEvent
  state.GetNextStateName = GetNextStateName
  state.GetNextStateName = GetNextStateName
  state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
  state.LaunchTimer = LaunchTimer
  state.ConfigureBPButtons = ConfigureBPButtons
  state.ConfigureGPIOButtons = ConfigureGPIOButtons
  
end sub


Sub newEventHandlerPlaylistItem(playlistItemDescription as object, state as object)
  
  state.stopPlayback = playlistItemDescription.stopPlayback
  
  state.HStateEventHandler = STEventHandlerEventHandler
  state.ExecuteTransition = ExecuteTransition
  state.MatchWssEvent = MatchWssEvent
  state.GetNextStateName = GetNextStateName
  state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
  state.LaunchTimer = LaunchTimer
  state.ConfigureBPButtons = ConfigureBPButtons
  state.ConfigureGPIOButtons = ConfigureGPIOButtons
  SetMediaItemEventHandlers(state)
  
end sub



Sub newHtml5PlaylistItem(bsp as object, playlistItemDescription as object, state as object)
  
  state.name$ = playlistItemDescription.name$
  state.htmlSiteName$ = playlistItemDescription.htmlSiteName$
  
  ' get the associated html site
  if bsp.htmlSites.DoesExist(state.htmlSiteName$) then
    htmlSite = bsp.htmlSites.Lookup(state.htmlSiteName$)
    state.isNodeServer = htmlSite.isNodeServer
    state.contentIsLocal = htmlSite.contentIsLocal
    if state.contentIsLocal then
      state.prefix$ = htmlSite.prefix$
      state.filePath$ = htmlSite.filePath$
      state.url = invalid
    else
      state.url = htmlSite.url
    end if

    state.queryString = htmlSite.queryString  
  else
    ' what to do here?
    stop
  end if
  
  state.enableBrightSignJavascriptObjects = playlistItemDescription.enableBrightSignJavascriptObjects
  state.enableCrossDomainPolicyChecks = playlistItemDescription.enableCrossDomainPolicyChecks
  state.ignoreHttpsCertificateErrors = playlistItemDescription.ignoreHttpsCertificateErrors
  if type(playlistItemDescription.enableCamera) = "Boolean" then
    state.enableCamera = playlistItemDescription.enableCamera
  else
    state.enableCamera = false
  endif
  state.enableMouseEvents = playlistItemDescription.enableMouseEvents
  state.displayCursor = playlistItemDescription.displayCursor
  state.hwzOn = playlistItemDescription.hwzOn
  state.useUserStylesheet = playlistItemDescription.useUserStylesheet
  if state.useUserStylesheet then
    state.userStylesheet = playlistItemDescription.userStylesheet
  end if
  
  state.customFonts = []
  customFonts = playlistItemDescription.customFonts
  for each customFont in customFonts
    state.customFonts.push(customFont)
  next
  
  state.HStateEventHandler = STHTML5PlayingEventHandler
  state.LoadHtmlWidgetStaticInitialization = LoadHtmlWidgetStaticInitialization
  SetMediaItemEventHandlers(state)
  state.ConfigureBPButtons = ConfigureBPButtons
  state.ConfigureGPIOButtons = ConfigureGPIOButtons
  state.LaunchTimer = LaunchTimer
  state.ExecuteTransition = ExecuteTransition
  state.MatchWssEvent = MatchWssEvent
  state.GetNextStateName = GetNextStateName
  state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
  
end sub


Sub newLiveVideoPlaylistItem(playlistItemDescription as object, state as object)
  
  state.volume% = playlistItemDescription.volume
  state.overscan = playlistItemDescription.overscan
  
  state.HStateEventHandler = STLiveVideoPlayingEventHandler
  SetMediaItemEventHandlers(state)
  state.ExecuteTransition = ExecuteTransition
  state.MatchWssEvent = MatchWssEvent
  state.GetNextStateName = GetNextStateName
  state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
  state.LaunchTimer = LaunchTimer
  
end sub


Function ParseFileNameXML(fileNameXML as object) as string
  stop
  if fileNameXML.Count() = 1 then
    fileElement = fileNameXML[0]
    fileAttributes = fileElement.GetAttributes()
    fileName$ = fileAttributes.Lookup("name")
  else
    fileName$ = ""
  end if
  
  return fileName$
  
end function


Function ParseBoolAttribute(elementXML as object, attr$ as string) as boolean
  stop
  element = elementXML
  attributes = element.GetAttributes()
  val = attributes.Lookup(attr$)
  if IsString(val) and lcase(val) = "true" then
    return true
  end if
  
  return false
  
end function


Function ParseNavigation(bsp as object, sign as object, navigationXML as object) as object
  stop
  navigation = invalid
  
  if navigationXML.Count() = 1 then
    navigationElement = navigationXML[0]
    userEventsList = navigationElement.userEvent
    if userEventsList.Count() > 0 then
      navigation = { }
      for each userEvent in userEventsList
        ParseUserEvent(bsp, sign, navigation, userEvent)
      next
    end if
  end if
  
  return navigation
  
end function


Sub ParseUserEvent(bsp as object, sign as object, aa as object, userEvent as object)
  stop
  userEventName$ = userEvent.name.GetText()
  
  if userEventName$ = "keyboard" then
    
    aa.keyboardChar$ = userEvent.parameters.parameter.GetText()
    if len(aa.keyboardChar$) > 1 then
      aa.keyboardChar$ = Lcase(aa.keyboardChar$)
    end if
    
  else if userEventName$ = "zoneMessage" then
    
    aa.zoneMessage$ = userEvent.parameters.parameter.GetText()
    
  else if userEventName$ = "remote" then
    
    aa.remoteEvent$ = ucase(userEvent.parameters.parameter.GetText())
    
    if type(bsp.remote) <> "roIRRemote" then
      bsp.remote = CreateObject("roIRRemote")
      bsp.remote.SetPort(bsp.msgPort)
    end if
    
  else if userEventName$ = "serial" then
    
    port$ = userEvent.parameters.parameter.GetText()
    
    ' convert USB port if necessary
    if bsp.usbDevicesByConnector.DoesExist(port$) then
      port$ = bsp.usbDevicesByConnector.Lookup(port$)
    end if
    
    serial$ = userEvent.parameters.parameter2.GetText()
    
    if IsUsbPort(port$) then
      usbHIDPortConfiguration = GetGlobalAA().usbHIDPortConfigurations[port$]
      protocol$ = usbHIDPortConfiguration.protocol$
    end if
    
    aa.serialEvent = { }
    aa.serialEvent.port$ = port$
    aa.serialEvent.protocol$ = protocol$
    
    if protocol$ = "Binary" then
      aa.serialEvent.inputSpec = ConvertToByteArray(serial$)
      aa.serialEvent.asciiSpec = serial$
    else
      aa.serialEvent.serial$ = serial$
    end if
    
    bsp.CreateSerial(bsp, aa.serialEvent.port$, false)
    
  else if userEventName$ = "bp900AUserEvent" or userEventName$ = "bp900BUserEvent" or userEventName$ = "bp900CUserEvent" or userEventName$ = "bp900DUserEvent" or userEventName$ = "bp200AUserEvent" or userEventName$ = "bp200BUserEvent" or userEventName$ = "bp200CUserEvent" or userEventName$ = "bp200DUserEvent" then
    
    aa.bpEvent = { }
    
    aa.bpEvent.buttonPanelIndex% = int(val(userEvent.parameters.buttonPanelIndex.GetText()))
    aa.bpEvent.buttonNumber$ = userEvent.parameters.buttonNumber.GetText()
    
    bsp.ConfigureBPInput(aa.bpEvent.buttonPanelIndex%, aa.bpEvent.buttonNumber$)
    
  else if userEventName$ = "gpioUserEvent" then
    
    if userEvent.parameters.buttonNumber.GetText() <> "" then
      buttonNumber$ = userEvent.parameters.buttonNumber.GetText()
    else
      buttonNumber$ = userEvent.parameters.parameter.GetText()
    end if
    
    aa.gpioEvent = { }
    aa.gpioEvent.buttonNumber$ = buttonNumber$
    
    bsp.ConfigureGPIOInput(buttonNumber$)
    
  end if
  
end sub


Sub scaleScreenElement(bsp as object, setDimensions as boolean, item as object, itemDescription as object)
  
  if bsp.configuredResX <> bsp.actualResX or bsp.configuredResY <> bsp.actualResY then
    xOffset = itemDescription.x / bsp.configuredResX
    x% = xOffset * bsp.actualResX
    item.x% = x%
    
    yOffset = itemDescription.Y / bsp.configuredResY
    y% = yOffset * bsp.actualResY
    item.y% = y%
    
    if setDimensions then
      width% = bsp.actualResX / bsp.configuredResX * itemDescription.width
      item.width% = width%
      
      height% = bsp.actualResY / bsp.configuredResY * itemDescription.height
      item.height% = height%
    end if
    
  else
    item.x% = itemDescription.x
    item.y% = itemDescription.y
    if setDimensions then
      item.width% = itemDescription.width
      item.height% = itemDescription.height
    end if
  end if
  
end sub


Sub newBaseTemplateItem(templateItem as object, templateItemDescription as object)
  
  ' scale text item if necessary
  bsp = GetGlobalAA().bsp
  scaleScreenElement(bsp, true, templateItem, templateItemDescription)
  templateItem.layer% = templateItemDescription.layer
  
end sub


Sub ParseTemplateWidgets(item as object, itemDescription as object)
  
  ' text widget items
  item.numberOfLines% = itemDescription.textWidget.numberOfLines
  
  item.rotation$ = "0"
  rotation$ = itemDescription.textWidget.rotation
  if rotation$ = "90" then
    item.rotation$ = "270"
  else if rotation$ = "180" then
    item.rotation$ = "180"
  else if rotation$ = "270" then
    item.rotation$ = "90"
  end if
  
  item.alignment$ = lcase(itemDescription.textWidget.alignment)
  
  ' widget items
  item.foregroundTextColor$ = GetHexColor(itemDescription.widget.foregroundTextColor)
  item.backgroundTextColor$ = GetHexColor(itemDescription.widget.backgroundTextColor)
  item.font$ = itemDescription.widget.font
  if item.font$ = "" then item.font$ = "System"
  item.fontSize% = itemDescription.widget.fontSize
  
  item.backgroundColorSpecified = itemDescription.backgroundColorSpecified
  
end sub


Sub newTextTemplateItem(templateItem as object, templateItemDescription as object)
  
  ' fill in base class members
  newBaseTemplateItem(templateItem, templateItemDescription)
  
  ParseTemplateWidgets(templateItem, templateItemDescription)
  
end sub


Sub newConstantTextTemplateItem(bsp as object, state as object, templateItems as object, templateItemDescription as object)
  
  templateItem = { }
  templateItem.type$ = "constantTextTemplateItem"
  
  newTextTemplateItem(templateItem, templateItemDescription)
  
  templateItem.textString$ = templateItemDescription.text
  
  templateItems.push(templateItem)
  
end sub


Sub newSystemVariableTemplateItem(bsp as object, state as object, templateItems as object, templateItemDescription as object)
  
  templateItem = { }
  templateItem.type$ = "systemVariableTemplateItem"
  
  newTextTemplateItem(templateItem, templateItemDescription)
  
  templateItem.systemVariableType$ = templateItemDescription.systemVariable

  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.PlayVideo = PlayVideo
  state.SetVideoTimeCodeEvents = SetVideoTimeCodeEvents
  
  state.LaunchWaitForContentTimer = LaunchWaitForContentTimer
  
end sub


Sub newTemplatePlaylistItem(bsp as object, templateItemDescription as object, state as object)
  
  ParseTemplateBackgroundImage(bsp, state, templateItemDescription)
  if templateItemDescription.mediaRssLiveDataFeedIds.Count() > 0 then
    state.mrssActive = true
    state.mrssLiveDataFeeds = []
    for each mediaRssLiveDataFeedId in templateItemDescription.mediaRssLiveDataFeedIds
      liveDataFeedId$ = CleanName(mediaRssLiveDataFeedId)
      liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
      if type(liveDataFeed) = "roAssociativeArray" then
        state.mrssLiveDataFeeds.push(liveDataFeed)
      end if
    next
  else
    state.mrssActive = false
  end if
  
  ' retrieve template items
  state.templateItems = CreateObject("roArray", 1, true)
  
  for each constantTextTemplateItem in templateItemDescription.constantTextTemplateItems
    newConstantTextTemplateItem(bsp, state, state.templateItems, constantTextTemplateItem)
  next
  
  for each systemVariableTemplateItem in templateItemDescription.systemVariableTemplateItems
    newSystemVariableTemplateItem(bsp, state, state.templateItems, systemVariableTemplateItem)
  next
  
  for each userVariableTemplateItem in templateItemDescription.userVariableTemplateItems
    newUserVariableTemplateItem(bsp, state.templateItems, userVariableTemplateItem)
  next
  
  for each mediaCounterLiveTextItem in templateItemDescription.mediaCounterLiveTextItems
    newMediaCounterTextTemplateItem(bsp, state.templateItems, mediaCounterLiveTextItem)
  next
  
  for each imageTemplateItem in templateItemDescription.imageTemplateItems
    newImageTemplateItem(bsp, state.templateItems, imageTemplateItem)
  next
  
  for each titledLiveTextDataItem in templateItemDescription.titledLiveTextDataItems
    newTitledLiveTextDataItem(bsp, state.templateItems, titledLiveTextDataItem)
  next
  
  for each indexedLiveTextDataItem in templateItemDescription.indexedLiveTextDataItems
    newIndexedLiveTextDataItem(bsp, state.templateItems, indexedLiveTextDataItem)
  next
  
  for each mediaRssTextTemplateItem in templateItemDescription.mediaRssTextTemplateItems
    
    templateItem = newMediaRssTextTemplateItem(bsp, state.templateItems, mediaRssTextTemplateItem)
    
    elementName$ = mediaRssTextTemplateItem.elementName
    if lcase(elementName$) = "title" then
      state.mrssTitleTemplateItem = templateItem
    else if lcase(elementName$) = "description" then
      state.mrssDescriptionTemplateItem = templateItem
    else ' custom field
      if type(state.mrssCustomFieldTemplateItems) <> "roAssociativeArray" then
        state.mrssCustomFieldTemplateItems = { }
      end if
      state.mrssCustomFieldTemplateItems.AddReplace(elementName$, templateItem)
    end if
    
  next
  
  for each mediaRssCustomFieldTemplateItem in templateItemDescription.mediaRssCustomFieldTemplateItems
    templateItem = newMediaRssTextTemplateItem(bsp, state.templateItems, mediaRssCustomFieldTemplateItem)
    elementName$ = mediaRssCustomFieldTemplateItem.elementName
    if type(state.mrssCustomFieldTemplateItems) <> "roAssociativeArray" then
      state.mrssCustomFieldTemplateItems = { }
    end if
    state.mrssCustomFieldTemplateItems.AddReplace(elementName$, templateItem)
  next
  
  if (not templateItemDescription.mediaRssMediaTemplateItem.IsEmpty()) then
    state.mrssMediaTemplateItem = newMediaRssMediaTemplateItem(bsp, state.templateItems, templateItemDescription.mediaRssMediaTemplateItem)
  end if

  for each simpleRssTemplateItem in templateItemDescription.simpleRssTemplateItems
    
    templateItem = newSimpleRssTemplateItem(bsp, state.templateItems, simpleRssTemplateItem)

    if type(state.simpleRSSTemplateItems) <> "roAssociativeArray" then
      state.simpleRSSTemplateItems = { }
    end if
    
    simpleRSS = state.simpleRSSTemplateItems.Lookup(templateItem.id$)
    if type(simpleRSS) <> "roAssociativeArray" then
      simpleRSS = { }
      simpleRSS.currentIndex% = 0

      simpleRSS.displayTime% = simpleRssTemplateItem.displayTime
      
      simpleRSS.rssLiveDataFeeds = []
      rssLiveDataFeedIds = simpleRssTemplateItem.rssLiveDataFeedIds
      if rssLiveDataFeedIds.Count() > 0 then
        for each rssLiveDataFeedId in rssLiveDataFeedIds
          liveDataFeedId$ = CleanName(rssLiveDataFeedId)
          liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
          if type(liveDataFeed) = "roAssociativeArray" then
            simpleRSS.rssLiveDataFeeds.push(liveDataFeed)
          end if
        next
      end if
      
      simpleRSS.currentLiveDataFeedIndex% = 0
      
      simpleRSS.items = []
      
      state.simpleRSSTemplateItems.AddReplace(templateItem.id$, simpleRSS)
      
    end if
    
    simpleRSS.items.push(templateItem)
    
  next
  
  SetTemplateHandlers(state)
  
end sub


Sub newBaseTemplateItemFromTextItem(templateItem as object, textItem as object)
  
  templateItem.x% = textItem.x
  templateItem.y% = textItem.y
  templateItem.width% = textItem.width
  templateItem.height% = textItem.height
  templateItem.layer% = 1
  
end sub


Sub newTextTemplateItemFromTextItem(templateItem as object, textItem as object)
  
  ' fill in base class members
  newBaseTemplateItemFromTextItem(templateItem, textItem)
  
  ' text widget items
  templateItem.numberOfLines% = textItem.numberOfLines%
  templateItem.rotation$ = textItem.rotation$
  templateItem.alignment$ = textItem.alignment$
  
  ' widget items
  templateItem.foregroundTextColor$ = textItem.foregroundTextColor$
  templateItem.backgroundTextColor$ = textItem.backgroundTextColor$
  templateItem.font$ = textItem.font$
  templateItem.fontSize% = textItem.fontSize%
  
  templateItem.backgroundColorSpecified = textItem.backgroundColorSpecified
  
end sub


Sub newMRSSPlaylistItem(bsp as object, zoneHSM as object, playlistItemDescription as object, state as object)
  
  state.slideTransition% = playlistItemDescription.slideTransition%
  liveDataFeedId$ = playlistItemDescription.liveDataFeedId
  if liveDataFeedId$ <> "" then
    state.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
  end if
  
  SetMRSSHandlers(state)
  
end sub


Sub newLocalPlaylistItem(bsp as object, playlistItemDescription as object, state as object)

  state.slideTransition% = 0

  liveDataFeedId$ = playlistItemDescription.dataFeedId
  if liveDataFeedId$ <> "" then
    state.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
  end if

  devicePlaylists = playlistItemDescription.devicePlaylists
  for each devicePlaylist in devicePlaylists
    deviceName$ = devicePlaylist.deviceName
    deviceLiveDataFeedId$ = devicePlaylist.dataFeedId
    if deviceName$ = bsp.sysInfo.deviceUniqueID$ then
      liveDataFeedId$ = CleanName(deviceLiveDataFeedId$)
      exit for
    end if
  next

  if liveDataFeedId$ <> "" then
    state.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
  end if

  SetMRSSHandlers(state)
  
end sub


Sub SetMRSSHandlers(state as object)
  
  state.HStateEventHandler = STPlayingMediaRSSEventHandler
  SetMediaItemEventHandlers(state)
  state.ExecuteTransition = ExecuteTransition
  state.MatchWssEvent = MatchWssEvent
  state.GetNextStateName = GetNextStateName
  state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
  state.LaunchTimer = LaunchTimer
  state.PreloadItem = PreloadItem
  state.ConfigureBPButtons = ConfigureBPButtons
  state.ConfigureGPIOButtons = ConfigureGPIOButtons
  
  state.LaunchWaitForContentTimer = LaunchWaitForContentTimer
  state.ProtectMRSSFeed = ProtectMRSSFeed
  state.ProtectMRSSItem = ProtectMRSSItem
  state.DisplayMRSSItem = DisplayMRSSItem
  state.AdvanceToNextMRSSItem = AdvanceToNextMRSSItem
  state.AtEndOfFeed = AtEndOfFeed
  state.GetHtmlWidgetFilePath = GetHtmlWidgetFilePath
  
  state.DisplayImage = DisplayImage
  state.PreDrawImage = PreDrawImage
  state.DrawImage = DrawImage
  state.PostDrawImage = PostDrawImage
  state.ClearVideo = ClearVideo
  state.LaunchVideo = LaunchVideo
  state.PrePlayVideo = PrePlayVideo
  state.PlayVideo = PlayVideo
  state.PostPlayVideo = PostPlayVideo
  state.SetVideoTimeCodeEvents = SetVideoTimeCodeEvents
  
  state.GetAnyMediaRSSTransition = GetAnyMediaRSSTransition
  
end sub


Function GetBoolFromXML(xmlValue$ as string) as boolean
  
  value$ = lcase(xmlValue$)
  if value$ = "true" then
    return true
  else
    return false
  end if
  
end function


Sub newPlayFilePlaylistItem(bsp as object, playlistItemDescription as object, state as object)
  
  state.filesTable = { }
  
  ' BACONTODO'
  ''  state.slideTransition% = playlistItemDescription.slideTransition%
  state.slideTransition% = 0
  
  state.payload$ = ""
  
  state.specifyLocalFiles = not playlistItemDescription.useDataFeed
  
  state.useDefaultMedia = playlistItemDescription.useDefaultMedia
  if state.useDefaultMedia then
    state.defaultMediaFileName$ = playlistItemDescription.defaultMediaContentItem.fileName
    state.defaultMediaFileType$ = playlistItemDescription.defaultMediaContentItem.type
  end if
  
  state.useUserVariable = playlistItemDescription.useUserVariable
  if state.useUserVariable then
    state.userVariable = bsp.GetUserVariable(playlistItemDescription.userVariableName)
    if type(state.userVariable) <> "roAssociativeArray" then
      state.useUserVariable = false
    end if
  end if
  
  liveDataFeedId$ = playlistItemDescription.dataFeedId
  if liveDataFeedId$ <> "" then
    state.liveDataFeed = bsp.liveDataFeeds.Lookup(CleanName(liveDataFeedId$))
  else
    state.liveDataFeed = invalid
  end if
  
  if state.specifyLocalFiles then
    playFileItems = playlistItemDescription.contentItems
    for each playFileItem in playFileItems
      fileTableEntry = { }
      fileTableEntry.label$ = playFileItem.label
      fileTableEntry.export = playFileItem.export
      
      contentItem = playFileItem.contentItem
      fileTableEntry.fileName$ = contentItem.fileName
      fileTableEntry.fileType$ = contentItem.type
      if fileTableEntry.fileType$ = "video" or fileTableEntry.fileType$ = "audio" then
        fileTableEntry.probeData = GetProbeData(bsp.assetPoolFiles, fileTableEntry.fileName$)
      end if
      fileTableEntry.userVariable = bsp.GetUserVariable(contentItem.fileName)
      fileTableEntry.automaticallyLoop = true
      fileTableEntry.isEncrypted = false
      
      fileTableEntry.videoDisplayMode% = 0
      
      state.filesTable.AddReplace(playFileItem.key, fileTableEntry)
    next
  end if
  
  state.HStateEventHandler = STPlayFileEventHandler
  state.PopulatePlayFileFromLiveDataFeed = PopulatePlayFileFromLiveDataFeed
  SetMediaItemEventHandlers(state)
  state.ExecuteTransition = ExecuteTransition
  state.MatchWssEvent = MatchWssEvent
  state.GetNextStateName = GetNextStateName
  state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
  state.LaunchTimer = LaunchTimer
  state.DisplayImage = DisplayImage
  state.PreDrawImage = PreDrawImage
  state.DrawImage = DrawImage
  state.PostDrawImage = PostDrawImage
  state.ClearVideo = ClearVideo
  state.ConfigureBPButtons = ConfigureBPButtons
  state.ConfigureGPIOButtons = ConfigureGPIOButtons
  state.LaunchVideo = LaunchVideo
  state.PrePlayVideo = PrePlayVideo
  state.PlayVideo = PlayVideo
  state.PostPlayVideo = PostPlayVideo
  state.SetVideoTimeCodeEvents = SetVideoTimeCodeEvents
  state.LaunchAudio = LaunchAudio
  state.PrePlayAudio = PrePlayAudio
  state.PlayAudio = PlayAudio
  state.PostPlayAudio = PostPlayAudio
  state.SetAudioTimeCodeEvents = SetAudioTimeCodeEvents
  state.PreloadItem = PreloadItem
  
end sub


Sub newStreamPlaylistItem(bsp as object, playlistItemDescription as object, state as object)
  
  state.url = newParameterValue(bsp, playlistItemDescription.url)
  
  state.HStateEventHandler = STStreamPlayingEventHandler
  SetMediaItemEventHandlers(state)
  state.ConfigureBPButtons = ConfigureBPButtons
  state.ConfigureGPIOButtons = ConfigureGPIOButtons
  state.LaunchTimer = LaunchTimer
  state.ExecuteTransition = ExecuteTransition
  state.MatchWssEvent = MatchWssEvent
  state.GetNextStateName = GetNextStateName
  state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
  
end sub


Sub newMjpegStreamPlaylistItem(bsp as object, playlistItemDescription as object, state as object)
  
  state.url = newParameterValue(bsp, playlistItemDescription.url)
  state.rotation% = int(val(playlistItemDescription.rotation))
  
  state.HStateEventHandler = STMjpegPlayingEventHandler
  SetMediaItemEventHandlers(state)
  state.ConfigureBPButtons = ConfigureBPButtons
  state.ConfigureGPIOButtons = ConfigureGPIOButtons
  state.LaunchTimer = LaunchTimer
  state.ExecuteTransition = ExecuteTransition
  state.MatchWssEvent = MatchWssEvent
  state.GetNextStateName = GetNextStateName
  state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
  
end sub


Sub newAudioPlaylistItem(bsp as object, playlistItemDescription as object, state as object, playlistItemBS as object)
  
  newMediaPlaylistItem(bsp, playlistItemDescription, state, playlistItemBS)
  
  playlistItemBS.probeData = GetProbeData(bsp.assetPoolFiles, playlistItemBS.fileName$)
  playlistItemBS.volume% = playlistItemDescription.volume
  
  state.HStateEventHandler = STAudioPlayingEventHandler
  state.AddAudioTimeCodeEvent = AddAudioTimeCodeEvent
  state.SetAudioTimeCodeEvents = SetAudioTimeCodeEvents
  state.LaunchAudio = LaunchAudio
  state.PrePlayAudio = PrePlayAudio
  state.PlayAudio = PlayAudio
  state.PostPlayAudio = PostPlayAudio
  state.LaunchMixerAudio = LaunchMixerAudio
  state.PlayMixerAudio = PlayMixerAudio
  state.ConfigureBPButtons = ConfigureBPButtons
  state.ConfigureGPIOButtons = ConfigureGPIOButtons
  
end sub


Function newUserVariablePlaylistItem(playlistItemDescription as object, bsp as object, zoneHSM as object) as object
  
  item = { }
  
  item.textStrings = []
  
  item.userVariableName = playlistItemDescription.userVariableInTickerItem.userVariableName
  
  userVariables = bsp.currentUserVariables
  
  for each userVariableKey in userVariables
    userVariable = userVariables.Lookup(userVariableKey)
    if userVariable.name$ = item.userVariableName then
      tickerItem = userVariable.GetCurrentValue()
      item.textStrings.push(tickerItem)
    end if
  next
  
  item.isRSSFeed = false
  item.isUserVariable = true
  
  return item
  
end function


Function newTextPlaylistItem(playlistItemDescription as object) as object
  
  item = { }
  
  ' TODO - may need to do some adjustment to make this work for XML
  strings = playlistItemDescription.textItem.strings
  
  item.textStrings = CreateObject("roArray", strings.count(), true)
  
  for each textString in strings
    item.textStrings.push(textString.textString)
  next
  
  item.isRSSFeed = false
  item.isUserVariable = false
  
  return item
  
end function


Function newTwitterPlaylistItem(bsp as object, playlistItemDescription as object) as object
  
  item = { }
  
  twitterItem = playlistItemDescription.twitterItem
  
  twitterUserName$ = twitterItem.userName
  jsonUrl$ = "https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=" + twitterUserName$ + "&tweet_mode=extended"
  url = newTextParameterValue(jsonUrl$)
  
  authData = { }
  authData.AuthType = "OAuth 1.0a"
  authData.AuthToken = twitterItem.authToken
  authData.ConsumerKey = twitterItem.BSConsumerKey
  authData.EncryptedTwitterSecrets = twitterItem.encryptedTwitterSecrets
  
  updateInterval% = twitterItem.updateInterval
  
  liveDataFeed = newLiveDataFeedWithAuthData(bsp, url, authData, updateInterval%)
  
  if not bsp.liveDataFeeds.DoesExist(liveDataFeed.id$) then
    liveDataFeed.isJSON = true
    liveDataFeed.isTwitterFeed = true
    
    if twitterItem.restrictNumberOfTweets = "ByCount" then
      liveDataFeed.restrictNumberOfItems = true
      liveDataFeed.numberOfItemsToDisplay% = twitterItem.numberOfTweetsToShow
    else if twitterItem.restrictNumberOfTweets = "ByRecentDays" then
      ' TODO - implement restriction by number of days
      liveDataFeed.restrictNumberOfItems = false
      liveDataFeed.numberOfRecentDaysToDisplay% = twitterItem.numberOfRecentDaysForTweets
    else
      liveDataFeed.restrictNumberOfItems = false
    end if
stop    
    bsp.liveDataFeeds.AddReplace(liveDataFeed.id$, liveDataFeed)
  else
    liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeed.id$)
  end if
  
  item.liveDataFeed = liveDataFeed
  item.rssTitle$ = item.liveDataFeed.id$
  
  item.twitterUserName$ = twitterUserName$ + ": "
  item.isRSSFeed = true
  item.isUserVariable = false
  
  return item
  
end function


Function newRSSDataFeedPlaylistItem(bsp as object, stateDescription as object) as object
  
  item = { }
  liveDataFeedId$ = stateDescription.rssDataFeedPlaylistItem.liveDataFeedId$
  item.liveDataFeed = bsp.liveDataFeeds.Lookup(liveDataFeedId$)
  item.rssTitle$ = item.liveDataFeed.id$
  item.isRSSFeed = true
  item.isUserVariable = false
  
  return item
  
end function


Sub newBackgroundImagePlaylistItem(bsp as object, playlistItemDescription as object, state as object, playlistItemBS as object)
  
  newMediaPlaylistItem(bsp, playlistItemDescription, state, playlistItemBS)
  state.HStateEventHandler = STDisplayingBackgroundImageEventHandler
  
end sub


Function GetViewModeValue(viewModeSpec$ as string) as integer
  
  viewMode% = 2
  
  if viewModeSpec$ = "Scale to Fill" then
    viewMode% = 0
  else if viewModeSpec$ = "Letterboxed and Centered" then
    viewMode% = 1
  end if
  
  return viewMode%
  
end function


Function GetImageModeValue(imageModeSpec$ as string) as integer
  
  imageMode% = 1
  
  if imageModeSpec$ = "Center Image" then
    imageMode% = 0
  else if imageModeSpec$ = "Scale to Fill and Crop" then
    imageMode% = 2
  else if imageModeSpec$ = "Scale to Fill" then
    imageMode% = 3
  end if
  
  return imageMode%
  
end function


Function GetSlideTransitionValue(slideTransitionSpec$ as string) as integer
  
  slideTransition% = 0
  
  if slideTransitionSpec$ = "Image wipe from top" then
    slideTransition% = 1
  else if slideTransitionSpec$ = "Image wipe from bottom" then
    slideTransition% = 2
  else if slideTransitionSpec$ = "Image wipe from left" then
    slideTransition% = 3
  else if slideTransitionSpec$ = "Image wipe from right" then
    slideTransition% = 4
  else if slideTransitionSpec$ = "Explode from center" then
    slideTransition% = 5
  else if slideTransitionSpec$ = "Explode top left" then
    slideTransition% = 6
  else if slideTransitionSpec$ = "Explode top right" then
    slideTransition% = 7
  else if slideTransitionSpec$ = "Explode bottom left" then
    slideTransition% = 8
  else if slideTransitionSpec$ = "Explode bottom right" then
    slideTransition% = 9
  else if slideTransitionSpec$ = "Venetian blinds - vertical" then
    slideTransition% = 10
  else if slideTransitionSpec$ = "Venetian blinds - horizontal" then
    slideTransition% = 11
  else if slideTransitionSpec$ = "Comb effect - vertical" then
    slideTransition% = 12
  else if slideTransitionSpec$ = "Comb effect - horizontal" then
    slideTransition% = 13
  else if slideTransitionSpec$ = "Fade to background color" then
    slideTransition% = 14
  else if slideTransitionSpec$ = "Fade to new image" then
    slideTransition% = 15
  else if slideTransitionSpec$ = "Slide from top" then
    slideTransition% = 16
  else if slideTransitionSpec$ = "Slide from bottom" then
    slideTransition% = 17
  else if slideTransitionSpec$ = "Slide from left" then
    slideTransition% = 18
  else if slideTransitionSpec$ = "Slide from right" then
    slideTransition% = 19
  end if
  
  return slideTransition%
  
end function

'endregion

'region BSP Methods
' *************************************************
'
' BSP Methods
'
' *************************************************
Sub InitializeTouchScreen(zone as object)
  
  if type(m.touchScreen) <> "roTouchScreen" then
    m.touchScreen = CreateObject("roTouchScreen")
    m.touchScreen.SetPort(m.msgPort)
    REM Puts up a cursor if a mouse is attached
    REM The cursor must be a 32 x 32 BMP
    REM The x,y position is the hot spot point
    m.touchScreen.SetCursorBitmap("cursor.bmp", 16, 16)
    
    videoMode = CreateObject("roVideoMode")
    resX = videoMode.GetResX()
    resY = videoMode.GetResY()
    ' If graphics are scaled, use video resolution to set the coordinates
    ' so that no need to scale in AddRectangleRegion
    useVideoResolution = ShouldScaleGraphicElements(videoMode)
    if useVideoResolution then
      resX = videoMode.GetVideoResX()
      resY = videoMode.GetVideoResY()
    else
      resX = videoMode.GetResX()
      resY = videoMode.GetResY()
    end if
    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) = "ls425" or lcase(model) = "ls445" or lcase(model) = "cl435" or lcase(model) = "md455" or lcase(model) = "hs124" or lcase(model) = "hs123" or lcase(model) = "hd225" or lcase(model) = "hd1025" or lcase(model) = "xs156" then

    usbTypeATargetFid = "B"
    if lcase(model) = "hd1025" or 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$
  
  ''    else
  ''        zoneHSM.name$ = zoneXML.name.GetText()
  
  ' scale the zones if necessary
  ''        zoneHSM.originalWidth% = int(val(zone.width.GetText()))
  ''        zoneHSM.originalHeight% = int(val(zone.height.GetText()))
  ''        scaleScreenElement(bsp, true, zoneHSM, zone)
  
  ''        zoneHSM.type$ = zone.type.GetText()
  ''       zoneHSM.id$ = zone.id.GetText()
  ''    endif
  
  zoneHSM.isVisible = true
  zoneHSM.imageHidden = false
  zoneHSM.canvasHidden = false
  zoneHSM.htmlHidden = false
  
  zoneHSM.mosaicDecoderName = ""
  
  zoneHSM.bsp = bsp
  
  zoneHSM.ConfigureAudioResources = ConfigureAudioResources
  zoneHSM.SetAudioOutputAndMode = SetAudioOutputAndMode
  
  zoneHSM.LogPlayStart = LogPlayStart
  zoneHSM.ClearImagePlane = ClearImagePlane
  zoneHSM.ShowImageWidget = ShowImageWidget
  zoneHSM.ShowCanvasWidget = ShowCanvasWidget
  zoneHSM.ShowHtmlWidget = ShowHtmlWidget
  zoneHSM.UpdateWidgetVisibility = UpdateWidgetVisibility
  
  zoneHSM.stTop = zoneHSM.newHState(bsp, "Top")
  zoneHSM.stTop.HStateEventHandler = STTopEventHandler
  
  zoneHSM.topState = zoneHSM.stTop
  
end sub


Sub InitializeZoneCommon(msgPort as object)
  
  zoneHSM = m
  
  zoneHSM.msgPort = msgPort
  
  zoneHSM.isVideoZone = false
  zoneHSM.preloadState = invalid
  zoneHSM.preloadedStateName$ = ""
  
  zoneHSM.rectangle = CreateScaledRectangle(zoneHSM.x%, zoneHSM.y%, zoneHSM.width%, zoneHSM.height%)
  
  ' byte arrays to store stream byte input
  zoneHSM.serialStreamInputBuffers = CreateObject("roArray", 8, true)
  for i% = 0 to 7
    zoneHSM.serialStreamInputBuffers[i%] = CreateObject("roByteArray")
  next
  
end sub


Function CreateScaledRectangle(x% as integer, y% as integer, width% as integer, height% as integer) as object

  rectangle = {
    x%: x%,
    y%: y%,
    width%: width%,
    height%: height%,
  }

  ScaleResolutionGraphics(rectangle)

  return CreateObject("roRectangle", rectangle.x%, rectangle.y%, rectangle.width%, rectangle.height%)

end function


Sub ScaleResolutionGraphics(rectangle as object)

  videoMode = GetVideoMode()

  ' No need to scale if cannot create video mode class
  if type(videoMode) <> "roVideoMode" then return

  if ShouldScaleGraphicElements(videoMode) then
    ' Scale x, y, width and height if exists
    scaleRatio = GetGraphicScaleRatio(videoMode)
    if IsInteger(rectangle.x%) then rectangle.x% = rectangle.x% * scaleRatio
    if IsInteger(rectangle.y%) then rectangle.y% = rectangle.y% * scaleRatio
    if IsInteger(rectangle.width%) then rectangle.width% = rectangle.width% * scaleRatio
    if IsInteger(rectangle.height%) then rectangle.height% = rectangle.height% * scaleRatio
  end if

  videoMode = invalid

end sub


Function ShouldScaleGraphicElements(videoMode as object) as boolean
  ' Check if we are running presentation on model that supports multi screen
  globalAA = getGlobalAA()
  useScreenModes = CanUseScreenModes(globalAA.bsp.sign, videoMode)

  ' If the graphics resolution differs from video resolution, we need to scale the other elements.
  ' Screen modes check is used to verify only scale for series 5 players.
  return videoMode.GetResX() <> videoMode.GetVideoResX() and useScreenModes

end function


Function GetGraphicScaleRatio(videoMode as object) as float

  return videoMode.GetResX() / videoMode.GetVideoResX()
  
end function

'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


Function ExecuteTransition(transition as object, stateData as object, payload$ as string) as string
  
  nextState$ = "init"
  
  while nextState$ <> ""
    
    ' before transitioning to next state, ensure that the transition is allowed
    nextState = m.GetNextStateName(transition)
    nextState$ = nextState.nextState$
    actualTarget = nextState.actualTarget
    
    if nextState$ <> "" then
      
      nextState = m.stateMachine.stateTable[nextState$]
      
      if nextState.type$ = "mediaRSS" and nextState.rssURL$ = "" then
        ' skip an empty localized playlist
        m.bsp.diagnostics.PrintDebug("Unassigned local playlist " + nextState.name$ + " encountered, attempt to navigate to next state.")
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_UNASSIGNED_LOCAL_PLAYLIST, nextState.name$)
        
        defaultTransition = nextState.GetAnyMediaRSSTransition()
        if defaultTransition <> invalid then
          transition = defaultTransition
        else
          ' no transition found - not sure what to do
          m.bsp.diagnostics.PrintDebug("Unable to navigate from unassigned local playlist " + nextState.name$)
          m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_UNASSIGNED_LOCAL_PLAYLIST_NO_NAVIGATION, nextState.name$)
          exit while
        end if
        
      else
        
        if nextState.type$ = "playFile" then
          if nextState.useUserVariable then
            userVariable = nextState.userVariable
            payload$ = userVariable.GetCurrentValue()
          end if
          
          if not nextState.useDefaultMedia and not nextState.filesTable.DoesExist(payload$) then
            m.bsp.diagnostics.PrintDebug("transition cancelled - payload " + payload$ + " not found in target state's table")
            return "HANDLED"
          else
            ' set payload$ member before ExecuteTransitionCommands is called - needed if there is a synchronize transition command
            nextState.payload$ = payload$
          end if
        end if
        
        exit while
        
      end if
      
    end if
    
  end while
  
  switchToNewPresentation = m.bsp.ExecuteTransitionCommands(m.stateMachine, actualTarget)
  
  if switchToNewPresentation then
    return "HANDLED"
  end if
  
  if nextState$ = "" then
    
    if transition.remainOnCurrentStateActions = "stop" then
      if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
        m.stateMachine.videoPlayer.Stop()
      end if
    else if transition.remainOnCurrentStateActions = "stopclear" then
      if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
        m.stateMachine.videoPlayer.StopClear()
      end if
      if type(m.stateMachine.imagePlayer) = "roImageWidget" then
        m.stateMachine.imagePlayer.StopDisplay()
      end if
    end if
    
    return "HANDLED"
    
  else
    stateData.nextState = m.stateMachine.stateTable[nextState$]
    stateData.nextState.payload$ = payload$
    
    m.UpdatePreviousCurrentStateNames()
    
    return "TRANSITION"
  end if
  
end function


Sub AssignWildcardInputToUserVariable(bsp as object, input$ as string)
  
  if type(m.variableToAssignFromWildcard) = "roAssociativeArray" then
    m.variableToAssignFromWildcard.SetCurrentValue(input$, true)
  end if
  
end sub


Sub AssignEventInputToUserVariable(bsp as object, input$ as string)

  if type(m.variableToAssignFromInput) = "roAssociativeArray" then
    
    m.variableToAssignFromInput.SetCurrentValue(input$, true)
    
  else
    
    userVariablesUpdated = false
    
    regex = CreateObject("roRegEx", "!!", "i")
    variableAssignments = regex.Split(input$)
    if variableAssignments.Count() > 0 then
      for each variableAssignment in variableAssignments
        regex = CreateObject("roRegEx", ":", "i")
        parts = regex.Split(variableAssignment)
        if parts.Count() = 2 then
          variableToAssign$ = parts[0]
          newValue$ = parts[1]
          variableToAssign = bsp.GetUserVariable(variableToAssign$)
          if variableToAssign = invalid then
            bsp.diagnostics.PrintDebug("User variable " + variableToAssign$ + " not found.")
          else
            variableToAssign.SetCurrentValue(newValue$, false)
            userVariablesUpdated = true
          end if
        end if
      next
    end if
    
    if userVariablesUpdated then
      userVariablesChanged = { }
      userVariablesChanged["EventType"] = "USER_VARIABLES_UPDATED"
      bsp.msgPort.PostMessage(userVariablesChanged)
      
      ' Notify controlling devices to refresh
      bsp.SendUDPNotification("refresh")
    end if
    
  end if
  
end sub


Sub SetMediaItemEventHandlers(state as object)
  state.MediaItemEventHandler = MediaItemEventHandler
  state.KeyboardPressEventHandler = KeyboardPressEventHandler
  state.DatagramEventHandler = DatagramEventHandler
  state.SerialStreamLineEventHandler = SerialStreamLineEventHandler
  state.GpsEventHandler = GpsEventHandler
  state.RemoteDownEventHandler = RemoteDownEventHandler
  state.WssEventHandler = WssEventHandler
end sub


Function GpsEventHandler(event as object, stateData as object) as string
  
  gpsData = ParseGPSdataGPRMCformat(event)
  
  if gpsData.valid then
    
    ' log GPS events on first event, then no more frequently than every 30 seconds
    logGPSEvent = false
    currentTime = m.bsp.systemTime.GetLocalDateTime()
    if type(m.nextTimeToLogGPSEvent$) = "roString" then
      if currentTime.GetString() > m.nextTimeToLogGPSEvent$ then
        logGPSEvent = true
      end if
    else
      logGPSEvent = true
    end if
    
    if logGPSEvent then
      currentTime.AddSeconds(30)
      m.nextTimeToLogGPSEvent$ = currentTime.GetString()
    end if
    
    if gpsData.fixActive then
      
      if logGPSEvent then
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_GPS_LOCATION, str(gpsData.latitude) + ":" + str(gpsData.longitude))
      end if
      
      m.bsp.diagnostics.PrintDebug("GPS location: " + str(gpsData.latitude) + "," + str(gpsData.longitude))
      m.bsp.gpsLocation.latitude = gpsData.latitude
      m.bsp.gpsLocation.longitude = gpsData.longitude
      
      latitudeInRadians = ConvertDecimalDegtoRad(m.bsp.gpsLocation.latitude)
      longitudeInRadians = ConvertDecimalDegtoRad(m.bsp.gpsLocation.longitude)
      
      for each transition in m.gpsEnterRegionEvents
        
        distance = CalcGPSDistance (latitudeInRadians, longitudeInRadians, transition.latitudeInRadians, transition.longitudeInRadians)
        m.bsp.diagnostics.PrintDebug("GPS distance from longitude " + str(transition.longitude) + ", latitude " + str(transition.latitude) + " = " + str(distance))
        
        if distance < transition.radiusInFeet then
          m.bsp.diagnostics.PrintDebug("GPS enter region")
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpsEnterRegion", str(gpsData.latitude) + ":" + str(gpsData.longitude), "1")
          return m.ExecuteTransition(transition, stateData, "")
        end if
        
      next
      
      for each transition in m.gpsExitRegionEvents
        
        distance = CalcGPSDistance (latitudeInRadians, longitudeInRadians, transition.latitudeInRadians, transition.longitudeInRadians)
        m.bsp.diagnostics.PrintDebug("GPS distance from longitude " + str(transition.longitude) + ", latitude " + str(transition.latitude) + " = " + str(distance))
        
        if distance > transition.radiusInFeet then
          m.bsp.diagnostics.PrintDebug("GPS exit region")
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpsExitRegion", str(gpsData.latitude) + ":" + str(gpsData.longitude), "1")
          return m.ExecuteTransition(transition, stateData, "")
        end if
        
      next
      
    else
      if logGPSEvent then
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_GPS_NOT_LOCKED, "")
      end if
      
      m.bsp.gpsLocation.latitude = invalid
      m.bsp.gpsLocation.longitude = invalid
    end if
  else
    ' print "GPS not valid"
  end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
end function


Function SerialStreamLineEventHandler(event as object, stateData as object) as string

  serialEvent$ = event.GetString()

  port$ = event.GetUserData()

  if not IsUsbPort(port$) then
    
    port% = int(val(port$))
    
    if m.bsp.gpsConfigured and m.bsp.sign.serialPortConfigurations[port%].gps then
      return m.GpsEventHandler(event, stateData)
    end if
    
  end if
  
  m.bsp.diagnostics.PrintDebug("Serial Line Event " + event.GetString())
  
  serialEvents = m.serialEvents
  
  if type(serialEvents) = "roAssociativeArray" then
    if type(serialEvents[port$]) = "roAssociativeArray" then
      ' look for an exact match first
      if type(serialEvents[port$][serialEvent$]) = "roAssociativeArray" then
        transition = serialEvents[port$][serialEvent$]
        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "1")
        return m.ExecuteTransition(transition, stateData, serialEvent$)
      else
        ' look for regular expression match with each of the possible serial events for the current state
        for each serialEventSpec in serialEvents[port$]
          ' only look for regular expressions if spec includes wildcard
          if instr(1, serialEventSpec, "(.*)") > 0 then
            r = CreateObject("roRegEx", serialEventSpec, "i")
            if type(r) = "roRegex" then
              matches = r.match(serialEvent$)
              if matches.Count() > 0 then
                transition = serialEvents[port$][serialEventSpec]
                m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "1")
                
                if transition.assignInputToUserVariable then
                  transition.AssignEventInputToUserVariable(m.bsp, serialEvent$)
                end if
                
                if matches.Count() > 1 and transition.assignWildcardToUserVariable then
                  transition.AssignWildcardInputToUserVariable(m.bsp, matches[1])
                end if
                
                return m.ExecuteTransition(transition, stateData, serialEvent$)
              end if
            end if
          end if
        next
      end if
    end if
  end if
  
  m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "serial", port$ + " " + serialEvent$, "0")
  
  stateData.nextState = m.superState
  return "SUPER"
  
end function


Function DatagramEventHandler(event as object, stateData as object) as string
    
  ' could be either a udp event or a synchronize event
  
  m.bsp.diagnostics.PrintDebug("UDP Event " + event.GetString())
  
  udpEvent$ = event.GetString()
  
  synchronizeEvents = m.synchronizeEvents
  udpEvents = m.udpEvents
  
  ' check to see if this is a synchronization preload or play event
  if type(synchronizeEvents) = "roAssociativeArray" then
    index% = instr(1, udpEvent$, "pre-")
    if index% = 1 then
      ' preload next file
      synchronizeEvent$ = mid(udpEvent$, 5)
      if type(synchronizeEvents[synchronizeEvent$]) = "roAssociativeArray" then
        
        ' get the next file and preload it
        nextState$ = synchronizeEvents[synchronizeEvent$].targetMediaState$
        nextState = m.stateMachine.stateTable[nextState$]
        
        preloadRequired = true
        if type(m.stateMachine.preloadState) = "roAssociativeArray" then
          if m.stateMachine.preloadedStateName$ = nextState.name$
            preloadRequired = false
          end if
        end if
        
        ' set this variable so that launchVideo knows what has been preloaded
        m.stateMachine.preloadState = nextState
        
        ' currently only support preload / synchronizing with images and videos
        if preloadRequired then
          m.stateMachine.preloadState.PreloadItem()
        end if
        
        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "synchronize-pre", synchronizeEvent$, "1")
        
        ' ?? return "HANDLED" ??
      end if
    end if
    
    index% = instr(1, udpEvent$, "ply-")
    if index% = 1 then
      ' just transition to the next state where the file will be played
      synchronizeEvent$ = mid(udpEvent$, 5)
      if type(synchronizeEvents[synchronizeEvent$]) = "roAssociativeArray" then
        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "synchronize-play", synchronizeEvent$, "1")
        return m.ExecuteTransition(m.synchronizeEvents[synchronizeEvent$], stateData, "")
      end if
    end if
    
  end if
  
  if type(udpEvents) = "roAssociativeArray" then
    if type(udpEvents[udpEvent$]) = "roAssociativeArray" then
      m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "udp", udpEvent$, "1")
      transition = udpEvents[udpEvent$]
      return m.ExecuteTransition(transition, stateData, udpEvent$)
    else
      ' look for regular expression match with each of the possible udp events for the current state
      for each udpEventSpec in udpEvents
        ' only look for regular expressions if spec includes wildcard
        if instr(1, udpEventSpec, "(.*)") > 0 then
          r = CreateObject("roRegEx", udpEventSpec, "i")
          if type(r) = "roRegex" then
            matches = r.match(udpEvent$)
            if matches.Count() > 0 then
              transition = udpEvents[udpEventSpec]
              m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "udp", udpEvent$, "1")

              if transition.assignInputToUserVariable then
                transition.AssignEventInputToUserVariable(m.bsp, event.GetString())
              end if
              
              if matches.Count() > 1 and transition.assignWildcardToUserVariable then
                transition.AssignWildcardInputToUserVariable(m.bsp, matches[1])
              end if
              
              return m.ExecuteTransition(transition, stateData, udpEvent$)
            end if
          end if
        end if
      next
    end if
  end if
  
  m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "udp", event.GetString(), "0")
  
  stateData.nextState = m.superState
  return "SUPER"
  
end function


Function RemoteDownEventHandler(event as object, stateData as object) as string

  m.bsp.diagnostics.PrintDebug("Remote Event" + stri(event))
  
  irRemoteControl = m.bsp.sign.irRemoteControl
  manufacturerCode = irRemoteControl.manufacturerCode
  buttons = irRemoteControl.buttons
  targetButtonCode = event - (manufacturerCode * 256)

  buttonCode$ = StripLeadingSpaces(stri(targetButtonCode))

  remoteEvents = m.remoteEvents
  if type(remoteEvents) = "roAssociativeArray" then
    if buttons.DoesExist(buttonCode$) then
      button = buttons[buttonCode$]
      buttonDescription = button.buttonDescription
      if type(remoteEvents[buttonDescription]) = "roAssociativeArray" then
        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "remote", buttonDescription, "1")
        return m.ExecuteTransition(m.remoteEvents[buttonDescription], stateData, buttonDescription)
      end if
    endif

  endif

  m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "remote", buttonCode$, "0")

  stateData.nextState = m.superState
  return "SUPER"

end function


Function KeyboardPressEventHandler(event as object, stateData as object) as string
  
  ' note - this code does not fully cover the case where one state is waiting for keyboard input
  ' and another state is waiting for barcode input.
  
  m.bsp.diagnostics.PrintDebug("Keyboard Press" + str(event.GetInt()))
  
  keyboardChar$ = chr(event.GetInt())
  keyboardCharForLog$ = StripLeadingSpaces(stri(event.GetInt()))
  
  usbStringEvents = m.usbStringEvents
  keyboardEvents = m.keyboardEvents
  
  checkUSBInputString = false
  if type(usbStringEvents) = "roAssociativeArray" then
    if event.GetInt() <> 13 then
      m.usbInputBuffer$ = m.usbInputBuffer$ + keyboardChar$
      if m.usbInputLogBuffer$ = "" then
        m.usbInputLogBuffer$ = keyboardCharForLog$
      else
        m.usbInputLogBuffer$ = m.usbInputLogBuffer$ + "," + keyboardCharForLog$
      end if
      checkUSBInputString = false
    else
      checkUSBInputString = true
    end if
  end if

  ' check for bar code input (usb characters terminated by an Enter key)
  if type(usbStringEvents) = "roAssociativeArray" then
    if checkUSBInputString then

      if type(usbStringEvents[m.usbInputBuffer$]) = "roAssociativeArray" then
        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "usb", m.usbInputLogBuffer$, "1")
        action$ = m.ExecuteTransition(m.usbStringEvents[m.usbInputBuffer$], stateData, m.usbInputBuffer$)
        if event.GetInt() = 13 then
          m.usbInputBuffer$ = ""
          m.usbInputLogBuffer$ = ""
        end if
        return action$
      else

        ' check for wildcards
        
        ' look for regular expression match with each of the possible usb events for the current state
        for each usbEventSpec in usbStringEvents
          ' only look for regular expressions if spec includes wildcard
          if instr(1, usbEventSpec, "(.*)") > 0 then
            r = CreateObject("roRegEx", usbEventSpec, "i")
            if type(r) = "roRegex" then
              matches = r.match(m.usbInputBuffer$)
              if matches.Count() > 0 then
                transition = usbStringEvents[usbEventSpec]
                m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "usb", m.usbInputBuffer$, "1")

                if transition.assignInputToUserVariable then
                  transition.AssignEventInputToUserVariable(m.bsp, m.usbInputBuffer$)
                end if
                
                if matches.Count() > 1 and transition.assignWildcardToUserVariable then
                  transition.AssignWildcardInputToUserVariable(m.bsp, matches[1])
                end if
                
                action$ = m.ExecuteTransition(transition, stateData, m.usbInputBuffer$)
                if event.GetInt() = 13 then
                  m.usbInputBuffer$ = ""
                  m.usbInputLogBuffer$ = ""
                end if
                return action$
              end if
            end if
          end if
        next

        ' legacy wildcard support
        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 stri(event.GetSourceIdentity()) = stri(m.mstimeoutTimer.GetIdentity()) then
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timer", "", "1")
          return m.ExecuteTransition(m.mstimeoutEvent, stateData, "")
        end if
      end if
    end if
    
    if type(m.timeClockEvents) = "roArray" then
      for each timeClockEvent in m.timeClockEvents
        if type(timeClockEvent.timer) = "roTimer" then
          if stri(event.GetSourceIdentity()) = stri(timeClockEvent.timer.GetIdentity()) then
            systemTime = CreateObject("roSystemTime")
            currentDateTime = systemTime.GetLocalDateTime()
            
            ' daily timer
            if type(timeClockEvent.timeClockDaily%) = "roInt" then
              
              triggerEvent = EventActiveToday(currentDateTime, timeClockEvent.daysOfWeek%)
              
              ' restart timer
              LaunchTimeClockEventTimer(m, timeClockEvent)
              
              if not triggerEvent then
                m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timeClock", "", "0")
                return "HANDLED"
              end if
              
              ' periodic timer
            else if type(timeClockEvent.timeClockPeriodicInterval%) = "roInt" then
              
              ' units in seconds rather than minutes?
              currentTime% = currentDateTime.GetHour() * 60 + currentDateTime.GetMinute()
              startTime% = timeClockEvent.timeClockPeriodicStartTime%
              endTime% = timeClockEvent.timeClockPeriodicEndTime%
              intervalTime% = timeClockEvent.timeClockPeriodicInterval%
              
              triggerEvent = false
              withinWindow = TimeWithinWindow(currentTime%, startTime%, endTime%)
              if withinWindow then
                triggerEvent = EventActiveToday(currentDateTime, timeClockEvent.daysOfWeek%)
              end if
              
              ' restart timer
              LaunchTimeClockEventTimer(m, timeClockEvent)
              
              if not triggerEvent then
                m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timeClock", "", "0")
                return "HANDLED"
              end if
            end if
            
            m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timeClock", "", "1")
            return m.ExecuteTransition(timeClockEvent.transition, stateData, "")
            
          end if
        end if
      next
    end if
    
    userData = event.GetUserData()
    if type(userData) = "roAssociativeArray" then
      if IsString(userData.id) then
        if userData.id = "mediaList" then
          mediaListState = userData.state
          mediaListState.playbackIndex% = mediaListState.startIndex%
          return "HANDLED"
        end if
      end if
    end if
    
    m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "timer", "", "0")

  else if type(event) = "roTouchEvent" then
    touchIndex$ = str(event.GetInt())
    m.bsp.diagnostics.PrintDebug("Touch event" + touchIndex$)
    if type(m.touchEvents) = "roAssociativeArray" then
      touchEvent = m.touchEvents[touchIndex$]
      if type(touchEvent) = "roAssociativeArray" then
        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "touch", touchIndex$, "1")
        return m.ExecuteTransition(touchEvent, stateData, "")
      end if
    end if
    m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "touch", touchIndex$, "0")

  else if type(event) = "roBmapDisconnectedEvent" then

    port$ = event.GetUserData()
    
    m.bsp.diagnostics.PrintDebug("roBmapDisconnectedEvent received on port " + port$)
    m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bmapDisconnected", port$, "0")
    m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BMAP_DISCONNECTED, port$)

    m.bsp.diagnostics.PrintDebug("process roBmapDisconnectedEvent")

    if type(m.bsp.bmapByPort) = "roAssociativeArray" and m.bsp.bmapByPort.DoesExist(port$) then
      m.bsp.bmapByPort[port$] = invalid
    end if

    m.bsp.ScheduleRetryCreateBMap(port$)

  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$ = event.GetId()
    
    synchronizeEvents = m.synchronizeEvents
    if type(synchronizeEvents) = "roAssociativeArray" then
      
      if type(synchronizeEvents[synchronizeEvent$]) = "roAssociativeArray" or type(synchronizeEvents["<any>"]) = "roAssociativeArray" then
        m.stateMachine.syncInfo = { }
        m.stateMachine.syncInfo.SyncDomain = event.GetDomain()
        m.stateMachine.syncInfo.SyncId = event.GetId()
        m.stateMachine.syncInfo.SyncIsoTimestamp = event.GetIsoTimestamp()
        
        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "enhancedSynchronize", synchronizeEvent$, "1")
        
        if type(synchronizeEvents[synchronizeEvent$]) = "roAssociativeArray"
          return m.ExecuteTransition(m.synchronizeEvents[synchronizeEvent$], stateData, synchronizeEvent$)
        else
          ' Synchronising with a PlayFile state
          return m.ExecuteTransition(m.synchronizeEvents["<any>"], stateData, synchronizeEvent$)
        end if
      end if
      
    end if

  ' 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
  
else if type(event) = "roBmapMessageEvent" then

  resp = m.processBMapMessageEvent(event, stateData)
  if resp <> "" then
    return resp
  endif

end if

stateData.nextState = m.superState
return "SUPER"

end function


Function getBmapOperator(bmapFunction as object, bmapOperatorIndex as integer) as object

  bmapOperators = bmapFunction.Operators

  for each bmapOperator in bmapOperators
    bmapOperatorValue = GetBmapOperatorValue(bmapOperator)
    if bmapOperatorValue = bmapOperatorIndex then
      return bmapOperator
    endif
  next

end function


Function getBmapFunction(functionBlock as object, bmapFunctionIndex as integer) as object

  bmapFunctions = functionBlock.functions

  for each bmapFunction in bmapFunctions
    if bmapFunction.Value = bmapFunctionIndex then
      return bmapFunction
    endif
  next

end function


Function getFunctionBlock(bmapCommunicationSpec as object, functionBlockIndex as integer) as object

  functionBlocks = bmapCommunicationSpec.functionBlocks
  
  for each functionBlock in functionBlocks
    if functionBlock.value = functionBlockIndex then
      return functionBlock
    endif
  next

  return invalid

end function


Function MatchBMapHexInputEvent(port$ as string, bmapMessage$ as string, stateData as object) as string

  eventMatchFound = false

  specifiedConnector$ = m.bsp.GetSpecifiedConnector(port$)
  
  if type(m.bmapHexInputEvents[specifiedConnector$]) = "roAssociativeArray" then
    
    transition = m.bmapHexInputEvents[specifiedConnector$][bmapMessage$]
    if type(transition) = "roAssociativeArray" then
      eventMatchFound = true
    else
      ' look for regular expression match if spec includes wildcard
      for each bmapHexInputSpec in m.bmapHexInputEvents[specifiedConnector$]
        if instr(1, bmapHexInputSpec, "(.*)") > 0 then
          r = CreateObject("roRegEx", bmapHexInputSpec, "i")
          if type(r) = "roRegex" then
            matches = r.match(bmapMessage$)
            if matches.Count() > 0 then
              transition = m.bmapHexInputEvents[specifiedConnector$][bmapHexInputSpec]
              if matches.Count() > 1 and transition.assignWildcardToUserVariable then
                transition.AssignWildcardInputToUserVariable(m, matches[1])
              end if
              eventMatchFound = true
            endif
          endif
        endif
      next
    endif
  endif

  if eventMatchFound then
    m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bmap", bmapMessage$, "1")
    if transition.assignInputToUserVariable then
      transition.AssignEventInputToUserVariable(m.bsp, bmapMessage$)
    end if
    return m.ExecuteTransition(transition, stateData, bmapMessage$)
  endif

  return ""

end function


Function GetBMapFieldLength(fieldType As String) As Integer

  if fieldType = "uint8" or fieldType = "int8" then
    fieldLength = 2
  else if fieldType = "uint16" or fieldType = "int16" then
    fieldLength = 4
  else if fieldType = "uint32" or fieldType = "int32" then
    fieldLength = 8
  else if fieldType = "uint64" or fieldType = "int64" then
    fieldLength = 16
  else
    stop
  endif

  return fieldLength

End Function


Function GetFieldValueAsStr(fieldValue) as String

  if type(fieldValue)= "Invalid" then
    return ""
  endif

  if (IsString(fieldValue)) then
    return fieldValue
  endif

  return StripLeadingSpaces(stri(fieldValue))

end function


Function MatchBMapEvent(event as object, port$ as string, bmapMessage$ as string, stateData as object) as string

  if type(m.bmapEvents) = "roArray" and m.bmapEvents.Count() > 0 then    
  
    tmpResponse = event.GetMessage()

    ' make deep copy of event.GetMessage() as the Shift operator is destructive
    hs = tmpResponse.ToHexString()
    newByteArray = CreateObject("roByteArray") 
    newByteArray.FromHexString(hs)
    response = newByteArray

    bmapFunctionBlock = response.Shift()
    bmapFunctionIndex = response.Shift()
    bmapOperatorIndex = response.Shift()
    bmapPayloadLength = response.Shift()
    bmapPayload = response.ToHexString()

    m.bsp.diagnostics.PrintDebug("bmapEvent")
    m.bsp.diagnostics.PrintDebug(stri(bmapFunctionBlock))
    m.bsp.diagnostics.PrintDebug(stri(bmapFunctionIndex))
    m.bsp.diagnostics.PrintDebug(stri(bmapOperatorIndex))
    m.bsp.diagnostics.PrintDebug(stri(bmapPayloadLength))

    specifiedConnector$ = m.bsp.GetSpecifiedConnector(port$)

    boseProduct = m.bsp.sign.boseProductsByConnector[specifiedConnector$]
    bmapCommunicationSpec = boseProduct.bmapCommunicationSpec

    functionBlock = getFunctionBlock(bmapCommunicationSpec, bmapFunctionBlock)

    if type(functionBlock) = "roAssociativeArray" then
      bmapFunction = getBmapFunction(functionBlock, bmapFunctionIndex) 
      
      if type(bmapFunction) = "roAssociativeArray" then
        bmapOperator = getBmapOperator(bmapFunction, bmapOperatorIndex)
        
        if type(bmapOperator) = "roAssociativeArray" then

          bmapFields = bmapOperator.fields

          for each specifiedBmapEvent in m.bmapEvents

            if lcase(specifiedBmapEvent.port) = lcase(specifiedConnector$) then
              if lcase(specifiedBmapEvent.functionBlockName) = lcase(functionBlock.Name) then
                if lcase(specifiedBmapEvent.functionName) = lcase(bmapFunction.Name) then
                  if lcase(specifiedBmapEvent.operatorName) = lcase(bmapOperator.Operator) then

                    ' only support matching to a single field in an event

                    bmapEventMatchFound = false
                    matchedPayload = ""
                    
                    if bmapFields = invalid and specifiedBmapEvent.fieldName = "" then
                      bmapEventMatchFound = true
                    else if bmapFields <> invalid and specifiedBmapEvent.fieldName <> "" then
                      
                      payloadOffsetIndex = 0

                      payloadBA = CreateObject("roByteArray")
                      payloadBA.FromHexString(bmapPayload)
                      payloadAsNumber = 0
                      for i% = 0 to payloadBA.Count() - 1
                        payloadAsNumber = (payloadAsNumber * 256) + payloadBA[i%]
                      next

                      for each bmapField in bmapFields

                        bmapFieldName = bmapField.Name
                        
                        if lcase(bmapFieldName) = lcase(specifiedBmapEvent.fieldName) then

                          if type(bmapField.BitFields) = "roArray" and bmapField.BitFields.Count() > 0 then

                            shiftCount = 0

                            for each bmapBitField in bmapField.BitFields

                              if bmapBitField.Name = specifiedBmapEvent.bitFieldName then

                                maskValue = ShiftLeft(2, bmapBitField.NumBits) - 1
                                maskedPayload = payloadAsNumber and maskValue

                                if instr(1, specifiedBmapEvent.fieldValue, "(.*)") then

                                  ' wildcard specified
                                  ' for bmapBitFields, only support wildcards against entire field.
                                  ' always match if the event includes the specified bitfield
                                  transition = specifiedBmapEvent.transition
                                  if transition.assignWildcardToUserVariable then
                                    transition.AssignWildcardInputToUserVariable(m, stri(maskedPayload))
                                  end if
                                  bmapEventMatchFound = true
                                  matchedPayload = stri(payloadAsNumber)
                                  
                                else
                                  
                                  fieldValueStr = specifiedBmapEvent.fieldValue
                                  fieldValueBA = CreateObject("roByteArray")
                                  if (len(fieldValueStr) and 1) = 1 then
                                    fieldValueStr = "0" + fieldValueStr
                                  endif

                                  fieldValueBA.FromHexString(fieldValueStr)
                                  ' TEDTODOBMAP - field value Num > 1 byte?
                                  fieldValueNum = fieldValueBA[0]

                                  if maskedPayload = fieldValueNum then
                                    bmapEventMatchFound = true
                                    matchedPayload = specifiedBmapEvent.fieldValue
                                  endif

                                endif

                                exit for

                              endif

                              payloadAsNumber = ShiftRight(payloadAsNumber, bmapBitField.NumBits)
                            next
                          
                          ' look for regular expression match if spec includes wildcard
                          else if instr(1, specifiedBmapEvent.fieldValue, "(.*)") > 0 then
                            
                            r = CreateObject("roRegEx", specifiedBmapEvent.fieldValue, "i")

                            if type(r) = "roRegex" then

                              if isBoolean(bmapField.variableLength) and bmapField.variableLength then
                                fieldPayload = bmapPayload.mid(payloadOffsetIndex)
                              else
                                fieldLength = GetBMapFieldLength(bmapField.Type)
                                fieldPayload = bmapPayload.mid(fieldLength)
                              endif

                              ' if units are ascii, attempt wildcard match on ascii, not hex
                              if bmapField.Units = "ASCII" then
                                ' convert payload field from hex to ascii
                                ba = CreateObject("roByteArray")
                                ba.FromHexString(fieldPayload)
                                fieldPayload = ba.ToAsciiString()
                              endif

                              matches = r.match(fieldPayload)
                            
                              if matches.Count() > 0 then
                                transition = specifiedBmapEvent.transition
                                if matches.Count() > 1 and transition.assignWildcardToUserVariable then
                                  transition.AssignWildcardInputToUserVariable(m, matches[1])
                                end if
                                bmapEventMatchFound = true
                                matchedPayload = fieldPayload
                                exit for
                              endif
                            endif

                          else if (bmapField.Units = "ASCII") then

                            ' convert specified value from ascii to a hex string for comparison with payload
                            ba = CreateObject("roByteArray")    
                            ba.FromAsciiString(specifiedBmapEvent.fieldValue)
                            fieldValueAsHex = ba.ToHexString()

                            if isBoolean(bmapField.variableLength) and bmapField.variableLength then
                              fieldPayload = bmapPayload.mid(payloadOffsetIndex)
                            else
                              fieldLength = GetBMapFieldLength(bmapField.Type)
                              fieldPayload = bmapPayload.mid(fieldLength)
                            endif

                            if fieldPayload = fieldValueAsHex then
                              stop    ' TODO - unable to test due to lack of appropriate test data
                              bmapEventMatchFound = true
                              matchedPayload = specifiedBmapEvent.fieldValue
                              exit for
                            endif

                          else if bmapField.Type = "uint8" or bmapField.Type = "int8" then

                            fieldPayload = bmapPayload.mid(payloadOffsetIndex, 2)

                            if int(val(fieldPayload)) = int(val(specifiedBmapEvent.fieldValue)) then
                              bmapEventMatchFound = true
                              matchedPayload = bmapPayload
                              exit for
                            endif

                          else if bmapField.Type = "uint16" or bmapField.Type = "int16" then
                            stop    ' TODO - unable to test due to lack of appropriate test data
                            fieldPayload = bmapPayload.mid(payloadOffsetIndex, 4)
                            if int(val(fieldPayload)) = int(val(specifiedBmapEvent.fieldValue)) then
                              bmapEventMatchFound = true
                              matchedPayload = bmapPayload
                              exit for
                            endif
                          
                          else if bmapField.Type = "uint32" or bmapField.Type = "int32" then
                            stop    ' TODO - unable to test due to lack of appropriate test data
                            fieldPayload = bmapPayload.mid(payloadOffsetIndex, 8)
                            if int(val(fieldPayload)) = int(val(specifiedBmapEvent.fieldValue)) then
                              bmapEventMatchFound = true
                              matchedPayload = bmapPayload
                              exit for
                            endif
                          
                          else
                            stop    ' TODO - unable to test due to lack of appropriate test data
                          endif

                        endif

                        ' get offset to next field in payload
                        payloadMultiplier = 1
                        if type(bmapField.count) = "Integer" then
                          payloadMultiplier = bmapField.count
                        endif

                        if bmapField.Type = "uint8" or bmapField.Type = "int8" then
                          fieldSize = 2
                        else if bmapField.Type = "uint16" or bmapField.Type = "int16" then
                          fieldSize = 4
                        else if bmapField.Type = "uint32" or bmapField.Type = "int32" then
                          fieldSize = 8
                        endif

                        payloadOffsetIndex = payloadOffsetIndex + (fieldSize * payloadMultiplier)
                      
                      next
                    endif

                    if bmapEventMatchFound then
                      m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bmap", bmapMessage$, "1")
                      transition = specifiedBmapEvent.transition                      
                      if transition.assignInputToUserVariable then
                        transition.AssignEventInputToUserVariable(m.bsp, matchedPayload)
                      end if
                      return m.ExecuteTransition(transition, stateData, bmapMessage$)                          
                    endif

                  endif
                endif
              endif
            endif
          next
        endif
      endif
    endif
  endif

  return ""

end function


Function processBMapMessageEvent(event as object, stateData as object) as string

  port$ = event.GetUserData()

  bmapMessage$ = event.GetMessage().ToHexString()
  m.bsp.diagnostics.PrintDebug(bmapMessage$)

  ' check for hex input events first
  
  result = m.MatchBMapHexInputEvent(port$, bmapMessage$, stateData)
  
  if result <> "" then
    return result
  endif

  ' no match found with a hex input event - look for match with standard bmap input event
  result = m.MatchBMapEvent(event, port$, bmapMessage$, stateData)
  
  if result <> "" then
    return result
  endif

  return ""

End function


Sub dumpJsonBody(eddieDumpFile as object, jsonBody as object)
  for each propertyName in jsonBody
    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)
    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
  
  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$
        if m.bsp.sign.isVideoWall and m.bsp.sign.videoWallType$ = "stretched" then
          aa["MultiscreenWidth"] = m.bsp.sign.videoWallNumColumns%
          aa["MultiscreenHeight"] = m.bsp.sign.videoWallNumRows%
          aa["MultiscreenX"] = m.bsp.sign.videoWallColumnPosition%
          aa["MultiscreenY"] = m.bsp.sign.videoWallRowPosition%
          aa["Mode"] = m.stateMachine.viewMode%
        end if
        
        loopMode% = 1
        if type(m.videoEndEvent) = "roAssociativeArray" then loopMode% = 0
        m.stateMachine.videoPlayer.SetLoopMode(loopMode%)
        
        if m.stateMachine.mosaicDecoderName <> "" then
          aa.Decoder = m.stateMachine.mosaicDecoderName
        end if
        
        ok = m.stateMachine.videoPlayer.PlayFile(aa)
        
        if ok = 0 then
          m.bsp.diagnostics.PrintDebug("Error playing rtsp file in STStreamPlayingEventHandler: url = " + url$)
          videoStreamPlaybackFailure = { }
          videoStreamPlaybackFailure["EventType"] = "VideoStreamPlaybackFailure"
          m.stateMachine.msgPort.PostMessage(videoStreamPlaybackFailure)
        end if
      else
        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.bsp.sign.isVideoWall and m.bsp.sign.videoWallType$ = "stretched" then
        aa["MultiscreenWidth"] = m.bsp.sign.videoWallNumColumns%
        aa["MultiscreenHeight"] = m.bsp.sign.videoWallNumRows%
        aa["MultiscreenX"] = m.bsp.sign.videoWallColumnPosition%
        aa["MultiscreenY"] = m.bsp.sign.videoWallRowPosition%
        aa["Mode"] = m.stateMachine.viewMode%
      end if
      
      if m.stateMachine.mosaicDecoderName <> "" then
        aa.Decoder = m.stateMachine.mosaicDecoderName
      end if
      
      ok = m.stateMachine.mjpegVideoPlayer.PlayFile(aa)
      m.bsp.SetTouchRegions(m)
      
      m.stateMachine.ClearImagePlane()
      
      m.LaunchTimer()
      
      ' state logging
      m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "mjpeg")
      
      ' playback logging
      m.stateMachine.LogPlayStart("mjpeg", url$)
      
      return "HANDLED"
      
    else if event["EventType"] = "EXIT_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
      
      m.stateMachine.mjpegUrl = invalid
      m.stateMachine.mjpegMimeStream = invalid
      m.stateMachine.mjpegVideoPlayer = invalid
      
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)
      
      '			else if event["EventType"] = "VideoPlaybackFailureEvent" then
      
      '				if type(m.videoEndEvent) = "roAssociativeArray" then
      '					return m.ExecuteTransition(m.videoEndEvent, stateData, "")
      '				endif
      
    else
      
      return m.MediaItemEventHandler(event, stateData)
      
    end if
    
  end if
  
else if type(event) = "roVideoEvent" and event.GetSourceIdentity() = m.stateMachine.mjpegVideoPlayer.GetIdentity() then
  if event.GetInt() = MEDIA_END then
    m.bsp.diagnostics.PrintDebug("Video Event" + stri(event.GetInt()))
    m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
    if m.bsp.ProcessMediaEndEvent() then
      return "HANDLED"
    end if
    if type(m.videoEndEvent) = "roAssociativeArray" then
      return m.ExecuteTransition(m.videoEndEvent, stateData, "")
    end if
    PostMediaEndEvent(m.bsp.msgPort)
  end if
else
  return m.MediaItemEventHandler(event, stateData)
end if

stateData.nextState = m.superState
return "SUPER"

end function


Function STVideoPlayingEventHandler(event as object, stateData as object) as object
  
  MEDIA_END = 8
  VIDEO_TIME_CODE = 12
  
  stateData.nextState = invalid
  
  if type(event) = "roAssociativeArray" then ' internal message event
  
  if IsString(event["EventType"]) then
    
    if event["EventType"] = "ENTRY_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
      
      m.LaunchVideo("video")
      
      return "HANDLED"
      
    else if event["EventType"] = "EXIT_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
      
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)
      
    else if event["EventType"] = "VideoPlaybackFailureEvent" then
      
      if m.bsp.ProcessMediaEndEvent() then
        return "HANDLED"
      end if
      if type(m.videoEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.videoEndEvent, stateData, "")
      end if
      PostMediaEndEvent(m.bsp.msgPort)
      
    else
      
      return m.MediaItemEventHandler(event, stateData)
      
    end if
    
  end if
  
else if type(event) = "roVideoEvent" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() then
  if event.GetInt() = MEDIA_END then
    m.bsp.diagnostics.PrintDebug("Video Event" + stri(event.GetInt()))
    m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
    if m.bsp.ProcessMediaEndEvent() then
      return "HANDLED"
    end if
    if type(m.videoEndEvent) = "roAssociativeArray" then
      return m.ExecuteTransition(m.videoEndEvent, stateData, "")
    else if not(type(m.synchronizeEvents) = "roAssociativeArray" or type(m.internalSynchronizeEvents) = "roAssociativeArray") then
      ' looping video - since LaunchVideo is not called, perform logging here.
      file$ = m.videoItem.fileName$
      m.stateMachine.LogPlayStart("video", file$)
    end if
    
    PostMediaEndEvent(m.bsp.msgPort)
    
  else if event.GetInt() = VIDEO_TIME_CODE then
    videoTimeCodeIndex$ = str(event.GetData())
    m.bsp.diagnostics.PrintDebug("Video TimeCode Event " + videoTimeCodeIndex$)
    if type(m.videoTimeCodeEvents) = "roAssociativeArray" then
      videoTimeCodeEvent = m.videoTimeCodeEvents[videoTimeCodeIndex$]
      if type(videoTimeCodeEvent) = "roAssociativeArray" then
        m.bsp.ExecuteTransitionCommands(m.stateMachine, videoTimeCodeEvent)
        m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "1")
        return "HANDLED"
      end if
      m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "0")
    end if
  end if
else
  return m.MediaItemEventHandler(event, stateData)
end if

stateData.nextState = m.superState
return "SUPER"

end function


Sub PostMediaEndEvent(msgPort as object)
  
  mediaEndEvent = { }
  mediaEndEvent["EventType"] = "MEDIA_END"
  msgPort.PostMessage(mediaEndEvent)
  
end sub


Sub SetAudioTimeCodeEvents()
  
  if type(m.stateMachine.audioPlayer) = "roAudioPlayer" then
    player = m.stateMachine.audioPlayer
  else
    player = m.stateMachine.videoPlayer
  end if
  
  player.ClearEvents()
  
  if type(m.audioTimeCodeEvents) = "roAssociativeArray" then
    for each eventNum in m.audioTimeCodeEvents
      m.AddAudioTimeCodeEvent(m.audioTimeCodeEvents[eventNum].timeInMS%, int(val(eventNum)))
    next
  end if
  
end sub


Sub AddAudioTimeCodeEvent(timeInMS% as integer, eventNum% as integer)
  
  if type(m.stateMachine.audioPlayer) = "roAudioPlayer" then
    m.stateMachine.audioPlayer.AddEvent(eventNum%, timeInMS%)
  else
    m.stateMachine.videoPlayer.AddEvent(eventNum%, timeInMS%)
  end if
  
end sub


Sub SetVideoTimeCodeEvents()
  
  m.stateMachine.videoPlayer.ClearEvents()
  
  if type(m.videoTimeCodeEvents) = "roAssociativeArray" then
    for each eventNum in m.videoTimeCodeEvents
      m.AddVideoTimeCodeEvent(m.videoTimeCodeEvents[eventNum].timeInMS%, int(val(eventNum)))
    next
  end if
  
end sub


Sub AddVideoTimeCodeEvent(timeInMS% as integer, eventNum% as integer)
  
  if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
    m.stateMachine.videoPlayer.AddEvent(eventNum%, timeInMS%)
  end if
  
end sub

'endregion

'region Video or Images State Machine
' *************************************************
'
' VideoOrImages State Machine
'
' *************************************************
Function newVideoOrImagesZoneHSM(bsp as object, zoneDescription as object) as object
  
  zoneHSM = newVideoZoneHSM(bsp, zoneDescription)
  zoneHSM.ConstructorHandler = VideoOrImagesZoneConstructor
  zoneHSM.InitialPseudostateHandler = VideoOrImagesZoneGetInitialState
  
  zoneHSM.imageMode% = zoneDescription.imageMode%
  
  zoneHSM.numImageItems% = 0
  
  return zoneHSM
  
end function

Sub VideoOrImagesZoneConstructor()
  
  m.InitializeVideoZoneObjects()
  
  zoneHSM = m
  
  ' create players
  if zoneHSM.numImageItems% > 0 then
    
    imagePlayer = CreateObject("roImageWidget", zoneHSM.rectangle)
    
    if 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
    PostMediaEndEvent(m.bsp.msgPort)
  end if
  
else if type(m.audioItem) = "roAssociativeArray" and IsAudioEvent(m.stateMachine, event) then
  if event.GetInt() = MEDIA_END then
    m.bsp.diagnostics.PrintDebug("Audio Event" + stri(event.GetInt()))
    m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
    if m.bsp.ProcessMediaEndEvent() then
      return "HANDLED"
    end if
    if type(m.audioEndEvent) = "roAssociativeArray" then
      return m.ExecuteTransition(m.audioEndEvent, stateData, "")
    end if
    PostMediaEndEvent(m.bsp.msgPort)
  end if
  
end if

return m.MediaItemEventHandler(event, stateData)

end function


' this code explicitly does not catch roAudioEventMx events - those are handled elsewhere.
Function IsAudioEvent(stateMachine as object, event as object) as boolean
  
  return (type(stateMachine.audioPlayer) = "roAudioPlayer" and type(event) = "roAudioEvent" and event.GetSourceIdentity() = stateMachine.audioPlayer.GetIdentity()) or (type(stateMachine.videoPlayer) = "roVideoPlayer" and type(event) = "roVideoEvent" and event.GetSourceIdentity() = stateMachine.videoPlayer.GetIdentity())
  
end function


Function IsAudioPlayer(audioPlayer as object) as boolean
  
  return type(audioPlayer) = "roAudioPlayer" or type(audioPlayer) = "roAudioPlayerMx"
  
end function


' BACONTODO - possible to combine these two subs?
Sub ConfigureIntraStateEventHandlersButton(navigationList as object)
  
  for each navigation in navigationList
    
    if IsBpNavigationEvent(navigation) then
      bpEvent = { }
      bpEvent.buttonPanelIndex% = int(val(GetButtonPanelIndexFromBpIndex(navigation.eventData.bpIndex)))
      bpEvent.buttonNumber$ = stri(navigation.eventData.buttonNumber)
      bpEvent.configuration$ = "press"
      m.bsp.ConfigureBPButton(bpEvent.buttonPanelIndex%, bpEvent.buttonNumber$, bpEvent)
    end if
    
    if navigation.eventName = "gpioUserEvent" then
      
      gpioEvent = { }
      gpioEvent.buttonNumber$ = StripLeadingSpaces(stri(navigation.eventData.buttonNumber))
      gpioEvent.configuration$ = "press"
      m.bsp.ConfigureGPIOButton(gpioEvent.buttonNumber$, gpioEvent)
    end if
    
  next
  
end sub


Function IsBpNavigationEvent(navigationEvent as object) as boolean
  
  if navigationEvent.eventName = "bp900AUserEvent" or navigationEvent.eventName = "bp900BUserEvent" or navigationEvent.eventName = "bp900CUserEvent" or navigationEvent.eventName = "bp900DUserEvent" then
    return true
  end if
  
  if navigationEvent.eventName = "bp200AUserEvent" or navigationEvent.eventName = "bp200BUserEvent" or navigationEvent.eventName = "bp200CUserEvent" or navigationEvent.eventName = "bp200DUserEvent" then
    return true
  end if
  
  return false
  
end function


Function GetButtonPanelIndexFromBpIndex(bpIndex$ as string) as string
  
  bpUserEventButtonPanelIndex$ = "0"
  if bpIndex$ = "a" then
    bpUserEventButtonPanelIndex$ = "0"
  else if bpIndex$ = "b" then
    bpUserEventButtonPanelIndex$ = "1"
  else if bpIndex$ = "c" then
    bpUserEventButtonPanelIndex$ = "2"
  else if bpIndex$ = "d" then
    bpUserEventButtonPanelIndex$ = "3"
  end if
  
  return bpUserEventButtonPanelIndex$
  
end function


Function GetMatchingNavigationEvent(navigationEventList as object, event as object) as object

  MEDIA_END = 8
  
  for each navigationEvent in navigationEventList
    
    if type(event) = "roAssociativeArray" and IsString(event["EventType"]) then
      
      if IsBpNavigationEvent(navigationEvent) and event["EventType"] = "BPControlDown" then
        
        bpIndex$ = event.ButtonPanelIndex
        bpNum$ = event["ButtonNumber"]
        m.bsp.diagnostics.PrintDebug("BP Press, button number " + bpNum$ + ", button index " + bpIndex$)
        navigationButtonNumber$ = StripLeadingSpaces(stri(navigationEvent.eventData.buttonNumber))
        
        bpUserEventButtonPanelIndex$ = GetButtonPanelIndexFromBpIndex(navigationEvent.eventData.bpIndex)
        
        if navigationButtonNumber$ = bpNum$ and bpUserEventButtonPanelIndex$ = bpIndex$ then
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "bpDown", bpIndex$ + " " + bpNum$, "1")
          return true
        end if
        
      else if event["EventType"] = "GPIOControlDown" and navigationEvent.eventName = "gpioUserEvent" then
        if (event["ButtonNumber"] = StripLeadingSpaces(stri(navigationEvent.eventData.buttonNumber))) then
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "gpioDown", event["ButtonNumber"], "1")
          return true
        end if
        
      else if navigationEvent.eventName = "zoneMessage" and event["EventType"] = "SEND_ZONE_MESSAGE"
        
        zoneMessage$ = event["EventParameter"]
        if navigationEvent.eventData.data = zoneMessage$ then
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "zoneMessage", zoneMessage$, "1")
          return true
        end if
        
      end if
      
    else if navigationEvent.eventName = "timeout" and type(event) = "roTimerEvent" and type(m.advanceOnImageTimeoutTimer) = "roTimer" and event.GetSourceIdentity() = m.advanceOnImageTimeoutTimer.GetIdentity() then
      return navigationEvent

    else if navigationEvent.eventName = "timeout" and type(event) = "roTimerEvent" and type(m.retreatOnImageTimeoutTimer) = "roTimer" and event.GetSourceIdentity() = m.retreatOnImageTimeoutTimer.GetIdentity() then
      return navigationEvent

    else if navigationEvent.eventName = "mediaEnd" and type(event) = "roVideoEvent" and event.GetInt() = MEDIA_END and type(m.stateMachine.videoPlayer) = "roVideoPlayer" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() then
      return navigationEvent
      
    else if navigationEvent.eventName = "mediaEnd" and type(event) = "roAudioEvent" and event.GetInt() = MEDIA_END and IsAudioPlayer(m.stateMachine.audioPlayer) 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

      synchronizeEvent$ = event.GetId()
      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
  
  MEDIA_END = 8
  
  navigationEvent = m.GetMatchingNavigationEvent(navigationEventList, event)
  if navigationEvent = invalid then
    return false
  end if
  
  return true
  
end function


Sub LaunchMediaListPlaybackItem(playImmediate as boolean, executeNextCommands as boolean, executePrevCommands as boolean)
  
  ' Make sure we have a valid list
  itemIndex = m.playbackIndices[m.playbackIndex%]
  if itemIndex = invalid then
    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


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 disableLoopMode or (type(m.videoItem.automaticallyLoop) = "roBoolean" and (not m.videoItem.automaticallyLoop)) or type(m.videoEndEvent) = "roAssociativeArray" or type(m.synchronizeEvents) = "roAssociativeArray" or type(m.internalSynchronizeEvents) = "roAssociativeArray" then loopMode$ = "NoLoop"
  ' Support seamless looping during synchronization - overwrite loopMode as necessary
  if (type(m.videoItem.automaticallyLoop) = "roBoolean" and m.videoItem.automaticallyLoop) and (type(m.synchronizeEvents) = "roAssociativeArray" or type(m.internalSynchronizeEvents) = "roAssociativeArray") then loopMode$ = "SeamlessLoopOrNotAtAll"
  m.stateMachine.videoPlayer.SetLoopMode(loopMode$)
  
  file$ = m.videoItem.fileName$
  if type(m.videoItem.filePath$) = "roString" then
    filePath$ = m.videoItem.filePath$
  else
    filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, file$)
  end if
  
  ' determine whether or not a preload has been performed
  preloaded = false
  if type(m.stateMachine.preloadState) = "roAssociativeArray" then
    if m.stateMachine.preloadedStateName$ = m.name$ then
      preloaded = true
    end if
  end if
  
  syncInProgress = false
  if type(m.stateMachine.syncInfo) = "roAssociativeArray" then
    syncInProgress = true
  end if
  
  m.stateMachine.videoPlayer.EnableSafeRegionTrimming(false)
  
  if not preloaded and not syncInProgress then
    m.stateMachine.videoPlayer.Stop()
  end if
  
  m.SetVideoTimeCodeEvents()
  
  if executeEntryCmds then
    m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
  end if
  
  if preloaded then
    ok = m.stateMachine.videoPlayer.Play()
    
    if ok = 0 then
      m.bsp.diagnostics.PrintDebug("Error playing preloaded file in PlayVideo: " + file$ + ", " + filePath$)
      m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
      videoPlaybackFailure = { }
      videoPlaybackFailure["EventType"] = "VideoPlaybackFailureEvent"
      m.stateMachine.msgPort.PostMessage(videoPlaybackFailure)
    end if
    
    m.stateMachine.preloadState = invalid
    m.stateMachine.preloadedStateName$ = ""
    
    m.bsp.diagnostics.PrintDebug("LaunchVideo: play preloaded file " + file$ + ", loopMode = " + loopMode$)
  else
    aa = { }
    aa.AddReplace("Filename", filePath$)
    
    if type(m.videoItem.probeData) = "roString" then
      m.bsp.diagnostics.PrintDebug("LaunchVideo: probeData = " + m.videoItem.probeData)
      aa.AddReplace("ProbeString", m.videoItem.probeData)
    end if
    
    if syncInProgress then
      
      aa.AddReplace("SyncDomain", m.stateMachine.syncInfo.SyncDomain)
      aa.AddReplace("SyncId", m.stateMachine.syncInfo.SyncId)
      aa.AddReplace("SyncIsoTimestamp", m.stateMachine.syncInfo.SyncIsoTimestamp)
      
      if m.bsp.sign.isVideoWall and m.bsp.sign.videoWallType$ = "stretched" then
        aa["MultiscreenWidth"] = m.bsp.sign.videoWallNumColumns%
        aa["MultiscreenHeight"] = m.bsp.sign.videoWallNumRows%
        aa["MultiscreenX"] = m.bsp.sign.videoWallColumnPosition%
        aa["MultiscreenY"] = m.bsp.sign.videoWallRowPosition%
        aa["Mode"] = m.stateMachine.viewMode%
      end if
      
    end if
    
    if ItemIsEncrypted(m.videoItem) then
      aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
      aa.AddReplace("EncryptionKey", file$)
    end if
    
    if m.stateMachine.mosaicDecoderName <> "" then
      aa.Decoder = m.stateMachine.mosaicDecoderName
    end if
    
    ok = m.stateMachine.videoPlayer.PlayFile(aa)
    if ok = 0 then
      m.bsp.diagnostics.PrintDebug("Error playing file in LaunchVideo: " + file$ + ", " + filePath$)
      m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
      videoPlaybackFailure = { }
      videoPlaybackFailure["EventType"] = "VideoPlaybackFailureEvent"
      m.stateMachine.msgPort.PostMessage(videoPlaybackFailure)
    end if
    
    if syncInProgress then
      m.bsp.diagnostics.PrintDebug("LaunchVideo: play synchronized file " + file$)
      m.stateMachine.syncInfo = invalid
    end if
  end if
  
  if type(m.videoItem.userVariable) = "roAssociativeArray" then
    m.videoItem.userVariable.Increment()
  end if
  
  ' playback logging
  m.stateMachine.LogPlayStart("video", file$)
  
end sub


Sub PostPlayVideo(stateType$ as string)
  
  m.bsp.SetTouchRegions(m)
  
  m.stateMachine.ClearImagePlane()
  
  m.LaunchTimer()
  
  ' state logging
  m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, stateType$)
  
end sub


Sub LaunchVideo(stateType$ as string)
  
  m.PrePlayVideo()
  
  m.PlayVideo(true, false)
  
  m.PostPlayVideo(stateType$)
  
end sub


Sub PreDrawImage()
  
  m.ConfigureBPButtons()
  m.ConfigureGPIOButtons()
  
  m.usbInputBuffer$ = ""
  m.usbInputLogBuffer$ = ""
  
  m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
  
end sub


Sub DrawImage(setTransition as boolean)
  
  file$ = m.imageItem.fileName$
  if type(m.imageItem.filePath$) = "roString" then
    filePath$ = m.imageItem.filePath$
  else
    filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, file$)
  end if
      
  if setTransition then
    m.stateMachine.imagePlayer.SetDefaultTransition(m.imageItem.slideTransition%)
  end if
  
  if type(m.imageItem.transitionDuration%) = "roInt" then
    m.stateMachine.imagePlayer.SetTransitionDuration(m.imageItem.transitionDuration%)
  end if
  
  if not true then
    stop
  else
    
    if type(m.stateMachine.syncInfo) = "roAssociativeArray" then
      
      aa = { }
      aa.AddReplace("Filename", filePath$)
      aa.AddReplace("SyncDomain", m.stateMachine.syncInfo.SyncDomain)
      aa.AddReplace("SyncId", m.stateMachine.syncInfo.SyncId)
      aa.AddReplace("SyncIsoTimestamp", m.stateMachine.syncInfo.SyncIsoTimestamp)
      aa.AddReplace("Transition", m.imageItem.slideTransition%)
      
      ok = m.stateMachine.imagePlayer.DisplayFile(aa)
      if ok = 0 then
        m.bsp.diagnostics.PrintDebug("Error displaying synchronized file: " + file$)
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
      else
        m.bsp.diagnostics.PrintDebug("DisplayImage: display synchronized file " + file$)
      end if
      
      m.stateMachine.syncInfo = invalid
      
    else
      
      ' determine whether or not a preload has been performed
      preloaded = false
      if type(m.stateMachine.preloadState) = "roAssociativeArray" then
        if m.stateMachine.preloadedStateName$ = m.name$
          preloaded = true
          m.bsp.diagnostics.PrintDebug("Use preloaded file " + file$ + " in DisplayImage: ")
          m.bsp.diagnostics.PrintDebug("DisplayPreload in DisplayImage: " + file$)
          ok = m.stateMachine.imagePlayer.DisplayPreload()
          if ok = 0 then
            m.bsp.diagnostics.PrintDebug("Error in DisplayPreload in DisplayImage: " + file$ + ", " + filePath$)
            m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
          end if
          
        end if
      end if
      
      if not preloaded then
        
        aa = { }
        aa.filename = filePath$
        
        if ItemIsEncrypted(m.imageItem) then
          aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
          aa.AddReplace("EncryptionKey", file$)
        end if
        
        ok = m.stateMachine.imagePlayer.DisplayFile(aa)
        if not ok then
          m.bsp.diagnostics.PrintDebug("Error displaying file in DisplayImage: " + file$ + ", " + filePath$)
          m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
        else
          m.bsp.diagnostics.PrintDebug("Displayed file in DisplayImage: " + file$)
        end if
        
      end if
      
    end if
    
  end if
      
  m.stateMachine.ShowImageWidget()
  
  m.stateMachine.preloadState = 0
  m.stateMachine.preloadedStateName$ = ""
  
  if type(m.imageItem.userVariable) = "roAssociativeArray" then
    m.imageItem.userVariable.Increment()
  end if
  
  ' playback logging
  m.stateMachine.LogPlayStart("image", file$)
  
end sub


Sub PostDrawImage(stateType$ as string)
  
  m.ClearVideo()
  
  m.LaunchTimer()
  
  m.bsp.SetTouchRegions(m)
  
  ' state logging
  m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, stateType$)
  
end sub


Sub ClearVideo()
  
  if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
    m.stateMachine.videoPlayer.StopClear()
  end if
  
end sub


Sub DisplayImage(stateType$ as string)
  
  m.PreDrawImage()
  
  m.DrawImage(true)
  
  m.PostDrawImage(stateType$)
  
end sub


Sub PrePlayAudio()
  
  m.ConfigureBPButtons()
  m.ConfigureGPIOButtons()
  
  m.usbInputBuffer$ = ""
  m.usbInputLogBuffer$ = ""
  
  if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
    m.stateMachine.videoPlayer.StopClear()
  end if
  
end sub


Sub PlayAudio(executeEntryCmds as boolean, disableLoopMode as boolean)
  
  loopMode% = 1
  if disableLoopMode or type(m.audioEndEvent) = "roAssociativeArray" then loopMode% = 0
  
  if type(m.stateMachine.audioPlayer) = "roAudioPlayer" then
    player = m.stateMachine.audioPlayer
  else
    player = m.stateMachine.videoPlayer
  end if
  
  player.SetLoopMode(loopMode%)
  
  player.Stop()
  
  m.SetAudioTimeCodeEvents()
  
  if executeEntryCmds then
    m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
  end if
  
  if type(m.audioItem.fileName$) = "roString" and len(m.audioItem.fileName$) > 0 then
    file$ = m.audioItem.fileName$
  else
    file$ = m.audioItem.filePath$
  end if
  
  if type(m.audioItem.filePath$) = "roString" then
    filePath$ = m.audioItem.filePath$
  else
    filePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, m.audioItem.fileName$)
  end if
  
  aa = { }
  aa.AddReplace("Filename", filePath$)
  
  if type(m.audioItem.probeData) = "roString" then
    m.bsp.diagnostics.PrintDebug("LaunchAudio: probeData = " + m.audioItem.probeData)
    aa.AddReplace("ProbeString", m.audioItem.probeData)
  end if
  
  ok = player.PlayFile(aa)
  
  if ok = 0 then
    m.bsp.diagnostics.PrintDebug("Error playing audio file: " + file$ + ", " + filePath$)
    m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, file$)
    
    audioPlaybackFailure = { }
    audioPlaybackFailure["EventType"] = "AudioPlaybackFailureEvent"
    m.stateMachine.msgPort.PostMessage(audioPlaybackFailure)
  end if
  
  m.stateMachine.ClearImagePlane()
  
  if type(m.audioItem.userVariable) = "roAssociativeArray" then
    m.audioItem.userVariable.Increment()
  end if
  
  ' playback logging
  m.stateMachine.LogPlayStart("audio", file$)
  
end sub


Sub PostPlayAudio(stateType$ as string)
  
  m.bsp.SetTouchRegions(m)
  
  m.LaunchTimer()
  
  ' state logging
  m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, stateType$)
  
end sub


Sub LaunchAudio(stateType$ as string)
  
  m.PrePlayAudio()
  
  m.PlayAudio(true, false)
  
  m.PostPlayAudio(stateType$)
  
end sub


Function STSuperStateEventHandler(event as object, stateData as object) as object
  
  stateData.nextState = invalid
  
  if type(event) = "roAssociativeArray" then ' internal message event
  
  if IsString(event["EventType"]) then
    
    if event["EventType"] = "ENTRY_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
      
      m.ConfigureBPButtons()
      m.ConfigureGPIOButtons()
      
      m.usbInputBuffer$ = ""
      m.usbInputLogBuffer$ = ""
      
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
      
      m.LaunchTimer()
      
      m.bsp.SetTouchRegions(m)
      
      ' state logging
      m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "superState")
      
      ' playback logging
      m.stateMachine.LogPlayStart("superState", "")
      
      return "HANDLED"
      
    else if event["EventType"] = "EXIT_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
      
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)
      
    else if event["EventType"] = "MEDIA_END" then
      
      if type(m.mediaEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.mediaEndEvent, stateData, "")
      end if
      
    else
      
      return m.MediaItemEventHandler(event, stateData)
      
    end if
    
  end if
  
else
  
  return m.MediaItemEventHandler(event, stateData)
  
end if

stateData.nextState = m.superState
return "SUPER"

end function


Function STEventHandlerEventHandler(event as object, stateData as object) as object
  
  stateData.nextState = invalid
  
  if type(event) = "roAssociativeArray" then ' internal message event
  
  if IsString(event["EventType"]) then
    
    if event["EventType"] = "ENTRY_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
      
      m.ConfigureBPButtons()
      m.ConfigureGPIOButtons()
      
      m.usbInputBuffer$ = ""
      m.usbInputLogBuffer$ = ""
      
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
      
      if m.stopPlayback then
        
        if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
          m.stateMachine.videoPlayer.StopClear()
        end if
        
        m.stateMachine.ClearImagePlane()
        
        if IsAudioPlayer(m.stateMachine.audioPlayer) then
          m.stateMachine.audioPlayer.Stop()
        end if
        
      end if
      
      m.LaunchTimer()
      
      m.bsp.SetTouchRegions(m)
      
      ' state logging
      m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "eventHandler")
      
      ' playback logging
      m.stateMachine.LogPlayStart("eventHandler", "")
      
      return "HANDLED"
      
    else if event["EventType"] = "EXIT_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
      
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)
      
    else
      
      return m.MediaItemEventHandler(event, stateData)
      
    end if
    
  end if
  
else
  return m.MediaItemEventHandler(event, stateData)
end if

stateData.nextState = m.superState
return "SUPER"

end function


Function STLiveVideoPlayingEventHandler(event as object, stateData as object) as object
  
  MEDIA_END = 8
  
  stateData.nextState = invalid
  
  if type(event) = "roAssociativeArray" then ' internal message event
  
  if IsString(event["EventType"]) then
    
    if event["EventType"] = "ENTRY_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
      
      m.usbInputBuffer$ = ""
      m.usbInputLogBuffer$ = ""
      
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
      
      m.stateMachine.videoPlayer.Stop()
      m.stateMachine.ClearImagePlane()
      
      ' HDMI In
      m.stateMachine.videoPlayer.EnableSafeRegionTrimming(m.overscan)
      
      aa = { }
      aa.AddReplace("Capture", m.stateMachine.videoInput)
      
      if type(m.stateMachine.syncInfo) = "roAssociativeArray" then
        
        aa.AddReplace("SyncDomain", m.stateMachine.syncInfo.SyncDomain)
        aa.AddReplace("SyncId", m.stateMachine.syncInfo.SyncId)
        aa.AddReplace("SyncIsoTimestamp", m.stateMachine.syncInfo.SyncIsoTimestamp)
        
        if m.bsp.sign.isVideoWall and m.bsp.sign.videoWallType$ = "stretched" then
          aa["MultiscreenWidth"] = m.bsp.sign.videoWallNumColumns%
          aa["MultiscreenHeight"] = m.bsp.sign.videoWallNumRows%
          aa["MultiscreenX"] = m.bsp.sign.videoWallColumnPosition%
          aa["MultiscreenY"] = m.bsp.sign.videoWallRowPosition%
          aa["Mode"] = m.stateMachine.viewMode%
        end if
        
      end if
      
      if m.stateMachine.mosaicDecoderName <> "" then
        aa.Decoder = m.stateMachine.mosaicDecoderName
      end if
      
      ok = m.stateMachine.videoPlayer.PlayFile(aa)
      
      m.LaunchTimer()
      
      m.bsp.SetTouchRegions(m)
      
      ' state logging
      m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "liveVideo")
      
      ' playback logging
      m.stateMachine.LogPlayStart("liveVideo", "")
      
      return "HANDLED"
      
    else if event["EventType"] = "EXIT_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
      
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)
      
    else
      
      return m.MediaItemEventHandler(event, stateData)
      
    end if
    
  end if
  
else if type(event) = "roVideoEvent" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() then
  if event.GetInt() = MEDIA_END then
    m.bsp.diagnostics.PrintDebug("Video Event" + stri(event.GetInt()))
    m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
    if m.bsp.ProcessMediaEndEvent() then
      return "HANDLED"
    end if
    if type(m.videoEndEvent) = "roAssociativeArray" then
      return m.ExecuteTransition(m.videoEndEvent, stateData, "")
    end if
    
    PostMediaEndEvent(m.bsp.msgPort)'
  end if
  
else
  return m.MediaItemEventHandler(event, stateData)
end if

stateData.nextState = m.superState
return "SUPER"

end function


Sub SetUserAgentForHtmlWidget(bsp as object, htmlWidget as object, 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 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
  m.stateMachine.loadingHtmlWidget = CreateObject("roHtmlWidget", m.stateMachine.rectangle, aa)
  
end sub


' TEDTODO - fix indentation
  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$ = "RFChannelCount" then
        text["text"] = StripLeadingSpaces(stri(m.bsp.scannedChannels.Count()))
      else if templateItem.systemVariableType$ = "RFChannelName" then
        text["text"] = StripLeadingSpaces(m.bsp.rfChannelName)
      else if templateItem.systemVariableType$ = "RFVirtualChannel" then
        text["text"] = StripLeadingSpaces(m.bsp.rfVirtualChannel)
      else if templateItem.systemVariableType$ = "EdidMonitorSerialNumber" then
        text["text"] = m.bsp.sysInfo["edidMonitorSerialNumber"+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$
      else if templateItem.systemVariableType$ = "SessionGuid" then
        text["text"] = m.bsp.sysInfo.sessionGuid$
      else if templateItem.systemVariableType$ = "PlayerModelNumber" then
        text["text"] = m.bsp.sysInfo.deviceModel$
      end if
      
      m.BuildTextTemplateItem(templateItem, text)
      
    else if templateItem.type$ = "mediaCounterTemplateItem" or templateItem.type$ = "userVariableTemplateItem" then
      
      if type(templateItem.userVariable) = "roAssociativeArray" then
        text = { }
        text["text"] = templateItem.userVariable.GetCurrentValue()
        m.BuildTextTemplateItem(templateItem, text)
      end if
      
    else if templateItem.type$ = "indexedLiveTextDataItem" or templateItem.type$ = "titledLiveTextDataItem" then
      liveDataFeed = templateItem.liveDataFeed
      if m.liveDataFeeds.DoesExist(liveDataFeed.id$) then
        if templateItem.type$ = "indexedLiveTextDataItem" then
          indexStr$ = templateItem.index.GetCurrentParameterValue()
          index% = int(val(indexStr$))
          if index% > 0 then
            index% = index% - 1
          end if
          if type(liveDataFeed.articles) = "roArray" then
            if index% <= (liveDataFeed.articles.count() - 1) then
              textValue$ = liveDataFeed.articles[index%]
            else
              textValue$ = ""
            end if
            
            text = { }
            text["text"] = textValue$
            m.BuildTextTemplateItem(templateItem, text)
          end if
        else
          title$ = templateItem.title.GetCurrentParameterValue()
          if type(liveDataFeed.articlesByTitle) = "roAssociativeArray" then
            if liveDataFeed.articlesByTitle.DoesExist(title$) then
              textValue$ = liveDataFeed.articlesByTitle.Lookup(title$)
            else
              textValue$ = ""
            end if
            
            text = { }
            text["text"] = textValue$
            m.BuildTextTemplateItem(templateItem, text)
          end if
        end if
      end if
      
    else if templateItem.type$ = "imageTemplateItem" then
      
      image = { }
      
      image["name"] = templateItem.fileName$
      image["filename"] = GetPoolFilePath(m.bsp.assetPoolFiles, templateItem.fileName$)
      image["CompositionMode"] = "source_over"
      
      ' TemplateToDo - do the images care about the stretch/crop mode?
      ' m.SetBackgroundImageSizeLocation(backgroundImage)
      BuildBaseTemplateItem(templateItem, image)
      
    else if templateItem.type$ = "mrssTextTemplateItem" then
      
      if type(templateItem.textString$) <> "Invalid" then
        text = { }
        text["text"] = templateItem.textString$
        m.BuildTextTemplateItem(templateItem, text)
      end if
      
    else if templateItem.type$ = "mrssImageTemplateItem" or templateItem.type$ = "mrssMediaTemplateItem" then
      
      if type(templateItem.fileName$) <> "Invalid" and templateItem.fileName$ <> "" then
        image = { }
        
        if type(templateItem.fileNameForEncryption) = "roString" then
          image["fileNameForEncryption"] = templateItem.fileNameForEncryption
        else
          image["fileNameForEncryption"] = ""
        end if
        
        image["filename"] = templateItem.fileName$
        image["CompositionMode"] = "source-over"
        
        ' TemplateToDo - do the images care about the stretch/crop mode?
        ' m.SetBackgroundImageSizeLocation(backgroundImage)
        BuildBaseTemplateItem(templateItem, image)
      end if
      
    end if
    
    m.BuildTemplateItem(text, image, templateItem)
    
  next
  
  ' now add any simple rss items
  if type(m.simpleRSSTemplateItems) = "roAssociativeArray" then
    for each simpleRSSId in m.simpleRSSTemplateItems
      simpleRSS = m.simpleRSSTemplateItems.Lookup(simpleRSSId)
      if type(simpleRSS) = "roAssociativeArray" then
        liveDataFeed = simpleRSS.rssLiveDataFeeds[simpleRSS.currentLiveDataFeedIndex%]
        if m.liveDataFeeds.DoesExist(liveDataFeed.id$) then
  
          if type(liveDataFeed.articles) = "roArray" then
            
            if simpleRSS.currentIndex% >= liveDataFeed.articles.count() then
              simpleRSS.currentIndex% = 0
            end if
            index% = simpleRSS.currentIndex%
            
            ' remove the next conditional - it's not needed
            if index% <= (liveDataFeed.articles.count() - 1) then
              for each templateItem in simpleRSS.items
                if lcase(templateItem.elementName$) = "title" then
                  textValue$ = liveDataFeed.articleTitles[index%]
                else
                  textValue$ = liveDataFeed.articles[index%]
                end if
                
                text = { }
                text["text"] = textValue$
                m.BuildTextTemplateItem(templateItem, text)
                
                m.BuildTemplateItem(text, image, templateItem)
              next
              
              ' first time display - start timer to display next item
              if type(simpleRSS.rssItemTimer) <> "roTimer" then
                simpleRSS.rssItemTimer = CreateObject("roTimer")
                simpleRSS.rssItemTimer.SetPort(m.stateMachine.msgPort)
                simpleRSS.rssItemTimer.SetElapsed(simpleRSS.displayTime%, 0)
                simpleRSS.rssItemTimer.Start()
              end if
            end if
          end if
        end if
      end if
    next
  end if
  
end sub


Sub BuildTemplateItem(text as object, image as object, templateItem as object)
  
  if type(text) = "roAssociativeArray" or type(image) = "roAssociativeArray" then
    
    backgroundLayer% = (templateItem.layer% - 1) * 2 + 1
    contentLayer% = backgroundLayer% + 1
    
    if type(m.stateMachine.templateObjectsByLayer[contentLayer%]) <> "roArray" then
      m.stateMachine.templateObjectsByLayer[contentLayer%] = CreateObject("roArray", 1, true)
    end if
    
    if type(text) = "roAssociativeArray" then
      
      m.stateMachine.templateObjectsByLayer[contentLayer%].push(text)
      
      if templateItem.backgroundColorSpecified then
        
        backgroundColor = { }
        backgroundColor["color"] = templateItem.backgroundTextColor$
        backgroundColor["targetRect"] = { x: templateItem.x%, y: templateItem.y%, w: templateItem.width%, h: templateItem.height% }
        
        if type(m.stateMachine.templateObjectsByLayer[backgroundLayer%]) <> "roArray" then
          m.stateMachine.templateObjectsByLayer[backgroundLayer%] = CreateObject("roArray", 1, true)
        end if
        
        m.stateMachine.templateObjectsByLayer[backgroundLayer%].push(backgroundColor)
        
      end if
      
    else
      
      m.stateMachine.templateObjectsByLayer[contentLayer%].push(image)
      
    end if
    
  end if
  
end sub


Sub SetupTemplateMRSS()
  
  if type(m.mrssTitleTemplateItem) = "roAssociativeArray" then
    m.mrssTitleTemplateItem.textString$ = invalid
  end if
  
  if type(m.mrssDescriptionTemplateItem) = "roAssociativeArray" then
    m.mrssDescriptionTemplateItem.textString$ = invalid
  end if
  
  if type(m.mrssImageTemplateItem) = "roAssociativeArray" or type(m.mrssMediaTemplateItem) = "roAssociativeArray" then
    m.mrssMediaTemplateItem.fileName$ = invalid
  end if
  
  if type(m.mrssCustomFieldTemplateItems) = "roAssociativeArray" then
    for each customFieldName in m.mrssCustomFieldTemplateItems
      customFieldTemplateItem = m.mrssCustomFieldTemplateItems[customFieldName]
      customFieldTemplateItem.textString$ = invalid
    next
  end if
  
end sub


Function STTemplatePlayingEventHandler(event as object, stateData as object) as object
  
  MEDIA_END = 8
  
  stateData.nextState = invalid
  
  if type(event) = "roAssociativeArray" then ' internal message event
  
  if IsString(event["EventType"]) then
    
    if event["EventType"] = "ENTRY_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
      
      if m.mrssActive then
        
        m.SetupTemplateMRSS()
        
        m.currentFeed = invalid
        m.pendingFeed = invalid
        
        m.mrssItemTimer = CreateObject("roTimer")
        m.mrssItemTimer.SetPort(m.stateMachine.msgPort)
        
        if type(m.mrssLiveDataFeeds) = "roArray" and m.mrssLiveDataFeeds.Count() > 0 then
          m.mrssLiveDataFeed = invalid
          m.currentFeed = invalid
          m.LaunchWaitForContentTimer()
        end if
        
      end if
      
      ' reset indices on entry to the state
      if type(m.simpleRSSTemplateItems) = "roAssociativeArray" then
        for each simpleRSSId in m.simpleRSSTemplateItems
          simpleRSS = m.simpleRSSTemplateItems.Lookup(simpleRSSId)
          if type(simpleRSS) = "roAssociativeArray" then
            simpleRSS.currentIndex% = 0
            simpleRSS.currentLiveDataFeedIndex% = 0
            simpleRSS.rssItemTimer = invalid
          end if
        next
      end if
      
      m.liveDataFeeds = { }
      for each liveDataFeedId in m.bsp.liveDataFeeds
        liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
        m.liveDataFeeds.AddReplace(liveDataFeedId, liveDataFeed)
      next
      
      m.ConfigureBPButtons()
      m.ConfigureGPIOButtons()
      
      m.usbInputBuffer$ = ""
      m.usbInputLogBuffer$ = ""
      
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.cmds)
      
      if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
        m.stateMachine.videoPlayer.StopClear()
      end if
      
      if type(m.stateMachine.canvasWidget) <> "roCanvasWidget" then
        r = CreateScaledRectangle(m.stateMachine.x%, m.stateMachine.y%, m.stateMachine.width%, m.stateMachine.height%)
        m.stateMachine.canvasWidget = CreateObject("roCanvasWidget", r)
      end if
      
      m.stateMachine.canvasWidget.EnableAutoRedraw(0)
      
      maxLayer% = 1
      if type(m.stateMachine.templateObjectsByLayer) = "roArray" then
        maxLayer% = m.stateMachine.templateObjectsByLayer.Count()
      end if
      
      for i% = 1 to maxLayer%
        m.stateMachine.canvasWidget.ClearLayer(i%)
      next
      
      ' display background image if it exists
      file$ = m.backgroundImage$
      if file$ <> "" then
        backgroundImage = { }
        backgroundImage["name"] = file$
        backgroundImage["filename"] = GetPoolFilePath(m.bsp.assetPoolFiles, file$)
        backgroundImage["CompositionMode"] = "source"
        
        if m.backgroundImageWidth% <= 0 or m.backgroundImageHeight% <= 0 then
          backgroundImage["targetRect"] = { x: 0, y: 0, w: m.stateMachine.width%, h: m.stateMachine.height% }
        else
          m.SetBackgroundImageSizeLocation(backgroundImage)
        end if
        
        if backgroundImage.DoesExist("name") then
          name$ = backgroundImage["name"]
          if type(m.bsp.encryptionByFile) = "roAssociativeArray" and m.bsp.encryptionByFile.DoesExist(name$) then
            'if m.bsp.contentEncrypted then
            backgroundImage.EncryptionAlgorithm = "AesCtrHmac"
            backgroundImage.EncryptionKey = name$
          end if
        end if
        
        m.stateMachine.canvasWidget.SetLayer(backgroundImage, 0)
        
      else
        
        m.stateMachine.canvasWidget.ClearLayer(0)
        
      end if
      
      ' build arrays of template items & background colors
      m.BuildTemplateItems()
      
      numLayers% = m.stateMachine.templateObjectsByLayer.Count()
      
      for i% = 0 to numLayers% - 1
        if type(m.stateMachine.templateObjectsByLayer[i%]) = "roArray" then
          
          templateObjects = m.stateMachine.templateObjectsByLayer[i%]
          
          ' must support multiple objects per layer??!!
          templateObject = templateObjects[0]
          
          if type(templateObject) = "roAssociativeArray" and templateObject.DoesExist("name") then
            name$ = templateObject["name"]
            if type(m.bsp.encryptionByFile) = "roAssociativeArray" and m.bsp.encryptionByFile.DoesExist(name$) then
              templateObject.EncryptionAlgorithm = "AesCtrHmac"
              templateObject.EncryptionKey = name$
            end if
          end if
          
          m.stateMachine.canvasWidget.SetLayer(templateObject, i% + 1)
        end if
      next
      
      m.stateMachine.canvasWidget.EnableAutoRedraw(1)
      
      m.stateMachine.ShowCanvasWidget()
      
      m.LaunchTimer()
      
      m.bsp.SetTouchRegions(m)
      
      ' state logging
      m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "liveText")
      
      ' playback logging
      m.stateMachine.LogPlayStart("liveText", "")
      
      return "HANDLED"
      
    else if event["EventType"] = "EXIT_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
      
      ' resize the video player in case it was used by an MRSS feed within this state
      if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
        m.stateMachine.videoPlayer.StopClear()
        m.stateMachine.videoPlayer.SetRectangle(m.stateMachine.rectangle)
      end if
      
      m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)
      
    else if event["EventType"] = "USER_VARIABLES_UPDATED" then
      
      if m.TemplateUsesAnyUserVariable() then
        m.RedisplayTemplateItems()
      end if
      
      return "HANDLED"
      
    else if event["EventType"] = "SYSTEM_VARIABLE_UPDATED" then
      
      if m.TemplateUsesSystemVariable() then
        m.RedisplayTemplateItems()
      end if
      
      return "HANDLED"
      
    else if event["EventType"] = "USER_VARIABLE_CHANGE" then
      
      userVariable = event["UserVariable"]
      
      if m.TemplateUsesUserVariable(userVariable) then
        m.RedisplayTemplateItems()
      end if
      
      return "HANDLED"
      
    else if event["EventType"] = "USER_VARIABLES_RESET" then
      
      m.RedisplayTemplateItems()
      
      return "HANDLED"
      
    else if event["EventType"] = "LIVE_DATA_FEED_UPDATE" then
      
      liveDataFeed = event["EventData"]
      
      m.liveDataFeeds.AddReplace(liveDataFeed.id$, liveDataFeed)
      
      m.RedisplayTemplateItems()
      
      return "HANDLED"
      
    else
      
      return m.MediaItemEventHandler(event, stateData)
      
    end if
    
  end if
  
else if type(event) = "roVideoEvent" and type(m.stateMachine.videoPlayer) = "roVideoPlayer" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() and event.GetInt() = MEDIA_END then
  
  m.GetMRSSTemplateItem()
  return "HANDLED"
  
else if type(event) = "roTimerEvent" then
  
  if type(m.waitForContentTimer) = "roTimer" and event.GetSourceIdentity() = m.waitForContentTimer.GetIdentity() then
    
    if m.FindMRSSContent() then
      m.GetMRSSTemplateItem()
    end if
    
    return "HANDLED"
    
  end if
  
  if type(m.mrssItemTimer) = "roTimer" and event.GetSourceIdentity() = m.mrssItemTimer.GetIdentity() then
    
    m.GetMRSSTemplateItem()
    return "HANDLED"
    
  end if
  
  if type(m.simpleRSSTemplateItems) = "roAssociativeArray" then
    for each simpleRSSId in m.simpleRSSTemplateItems
      simpleRSS = m.simpleRSSTemplateItems.Lookup(simpleRSSId)
      if type(simpleRSS) = "roAssociativeArray" then
        if type(simpleRSS.rssItemTimer) = "roTimer" then
          if event.GetSourceIdentity() = simpleRSS.rssItemTimer.GetIdentity() then
            
            itemExists = false
            
            liveDataFeed = simpleRSS.rssLiveDataFeeds[simpleRSS.currentLiveDataFeedIndex%]
            if m.liveDataFeeds.DoesExist(liveDataFeed.id$) then
              if type(liveDataFeed.articles) = "roArray" then
                simpleRSS.currentIndex% = simpleRSS.currentIndex% + 1
                if simpleRSS.currentIndex% >= liveDataFeed.articles.count() then
                  simpleRSS.currentIndex% = 0
                  simpleRSS.currentLiveDataFeedIndex% = simpleRSS.currentLiveDataFeedIndex% + 1
                  if simpleRSS.currentLiveDataFeedIndex% >= simpleRSS.rssLiveDataFeeds.Count() then
                    simpleRSS.currentLiveDataFeedIndex% = 0
                  end if
                  liveDataFeed = simpleRSS.rssLiveDataFeeds[simpleRSS.currentLiveDataFeedIndex%]
                  if m.liveDataFeeds.DoesExist(liveDataFeed.id$) then
                    if type(liveDataFeed.articles) = "roArray" then
                      itemExists = true
                    end if
                  end if
                else
                  itemExists = true
                end if
              end if
            end if
            
            if itemExists then
              m.RedisplayTemplateItems()
              
              ' restart timer
              simpleRSS.rssItemTimer.SetElapsed(simpleRSS.displayTime%, 0)
              simpleRSS.rssItemTimer.Start()
            end if
            
            return "HANDLED"
            
          end if
        end if
      end if
    next
  end if
  
  return m.MediaItemEventHandler(event, stateData)
  
else
  
  return m.MediaItemEventHandler(event, stateData)
  
end if

stateData.nextState = m.superState
return "SUPER"

end function


Function FindMRSSContent() as boolean
  
  ' get the mrss live data feed to start searching with
  if m.mrssLiveDataFeed = invalid then
    mrssLiveDataFeedIndex% = 0
  else
    mrssLiveDataFeedIndex% = m.mrssLiveDataFeedIndex%
  end if
  
  startingIndex% = mrssLiveDataFeedIndex%
  
  while true
    
    mrssLiveDataFeed = m.mrssLiveDataFeeds[mrssLiveDataFeedIndex%]
    
    if type(mrssLiveDataFeed.assetPoolFiles) = "roAssetPoolFiles" then
      currentFeed = mrssLiveDataFeed.feed
      if currentFeed.AllContentExists(mrssLiveDataFeed.assetPoolFiles) then
        m.mrssLiveDataFeed = mrssLiveDataFeed
        m.currentFeed = m.mrssLiveDataFeed.feed
        m.mrssLiveDataFeedIndex% = mrssLiveDataFeedIndex%
        m.displayIndex = 0
        return true
      end if
    end if
    
    mrssLiveDataFeedIndex% = mrssLiveDataFeedIndex% + 1
    if mrssLiveDataFeedIndex% >= m.mrssLiveDataFeeds.Count() then
      mrssLiveDataFeedIndex% = 0
    end if
    
    if mrssLiveDataFeedIndex% = startingIndex% then
      ' search has wrapped around - nothing was found - continue waiting
      m.LaunchWaitForContentTimer()
      return false
    end if
    
  end while
  
end function


Function GetNextMRSSTemplateItem() as object
  
  if m.currentFeed.items.Count() = 0 then
    return invalid
  end if
  foundItem = false
  
  while not foundItem
    
    if m.displayIndex >= m.currentFeed.items.Count() then
      
      m.mrssLiveDataFeedIndex% = m.mrssLiveDataFeedIndex% + 1
      if m.mrssLiveDataFeedIndex% >= m.mrssLiveDataFeeds.Count() then
        m.mrssLiveDataFeedIndex% = 0
      end if
      
      contentFound = m.FindMRSSContent()
      
      if not contentFound then
        return invalid
      end if
      
    end if
    
    displayItem = m.currentFeed.items[m.displayIndex]
    
    if displayItem = invalid then
      return invalid
    end if
    
    filePath$ = GetPoolFilePath(m.mrssLiveDataFeed.assetPoolFiles, displayItem.url)
    if filePath$ <> "" then
      foundItem = true
      displayItem.filePath$ = filePath$
    end if
    
    m.displayIndex = m.displayIndex + 1
    
  end while
  
  return displayItem
  
end function


Sub GetMRSSTemplateItem()
  
  item = m.GetNextMRSSTemplateItem()
  
  if type(item) <> "roAssociativeArray" then
    
    ' no valid content - clear old content
    if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
      m.stateMachine.videoPlayer.StopClear()
    end if
    if type(m.stateMachine.imagePlayer) = "roImageWidget" then
      m.stateMachine.imagePlayer.StopDisplay()
    end if
    
    m.ClearTemplateItems()
    
    m.LaunchWaitForContentTimer()
    return
    
  end if
  
  ' check to see if the item is an image or a video.
  ' if an image, set a timer
  ' if a video, need to wait for media end event
  if isImage(item) then
    m.mrssItemTimer.SetElapsed(item.duration, 0)
    m.mrssItemTimer.Start()
  end if
  
  if type(m.mrssTitleTemplateItem) = "roAssociativeArray" then
    m.mrssTitleTemplateItem.textString$ = item.title
  end if
  
  if type(m.mrssDescriptionTemplateItem) = "roAssociativeArray" then
    m.mrssDescriptionTemplateItem.textString$ = item.description
  end if
  
  ' need to distinguish here between image items and video items
  m.mrssVideoTemplateItem = { }
  if type(m.mrssMediaTemplateItem) = "roAssociativeArray" then
    if isImage(item) then
      m.mrssMediaTemplateItem.fileName$ = item.filePath$
      m.mrssMediaTemplateItem.fileNameForEncryption = item.title
      m.mrssVideoTemplateItem.fileName$ = ""
    else
      m.mrssMediaTemplateItem.fileName$ = ""
      m.mrssVideoTemplateItem.fileName$ = item.filePath$
      m.mrssVideoTemplateItem.fileNameForEncryption = item.title
    end if
  end if
  
  if type(m.mrssCustomFieldTemplateItems) = "roAssociativeArray" then
    
    for each customFieldName in m.mrssCustomFieldTemplateItems
      
      customFieldTemplateItem = m.mrssCustomFieldTemplateItems[customFieldName]
      
      ' see if the corresponding custom field exists in this item
      customFieldValue = item.mrssCustomFields.lookup(customFieldName)
      if type(customFieldValue) = "roString" then
        customFieldTemplateItem.textString$ = customFieldValue
      end if
      
    next
    
  end if
  
  ' different/additional path needed to display video
  if type(m.mrssVideoTemplateItem) = "roAssociativeArray" and m.mrssVideoTemplateItem.fileName$ <> invalid and m.mrssVideoTemplateItem.fileName$ <> "" and type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
    
    ' ensure that the image gets cleared
    
    item = m.mrssMediaTemplateItem
    
    r = CreateScaledRectangle(item.x% + m.stateMachine.rectangle.getX(), item.y% + m.stateMachine.rectangle.getY(), item.width%, item.height%)
    m.stateMachine.videoPlayer.SetRectangle(r)
    
    m.videoItem = { }
    m.videoItem.videoDisplayMode% = 0
    m.videoItem.automaticallyLoop = false
    m.videoItem.fileName$ = m.mrssVideoTemplateItem.fileNameForEncryption
    m.videoItem.filePath$ = m.mrssVideoTemplateItem.fileName$
    m.stateMachine.preloadState = invalid
    m.stateMachine.syncInfo = invalid
    m.videoItem.userVariable = invalid
    m.videoTimeCodeEvents = invalid
    
    m.videoItem.isEncrypted = false
    if m.bsp.contentEncrypted and type(m.currentFeed) = "roAssociativeArray" and type(m.currentFeed.liveDataFeed) = "roAssociativeArray" then
      if m.currentFeed.liveDataFeed.isDynamicPlaylist or m.currentFeed.liveDataFeed.isLiveMediaFeed then
        m.videoItem.isEncrypted = true
      end if
    end if
    
    m.PlayVideo(false, true)
  else
    if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
      m.stateMachine.videoPlayer.StopClear()
    end if
  end if
  
  m.RedisplayTemplateItems()
  
end sub


Function STPlayingMediaRSSEventHandler(event as object, stateData as object) as object
  
  MEDIA_START = 3
  MEDIA_END = 8
  
  stateData.nextState = invalid
  
  if type(event) = "roAssociativeArray" then ' internal message event
  
  if IsString(event["EventType"]) then
    
    if event["EventType"] = "ENTRY_SIGNAL" then
      m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
      
      ' execute entry commands; perform other setup functions
      m.firstItemDisplayed = false
      m.PreDrawImage()
      
      ' set default transition
      if type(m.stateMachine.imagePlayer) = "roImageWidget" then
        m.stateMachine.imagePlayer.SetDefaultTransition(m.slideTransition%)
      end if
      
      m.currentFeed = invalid
      m.pendingFeed = invalid
      
      ' see if the designated feed has already been downloaded (doesn't imply content exists)
      if type(m.liveDataFeed) <> "roAssociativeArray" stop
        
        if type(m.liveDataFeed.assetPoolFiles) = "roAssetPoolFiles" then
          
          ' create local versions of key objects
          m.assetCollection = m.liveDataFeed.assetCollection
          m.assetPoolFiles = m.liveDataFeed.assetPoolFiles
          m.currentFeed = m.liveDataFeed.feed
          
          ' protect the feed that is getting displayed
          m.ProtectMRSSFeed("display-" + m.liveDataFeed.id$, m.assetCollection)
          
          m.displayIndex = 0
          ' distinguish between a feed that has no content and a feed in which no content has been downloaded
          if m.currentFeed.items.Count() = 0 or not m.currentFeed.AllContentExists(m.assetPoolFiles) then
            ' no content in feed - send a message to self to trigger exit from state (like video playback failure)
            mrssNotFullyLoadedPlaybackEvent = { }
            mrssNotFullyLoadedPlaybackEvent["EventType"] = "MRSSNotFullyLoadedPlaybackEvent"
            mrssNotFullyLoadedPlaybackEvent["EventParameter"] = m.liveDataFeed.id$
            m.stateMachine.msgPort.PostMessage(mrssNotFullyLoadedPlaybackEvent)
          else
            m.AdvanceToNextMRSSItem()
          end if
        else
          ' this situation will occur when the feed itself has not downloaded yet - send a message to self to trigger exit from state (like video playback failure)
          mrssNotFullyLoadedPlaybackEvent = { }
          mrssNotFullyLoadedPlaybackEvent["EventType"] = "MRSSNotFullyLoadedPlaybackEvent"
          mrssNotFullyLoadedPlaybackEvent["EventParameter"] = m.liveDataFeed.id$
          m.stateMachine.msgPort.PostMessage(mrssNotFullyLoadedPlaybackEvent)
          return "HANDLED"
        end if
        
        m.LaunchTimer()
        m.bsp.SetTouchRegions(m)
        m.bsp.logging.WriteStateLogEntry(m.stateMachine, m.id$, "mrss")
        
        return "HANDLED"
        
      else if event["EventType"] = "EXIT_SIGNAL" then
        m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
        
        m.bsp.ExecuteMediaStateCommands(m.stateMachine, m.exitCmds)
        
        return "HANDLED"
        
      else if event["EventType"] = "VideoPlaybackFailureEvent" then
        
        if type(m.currentFeed) <> "roAssociativeArray" then
          return "HANDLED"
        end if
        
        if m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray" then
          return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
        else
          m.AdvanceToNextMRSSItem()
          return "HANDLED"
        end if
        
      else if event["EventType"] = "MRSSNotFullyLoadedPlaybackEvent" then
        liveDataFeedId$ = event["EventParameter"]
        if liveDataFeedId$ = m.liveDataFeed.id$ then
          if type(m.signChannelEndEvent) = "roAssociativeArray" then
            return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
          else if type(m.currentFeed) = "roAssociativeArray" and m.currentFeed.ContentExists(m.assetPoolFiles) then
            m.AdvanceToNextMRSSItem()
            '' redundant check
            ''					else if type(m.currentFeed) = "roAssociativeArray" and type(m.currentFeed.items) = "roArray" and m.currentFeed.items.Count() = 0 then
            ''						m.LaunchWaitForContentTimer()
          else
            m.LaunchWaitForContentTimer()
          end if
        end if
        return "HANDLED"
        
      else if event["EventType"] = "MRSS_SPEC_UPDATED" then
        
        updatedLiveDataFeed = event["LiveDataFeed"]
        
        if updatedLiveDataFeed.id$ = m.liveDataFeed.id$ then
          
          m.liveDataFeed = updatedLiveDataFeed ' this seems completely unnecessary
          
          if type(m.currentFeed) <> "roAssociativeArray" or (not m.currentFeed.ContentExists(m.assetPoolFiles)) then
            
            ' this is the first time that data is available
            m.pendingFeed = invalid
            m.currentFeed = m.liveDataFeed.feed
            m.assetCollection = m.liveDataFeed.assetCollection
            m.assetPoolFiles = m.liveDataFeed.assetPoolFiles
            
            ' protect the feed that is getting displayed
            m.ProtectMRSSFeed("display-" + m.liveDataFeed.id$, m.assetCollection)
            
            ' feed may have been downloaded but it might not have content yet (empty mrss feed)
            ' or feed has been downloaded but not all of its content has been downloaded yet - in this case, move on to the next item if possible
            if m.currentFeed.items.Count() = 0 or not m.currentFeed.AllContentExists(m.assetPoolFiles)
              if type(m.currentFeed) = "roAssociativeArray" and m.currentFeed.ContentExists(m.assetPoolFiles) then
                m.AdvanceToNextMRSSItem()
              else if type(m.signChannelEndEvent) = "roAssociativeArray" then
                return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
              else
                m.LaunchWaitForContentTimer()
                return "HANDLED"
              end if
            end if
            
            ' all content exists - display an item
            m.displayIndex = 0
            m.AdvanceToNextMRSSItem()
            
          else
            
            ' feed was updated. play through existing feed until it reaches the end; then switch to new feed.
            ' note - this does not imply that the feed actually changed.
            m.pendingFeed = m.liveDataFeed.feed
            m.pendingAssetCollection = m.liveDataFeed.assetCollection
            m.pendingAssetPoolFiles = m.liveDataFeed.assetPoolFiles
            
          end if
          
        end if
        
        return "HANDLED"
        
      else
        
        return m.MediaItemEventHandler(event, stateData)
        
      end if
      
    end if
    
  else if type(event) = "roHtmlWidgetEvent" and type(m.stateMachine.loadingHtmlWidget) = "roHtmlWidget" then
    
    eventData = event.GetData()
    if type(eventData) = "roAssociativeArray" and type(eventData.reason) = "roString" then
      userData = event.GetUserData()
      if userData <> invalid and userData.stateId$ <> invalid and userData.stateId$ = m.id$ then
        m.bsp.diagnostics.PrintDebug("reason = " + eventData.reason)
        if eventData.reason = "load-error" then
          m.bsp.diagnostics.PrintDebug("message = " + eventData.message)
          m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_HTML5_LOAD_ERROR, eventData.message)
          
          m.AdvanceToNextMRSSItem()
          
        else if eventData.reason = "load-finished" then
          if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
            m.stateMachine.videoPlayer.StopClear()
          end if
          
          '					m.stateMachine.displayedHtmlWidget = m.stateMachine.loadingHtmlWidget
          '					m.stateMachine.ShowHtmlWidget()
          ' Do a swap instead of just an assignment
          
          m.stateMachine.onDisplayHtmlWidget = m.stateMachine.displayedHtmlWidget
          m.stateMachine.displayedHtmlWidget = m.stateMachine.loadingHtmlWidget
          m.stateMachine.ShowHtmlWidget()
          m.stateMachine.onDisplayHtmlWidget = invalid
          
        end if
      end if
    end if
    
  else if type(event) = "roTimerEvent" then
    
    if type(m.imageTimeoutTimer) = "roTimer" and event.GetSourceIdentity() = m.imageTimeoutTimer.GetIdentity() then
          
      if m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
      else
        m.AdvanceToNextMRSSItem()
        return "HANDLED"
      end if
      
    else if type(m.waitForContentTimer) = "roTimer" and event.GetSourceIdentity() = m.waitForContentTimer.GetIdentity() then
      
      if type(m.currentFeed) <> "roAssociativeArray" or not m.currentFeed.AllContentExists(m.assetPoolFiles) then
        if type(m.currentFeed) = "roAssociativeArray" and m.currentFeed.ContentExists(m.assetPoolFiles) then
          if type(m.displayIndex) = "Invalid" then
            m.displayIndex = 0
          end if
          m.AdvanceToNextMRSSItem()
        else
          m.LaunchWaitForContentTimer()
        end if
      else if type(m.currentFeed) = "roAssociativeArray" and type(m.currentFeed.items) = "roArray" and m.currentFeed.items.Count() = 0 then
        m.LaunchWaitForContentTimer()
      else
        m.displayIndex = 0
        m.AdvanceToNextMRSSItem()
      end if
      
      return "HANDLED"
      
    end if
    
    return m.MediaItemEventHandler(event, stateData)
  else if type(event) = "roVideoEvent" and type(m.stateMachine.videoPlayer) = "roVideoPlayer" and event.GetSourceIdentity() = m.stateMachine.videoPlayer.GetIdentity() and event.GetInt() = MEDIA_END then
    if m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray" then
      return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
    else
      m.AdvanceToNextMRSSItem()
      return "HANDLED"
    end if
    
  else if m.stateMachine.type$ = "EnhancedAudio" and type(event) = "roAudioEventMx" then
    
    if event.GetInt() = MEDIA_START then
      
      if not (m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray") then
        m.AdvanceToNextMRSSItem()
        return "HANDLED"
      end if
      
    else if event.GetInt() = MEDIA_END then
      
      m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
      
      if m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
      else
        m.AdvanceToNextMRSSItem()
        return "HANDLED"
      end if
      
    end if
    
  else if IsAudioEvent(m.stateMachine, event) and event.GetInt() = MEDIA_END then
    
    if m.AtEndOfFeed() and type(m.signChannelEndEvent) = "roAssociativeArray" then
      return m.ExecuteTransition(m.signChannelEndEvent, stateData, "")
    else
      m.AdvanceToNextMRSSItem()
      return "HANDLED"
    end if
    
  else
    
    return m.MediaItemEventHandler(event, stateData)
    
  end if
  
  stateData.nextState = m.superState
  return "SUPER"
  
end function


Sub LaunchWaitForContentTimer()
  
  if type(m.waitForContentTimer) = "roTimer" then
    m.waitForContentTimer.Stop()
  else
    m.waitForContentTimer = CreateObject("roTimer")
    m.waitForContentTimer.SetPort(m.bsp.msgPort)
  end if
  
  m.waitForContentTimer.SetElapsed(1, 0)
  m.waitForContentTimer.Start()
  
end sub


' need to also consider the case where it's not at the end but there's no more content.
Function AtEndOfFeed() as boolean
  
  return m.displayIndex >= m.currentFeed.items.Count()
  
end function


Sub AdvanceToNextMRSSItem()
  
  displayedItem = false
  
  while not displayedItem
    if m.displayIndex >= m.currentFeed.items.Count() then
      
      m.displayIndex = 0
      ' switch to new feed if available
      if type(m.pendingFeed) = "roAssociativeArray" then
        
        m.currentFeed = m.pendingFeed
        m.assetCollection = m.pendingAssetCollection
        m.assetPoolFiles = m.pendingAssetPoolFiles
        
        m.pendingFeed = invalid
        
        ' protect the feed that we're switching to
        m.ProtectMRSSFeed("display-" + m.liveDataFeed.id$, m.assetCollection)
        
        if m.currentFeed.items.Count() = 0 or not m.currentFeed.AllContentExists(m.assetPoolFiles) then
          if type(m.currentFeed) = "roAssociativeArray" and m.currentFeed.ContentExists(m.assetPoolFiles) then
            if type(m.displayIndex) = "Invalid" then
              m.displayIndex = 0
            end if
            m.AdvanceToNextMRSSItem()
          else
            m.LaunchWaitForContentTimer()
          end if
          return
        end if
        
      end if
      
    end if
    
    displayItem = m.currentFeed.items[m.displayIndex]
    
    if isHtml(displayItem) then
      if displayItem.type = "application/widget" then
        filePath$ = GetPoolFilePath(m.assetPoolFiles, displayItem.url)
        if filePath$ <> "" then
          widgetPath$ = m.GetHtmlWidgetFilePath(displayItem, filePath$)
          if widgetPath$ <> invalid then
            m.DisplayMRSSItem(displayItem, widgetPath$)
            displayedItem = true
          end if
        end if
      else if displayItem.type = "text/html" then
        m.DisplayMRSSItem(displayItem, displayItem.url)
        displayedItem = true
      end if
    else
      filePath$ = GetPoolFilePath(m.assetPoolFiles, displayItem.url)
      if filePath$ <> "" then
        m.ProtectMRSSItem(displayItem) ' with the current code, this may be unnecessary since the entire feed is protected.
        m.DisplayMRSSItem(displayItem, filePath$)
        ' check return value before doing this??
        displayedItem = true
      end if
    end if
    
    m.displayIndex = m.displayIndex + 1
    
  end while
  
end sub


Sub ProtectMRSSFeed(name$ as string, assets as object)
  
  if not m.bsp.feedPool.ProtectAssets(name$, assets) then
    m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, m.bsp.feedPool.GetFailureReason())
    m.bsp.logging.FlushLogFile()
    m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + m.bsp.feedPool.GetFailureReason())
    stop
  end if
  
end sub


Sub ProtectMRSSItem(displayItem as object)
  
  m.playingItemAssetCollection = CreateObject("roAssetCollection")
  
  asset = { }
  asset.link = displayItem.url
  asset.name = displayItem.url
  if IsNonEmptyString(displayItem.guid) then
    asset.change_hint = displayItem.guid
  else if IsString(displayItem.url) then
    asset.change_hint = displayItem.url
  end if
  m.playingItemAssetCollection.AddAsset(asset)
  
  '	m.ProtectMRSSFeed( "playing-" + m.currentFeed.title, m.playingItemAssetCollection )
  m.ProtectMRSSFeed("playing-" + m.liveDataFeed.id$, m.playingItemAssetCollection)
  
end sub


Sub DisplayMRSSItem(displayItem as object, filePath$ as string)
  
  if ItemIsEncrypted(displayItem) and (m.liveDataFeed.isDynamicPlaylist or m.liveDataFeed.isLiveMediaFeed)
    isEncrypted = true
  else
    isEncrypted = false
  end if
  
  if isImage(displayItem) then
    m.imageItem = { }
    
    if IsString(displayItem.title) then
      m.imageItem.fileName$ = displayItem.title
    else
      m.imageItem.fileName$ = "noTitle"
    end if
    
    m.imageItem.filePath$ = filePath$
    m.imageItem.userVariable = invalid
    m.imageItem.imageTimeout% = displayItem.duration
    m.imageItem.isEncrypted = isEncrypted
    
    ' m.DisplayImage("mrss")
    m.DrawImage(false)
    
    if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
      m.stateMachine.videoPlayer.StopClear()
    end if
    
    if type(m.imageTimeoutTimer) = "roTimer" then
      m.imageTimeoutTimer.Stop()
    end if
    
    if type(m.imageTimeoutTimer) <> "roTimer" then
      m.imageTimeoutTimer = CreateObject("roTimer")
      m.imageTimeoutTimer.SetPort(m.stateMachine.msgPort)
    end if
    m.imageTimeoutTimer.SetElapsed(m.imageItem.imageTimeout%, 0)
    m.imageTimeoutTimer.Start()
    
    ' Set transition after image display to be default transition
    if type(m.stateMachine.imagePlayer) = "roImageWidget" then
      m.stateMachine.imagePlayer.SetDefaultTransition(m.slideTransition%)
    end if
    
  else if isAudio(displayItem)
    
    fileName$ = displayItem.title
    
    if type(m.stateMachine.audioPlayer) = "roAudioPlayerMx" then
      
      track = { }
      track["Filename"] = filePath$
      track["QueueNext"] = 1
      
      fadeLength% = m.stateMachine.fadelength% * 1000
      track["FadeInLength"] = fadeLength%
      track["FadeOutLength"] = fadeLength%
      
      if not m.firstItemDisplayed then
        track["FadeCurrentPlayNext"] = 0
        m.firstItemDisplayed = true
      end if
      
      ok = m.stateMachine.audioPlayer.PlayFile(track)
      
      m.stateMachine.LogPlayStart("audioMx", fileName$)
      
    else
      
      player = invalid
      if type(m.stateMachine.audioPlayer) = "roAudioPlayer" then
        player = m.stateMachine.audioPlayer
      else if type(m.stateMachine.videoPlayer) = "roVideoPlayer" then
        player = m.stateMachine.videoPlayer
      end if
      
      if player <> invalid then
        
        player.SetLoopMode(0)
        
        aa = { }
        aa.AddReplace("Filename", filePath$)
        
        if type(displayItem.probeData) = "roString" and displayItem.probeData <> "" then
          aa.AddReplace("ProbeString", displayItem.probeData)
        end if
        
        ok = player.PlayFile(aa)
        
        if ok = 0 then
          m.bsp.diagnostics.PrintDebug("Error playing audio file: " + fileName$ + ", " + filePath$)
          m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_PLAYBACK_FAILURE, fileName$)
        end if
        
        ' playback logging
        m.stateMachine.LogPlayStart("audio", fileName$)
        
      end if
      
    end if
    
  else if isHtml(displayItem)
    
    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


Sub TickerZoneConstructor()
  
  m.InitializeZoneCommon(m.bsp.msgPort)
  
  zoneHSM = m
  
  a = { }
  a["PauseTime"] = zoneHSM.delay%
  a["Rotation"] = zoneHSM.rotation%
  a["Alignment"] = zoneHSM.alignment%
  textWidget = CreateObject("roTextWidget", zoneHSM.rectangle, zoneHSM.numberOfLines%, zoneHSM.scrollingMethod%, a)
  
  zoneHSM.widget = textWidget
  
  if zoneHSM.scrollingMethod% = 3 then
    zoneHSM.widget.SetAnimationSpeed(zoneHSM.scrollSpeed%)
  end if
  
  if type(zoneHSM.foregroundTextColor%) = "roInt" then
    zoneHSM.widget.SetForegroundColor(zoneHSM.foregroundTextColor%)
  end if
  
  if type(zoneHSM.backgroundTextColor%) = "roInt" then
    zoneHSM.widget.SetBackgroundColor(zoneHSM.backgroundTextColor%)
  end if
  
  if zoneHSM.font$ <> "" and zoneHSM.font$ <> "System" then
    fontPath$ = GetPoolFilePath(m.bsp.assetPoolFiles, zoneHSM.font$)
    zoneHSM.widget.SetFont(fontPath$)
  end if
  
  if IsString(zoneHSM.backgroundBitmapFile$) then
    backgroundBitmapFilePath$ = GetPoolFilePath(m.bsp.assetPoolFiles, zoneHSM.backgroundBitmapFile$)
    aa = { }
    m.bsp.SetEncryptionAttributes(aa, zoneHSM.backgroundBitmapFile$, backgroundBitmapFilePath$)
    zoneHSM.widget.SetBackgroundBitmap(aa, zoneHSM.stretch)
  end if
  
  if type(zoneHSM.safeTextRegionX%) = "roInt" then
    r = CreateScaledRectangle(zoneHSM.safeTextRegionX%, zoneHSM.safeTextRegionY%, zoneHSM.safeTextRegionWidth%, zoneHSM.safeTextRegionHeight%)
    zoneHSM.widget.SetSafeTextRegion(r)
    r = invalid
  end if
  
  m.includesRSSFeeds = false
  for each rssDataFeedItem in m.rssDataFeedItems
    if rssDataFeedItem.isRSSFeed then
      m.includesRSSFeeds = true
    end if
  next
  
end sub


Sub SetEncryptionAttributes(aa as object, fileName$ as string, filePath$ as string)
  
  aa.fileName = filePath$
  if type(m.encryptionByFile) = "roAssociativeArray" then
    if m.encryptionByFile.DoesExist(fileName$) then
      ' if m.bsp.contentEncrypted
      aa.AddReplace("EncryptionAlgorithm", "AesCtrHmac")
      aa.AddReplace("EncryptionKey", fileName$)
    end if
  end if
  
end sub


Function TickerZoneGetInitialState() as object
  
  if m.includesRSSFeeds then
    return m.stRSSDataFeedInitialLoad
  else
    return m.stRSSDataFeedPlaying
  end if
  
end function


Function GetRSSTempFilename()
  fileName$ = "tmp:/rss" + StripLeadingSpaces(stri(m.rssFileIndex%)) + ".xml"
  m.rssFileIndex% = m.rssFileIndex% + 1
  return fileName$
end function


Function STRSSDataFeedInitialLoadEventHandler(event as object, stateData as object) as object
  
  stateData.nextState = invalid
  
  if type(event) = "roAssociativeArray" then ' internal message event
  
  if IsString(event["EventType"]) then
    
    if event["EventType"] = "ENTRY_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
      
      for each rssDataFeedItem in m.stateMachine.rssDataFeedItems
        rssDataFeedItem.loadAttemptComplete = not rssDataFeedItem.isRSSFeed
      next
      
      return "HANDLED"
      
    else if event["EventType"] = "LIVE_DATA_FEED_UPDATE" or event["EventType"] = "LIVE_DATA_FEED_UPDATE_FAILURE" then
      
      liveDataFeed = event["EventData"]
      
      allLoadsComplete = true
      
      for each rssDataFeedItem in m.stateMachine.rssDataFeedItems
        if rssDataFeedItem.isRSSFeed then
          if liveDataFeed.id$ = rssDataFeedItem.liveDataFeed.id$ then
            rssDataFeedItem.loadAttemptComplete = true
          else if not rssDataFeedItem.loadAttemptComplete then
            allLoadsComplete = false
          end if
        end if
      next
      
      if allLoadsComplete then
        stateData.nextState = m.stateMachine.STRSSDataFeedPlaying
        return "TRANSITION"
      else
        return "HANDLED"
      end if
      
    else if event["EventType"] = "PREPARE_FOR_RESTART" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + " - PREPARE_FOR_RESTART")
      m.stateMachine.widget = invalid
      return "HANDLED"
      
    end if
    
  end if
  
end if

stateData.nextState = m.superState
return "SUPER"

end function


Sub PopulateRSSDataFeedWidget()
  
  ' clear existing strings
  rssStringCount = m.stateMachine.widget.GetStringCount()
  m.stateMachine.widget.PopStrings(rssStringCount)
  
  ' populate widget with new strings
  for each rssDataFeedItem in m.stateMachine.rssDataFeedItems
    if type(rssDataFeedItem.textStrings) = "roArray" then
      for each textString in rssDataFeedItem.textStrings
        m.stateMachine.widget.PushString(textString)
      next
    else
      for each article in rssDataFeedItem.liveDataFeed.articles
        m.stateMachine.widget.PushString(article)
      next
    end if
  next
  
  if m.stateMachine.isVisible then m.stateMachine.widget.Show()
  
end sub


Function STRSSDataFeedPlayingEventHandler(event as object, stateData as object) as object
  
  stateData.nextState = invalid
  
  if type(event) = "roAssociativeArray" then ' internal message event
  
  if IsString(event["EventType"]) then
    
    if event["EventType"] = "ENTRY_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
      
      m.PopulateRSSDataFeedWidget()
      
      return "HANDLED"
      
    else if event["EventType"] = "LIVE_DATA_FEED_UPDATE" then
      
      liveDataFeed = event["EventData"]
      
      ' check that the live data feed is for one of the rss feeds
      
      rssDataFeedItemLoaded = false
      
      for each rssDataFeedItem in m.stateMachine.rssDataFeedItems
        if rssDataFeedItem.isRSSFeed then
          if liveDataFeed.id$ = rssDataFeedItem.liveDataFeed.id$ then
            rssDataFeedItemLoaded = true
            exit for
          end if
        end if
      next
      
      if rssDataFeedItemLoaded then
        
        m.PopulateRSSDataFeedWidget()
        
      end if
      
    else if event["EventType"] = "USER_VARIABLES_UPDATED" then
      
      rssDataFeedsUpdated = false
      
      userVariables = m.bsp.currentUserVariables
      
      for each rssDataFeedItem in m.stateMachine.rssDataFeedItems
        if rssDataFeedItem.isUserVariable then
          for each userVariableKey in userVariables
            userVariable = userVariables.Lookup(userVariableKey)
            if userVariable.name$ = rssDataFeedItem.userVariableName then
              tickerItemValue = userVariable.GetCurrentValue()
              rssDataFeedItem.textStrings = []
              rssDataFeedItem.textStrings.push(tickerItemValue)
              rssDataFeedsUpdated = true
            end if
          next
        end if
      next
      
      if rssDataFeedsUpdated then
        m.PopulateRSSDataFeedWidget()
      end if
      
      return "HANDLED"
      
    else if event["EventType"] = "PREPARE_FOR_RESTART" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + " - PREPARE_FOR_RESTART")
      m.stateMachine.widget = invalid
      return "HANDLED"
      
    end if
    
  end if
  
end if

stateData.nextState = m.superState
return "SUPER"

end function

'endregion

'region Player State Machine
' *************************************************
'
' Player State Machine
'
' *************************************************
Function newPlayerStateMachine(bsp as object) as object
  
  PlayerStateMachine = newHSM()
  PlayerStateMachine.InitialPseudostateHandler = InitializePlayerStateMachine
  
  PlayerStateMachine.bsp = bsp
  PlayerStateMachine.msgPort = bsp.msgPort
  PlayerStateMachine.logging = bsp.logging
  
  PlayerStateMachine.SetSystemInfo = SetSystemInfo
  PlayerStateMachine.CheckForUSBUpdate = CheckForUSBUpdate
  PlayerStateMachine.DisplayUSBUpdateStatus = DisplayUSBUpdateStatus
  
  PlayerStateMachine.dataFeedRetryInterval% = 30
  
  PlayerStateMachine.POOL_EVENT_FILE_DOWNLOADED = 1
  PlayerStateMachine.POOL_EVENT_FILE_FAILED = -1
  PlayerStateMachine.POOL_EVENT_ALL_DOWNLOADED = 2
  PlayerStateMachine.POOL_EVENT_ALL_FAILED = -2
  
  PlayerStateMachine.SYNC_ERROR_CANCELLED = -10001
  PlayerStateMachine.SYNC_ERROR_CHECKSUM_MISMATCH = -10002
  PlayerStateMachine.SYNC_ERROR_EXCEPTION = -10003
  PlayerStateMachine.SYNC_ERROR_DISK_ERROR = -10004
  PlayerStateMachine.SYNC_ERROR_POOL_UNSATISFIED = -10005
  
  PlayerStateMachine.EVENT_REALIZE_SUCCESS = 101
  
  PlayerStateMachine.stTop = PlayerStateMachine.newHState(bsp, "Top")
  PlayerStateMachine.stTop.HStateEventHandler = STTopEventHandler
  
  PlayerStateMachine.stPlayer = PlayerStateMachine.newHState(bsp, "Player")
  PlayerStateMachine.stPlayer.HStateEventHandler = STPlayerEventHandler
  PlayerStateMachine.stPlayer.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" 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
        m.bsp.btManager.ResetPresentationBeacons()
        
        return "HANDLED"
        
      else if event["EventType"] = "SWITCH_PRESENTATION" then
        
        presentationName$ = event["Presentation"]
        
        m.bsp.diagnostics.PrintDebug("STPlayerEventHandler - Switch to presentation " + presentationName$)
        
        m.bsp.Restart(presentationName$)
        
        stateData.nextState = m.bsp.playerHSM.stPlaying
        
        return "TRANSITION"
        
      else if event["EventType"] = "CONTENT_UPDATED" then

        ' new content was downloaded from the network
        m.bsp.diagnostics.PrintDebug("STPlayerEventHandler - CONTENT_UPDATED")

        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" 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
    
    if type(m.bsp.sign) = "roAssociativeArray" and type(m.bsp.bmapPortsToRetry) = "roAssociativeArray" then
      for each bmapPortToRetryName in m.bsp.bmapPortsToRetry
        bmapPortToRetry = m.bsp.bmapPortsToRetry[bmapPortToRetryName]
        timer = bmapPortToRetry.timer
        if stri(event.GetSourceIdentity()) = stri(timer.GetIdentity()) then
          m.bsp.diagnostics.PrintDebug("RetryCreateBMap timeout")
          ok = m.bsp.RetryCreateBMap(bmapPortToRetry.port$)
          if not ok then
            m.bsp.diagnostics.PrintDebug("RetryCreateBMap 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"
      
    else
      actionsXMLFilePath$ = event.GetString() + "actions.xml"
      
      actionsSpec$ = ReadAsciiFile(actionsXMLFilePath$)
      if actionsSpec$ <> "" then
        
        actionsXML = CreateObject("roXMLElement")
        actionsXML.Parse(actionsSpec$)
        
        if type(actionsXML.action) = "roXMLList" then
          if actionsXML.action.Count() > 0 then
            
            attributes = actionsXML.GetAttributes()
            displayStatus$ = attributes.Lookup("displayStatus")
            ' initialize as false, and will update to true when needed
            displayStatus = false
            if lcase(displayStatus$) = "true" then
              
              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 / 64, resX, resY / 32)
                tw = CreateObject("roTextWidget", r, 1, 2, twParams)
                
                displayStatus = true
              end if
              
            end if
            
            deletedLogFiles = false
            
            errorEncountered = false
            
            for each action in actionsXML.action
              
              action$ = action.GetText()
              
              if action$ = "copyLogs" then
                
                if displayStatus then
                  tw.Clear()
                  tw.PushString("Copying log files.")
                  tw.Show()
                end if
                
                ok = m.bsp.logging.CopyAllLogFiles(storagePath$)
                if ok then
                  m.bsp.diagnostics.PrintDebug("CopyAllLogFiles completed successfully")
                else
                  errorEncountered = true
                  m.bsp.diagnostics.PrintDebug("CopyAllLogFiles failed")
                  
                  if displayStatus then
                    
                    tw.Clear()
                    tw.PushString("Error encountered while copying log files.")
                    tw.Show()
                    
                    sleep(5000)
                    
                  end if
                  
                  exit for
                end if
                
              else if action$ = "deleteLogs" then
                
                if displayStatus then
                  tw.Clear()
                  tw.PushString("Deleting log files.")
                  tw.Show()
                end if
                
                m.bsp.logging.DeleteAllLogFiles()
                m.bsp.diagnostics.PrintDebug("DeleteAllLogFiles complete")
                deletedLogFiles = true
                
              else if action$ = "resetVariables" then
                
                if displayStatus then
                  tw.Clear()
                  tw.PushString("Resetting variables.")
                  tw.Show()
                end if
                
                m.bsp.ResetVariables()
                
                m.bsp.diagnostics.PrintDebug("Resetting variables complete")
                
              else if action$ = "copyVariablesDB" then
                
                if m.bsp.variablesDBExists then
                  
                  if displayStatus then
                    tw.Clear()
                    tw.PushString("Copying variables database.")
                    tw.Show()
                  end if
                  
                  serialNumber$ = m.bsp.sysInfo.deviceUniqueID$
                  
                  dtLocal = m.bsp.systemTime.GetLocalDateTime()
                  year$ = Right(stri(dtLocal.GetYear()), 2)
                  month$ = StripLeadingSpaces(stri(dtLocal.GetMonth()))
                  if len(month$) = 1 then
                    month$ = "0" + month$
                  end if
                  day$ = StripLeadingSpaces(stri(dtLocal.GetDay()))
                  if len(day$) = 1 then
                    day$ = "0" + day$
                  end if
                  hour$ = StripLeadingSpaces(stri(dtLocal.GetHour()))
                  if len(hour$) = 1 then
                    hour$ = "0" + hour$
                  end if
                  minute$ = StripLeadingSpaces(stri(dtLocal.GetMinute()))
                  if len(minute$) = 1 then
                    minute$ = "0" + minute$
                  end if
                  'date$ = year$ + month$ + day$ + hour$ + minute$
                  date$ = year$ + month$ + day$
                  
                  fileName$ = "BrightSignVariables." + serialNumber$ + "-" + date$ + ".txt"
                  filePath$ = storagePath$ + fileName$
                  
                  variablesFile = CreateObject("roCreateFile", filePath$)
                  
                  if type(variablesFile) = "roCreateFile" then
                    
                    m.bsp.ExportVariablesDBToAsciiFile(variablesFile)
                    
                    ' determine if write was successful
                    ' partial fix - only works if card was full before this step
                    variablesFile.SeekToEnd()
                    position% = variablesFile.CurrentPosition()
                    if position% = 0 then
                      errorEncountered = true
                      m.bsp.diagnostics.PrintDebug("copyVariablesDB failed - fileLength = 0")
                    else
                      m.bsp.diagnostics.PrintDebug("Wrote variables file to " + filePath$)
                    end if
                    
                    variablesFile = invalid
                    
                  else
                    
                    errorEncountered = true
                    m.bsp.diagnostics.PrintDebug("copyVariablesDB failed - create file failed")
                    
                  end if
                  
                  if errorEncountered then
                    
                    if displayStatus then
                      
                      tw.Clear()
                      tw.PushString("Error encountered while copying variables database.")
                      tw.Show()
                      
                      sleep(5000)
                      
                    end if
                    
                    exit for
                    
                  end if
                  
                else if displayStatus then
                  
                  tw.Clear()
                  tw.PushString("No variables to copy.")
                  tw.Show()
                  
                  sleep(3000)
                  
                end if
                
              else if action$ = "reboot" then
                
                if displayStatus then
                  tw.Clear()
                  tw.PushString("Finalizing data writes, do not remove your drive yet.")
                  tw.Show()
                  
                  EjectStorage(storagePath$)
                  
                  tw.Clear()
                  tw.PushString("Data capture complete - you may remove your drive. The system will reboot shortly.")
                  tw.Show()
                  
                  sleep(5000)
                  tw.Clear()
                else
                  EjectStorage(storagePath$)
                end if
                
                RebootSystem()
                
                return "HANDLED"
                
              end if
            next
            
            if displayStatus then
              
              tw.Clear()
              tw.PushString("Finalizing data writes, do not remove your drive yet.")
              tw.Show()
              
              EjectStorage(storagePath$)
              
              tw.Clear()
              
              if errorEncountered then
                tw.PushString("Data capture failed  - you may remove your drive.")
              else
                tw.PushString("Data capture completed successfully  - you may remove your drive.")
              end if
              
              tw.Show()
              
              sleep(5000)
              
              tw = invalid
              
            end if
            
            ' if the log files were deleted but the system is not rebooting, open a log file
            if m.bsp.logging.loggingEnabled then m.bsp.logging.OpenOrCreateCurrentLog()
            
          end if
        end if
      end if
    end if
    
  else if type(event) = "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"
    
    if IsString(event.getUserData()) and event.GetUserData() = "bootstrap" then
      m.bsp.diagnostics.PrintDebug("supervisor / bootstrap roDatagramEvent received")
      payload = ParseJson(event.GetString())
      m.ProcessSupervisorCheckForContentMessage(payload)
      return "HANDLED"
    endif

  else if type(event) = "roBtEvent" then  
    
    m.bsp.btManager.HandleEvent(event)
    return "HANDLED"
  
  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
  
  registry = GetGlobalAA().registrySection
  accessToken = registry.Read("access_token")
  
  headers["Authorization"] = "Bearer " + accessToken
  headers["accessToken"] = accessToken

  if type(m.bsp.activePresentation$) = "roString" then
    presentationName$ = m.bsp.activePresentation$
  else
    presentationName$ = ""
  end if
  headers["BSN-PresentationName"] = presentationName$
  
  localDateTime = m.stateMachine.systemTime.GetLocalDateTime()
  headers["BSN-LocalTimestamp"] = FormatDateTime(localDateTime)
  
  utcDateTime = m.stateMachine.systemTime.GetUtcDateTime()
  headers["BSN-UTCTimestamp"] = FormatDateTime(utcDateTime) + "Z"
  
  headers["BSN-SecurityToken"] = m.stateMachine.securityToken
  
  snapshotFilePath$ = "snapshots/" + snapshotName$
  fileSize% = GetFileSize(snapshotFilePath$)
  
  headers["Content-Length"] = stri(fileSize%)
  
  m.stateMachine.uploadSnapshotToBSNEEUrl = CreateObject("roUrlTransfer")
  m.stateMachine.uploadSnapshotToBSNEEUrl.SetPort(m.stateMachine.msgPort)
  m.stateMachine.uploadSnapshotToBSNEEUrl.SetTimeout(5000)
  m.stateMachine.uploadSnapshotToBSNEEUrl.SetUrl(m.stateMachine.uploadDeviceScreenshotHandlerAddress)
  m.stateMachine.uploadSnapshotToBSNEEUrl.SetHeaders(headers)
  m.stateMachine.uploadSnapshotToBSNEEUrl.SetUserAgent(m.bsp.userAgent$)
  if not m.stateMachine.uploadSnapshotToBSNEEUrl.AsyncPostFromFile(snapshotFilePath$) then stop
  
end sub


Sub UploadSnapshotToBSN(snapshotName$ as string)
  
  if type(m.stateMachine.uploadSnapshotUrl) = "roUrlTransfer" then
    m.stateMachine.pendingSnapshotsToUpload.AddReplace(snapshotName$, snapshotName$)
    return
  end if
  
  globalAA = GetGlobalAA()
  
  snapshotFilePath$ = "snapshots/" + snapshotName$
  
  serialNumber$ = m.bsp.sysInfo.deviceUniqueID$
  
  photoTimestamp$ = GetPhotoTimestamp(snapshotName$)
  
  AWSResourceKey = m.stateMachine.AwsIncomingDirectory + serialNumber$ + "/" + photoTimestamp$ + ".jpg"
  
  headers = { }
  headers["Content-Type"] = "image/jpeg"
  headers["Content-Length"] = StripLeadingSpaces(stri(GetFileSize(snapshotFilePath$)))
  headers["x-amz-security-token"] = m.stateMachine.awsSessionToken$
  headers["X-Amz-Date"] = GetDateTime(m.stateMachine.systemTime.GetUtcDateTime())
  
  stringToSign$ = m.BuildStringToSign(headers, AWSResourceKey)
  
  auth$ = GetHMACSign(stringToSign$, m.stateMachine.awsSecretAccessKey$)
  
  authorization$ = "AWS " + m.stateMachine.awsAccessKeyId$ + ":" + auth$
  headers["Authorization"] = authorization$
  
  m.stateMachine.uploadSnapshotUrl = CreateObject("roUrlTransfer")
  
  ' url to send snapshots to
  url = m.stateMachine.deviceScreenShotsTemporaryStorage$ + serialNumber$ + "/" + photoTimestamp$ + ".jpg"
  if not m.stateMachine.uploadSnapshotUrl.SetUrl(url) then stop
  
  snapshot = { }
  snapshot.name = snapshotName$
  snapshot.url = url
  m.stateMachine.uploadSnapshotUrl.SetUserData(snapshot)
  
  if not m.stateMachine.uploadSnapshotUrl.AddHeader("Authorization", headers["Authorization"]) then stop
  if not m.stateMachine.uploadSnapshotUrl.AddHeader("Content-Type", "image/jpeg") then stop
  if not m.stateMachine.uploadSnapshotUrl.AddHeader("x-amz-security-token", headers["x-amz-security-token"]) then stop
  if not m.stateMachine.uploadSnapshotUrl.AddHeader("X-Amz-Date", headers["X-Amz-Date"]) then stop
  
  m.stateMachine.uploadSnapshotUrl.SetPort(m.stateMachine.msgPort)
  m.stateMachine.uploadSnapshotUrl.SetTimeout(5000)
  m.stateMachine.uploadSnapshotUrl.SetUserAgent(m.bsp.userAgent$)
  
  ok = m.stateMachine.uploadSnapshotUrl.AsyncPutFromFile(snapshotFilePath$)
  if not ok then
    m.stateMachine.logging.WriteDiagnosticLogEntry(m.stateMachine.diagnosticCodes.EVENT_SNAPSHOT_PUT_TO_SERVER_ERROR, m.stateMachine.uploadSnapshotUrl.GetFailureReason())
    m.bsp.diagnostics.PrintDebug("### AsyncPutFromFile failed: file " + snapshotFilePath$ + ", reason " + m.stateMachine.uploadSnapshotUrl.GetFailureReason())
  end if
  
end sub


Function BuildStringToSign(headers as object, AWSResourceKey as string) as string
  
  result$ = ""
  result$ = result$ + "PUT"
  result$ = result$ + chr(10)
  result$ = result$ + chr(10)
  result$ = result$ + headers["Content-Type"]
  result$ = result$ + chr(10)
  result$ = result$ + chr(10)
  
  result$ = result$ + "x-amz-date" + ":" + headers["X-Amz-Date"] + chr(10)
  result$ = result$ + "x-amz-security-token" + ":" + headers["x-amz-security-token"] + chr(10)
  
  result$ = result$ + "/" + m.stateMachine.AwsBucketName + "/" + AwsResourceKey
  
  return result$
  
end function


Function GetHMACSign(data as string, key as string) as string
  
  binaryData = CreateObject("roByteArray")
  binaryData.FromAsciiString(data)
  
  keyData = CreateObject("roByteArray")
  keyData.FromAsciiString(key)
  
  hashGenerator = CreateObject("roHashGenerator", "SHA1")
  ok = hashGenerator.SetHmacKey(keyData)
  
  bytes = hashGenerator.hash(binaryData)
  signature = bytes.ToBase64String()
  return signature
  
end function

' Return RFC1123 date/time string from roDateTime

Function GetDateTime(currentTime as object) as string
  
  daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
  months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
  
  dayOfWeek$ = daysOfWeek[currentTime.GetDayOfWeek()]
  
  day$ = StripLeadingSpaces(stri(currentTime.GetDay()))
  if len(day$) = 1 then day$ = "0" + day$
  
  month$ = months[currentTime.GetMonth() - 1]
  
  year$ = StripLeadingSpaces(stri(currentTime.GetYear()))
  
  hour$ = StripLeadingSpaces(stri(currentTime.GetHour()))
  if len(hour$) = 1 then hour$ = "0" + hour$
  
  minute$ = StripLeadingSpaces(stri(currentTime.GetMinute()))
  if len(minute$) = 1 then minute$ = "0" + minute$
  
  second$ = StripLeadingSpaces(stri(currentTime.GetSecond()))
  if len(second$) = 1 then second$ = "0" + second$
  
  currentDateTime = dayOfWeek$ + ", " + day$ + " " + month$ + " " + year$ + " " + hour$ + ":" + minute$ + ":" + second$ + " GMT"
  
  return currentDateTime
  
end function

' Parse an HTTP date/time value, as specified in RFC 2616 sec 3.3
' Preferred format is RFC1123, e.g., "Sun, 15 Mar 2016 08:15:10 GMT"

Function ParseHTTPDateTime(httpDateTime as string) as object
  
  months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
  
  ' Currently, handle only RFC 1123
  ' TODO - handle the other 'obsolete' HTTP formats, RFC 850/1036, and ANSI C asctime()format
  
  ' return current time if time string not valid
  systemTime = CreateObject("roSystemTime")
  dateTime = systemTime.GetUtcDateTime()
  dateTime.SetMillisecond(0)
  
  if mid(httpDateTime, 4, 1) = "," then
    day% = val(mid(httpDateTime, 6, 2))
    if day% > 0 and day% < 32 then
      dateTime.SetDay(day%)
    end if
    httpMonth = mid(httpDateTime, 9, 3)
    for month% = 0 to 11
      if months[month%] = httpMonth then
        dateTime.SetMonth(month% + 1)
        exit for
      end if
    next
    year% = val(mid(httpDateTime, 13, 4))
    if year% > 0 and year% < dateTime.GetYear() then
      dateTime.SetYear(year%)
    end if
    hour% = val(mid(httpDateTime, 18, 2))
    if hour% < 24 then
      dateTime.SetHour(hour%)
    end if
    minute% = val(mid(httpDateTime, 21, 2))
    if minute% < 60 then
      dateTime.SetMinute(minute%)
    end if
    second% = val(mid(httpDateTime, 24, 2))
    if second% < 60 then
      dateTime.SetSecond(second%)
    end if
  end if
  
  return dateTime
  
end function

Function GetPhotoTimestamp(snapshotName$ as string) as string
  
  year$ = mid(snapshotName$, 1, 4)
  month$ = mid(snapshotName$, 5, 2)
  day$ = mid(snapshotName$, 7, 2)
  hour$ = mid(snapshotName$, 10, 2)
  minute$ = mid(snapshotName$, 12, 2)
  second$ = mid(snapshotName$, 14, 2)
  photoTimestamp$ = year$ + "-" + month$ + "-" + day$ + "T" + hour$ + "-" + minute$ + "-" + second$ + ".0000000Z"
  return photoTimestamp$
  
end function


Sub EjectStorage(storagePath$ as string)
  
  ok = EjectDrive(storagePath$)
  if not ok then
    sleep(30000)
  end if
  
end sub


Function SetDeviceSetupSplashScreen(setupType as String, msgPort as Object) as object

	filepath = "sys:/web-client/postDeviceSetupSplashScreen/dist/index.html"
	file = CreateObject("roReadFile", filepath)
	
	if file <> invalid then
	  filepath = "file:/" + filepath
		videoMode = CreateObject("roVideoMode")
		resX = videoMode.GetResX()
		resY = videoMode.GetResY()
		r = CreateObject("roRectangle", 0, 0, resX, resY)
		config = {
			url: filepath
			brightsign_js_objects_enabled: true
			nodejs_enabled: true
		}
		htmlWidget = CreateObject("roHtmlWidget", r, config)
		if type(htmlWidget) = "roHtmlWidget" then
			htmlWidget.SetPort(msgPort)
			htmlWidget.AllowJavascriptUrls({ all: "*" })
			htmlWidget.EnableJavascript(true)
			sleep(5000)
			if lcase(setupType) = "lfn" then
				htmlWidget.PostJSMessage({
					htmlcommand: "setupsplashscreenmessage",
					headermsg: "Congratulations, your BrightSign player is set up!",
					message: "Use brightAuthor connected to publish content via the Local Network mode."
				})
			end if

			htmlWidget.Show()
			return htmlWidget
		else
			stop
		endif
	endif

	return invalid

End Function


Function STWaitingEventHandler(event as object, stateData as object) as object
  
  stateData.nextState = invalid
  
  if type(event) = "roAssociativeArray" then ' internal message event
  
  if IsString(event["EventType"]) then
    
    if event["EventType"] = "ENTRY_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
      
      ' set a timer for when the system should become active again
      if type(m.bsp.schedule.nextScheduledEventTime) = "roDateTime" then
        dateTime = m.bsp.schedule.nextScheduledEventTime
        newTimer = CreateObject("roTimer")
        newTimer.SetTime(dateTime.GetHour(), dateTime.GetMinute(), 0)
        newTimer.SetDate(dateTime.GetYear(), dateTime.GetMonth(), dateTime.GetDay())
        newTimer.SetDayOfWeek(dateTime.GetDayOfWeek())
        newTimer.SetPort(m.stateMachine.msgPort)
        newTimer.Start()
        m.stateMachine.timer = newTimer
      end if
      
      return "HANDLED"
      
    else if event["EventType"] = "EXIT_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
      
    end if
    
  end if
  
end if

stateData.nextState = m.superState
return "SUPER"

end function


Function STPlayingEventHandler(event as object, stateData as object) as object
  
  stateData.nextState = invalid
  
  if type(event) = "roAssociativeArray" then ' internal message event
  
  if IsString(event["EventType"]) then
    
    if event["EventType"] = "ENTRY_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
      
      ' set a timer for when the current presentation should end
      activeScheduledPresentation = m.bsp.schedule.activeScheduledEvent
      
      if type(activeScheduledPresentation) = "roAssociativeArray" then
        
        if m.bsp.schedule.activeScheduledEventEndDateTime <> invalid then
          
          endDateTime = m.bsp.schedule.activeScheduledEventEndDateTime
          
          newTimer = CreateObject("roTimer")
          newTimer.SetTime(endDateTime.GetHour(), endDateTime.GetMinute(), 0)
          newTimer.SetDate(endDateTime.GetYear(), endDateTime.GetMonth(), endDateTime.GetDay())
          newTimer.SetDayOfWeek(endDateTime.GetDayOfWeek())
          newTimer.SetPort(m.stateMachine.msgPort)
          newTimer.Start()
          
          m.stateMachine.timer = newTimer
          
          m.bsp.diagnostics.PrintDebug("Set STPlayingEventHandler timer to " + endDateTime.GetString())
          
        end if
        
        ' check for live data feeds that include content (either MRSS or content for Media Lists / PlayFiles). for each of them, check to see if the feed and/or content already exists.
        for each liveDataFeedId in m.bsp.liveDataFeeds
          liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
          liveDataFeed.ReadFeedContent()
        next
        
        ' load live data feeds
        m.liveDataFeeds = { }
        m.bsp.liveDataFeedsByTimer = { }
        
        ' queue live data feeds for downloading
        m.bsp.liveDataFeedsToDownload = []
        for each liveDataFeedId in m.bsp.liveDataFeeds
          liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
          m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
        next
        
        ' launch playback
        m.bsp.StartPlayback()
        
      end if
      
      return "HANDLED"
      
    else if event["EventType"] = "UPDATE_DATA_FEED" then
      
      dataFeedId$ = event["DataFeedId"]
      for each liveDataFeedId in m.bsp.liveDataFeeds
        liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
        if dataFeedId$ = liveDataFeedId then
          liveDataFeed.forceUpdate = true
          m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
        end if
      next
      
    else if event["EventType"] = "UPDATE_DATA_FEED_BY_CATEGORY" then
      
      categoryName$ = event["Name"]
      for each liveDataFeedId in m.bsp.liveDataFeeds
        liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
        if liveDataFeed.title$ = categoryName$ then
          liveDataFeed.forceUpdate = true
          m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
        end if
      next
      
    else if event["EventType"] = "UPDATE_ALL_DATA_FEEDS" then
      
      for each liveDataFeedId in m.bsp.liveDataFeeds
        liveDataFeed = m.bsp.liveDataFeeds.Lookup(liveDataFeedId)
        if liveDataFeed.autoGenerateUserVariables then
          liveDataFeed.forceUpdate = true
          m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
        end if
      next
      
    else if event["EventType"] = "CONTENT_DATA_FEED_LOADED" then
      
      sign = m.bsp.sign
      
      for each zone in sign.zonesHSM
        for each stateName in zone.stateTable
          state = zone.stateTable[stateName]
          if state.type$ = "playFile" then
            if type(state.liveDataFeed) = "roAssociativeArray" and event["Name"] = state.liveDataFeed.id$ then
              if type(state.liveDataFeed.assetPoolFiles) = "roAssetPoolFiles" then
                state.PopulatePlayFileFromLiveDataFeed()
                ' following line is commented out as the code should fall through to the next if statement
                '									return "HANDLED"
              end if
            end if
          end if
        next
      next
      
    else if event["EventType"] = "EXIT_SIGNAL" then
      
      m.bsp.diagnostics.PrintDebug(m.id$ + ": exit signal")
      
    end if
    
    if event["EventType"] = "MRSS_DATA_FEED_LOADED" or event["EventType"] = "CONTENT_DATA_FEED_LOADED" or event["EventType"] = "CONTENT_DATA_FEED_UNCHANGED" then
      
      m.bsp.AdvanceToNextLiveDataFeedInQueue(m.liveDataFeeds)
      
    end if
    
  end if
  
end if

if type(event) = "roTimerEvent" then
  
  eventIdentity$ = stri(event.GetSourceIdentity())
  
  if m.bsp.liveDataFeedsByTimer.DoesExist(eventIdentity$) then
    liveDataFeed = m.bsp.liveDataFeedsByTimer.Lookup(eventIdentity$)
    
    ' if this feed's download failed, launch download of any pending feeds
    ' if this isn't done, no new downloads are ever retrieved as the queue is never empty and there's never an event that causes the
    ' next feed to be retrieved
    if lcase(type(liveDataFeed.lastDownloadedFailed)) = "roboolean" or lcase(type(liveDataFeed.lastDownloadedFailed)) = "boolean" then
      if liveDataFeed.lastDownloadedFailed then
        m.bsp.RetrievePendingLiveDataFeed(m.liveDataFeeds)
      end if
    end if
    
    ' requeue the feed that failed
    m.bsp.QueueRetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
  end if
  
else if type(event) = "roUrlEvent" then
  
  return m.PlayingEventUrlHandler(event, stateData)
  
end if

stateData.nextState = m.superState
return "SUPER"

end function


Function PlayingEventUrlHandler(event as object, stateData as object) as object

  eventIdentity$ = stri(event.GetSourceIdentity())
  if m.liveDataFeeds.DoesExist(eventIdentity$) then
    liveDataFeed = m.liveDataFeeds.Lookup(eventIdentity$)
    m.liveDataFeeds.Delete(eventIdentity$)
    if event.GetResponseCode() = 200 or event.GetResponseCode() = 0 then
    
      updateInterval% = 0
      
      headers = event.GetResponseHeaders()
      lastModifiedTime = invalid
      lastModifiedTimeStr = headers["Last-Modified"]
      if lastModifiedTimeStr <> invalid then
        lastModifiedTime = ParseHTTPDateTime(lastModifiedTimeStr)
      end if
      
      ' indicate that last download was successful
      liveDataFeed.lastDownloadedFailed = false
      if liveDataFeed.headRequest then
        
        getFeed = false
        if type(liveDataFeed.currentModifiedTime) <> "roDateTime" then
          ' retrieve the feed if there is no currentModifiedTime stamp for the feed
          m.bsp.diagnostics.PrintDebug("### Check live data feed - no current timestamp - updating")
          getFeed = true
        else
          headers = event.GetResponseHeaders()
          lastModifiedTimeStr = headers["Last-Modified"]
          if lastModifiedTimeStr <> invalid then
            lastModifiedTime = ParseHTTPDateTime(lastModifiedTimeStr)
            if lastModifiedTime <> liveDataFeed.currentModifiedTime then
              m.bsp.diagnostics.PrintDebug("### Check live data feed - updating '" + liveDataFeed.id$ + "', last modified = " + lastModifiedTime.GetString() + ", currentModified = " + liveDataFeed.currentModifiedTime.GetString())
              ' retrieve the feed if there is a Last-Modified header and it is greater than the modified time of the current feed
              getFeed = true
            else
              m.bsp.diagnostics.PrintDebug("### Check live data feed - no update needed for '" + liveDataFeed.id$ + "', last modified = " + lastModifiedTime.GetString() + ", currentModified = " + liveDataFeed.currentModifiedTime.GetString())
            end if
          else
            ' retrieve the feed if there is no Last-Modified header
            m.bsp.diagnostics.PrintDebug("### Check live data feed - no Last-Modified header - updating")
            getFeed = true
          end if
        end if
        
        if getFeed then
          liveDataFeed.headRequest = false
          m.bsp.RetrieveLiveDataFeed(m.liveDataFeeds, liveDataFeed)
        else
          ' No feed download needed, pop the item off the queue and see if something else is pending
          m.bsp.AdvanceToNextLiveDataFeedInQueue(m.liveDataFeeds)
          ' Send next HEAD request after interval
          updateInterval% = liveDataFeed.updateInterval%
        end if
        
      else
        
        userVariables = m.bsp.currentUserVariables
        
        liveDataFeed.currentModifiedTime = lastModifiedTime
        
        if liveDataFeed.usage$ <> "mrss" and liveDataFeed.usage$ <> "mrsswith4k" then
          ' simple RSS or content
          
          if liveDataFeed.usage$ = "content" then
            if (liveDataFeed.isDynamicPlaylist or liveDataFeed.isLiveMediaFeed) then
              liveDataFeed.ParseMRSSFeed(liveDataFeed.rssFileName$)
              liveDataFeed.ConvertMRSSFormatToContent()
            else
              liveDataFeed.ParseCustomContentFormat(liveDataFeed.rssFileName$)
            endif
          else
            liveDataFeed.ParseSimpleRSSFeed(liveDataFeed.rssFileName$)
          end if
          
          ' parsing for autogenerated user variables - make it conditional on using autogenerated user variables
          ' check for uv parser. if exists, send rss feed, array of elements where each element is title, description, mediaURL. also need to get the title back (not sure how).
          ' if no parser exists, parse it here, filling in the array as above. determine title of feed.
          liveDataFeed.items = CreateObject("roArray", 1, true)
          if liveDataFeed.autoGenerateUserVariables then
            if liveDataFeed.uvParser$ <> "" then
              ERR_NORMAL_END = &hFC
              retVal = eval(liveDataFeed.uvParser$ + "(liveDataFeed.rssFileName$, liveDataFeed.items, userVariables, m.bsp)")
              if retVal <> ERR_NORMAL_END then
                ' log the failure
                m.bsp.diagnostics.PrintDebug("Failure invoking Eval to parse live data feed for user variables: return value = " + stri(retVal) + ", parser is " + liveDataFeed.uvParser$)
                m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_LIVE_TEXT_PLUGIN_FAILURE, stri(retVal) + chr(9) + liveDataFeed.uvParser$)
              end if
            else
              dataFeedXML = CreateObject("roXMLElement")
              dataFeedXML.Parse(ReadAsciiFile(liveDataFeed.rssFileName$))
              liveDataFeed.title$ = dataFeedXML.channel.title.gettext()
              
              allItemsXML = dataFeedXML.channel.item
              position% = 0
              for each itemXML in allItemsXML
                item = { }
                item.title$ = itemXML.title.gettext()
                item.description$ = itemXML.description.gettext()
                
                mediaContent = itemXML.GetNamedElements("media:content")[0]
                if mediaContent = invalid then
                  item.mediaUrl$ = ""
                else
                  item.mediaUrl$ = mediaContent.GetAttributes()["url"]
                end if
                
                item.position% = position%
                position% = position% + 1
                
                liveDataFeed.items.push(item)
              end for
            end if
          end if
        else
          ' These must be valid objects even for MRSS feeds (at least for now)
          liveDataFeed.articles = CreateObject("roArray", 1, true)
          liveDataFeed.articleTitles = CreateObject("roArray", 1, true)
          liveDataFeed.articlesByTitle = { }
        end if
        
        liveDataFeed.isMRSSFeed = liveDataFeed.FeedIsMRSS(liveDataFeed.rssFileName$)
        
        if liveDataFeed.usage$ = "content" then
          liveDataFeed.DownloadLiveFeedContent()
        else if liveDataFeed.usage$ = "mrss" or liveDataFeed.usage$ = "mrsswith4k" and (liveDataFeed.parser$ <> "" or liveDataFeed.isMRSSFeed) then
          liveDataFeed.DownloadMRSSContent()
        end if
        
        if liveDataFeed.autoGenerateUserVariables then
          m.bsp.CreateUserVariablesFromDataFeed(liveDataFeed)
        end if
        
        DeleteFile(liveDataFeed.rssFileName$)
        
        ' update user variables
        if type(userVariables) = "roAssociativeArray" then
          
          updatedUserVariables = { }
          
          for each title in liveDataFeed.articlesByTitle
            ' update user variable if appropriate
            if userVariables.DoesExist(title) then
              userVariable = userVariables.Lookup(title)
              if type(userVariable.liveDataFeed) = "roAssociativeArray" and userVariable.liveDataFeed.id$ = liveDataFeed.id$ then
                description = liveDataFeed.articlesByTitle[title]
                userVariable.SetCurrentValue(description, true)
                updatedUserVariables.AddReplace(title, userVariable)
              end if
            end if
          next
          
          m.UpdateTimeClockEvents(updatedUserVariables)
          
        end if
        
        ' send internal message indicating that the data feed has been updated
        liveTextDataUpdatedEvent = { }
        liveTextDataUpdatedEvent["EventType"] = "LIVE_DATA_FEED_UPDATE"
        liveTextDataUpdatedEvent["EventData"] = liveDataFeed
        m.bsp.msgPort.PostMessage(liveTextDataUpdatedEvent)
        
        liveDataFeed.forceUpdate = false
        
        if liveDataFeed.useHeadRequest then
          ' Set headRequest so that next call will be a HEAD call
          liveDataFeed.headRequest = true
        end if
        
        updateInterval% = liveDataFeed.updateInterval%
        
      end if 'feed download, not HEAD
      
    else

      url$ = liveDataFeed.url.GetCurrentParameterValue()
      m.bsp.diagnostics.PrintDebug("Failure downloading Live Text Data feed " + url$ + ", responseCode = " + stri(event.GetResponseCode()))
      m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_LIVE_TEXT_FEED_DOWNLOAD_FAILURE, url$ + chr(9) + stri(event.GetResponseCode()) + chr(9) + event.GetFailureReason())

      if m.bsp.textDataFeedsNumRetries% >= m.bsp.textMaxRetries% then
        
        globalAA = GetGlobalAA()
        if not globalAA.networkInterfacePriorityLists.DoesExist("textFeedsDownloadEnabled") then
          ' TEDTODO
          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%)
  end if
  
  return "HANDLED"
  
end function


Sub UpdateDataFeed(parameters as object)

  updateDataFeedParameter = parameters["dataFeed"]
  dataFeedId$ = updateDataFeedParameter.GetCurrentParameterValue()
  
  updateDataFeedMsg = { }
  updateDataFeedMsg["EventType"] = "UPDATE_DATA_FEED"
  updateDataFeedMsg["DataFeedId"] = dataFeedId$
  m.msgPort.PostMessage(updateDataFeedMsg)
  
end sub


Sub CreateUserVariablesFromDataFeed(liveDataFeed as object)
  
  ' get the section name
  if lcase(liveDataFeed.userVariableAccess$) = "shared" then
    sectionName$ = "Shared"
  else
    sectionName$ = m.activePresentation$
  end if
  
  sectionId% = m.GetDBSectionId(sectionName$)
  if sectionId% < 0 then
    m.AddDBSection(sectionName$)
    sectionId% = m.GetDBSectionId(sectionName$)
  end if
  
  categoryName$ = liveDataFeed.title$
  categoryId% = m.GetDBCategoryId(sectionId%, categoryName$)
  
  ' desired behavour
  '	if the category does not exist, create it and populate it
  '	if the category does exist and the update interval is not Once
  '		add new user variables
  '		for matching user variables, update the current value but leave the default alone
  '		remove deleted user variables
  
  if categoryId% < 0 then
    
    m.AddDBCategory(sectionId%, categoryName$)
    categoryId% = m.GetDBCategoryId(sectionId%, categoryName$)
    
    for each item in liveDataFeed.items
      m.AddDBVariable(categoryId%, item.title$, item.description$, item.mediaUrl$, item.position%)
    next
    
  else if liveDataFeed.updateInterval% > 0 or liveDataFeed.forceUpdate then
    
    ' get existing user variables for this section (and Shared) / category
    userVariablesList = m.GetUserVariablesByCategoryList(categoryName$)
    
    ' populate an associative array with the items in the live data feed
    dataFeedItems = { }
    for each item in liveDataFeed.items
      dataFeedItems.AddReplace(item.title$, item)
    next
    
    ' delete each user variable that exists but is not in the data feed; update variables that remain
    userVariables = { }
    for each userVariable in userVariablesList
      userVariableName$ = userVariable.name$
      userVariables.AddReplace(userVariableName$, userVariable)
      if not dataFeedItems.DoesExist(userVariableName$) then
        ' delete variable
        m.DeleteDBVariable(categoryId%, userVariableName$)
      else
        ' update variable
        dataFeedItem = dataFeedItems.Lookup(userVariableName$)
        m.UpdateDBVariable(categoryId%, dataFeedItem.title$, dataFeedItem.description$)
        m.UpdateDBVariableMediaUrl(categoryId%, dataFeedItem.title$, dataFeedItem.mediaUrl$)
        m.UpdateDBVariablePosition(categoryId%, dataFeedItem.title$, dataFeedItem.position%)
      end if
    next
    
    ' add new variables
    for each item in liveDataFeed.items
      if not userVariables.DoesExist(item.title$) then
        m.AddDBVariable(categoryId%, item.title$, item.description$, item.mediaUrl$, item.position%)
      end if
    next
    
  end if
  
end sub


' 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)
  
  ' If the text widget is invalid, 
  ' log it instead of show on screen
  if type(m.usbUpdateTW) <> "roTextWidget" then
    m.bsp.diagnostics.PrintDebug(status$)
  else
    m.usbUpdateTW.Clear()
    m.usbUpdateTW.PushString(status$)
    m.usbUpdateTW.Show()
  end if
  
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
        ' TEDTODO
        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
      ' TEDTODO
      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 BrightWall
  m.xfer.AddHeader("BrightWallName", GetGlobalAA().registrySettings.brightWallName$)
  m.xfer.AddHeader("BrightWallScreenNumber", GetGlobalAA().registrySettings.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"

    ' Determine if device is in BrightWall group, then clear brightWall settings in registry if they are currently set but device is no longer part of BrightWall group
    if GetGlobalAA().registrySettings.brightWallName$ <> "" and GetGlobalAA().registrySettings.brightWallScreenNumber$ <> "" then
      
      clearBrightWallSettings = false
      
      serverMetadata = m.stateMachine.newSync.GetMetadata("server")
      if type(serverMetadata) = "roAssociativeArray" then
        if not serverMetadata.DoesExist("brightWall") or serverMetadata["brightWall"] = "" then
          clearBrightWallSettings = true
        end if
      end if
      
      if clearBrightWallSettings then
        registrySection = GetGlobalAA().registrySection
        registrySettings = GetGlobalAA().registrySettings
        registrySection.Write("brightWallName", "")
        registrySection.Write("brightWallScreenNumber", "")
        registrySection.Flush()
        registrySettings.brightWallName$ = ""
        registrySettings.brightWallScreenNumber$ = ""
      end if
    end if
    
    ' 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" and stri(event.GetSourceIdentity()) = m.inputPortIdentity$ and m.buttonNumber% = event.GetInt() then
      
      m.bsp.diagnostics.PrintDebug("BP control down" + str(event.GetInt()))
      
      bpControlDown = { }
      bpControlDown["EventType"] = "BPControlDown"
      bpControlDown["ButtonPanelIndex"] = StripLeadingSpaces(str(m.buttonPanelIndex%))
      bpControlDown["ButtonNumber"] = StripLeadingSpaces(str(event.GetInt()))
      m.msgPort.PostMessage(bpControlDown)
      
      if m.configuration$ = "pressContinuous" then
        m.timer = CreateObject("roTimer")
        m.timer.SetPort(m.msgPort)
        m.timer.SetElapsed(0, m.initialHoldoff%)
        m.timer.Start()
      end if
      
      m.state$ = "ButtonDown"
      
    end if
    
  else
    
    if type(event) = "roControlUp" and stri(event.GetSourceIdentity()) = m.inputPortIdentity$ and m.buttonNumber% = event.GetInt() then
      
      m.bsp.diagnostics.PrintDebug("BP control up" + str(event.GetInt()))
      
      ' if continuous, stop and destroy the timer
      if type(m.timer) = "roTimer" then
        m.timer.Stop()
        m.timer = invalid
      end if
      
      m.state$ = "ButtonUp"
      
      ' else check for repeat timeout
    else if type(event) = "roTimerEvent" and type(m.timer) = "roTimer" then
      if stri(event.GetSourceIdentity()) = stri(m.timer.GetIdentity()) then
        
        m.bsp.diagnostics.PrintDebug("BP REPEAT control down" + str(m.buttonNumber%))
        
        bpControlDown = { }
        bpControlDown["EventType"] = "BPControlDown"
        bpControlDown["ButtonPanelIndex"] = StripLeadingSpaces(str(m.buttonPanelIndex%))
        bpControlDown["ButtonNumber"] = StripLeadingSpaces(str(m.buttonNumber%))
        m.msgPort.PostMessage(bpControlDown)
        
        m.timer.SetElapsed(0, m.repeatInterval%)
        m.timer.Start()
        
      end if
    end if
  end if
  
end sub


Sub ConfigureButton(bpSpec as object)
  
  bpConfiguration$ = bpSpec.configuration$
  
  ' no change necessary if the old and new configurations are the same and are a simple press
  if bpConfiguration$ = "press" and m.configuration$ = "press" then return
  
  ' if the old configuration was continuous and the new configuration is a simple press, stop the timer and destroy the timer object
  if bpConfiguration$ = "press" and m.configuration$ = "pressContinuous" then
    if type(m.timer) = "roTimer" then
      m.timer.Stop()
      m.timer = invalid
    end if
    m.configuration$ = "press"
    return
  end if
  
  ' capture the repeat rates if the new configuration is continuous
  if bpConfiguration$ = "pressContinuous" then
    m.initialHoldoff% = int(val(bpSpec.initialHoldoff$))
    m.repeatInterval% = int(val(bpSpec.repeatInterval$))
  end if
  
  ' if both the old and new configurations are continuous, restart the timer (if it is active)
  if bpConfiguration$ = "pressContinuous" and m.configuration$ = "pressContinuous" then
    if type(m.timer) = "roTimer" then
      m.timer.Stop()
      m.timer.SetElapsed(0, m.initialHoldoff%)
      m.timer.Start()
    end if
  end if
  
  ' if the old configuration was simple press and the new configuration is continuous, then capture the new values
  ' but don't start a timer (repeating won't start if the button was down at the time the state is entered).
  m.configuration$ = bpConfiguration$
  
end sub

'endregion


'region GPIO State Machine
' *************************************************
'
' GPIO State Machine
'
' *************************************************
Function newGPIOStateMachine(bsp as object, controlPort as object, inputPortIdentity$ as string, buttonNumber% as integer) as object
  
  GPIOStateMachine = { }
  
  GPIOStateMachine.bsp = bsp
  GPIOStateMachine.msgPort = bsp.msgPort
  GPIOStateMachine.inputPortIdentity$ = inputPortIdentity$
  GPIOStateMachine.buttonNumber% = buttonNumber%
  GPIOStateMachine.timer = invalid
  
  GPIOStateMachine.configuration$ = "press"
  GPIOStateMachine.initialHoldoff% = -1
  GPIOStateMachine.repeatInterval% = -1
  
  GPIOStateMachine.ConfigureButton = ConfigureButton
  GPIOStateMachine.EventHandler = GPIOEventHandler
  
  if IsControlPort(controlPort) then
    if controlPort.IsInputActive(buttonNumber%) then
      GPIOStateMachine.state$ = "ButtonDown"
    else
      GPIOStateMachine.state$ = "ButtonUp"
    end if
  end if
  
  return GPIOStateMachine
  
end function


Sub GPIOEventHandler(event as object)
  
  if m.state$ = "ButtonUp" then
    
    if type(event) = "roControlDown" and stri(event.GetSourceIdentity()) = m.inputPortIdentity$ and m.buttonNumber% = event.GetInt() then
      
      m.bsp.diagnostics.PrintDebug("GPIO control down" + str(event.GetInt()))
      
      gpioControlDown = { }
      gpioControlDown["EventType"] = "GPIOControlDown"
      gpioControlDown["ButtonNumber"] = StripLeadingSpaces(str(event.GetInt()))
      m.msgPort.PostMessage(gpioControlDown)
      
      if m.configuration$ = "pressContinuous" then
        m.timer = CreateObject("roTimer")
        m.timer.SetPort(m.msgPort)
        m.timer.SetElapsed(0, m.initialHoldoff%)
        m.timer.Start()
      end if
      
      m.state$ = "ButtonDown"
      
    end if
    
  else
    
    if type(event) = "roControlUp" and stri(event.GetSourceIdentity()) = m.inputPortIdentity$ and m.buttonNumber% = event.GetInt() then
      
      m.bsp.diagnostics.PrintDebug("GPIO control up" + str(event.GetInt()))
      
      ' if continuous, stop and destroy the timer
      if type(m.timer) = "roTimer" then
        m.timer.Stop()
        m.timer = invalid
      end if
      
      gpioControlUp = { }
      gpioControlUp["EventType"] = "GPIOControlUp"
      gpioControlUp["ButtonNumber"] = StripLeadingSpaces(str(event.GetInt()))
      m.msgPort.PostMessage(gpioControlUp)
      
      m.state$ = "ButtonUp"
      
      ' else check for repeat timeout
    else if type(event) = "roTimerEvent" and type(m.timer) = "roTimer" then
      if stri(event.GetSourceIdentity()) = stri(m.timer.GetIdentity()) then
        
        m.bsp.diagnostics.PrintDebug("GPIO REPEAT control down" + str(m.buttonNumber%))
        
        gpioControlDown = { }
        gpioControlDown["EventType"] = "GPIOControlDown"
        gpioControlDown["ButtonNumber"] = StripLeadingSpaces(str(m.buttonNumber%))
        m.msgPort.PostMessage(gpioControlDown)
        
        m.timer.SetElapsed(0, m.repeatInterval%)
        m.timer.Start()
        
      end if
    end if
  end if
  
end sub

'endregion


'region EventLoop
' *************************************************
'
' Event Loop and associated processing
'
' *************************************************
Sub EventLoop()

  SQLITE_COMPLETE = 100
  
  while true
    
    msg = wait(0, m.msgPort)

    m.diagnostics.PrintTimestamp()
    m.diagnostics.PrintDebug("msg received - type=" + type(msg))

    if type(msg) = "roControlDown" and stri(msg.GetSourceIdentity()) = stri(m.svcPort.GetIdentity()) then
      if msg.GetInt() = 12 then
        stop
      end if
    end if
    
    eventHandled = false
    
    for each scriptPlugin in m.scriptPlugins
      '			ERR_NORMAL_END = &hFC
      if scriptPlugin.plugin = invalid then
        m.diagnostics.PrintDebug("Plugin for " + scriptPlugin.name$ + " is invalid.")
        m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_SCRIPT_PLUGIN_FAILURE, scriptPlugin.name$)
      else
        eventHandled = scriptPlugin.plugin.ProcessEvent(msg)
        if eventHandled then
          exit for
        end if
      end if
      '			retVal = Eval("eventHandled = scriptPlugin.plugin.ProcessEvent(msg)")
      '			if retVal <> &hFC then
      '				' log the failure
      '				m.diagnostics.PrintDebug("Failure executing Eval to execute script plugin file event handler: return value = " + stri(retVal))
      '				m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_SCRIPT_PLUGIN_FAILURE, stri(retVal) + chr(9) + "EventHandler")
      '			endif
    next
    
    ' don't propagate the event if it was handled by a plugin
    if not eventHandled then
      
      if type(msg) = "roSqliteEvent" then
        if msg.GetSqlResult() <> SQLITE_COMPLETE then
          m.diagnostics.PrintDebug("roSqliteEvent.GetSqlResult() <> SQLITE_COMPLETE")
          if type(msg.GetSqlResult()) = "roInt" then
            m.diagnostics.PrintDebug("roSqliteEvent.GetSqlResult() = " + stri(roSqliteEvent.GetSqlResult()))
          end if
        end if
      end if

      if type(msg) = "roHttpEvent" then
        
        userdata = msg.GetUserData()
        if type(userdata) = "roAssociativeArray" and type(userdata.HandleEvent) = "roFunction" then
          userData.HandleEvent(userData, msg)
        end if

      else
        
        m.playerHSM.Dispatch(msg)
        
        for buttonPanelIndex% = 0 to 3
          for i% = 0 to 10
            if type(m.bpSM[buttonPanelIndex%, i%]) = "roAssociativeArray" then
              m.bpSM[buttonPanelIndex%, i%].EventHandler(msg)
            end if
          next
        next
        
        for i% = 0 to 7
          if type(m.gpioSM[i%]) = "roAssociativeArray" then
            m.gpioSM[i%].EventHandler(msg)
          end if
        next
        
        if type(m.sign) = "roAssociativeArray" then
          
          numZones% = m.sign.zonesHSM.Count()
          for i% = 0 to numZones% - 1
            m.dispatchingZone = m.sign.zonesHSM[i%]
            m.dispatchingZone.Dispatch(msg)
          next
          
        end if
        
        if m.networkingActive then
          m.networkingHSM.Dispatch(msg)
        end if

        m.diagnostics.DiagnoseAndRecoverWifiNetwork(msg)
        
      end if
      
    end if
    
  end while
  
end sub


Function ExecuteSwitchPresentationCommand(presentationName$ as string) as boolean
  
  ' retrieve target presentation
  presentation = m.presentations.Lookup(presentationName$)
  
  if type(presentation) = "roAssociativeArray" then
    
    ' check for existence of target presentation - if it's not present, don't try to switch to it
    autoplayFileName$ = "autoplay-" + presentation.presentationName + ".json"
    jsonFileName$ = m.assetPoolFiles.GetPoolFilePath(autoplayFileName$)
    if jsonFileName$ = "" then
      m.diagnostics.PrintDebug("switchPresentation: target presentation not found - " + presentationName$)
      return false
    end if
    
    ' send internal message to prepare for restart
    prepareForRestartEvent = { }
    prepareForRestartEvent["EventType"] = "PREPARE_FOR_RESTART"
    m.msgPort.PostMessage(prepareForRestartEvent)
    
    ' send switch presentation internal message
    switchPresentationEvent = { }
    switchPresentationEvent["EventType"] = "SWITCH_PRESENTATION"
    switchPresentationEvent["Presentation"] = presentation.presentationName
    m.msgPort.PostMessage(switchPresentationEvent)
    
    return true
    
  end if
  
  return false
  
end function


Sub ExecuteMediaStateCommands(zoneHSM as object, cmds as object)
  
  if type(cmds) = "roArray" then
    
    for each cmd in cmds
      
      if cmd.name$ = "switchPresentation" then
        m.diagnostics.PrintDebug("switchPresentation: not supported by media state commands")
        return
      end if
      
      m.ExecuteCmd(zoneHSM, cmd.name$, cmd.parameters)
      
    next
    
  end if
  
end sub


Function ExecuteTransitionCommands(zoneHSM as object, transition as object) as boolean
  
  transitionCmds = transition.transitionCmds
  if type(transitionCmds) = "roArray" then
    
    for each transitionCmd in transitionCmds
      
      command$ = transitionCmd.name$
      
      if command$ = "synchronize" and type(m.SyncManager) <> "roSyncManager" then
        
        ' if the next command is synchronize, get the file to preload
        nextState$ = transition.targetMediaState$
        zoneHSM.preloadState = zoneHSM.stateTable[nextState$]
        
      else if command$ = "switchPresentation" then
        
        presentationName$ = transitionCmd.parameters["presentationName"].GetCurrentParameterValue()
        
        return m.ExecuteSwitchPresentationCommand(presentationName$)
        
      else if command$ = "internalSynchronize" then
        
        if type(transition.internalSynchronizeEventsMaster) = "roAssociativeArray" then
          
          activeState = zoneHSM.activeState
          if type(activeState) = "roAssociativeArray" then
            activeState.internalSynchronizeEventsMaster = transition.internalSynchronizeEventsMaster
          end if
          
        end if
        
      end if
      
      m.ExecuteCmd(zoneHSM, transitionCmd.name$, transitionCmd.parameters)
      
    next
    
  end if
  
  return false
  
end function


Function GetNonPrintableKeyboardCode(keyboardInput% as integer) as string
  
  keyboardInput$ = LCase(StripLeadingSpaces(stri(keyboardInput%)))
  if m.nonPrintableKeyboardKeys.DoesExist(keyboardInput$) then
    return m.nonPrintableKeyboardKeys.Lookup(keyboardInput$)
  end if
  
  return ""
  
end function


Sub InitializeNonPrintableKeyboardCodeList()
  ' Space				<sp>	32
  ' Left arrow        <la>    32848
  ' Right arrow       <ra>    32847
  ' Up arrow          <ua>    32850
  ' Down arrow        <da>    32849
  ' Return            <rn>    10
  ' Enter             <en>    13
  ' Escape            <es>    27
  ' Page Up           <pu>    32843
  ' Page Down         <pd>    32846
  ' F1                <f1>    32826
  ' F2                <f2>    32827
  ' F3                <f3>    32828
  ' F4                <f4>    32829
  ' F5                <f5>    32830
  ' F6                <f6>    32831
  ' F7                <f7>    32832
  ' F8                <f8>    32833
  ' F9                <f9>    32834
  ' F10               <f10>   32835
  ' F11               <f11>   32836
  ' F12               <f12>   32837
  ' F13 (Print Screen)<ps>    32838
  ' F14 (Scroll Lock) <sl>    32839
  ' F15 (Pause Break) <pb>    32840
  ' Backspace         <bs>    8
  ' Tab               <tb>    9
  ' Insert            <in>    32841
  ' Delete            <de>    127
  ' Home              <ho>    32842
  ' End               <ed>    32845
  ' Capslock          <cl>    32825
  ' Mute				<mu>	32895
  ' Volume down		<vd>	32897
  ' Volume up			<vu>	32896
  ' Next track		<nt>	786613
  ' Previous track	<pt>	786614
  ' Play/Pause		<pp>	786637
  ' Stop music		<sm>	786615
  ' Stop browsing		<sm>	786982
  ' Power				<pwr>	65665
  ' Back				<bk>	786980
  ' Forward			<fw>	786981
  ' Refresh			<rf>	786983
  
  
  m.nonPrintableKeyboardKeys = { }
  m.nonPrintableKeyboardKeys.AddReplace("8", "<bs>")
  m.nonPrintableKeyboardKeys.AddReplace("9", "<tb>")
  m.nonPrintableKeyboardKeys.AddReplace("10", "<rn>")
  m.nonPrintableKeyboardKeys.AddReplace("13", "<en>")
  m.nonPrintableKeyboardKeys.AddReplace("27", "<es>")
  m.nonPrintableKeyboardKeys.AddReplace("32", "<sp>")
  m.nonPrintableKeyboardKeys.AddReplace("127", "<de>")
  m.nonPrintableKeyboardKeys.AddReplace("32848", "<la>")
  m.nonPrintableKeyboardKeys.AddReplace("32847", "<ra>")
  m.nonPrintableKeyboardKeys.AddReplace("32850", "<ua>")
  m.nonPrintableKeyboardKeys.AddReplace("32849", "<da>")
  m.nonPrintableKeyboardKeys.AddReplace("32843", "<pu>")
  m.nonPrintableKeyboardKeys.AddReplace("32846", "<pd>")
  m.nonPrintableKeyboardKeys.AddReplace("32826", "<f1>")
  m.nonPrintableKeyboardKeys.AddReplace("32827", "<f2>")
  m.nonPrintableKeyboardKeys.AddReplace("32828", "<f3>")
  m.nonPrintableKeyboardKeys.AddReplace("32829", "<f4>")
  m.nonPrintableKeyboardKeys.AddReplace("32830", "<f5>")
  m.nonPrintableKeyboardKeys.AddReplace("32831", "<f6>")
  m.nonPrintableKeyboardKeys.AddReplace("32832", "<f7>")
  m.nonPrintableKeyboardKeys.AddReplace("32833", "<f8>")
  m.nonPrintableKeyboardKeys.AddReplace("32834", "<f9>")
  m.nonPrintableKeyboardKeys.AddReplace("32835", "<f10>")
  m.nonPrintableKeyboardKeys.AddReplace("32836", "<f11>")
  m.nonPrintableKeyboardKeys.AddReplace("32837", "<f12>")
  m.nonPrintableKeyboardKeys.AddReplace("32838", "<ps>")
  m.nonPrintableKeyboardKeys.AddReplace("32839", "<sl>")
  m.nonPrintableKeyboardKeys.AddReplace("32840", "<pb>")
  m.nonPrintableKeyboardKeys.AddReplace("32841", "<in>")
  m.nonPrintableKeyboardKeys.AddReplace("32842", "<ho>")
  m.nonPrintableKeyboardKeys.AddReplace("32845", "<ed>")
  m.nonPrintableKeyboardKeys.AddReplace("32825", "<cl>")
  m.nonPrintableKeyboardKeys.AddReplace("32895", "<mu>")
  m.nonPrintableKeyboardKeys.AddReplace("32897", "<vd>")
  m.nonPrintableKeyboardKeys.AddReplace("32896", "<vu>")
  m.nonPrintableKeyboardKeys.AddReplace("786613", "<nt>")
  m.nonPrintableKeyboardKeys.AddReplace("786614", "<pt>")
  m.nonPrintableKeyboardKeys.AddReplace("786637", "<pp>")
  m.nonPrintableKeyboardKeys.AddReplace("786615", "<sm>")
  m.nonPrintableKeyboardKeys.AddReplace("786982", "<sb>")
  m.nonPrintableKeyboardKeys.AddReplace("65665", "<pwr>")
  m.nonPrintableKeyboardKeys.AddReplace("786980", "<bk>")
  m.nonPrintableKeyboardKeys.AddReplace("786981", "<fw>")
  m.nonPrintableKeyboardKeys.AddReplace("786983", "<rf>")
  
end sub


Function ConvertToRemoteCommand(remoteCommand% as integer) as string
  
  dim remoteCommands[19]
  remoteCommands[0] = "WEST"
  remoteCommands[1] = "EAST"
  remoteCommands[2] = "NORTH"
  remoteCommands[3] = "SOUTH"
  remoteCommands[4] = "SEL"
  remoteCommands[5] = "EXIT"
  remoteCommands[6] = "PWR"
  remoteCommands[7] = "MENU"
  remoteCommands[8] = "SEARCH"
  remoteCommands[9] = "PLAY"
  remoteCommands[10] = "FF"
  remoteCommands[11] = "RW"
  remoteCommands[12] = "PAUSE"
  remoteCommands[13] = "ADD"
  remoteCommands[14] = "SHUFFLE"
  remoteCommands[15] = "REPEAT"
  remoteCommands[16] = "VOLUP"
  remoteCommands[17] = "VOLDWN"
  remoteCommands[18] = "BRIGHT"
  
  if remoteCommand% < 0 or remoteCommand% > 18 then
    return ""
  end if
  
  return remoteCommands[remoteCommand%]
  
end function


Function GetIntegerParameterValue(parameters as object, parameterName$ as string, defaultValue% as integer) as integer
  
  parameter = parameters[parameterName$]
  parameter$ = parameter.GetCurrentParameterValue()
  parameter% = defaultValue%
  if parameter$ <> "" then
    parameter% = int(val(parameter$))
  end if
  
  return parameter%
end function


Function GetBmapOperatorValue(bmapOperator as object) as integer
  
  operatorValue = -1
  
  operator = bmapOperator.Operator
  if operator = "Operators.Set" then
    operatorValue = 0
  else if operator = "Operators.Get" then
    operatorValue = 1
  else if operator = "Operators.SetGet" then
    operatorValue = 2
  else if operator = "Operators.Status" then
    operatorValue = 3
  else if operator = "Operators.Error" then
    operatorValue = 4
  else if operator = "Operators.Start" then
    operatorValue = 5
  else if operator = "Operators.Result" then
    operatorValue = 6
  else if operator = "Operators.Processing" then
    operatorValue = 7
  end if
  
  return operatorValue
  
end function


Sub ExecuteSendBMapHexCommand(parameters as object)

  port$ = parameters["port"].getCurrentParameterValue()
  runtimeConnector$ = m.GetRuntimeUsbConnector(port$)

  if type(m.bmapByPort) = "roAssociativeArray" then
    bmap = m.bmapByPort.Lookup(runtimeConnector$)
  endif

  if type(bmap) <> "roBmap" then  
    m.diagnostics.PrintDebug("ExecuteSendBmapSendHexCommand: bmap object does not exist on runtimeConnector: " + runtimeConnector$)
  else

    bmapHexStringParameter = parameters["message"]
    hexString$ = bmapHexStringParameter.GetCurrentParameterValue()

    request = CreateObject("roByteArray")
    request.FromHexString(hexString$)
    
    m.diagnostics.PrintDebug("BMAP Hex Command byte array:")
    m.diagnostics.PrintDebug(hexString$)
    
    if not bmap.Send(request) then
      m.diagnostics.PrintDebug("BMAP send failure")
      m.diagnostics.PrintDebug(bmap.GetFailureReason())
    end if
  endif

End Sub


Sub ExecuteSendBMapCommand(parameters as object)
  
  connector$ = parameters["port"].getCurrentParameterValue()

  port$ = m.GetRuntimeUsbConnector(connector$)

  if type(m.bmapByPort) = "roAssociativeArray" then
    bmap = m.bmapByPort.Lookup(port$)
  endif

  if type(bmap) <> "roBmap" then
  
    m.diagnostics.PrintDebug("ExecuteSendBMapCommand: bmap object does not exist")
  
  else

    boseProduct = m.sign.boseProductsByConnector[connector$]
    bmapCommunicationSpec = boseProduct.bmapCommunicationSpec
    
    bmapFunctionalBlockName = parameters.bmapFunctionBlock.getCurrentParameterValue()
    bmapFunctionName = parameters.bmapFunction.getCurrentParameterValue()
    bmapOperatorName = parameters.bmapOperator.getCurrentParameterValue()

    matchedFunctionBlock = invalid
    matchedFunction = invalid
    matchedOperator = invalid
    
    payload = []
    payloadSize = 0

    functionBlocks = bmapCommunicationSpec.FunctionBlocks
    for each functionBlock in functionBlocks
      if functionBlock.Name = bmapFunctionalBlockName then
        matchedFunctionBlock = functionBlock
        functionBlockValue = matchedFunctionBlock.Value
        exit for
      end if
    next
  
    if matchedFunctionBlock <> invalid then
    
      for each bmapFunction in matchedFunctionBlock.Functions
        if bmapFunction.Name = bmapFunctionName then
          matchedFunction = bmapFunction
          functionValue = matchedFunction.Value
          exit for
        end if
      next

      if matchedFunction <> invalid then
        for each operator in matchedFunction.operators
          if operator.Operator = bmapOperatorName then
            operatorValue = GetBmapOperatorValue(operator)
   
            fields = operator.Fields
            for each field in fields

              fieldName = field.Name
              bitfields = field.bitfields
              
              if type(bitfields) = "roArray" and bitfields.Count() > 0 then

                fieldValueInt = 0
                shiftNextBitcount = 0
                
                for each bitfield in bitfields

                  key = fieldName + ":" + bitfield.Name
                  bitfieldParameter = parameters.Lookup(key)

                  bitFieldValue$ = bitfieldParameter.getCurrentParameterValue()                

                  ' TODOBMAP
                  'bitFieldValue = int(val(bitFieldValue$)) 'bitFieldValue$ is hex, at least some of the time. all the time?
                  ' length of bitFieldValue$ - numBits > 4?
                  ba = CreateObject("roByteArray")
                  if len(bitFieldValue$) = 1 then
                    bitFieldValue$ = "0" + bitFieldValue$
                  endif
                  ba.FromHexString(bitFieldValue$)
                  bitFieldValue = ba[0]

                  bitFieldValue = ShiftLeft(bitfieldValue, shiftNextBitcount)
                  
                  fieldValueInt = fieldValueInt + bitFieldValue

                  shiftNextBitcount = shiftNextBitcount + bitfield.NumBits

                next

                ba = CreateObject("roByteArray")
                ba.push(fieldValueInt)
    
                fieldValue = ba.ToHexString()
                if len(fieldValue) = 0 then
                  str = "00"
                else if len(fieldValue) = 1 then
                  str = "0" + fieldValue
                else if len(fieldValue) > 2 then
                  str = fieldValue
                else
                  str = fieldValue
                endif

                for i = 0 to len(str) - 1
                  payload.push(mid(str, i + 1, 1))
                next

                payloadSize = payloadSize + (len(str) / 2)

              else

                fieldParameter = parameters.Lookup(field.Name)

                if fieldParameter <> invalid then

                  fieldValue = fieldParameter.getCurrentParameterValue()

                  if field.Units = "ASCII" then

                    ba = CreateObject("roByteArray")
                    ba.FromAsciiString(fieldValue)
                    hex$ = ba.ToHexString()
                    for i = 0 to len(hex$) - 1
                      charValue = hex$.mid(i, 1)
                      payload.push(charValue)
                    next
                    payloadSize = payloadSize + len(hex$)

                  else if field.type = "uint8" then

                    if len(fieldValue) = 0 then
                      str = "00"
                    else if len(fieldValue) = 1 then
                      str = "0" + fieldValue
                    else if len(fieldValue) > 2 then
                      str = fieldValue
                    else
                      str = fieldValue
                    endif

                    for i = 0 to len(str) - 1
                      payload.push(mid(str, i + 1, 1))
                    next

                    payloadSize = payloadSize + (len(str) / 2)

                  else if field.type = "uint16" then

                    ' disregard count for now.
                    ' assume fieldValue is a single uint16
                    if len(fieldValue) = 0 then
                      str = "0000"
                    else if len(fieldValue) = 1 then
                      str = "000" + fieldValue
                    else if len(fieldValue) = 2 then
                      str = "00" + fieldValue
                    else if len(fieldValue) = 3 then
                      str = "0" + fieldValue
                    else if len(fieldValue) > 4 then
                      stop
                    else
                      str = fieldValue
                    endif

                    for i = 0 to len(str) - 1
                      payload.push(mid(str, i + 1, 1))
                    next

                    payloadSize = payloadSize + (len(str) / 2)

                  else if field.type = "int8"

                    if len(fieldValue) = 0 then
                      str = "00"
                    else if len(fieldValue) = 1 then
                      str = "0" + fieldValue
                    else if len(fieldValue) = 2 then
                      str = fieldValue
                    else if len(fieldValue) > 2 then
                      stop
                    endif
                    for i = 0 to len(str) - 1
                      payload.push(mid(str, i + 1, 1))
                    next

                    payloadSize = payloadSize + (len(str) / 2)

                  else
                    ' unsupposed field type
                    stop
                  endif

                end if
              
              endif

            next
          end if
        next
      end if
    
    endif
  
    request = CreateObject("roByteArray")
    request.push(functionBlockValue)
    request.push(functionValue)
    request.push(operatorValue)
    request.push(payloadSize)

    i = 0
    while i < payload.count()
      firstSubstring = payload[i]
      secondSubString = payload[i + 1]
      substring = firstSubstring + secondSubString
      hexValueBA = CreateObject("roByteArray")
      hexValueBA.FromHexString(substring)
      request.push(hexValueBA[0])
      i = i + 2
    endwhile

    m.diagnostics.PrintDebug("BMAP Command byte array:")
    m.diagnostics.PrintDebug(request.ToHexString())

    if not bmap.Send(request) then
      print "BMAP FAILURE"
      print bmap.GetFailureReason()
      bmap = m.CreateBMap(connector$, m.sign)
      if type(bmap) = "roBmap" then
        ok = bmap.Send(request)
        if not ok then
          print "BMAP FAILURE again"
          print bmap.GetFailureReason()
          stop
        endif
      endif
    end if

  endif
  
end sub


Function IsBmapField(parameterName as string) as boolean
  
  if parameterName = "port" or parameterName = "bmapFunctionBlock" or parameterName = "bmapFunction" or parameterName = "bmapOperator" then
    return false
  end if
  return true
  
end function


Function ShiftLeft(val as integer, shiftCount as integer) as integer
  
  for i% = 0 to shiftCount -1
    val = val * 2
  next
  return val

end function


Function ShiftRight(val as integer, shiftCount as integer) as integer
  
  for i% = 0 to shiftCount -1
    val = val / 2
  next
  return val

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

Sub ExecuteSynchronizeCommand(zoneHSM as object, command$ as string, parameters as object)
  synchronizeKeywordParameter = parameters["message"]
  synchronizeKeyword$ = synchronizeKeywordParameter.GetCurrentParameterValue()
  
  if type(m.SyncManager) = "roSyncManager" then
    
    m.diagnostics.PrintDebug("Send synchronize command " + synchronizeKeyword$ + " using SyncManager.")
    
    syncManagerEvent = m.SyncManager.Synchronize(synchronizeKeyword$, 300)
    m.diagnostics.PrintDebug("@@@ Created syncManagerEvent with sync keyword: " + synchronizeKeyword$)
    zoneHSM.syncInfo = { }
    zoneHSM.syncInfo.SyncDomain = syncManagerEvent.GetDomain()
    zoneHSM.syncInfo.SyncId = syncManagerEvent.GetId()
    zoneHSM.syncInfo.SyncIsoTimestamp = syncManagerEvent.GetIsoTimestamp()
    
  else
    
    m.diagnostics.PrintDebug("Send synchronize command " + synchronizeKeyword$)
    
    m.udpSender.Send("pre-" + synchronizeKeyword$)
    
    if type(zoneHSM.preloadState) = "roAssociativeArray" and (zoneHSM.preloadedStateName$ <> zoneHSM.preloadState.name$) then
      ' currently only support preload / synchronizing with images and videos
      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 = CreateScaledRectangle(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
      
      zoneHSM.widget.Hide()
      
    else if zoneHSM.type$ = "BackgroundImage" then
      
    end if
    
    zoneHSM.isVisible = false
    
  end if
end sub

Sub ExecuteShowZoneCommand(zoneHSM as object, command$ as string, parameters as object)
  m.diagnostics.PrintDebug("Execute showZone command")
  
  zoneId$ = parameters["zoneId"].GetCurrentParameterValue()
  zoneHSM = m.GetZone(zoneId$)
  
  if type(zoneHSM) = "roAssociativeArray" then
    
    if zoneHSM.isVisible then
      return
    end if
    
    if zoneHSM.type$ = "VideoOrImages" or zoneHSM.type$ = "Images" then
      
      if type(zoneHSM.videoPlayer) = "roVideoPlayer" then zoneHSM.videoPlayer.Show()
      if type(zoneHSM.imagePlayer) <> "Invalid" and not zoneHSM.imageHidden then zoneHSM.imagePlayer.Show()
      if type(zoneHSM.canvasWidget) <> "Invalid" and not zoneHSM.canvasHidden then zoneHSM.canvasWidget.Show()
      if type(zoneHSM.loadingHtmlWidget) <> "Invalid" and not zoneHSM.htmlHidden then zoneHSM.loadingHtmlWidget.Show()
      if type(zoneHSM.displayedHtmlWidget) <> "Invalid" and not zoneHSM.htmlHidden then zoneHSM.displayedHtmlWidget.Show()
      
    else if zoneHSM.type$ = "VideoOnly" then
      
      if type(zoneHSM.videoPlayer) = "roVideoPlayer" then zoneHSM.videoPlayer.Show()
      
    else if zoneHSM.type$ = "Clock" then
      
      zoneHSM.widget.Show()
      
    else if zoneHSM.type$ = "Ticker" then
      
      zoneHSM.widget.Show()
      
    else if zoneHSM.type$ = "BackgroundImage" then
      
    end if
    
    zoneHSM.isVisible = true
    
  end if
end sub

Sub ExecutePauseZonePlaybackCommand(zoneHSM as object, command$ as string, parameters as object)
  
  m.diagnostics.PrintDebug("Execute pauseZonePlayback command")
  
  zoneId$ = parameters["zoneId"].GetCurrentParameterValue()
  zoneHSM = m.GetZone(zoneId$)
  
  if type(zoneHSM) = "roAssociativeArray" then

    if type(zoneHSM.videoPlayer) = "roVideoPlayer" then
      zoneHSM.videoPlayer.Pause()
    endif

    if IsAudioPlayer(zoneHSM.audioPlayer) then
      zoneHSM.audioPlayer.Pause()
    endif

  endif

end sub

Sub ExecuteResumeZonePlaybackCommand(zoneHSM as object, command$ as string, parameters as object)
  
  m.diagnostics.PrintDebug("Execute resumeZonePlayback command")
  
  zoneId$ = parameters["zoneId"].GetCurrentParameterValue()
  zoneHSM = m.GetZone(zoneId$)
  
  if type(zoneHSM) = "roAssociativeArray" then

    if type(zoneHSM.videoPlayer) = "roVideoPlayer" then
      zoneHSM.videoPlayer.Resume()
    endif

    if IsAudioPlayer(zoneHSM.audioPlayer) then
      zoneHSM.audioPlayer.Resume()
    endif

  endif

end sub

Sub ExecuteInternalSynchronizeCommand(zoneHSM as object, command$ as string, parameters as object)
  m.diagnostics.PrintDebug("Execute internalSynchronize command")
  
  internalSyncParameter = parameters["message"]
  internalSyncParameter$ = internalSyncParameter.GetCurrentParameterValue()
  
  ' send InternalSyncPreload message
  internalSyncPreload = { }
  internalSyncPreload["EventType"] = "INTERNAL_SYNC_PRELOAD"
  internalSyncPreload["EventParameter"] = internalSyncParameter$
  m.msgPort.PostMessage(internalSyncPreload)
  
  ' send InternalSyncMasterPreload message
  internalSyncMasterPreload = { }
  internalSyncMasterPreload["EventType"] = "INTERNAL_SYNC_MASTER_PRELOAD"
  internalSyncMasterPreload["EventParameter"] = internalSyncParameter$
  m.msgPort.PostMessage(internalSyncMasterPreload)
  
  ' current state is zoneHSM.activeState
  activeState = zoneHSM.activeState
  if type(activeState) = "roAssociativeArray" then
    if type(activeState.internalSynchronizeEventsMaster) = "roAssociativeArray" then
      if type(activeState.internalSynchronizeEventsMaster[internalSyncParameter$]) = "roAssociativeArray" then
        transition = activeState.internalSynchronizeEventsMaster[internalSyncParameter$]
        nextState$ = transition.targetMediaState$
        if nextState$ <> "" then
          zoneHSM.preloadState = zoneHSM.stateTable[nextState$]
          zoneHSM.preloadState.PreloadItem()
        end if
      end if
    end if
  end if
  
end sub


' 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
    userVariable.SetCurrentValue(variableValue$, true)
    
    userVariablesChanged = { }
    userVariablesChanged["EventType"] = "USER_VARIABLES_UPDATED"
    m.msgPort.PostMessage(userVariablesChanged)
    
    ' Notify controlling devices to refresh
    m.SendUDPNotification("refresh")
  else
    m.diagnostics.PrintDebug("User variable " + variableName$ + " not found.")
    m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND, variableName$)
  end if
end sub


Sub ExecuteBeaconStartCommand(zoneHSM as object, command$ as string, parameters as object)
  nameParameter = parameters["beaconName"]
  name$ = nameParameter.GetCurrentParameterValue()
  m.btManager.StartBeacon(name$)
end sub


Sub ExecuteBeaconStopCommand(zoneHSM as object, command$ as string, parameters as object)
  nameParameter = parameters["beaconName"]
  name$ = nameParameter.GetCurrentParameterValue()
  m.btManager.StopBeacon(name$)
end sub


' m is bsp
Sub ExecuteCmd(zoneHSM as object, command$ as string, parameters as object)
  
  m.diagnostics.PrintDebug("ExecuteCmd " + command$)
  
  if command$ = "gpioOnCommand" then
    m.ExecuteGpioOnCommand(zoneHSM, command$, parameters)
  else if command$ = "gpioOffCommand" then
    m.ExecuteGpioOffCommand(zoneHSM, command$, parameters)
  else if command$ = "gpioSetStateCommand" then
    m.ExecuteGpioSetStateCommand(zoneHSM, command$, parameters)
  else if command$ = "sendWss" then
    m.ExecuteSendWssCommand(parameters)
  else if command$ = "sendBMap" then    
    m.ExecuteSendBMapCommand(parameters)
  else if command$ = "sendBMapHex" then
    m.ExecuteSendBMapHexCommand(parameters)
  else if command$ = "setAllAudioOutputs" then
    m.ExecuteSetAllAudioOutputsCommand(parameters)
  else if command$ = "setAudioMode" then
    m.SetAudioMode(parameters)
  else if command$ = "muteAudioOutputs" then
    m.MuteAudioOutputs(true, parameters)
  else if command$ = "unmuteAudioOutputs" then
    m.MuteAudioOutputs(false, parameters)
  else if command$ = "setConnectorVolume" then
    m.SetConnectorVolume(parameters)
  else if command$ = "incrementConnectorVolume" then
    m.ChangeConnectorVolume(1, parameters)
  else if command$ = "decrementConnectorVolume" then
    m.ChangeConnectorVolume( - 1, parameters)
  else if command$ = "setZoneVolume" then
    m.SetZoneVolume(parameters)
  else if command$ = "incrementZoneVolume" then
    m.ChangeZoneVolume(1, parameters)
  else if command$ = "decrementZoneVolume" then
    m.ChangeZoneVolume( - 1, parameters)
  else if command$ = "setZoneChannelVolume" then
    m.SetZoneChannelVolume(parameters)
  else if command$ = "incrementZoneChannelVolume" then
    m.ChangeZoneChannelVolume(1, parameters)
  else if command$ = "decrementZoneChannelVolume" then
    m.ChangeZoneChannelVolume( - 1, parameters)
  else if command$ = "serialSendStringCommand" or command$ = "sendSerialStringCommand" or command$ = "serialSendString" then
    m.ExecuteSerialSendStringCommand(zoneHSM, command$, parameters)
  else if command$ = "sendSerialBlockCommand" then
    m.ExecuteSendSerialBlockCommand(zoneHSM, command$, parameters)
  else if command$ = "sendSerialByteCommand" then
    m.ExecuteSendSerialByteCommand(zoneHSM, command$, parameters)
  else if command$ = "sendSerialBytesCommand" then
    m.ExecuteSendSerialBytesCommand(zoneHSM, command$, parameters)
  else if command$ = "sendUDPCommand" then
    m.ExecuteSendUDPCommand(zoneHSM, command$, parameters)
  else if command$ = "sendUDPBytesCommand" then
    m.ExecuteSendUDPBytesCommand(zoneHSM, command$, parameters)
  else if command$ = "sendProntoIRRemote" then
    m.ExecuteSendProntoIRRemote(zoneHSM, command$, parameters)
  else if command$ = "sendIRRemote" then
    m.ExecuteSendIRRemoteCommand(zoneHSM, command$, parameters)
  else if command$ = "sendBLC400Output" then
    m.ExecuteSendBLC400OutputCommand(zoneHSM, command$, parameters)
  else if command$ = "sendBPOutput" then
    m.ExecuteSendBPOutputCommand(zoneHSM, command$, parameters)
  else if command$ = "synchronize" then
    m.ExecuteSynchronizeCommand(zoneHSM, command$, parameters)
  else if command$ = "sendZoneMessage" then
    m.ExecuteSendZoneMessageCommand(zoneHSM, command$, parameters)
  else if command$ = "sendPluginMessage" then
    m.ExecuteSendPluginMessageCommand(zoneHSM, command$, parameters)
  else if command$ = "resizeZone" then
    m.ExecuteResizeZoneCommand(zoneHSM, command$, parameters)
  else if command$ = "hideZone" then
    m.ExecuteHideZoneCommand(zoneHSM, command$, parameters)
  else if command$ = "showZone" then
    m.ExecuteShowZoneCommand(zoneHSM, command$, parameters)
  else if command$ = "pauseZonePlayback" then
    m.ExecutePauseZonePlaybackCommand(zoneHSM, command$, parameters)
  else if command$ = "resumeZonePlayback" then
    m.ExecuteResumeZonePlaybackCommand(zoneHSM, command$, parameters)
  else if command$ = "internalSynchronize" then
    m.ExecuteInternalSynchronizeCommand(zoneHSM, command$, parameters)
  else if command$ = "reboot" then
    RebootSystem()
  else if command$ = "cecDisplayOn" then
    m.CecDisplayOn(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$ = "generateSessionGuid" then
    m.GenerateSessionGuid()
  else if command$ = "clearSessionGuid" then
    m.ClearSessionGuid()
  else if command$ = "configureAudioResources" then
    zoneHSM.ConfigureAudioResources()
  else if command$ = "updateDataFeed" then
    m.UpdateDataFeed(parameters)
  else if command$ = "beaconStart" then
    m.ExecuteBeaconStartCommand(zoneHSM, command$, parameters)
  else if command$ = "beaconStop" then
    m.ExecuteBeaconStopCommand(zoneHSM, command$, parameters)
  end if
  
end sub


Sub ResetVariable(parameters as object)
  
  variableNameParameter = parameters["variableName"]
  variableName$ = variableNameParameter.GetVariableName()
  
  userVariable = m.GetUserVariable(variableName$)
  if type(userVariable) = "roAssociativeArray" then
    userVariable.Reset(true)
  else
    m.diagnostics.PrintDebug("User variable " + variableName$ + " not found.")
    m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND, variableName$)
  end if
  
end sub


Sub ChangeUserVariableValue(parameters as object, delta% as integer)
  
  variableNameParameter = parameters["variableName"]
  variableName$ = variableNameParameter.GetVariableName()
  
  userVariable = m.GetUserVariable(variableName$)
  if type(userVariable) = "roAssociativeArray" then
    currentValue% = val(userVariable.GetCurrentValue())
    currentValue% = currentValue% + delta%
    userVariable.SetCurrentValue(StripLeadingSpaces(stri(currentValue%)), true)
  else
    m.diagnostics.PrintDebug("User variable " + variableName$ + " not found.")
    m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND, variableName$)
  end if
  
end sub


Function STTopEventHandler(event as object, stateData as object) as object
  
  stateData.nextState = invalid
  return "IGNORED"
  
end function


Function GetPoolFilePath(assetPoolFiles as object, fileName$ as string) as string
  
  if type(assetPoolFiles) = "roAssetPoolFiles" then
    return assetPoolFiles.GetPoolFilePath(fileName$)
  else
    return fileName$
  end if
  
end function

'endregion

'region Logging
REM *******************************************************
REM *******************************************************
REM ***************                    ********************
REM *************** LOGGING OBJECT     ********************
REM ***************                    ********************
REM *******************************************************
REM *******************************************************

REM
REM construct a new logging BrightScript object
REM
Function newLogging() as object
  
  logging = { }
  
  logging.bsp = m
  logging.msgPort = m.msgPort
  logging.systemTime = m.systemTime
  logging.diagnostics = m.diagnostics
  
  logging.SetSystemInfo = SetSystemInfo
  
  logging.CreateLogFile = CreateLogFile
  logging.MoveExpiredCurrentLog = MoveExpiredCurrentLog
  logging.MoveCurrentLog = MoveCurrentLog
  logging.InitializeLogging = InitializeLogging
  logging.ReinitializeLogging = ReinitializeLogging
  logging.InitializeCutoverTimer = InitializeCutoverTimer
  logging.WritePlaybackLogEntry = WritePlaybackLogEntry
  logging.WriteEventLogEntry = WriteEventLogEntry
  logging.WriteStateLogEntry = WriteStateLogEntry
  logging.WriteDiagnosticLogEntry = WriteDiagnosticLogEntry
  logging.WriteDiagnosticLogEntryForForceLogUpload = WriteDiagnosticLogEntryForForceLogUpload
  logging.WriteVariableLogEntry = WriteVariableLogEntry
  logging.PushLogFile = PushLogFile
  logging.CutoverLogFile = CutoverLogFile
  logging.HandleTimerEvent = HandleLoggingTimerEvent
  logging.PushLogFilesOnBoot = PushLogFilesOnBoot
  logging.OpenOrCreateCurrentLog = OpenOrCreateCurrentLog
  logging.DeleteExpiredFiles = DeleteExpiredFiles
  logging.DeleteOlderFiles = DeleteOlderFiles
  logging.DeleteLogFiles = DeleteLogFiles
  logging.DeleteAllLogFiles = DeleteAllLogFiles
  logging.GetLogFiles = GetLogFiles
  logging.CopyAllLogFiles = CopyAllLogFiles
  logging.CopyLogFiles = CopyLogFiles
  logging.FlushLogFile = FlushLogFile
  logging.UpdateLogCounter = UpdateLogCounter
  logging.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


Function CopyAllLogFiles(storagePath$ as string) as boolean
  
  if type(m.logFile) = "roCreateFile" or type(m.logFile) = "roAppendFile" then
    m.logFile.Flush()
  end if
  
  ok = m.CopyLogFiles(storagePath$, "currentLog")
  if not ok then
    return ok
  end if
  
  ok = m.CopyLogFiles(storagePath$, "logs")
  if not ok then
    return ok
  end if
  
  ok = m.CopyLogFiles(storagePath$, "failedLogs")
  if not ok then
    return ok
  end if
  
  ok = m.CopyLogFiles(storagePath$, "archivedLogs")
  return ok
  
end function


Function CopyLogFiles(storagePath$ as string, folderName$ as string)
  
  listOfLogFiles = MatchFiles("/" + folderName$, "*")
  
  for each file in listOfLogFiles
    sourceFilePath$ = "/" + folderName$ + "/" + file
    destinationFilePath$ = storagePath$ + file
    ok = CopyFile(sourceFilePath$, destinationFilePath$)
    if not ok then
      return ok
    end if
    
  next
  
  return true
  
end function


Sub DeleteAllLogFiles()
  
  ' close the current log file before deleting
  
  if type(m.logFile) = "roCreateFile" or type(m.logFile) = "roAppendFile" then
    m.logFile.Flush()
    m.logFile = invalid
  end if
  
  m.DeleteLogFiles("currentLog")
  m.DeleteLogFiles("logs")
  m.DeleteLogFiles("failedLogs")
  m.DeleteLogFiles("archivedLogs")
  
end sub


Sub DeleteLogFiles(folderName$ as string)
  
  listOfLogFiles = MatchFiles("/" + folderName$, "*")
  
  for each file in listOfLogFiles
    fullFilePath$ = "/" + folderName$ + "/" + file
    DeleteFile(fullFilePath$)
  next
  
end sub


Sub DeleteExpiredFiles()
  
  if m.useDate then
    
    ' delete any files that are more than 30 days old
    
    dtExpired = m.systemTime.GetLocalDateTime()
    dtExpired.SubtractSeconds(60 * 60 * 24 * 30)
    
    ' look in the following folders
    '   logs
    '   failedLogs
    '   archivedLogs
    
    m.DeleteOlderFiles("logs", dtExpired)
    m.DeleteOlderFiles("failedLogs", dtExpired)
    m.DeleteOlderFiles("archivedLogs", dtExpired)
    
  else
    
    MAX_FILES_TO_KEEP = 60
    
    ' get a list of all log files
    logFiles = CreateObject("roArray", 1, true)
    m.GetLogFiles("logs", logFiles)
    m.GetLogFiles("failedLogs", logFiles)
    m.GetLogFiles("archivedLogs", logFiles)
    
    ' sort them in ascending order
    sortedIndices = CreateObject("roArray", 1, true)
    SortItems(logFiles, sortedIndices)
    
    ' if the count is > than the number to keep, delete the first n in the list
    while sortedIndices.Count() > MAX_FILES_TO_KEEP
      
      fullFilePath$ = logFiles[sortedIndices[0]].fullFilePath$
      m.diagnostics.PrintDebug("Delete log file " + fullFilePath$)
      DeleteFile(fullFilePath$)
      
      sortedIndices.shift()
      
    end while
    
  end if
  
end sub


' sorted indices is an array that can grow and has no values on entry
' items is an array of associative arrays
Sub SortItems(logFiles as object, sortedIndices as object)
  
  ' initialize array with indices.
  for i% = 0 to logFiles.Count() - 1
    sortedIndices[i%] = i%
  next
  
  numItemsToSort% = logFiles.Count()
  
  for i% = numItemsToSort% - 1 to 1 step -1
    for j% = 0 to i% - 1
      index0% = sortedIndices[j%]
      logCounter0% = logFiles[index0%].counter%
      index1% = sortedIndices[j% + 1]
      logCounter1% = logFiles[index1%].counter%
      if logCounter0% > logCounter1% then
        k% = sortedIndices[j%]
        sortedIndices[j%] = sortedIndices[j% + 1]
        sortedIndices[j% + 1] = k%
      end if
    next
  next
  
end sub


Sub GetLogFiles(folderName$ as string, logFiles as object)
  
  listOfLogFiles = MatchFiles("/" + folderName$, "*")
  
  for each file in listOfLogFiles
    logFile = { }
    logFile.counter% = int(val(left(right(file, 7), 3)))
    logFile.fullFilePath$ = "/" + folderName$ + "/" + file
    logFiles.push(logFile)
  next
  
end sub


Sub DeleteOlderFiles(folderName$ as string, dtExpired as object)
  
  listOfLogFiles = MatchFiles("/" + folderName$, "*")
  
  for each file in listOfLogFiles
    
    year$ = "20" + left(right(file, 13), 2)
    month$ = left(right(file, 11), 2)
    day$ = left(right(file, 9), 2)
    dtFile = CreateObject("roDateTime")
    dtFile.SetYear(int(val(year$)))
    dtFile.SetMonth(int(val(month$)))
    dtFile.SetDay(int(val(day$)))
    
    if dtFile < dtExpired then
      fullFilePath$ = "/" + folderName$ + "/" + file
      m.diagnostics.PrintDebug("Delete expired log file " + fullFilePath$)
      DeleteFile(fullFilePath$)
    end if
    
  next
  
end sub


Sub FlushLogFile()
  
  if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return
  
  m.logFile.Flush()
  
end sub


Sub WritePlaybackLogEntry(zoneName$ as string, startTime$ as string, endTime$ as string, itemType$ as string, fileName$ as string)
  
  if not m.playbackLoggingEnabled then return
  
  if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return
  
  t$ = chr(9)
  m.logFile.SendLine("L=p" + t$ + "Z=" + zoneName$ + t$ + "S=" + startTime$ + t$ + "E=" + endTime$ + t$ + "I=" + itemType$ + t$ + "N=" + fileName$)
  m.logFile.AsyncFlush()
  
end sub


Sub WriteStateLogEntry(stateMachine as object, stateName$ as string, stateType$ as string)
  
  if not m.stateLoggingEnabled then return
  
  if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return
  
  timestamp$ = m.systemTime.GetLocalDateTime().GetString()
  
  t$ = chr(9)
  
  if type(stateMachine.lastStateName$) = "roString" then
    lastStateName$ = stateMachine.lastStateName$
    lastEventType$ = stateMachine.lastEventType$
    lastEventData$ = stateMachine.lastEventData$
  else
    lastStateName$ = ""
    lastEventType$ = ""
    lastEventData$ = ""
  end if
  
  m.logFile.SendLine("L=s" + t$ + "S=" + stateName$ + t$ + "T=" + timestamp$ + t$ + "Y=" + stateType$ + t$ + "LS=" + lastStateName$ + t$ + "LE=" + lastEventType$ + t$ + "LD=" + lastEventData$)
  m.logFile.AsyncFlush()
  
end sub


Sub WriteEventLogEntry(stateMachine as object, stateName$ as string, eventType$ as string, eventData$ as string, eventActedOn$ as string)
  
  if not (m.eventLoggingEnabled or m.stateLoggingEnabled) then return
  
  if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return
  
  timestamp$ = m.systemTime.GetLocalDateTime().GetString()
  
  if eventActedOn$ = "1" then
    stateMachine.lastStateName$ = stateName$
    stateMachine.lastEventType$ = eventType$
    stateMachine.lastEventData$ = eventData$
  end if
  
  if m.eventLoggingEnabled then
    t$ = chr(9)
    logStr$ = "L=e" + t$ + "S=" + stateName$ + t$ + "T=" + timestamp$ + t$ + "E=" + eventType$ + t$ + "D=" + eventData$ + t$ + "A=" + eventActedOn$
    if isString(m.bsp.sysInfo.sessionGuid$) and m.bsp.sysInfo.sessionGuid$ <> "" then
      logStr$ = logStr$ + t$ + ("I=" + m.bsp.sysInfo.sessionGuid$)
    end if
    m.logFile.SendLine(logStr$)
    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

  diagnostics.DiagnoseAndRecoverWifiNetwork = DiagnoseAndRecoverWifiNetwork
  diagnostics.minusSixFailureCount = 0
  diagnostics.minusTwentyEightFailureCount = 0
  diagnostics.minusFiftySixFailureCount = 0
  diagnostics.lastWifiReconnectTimestampInSeconds = 0
  
  return diagnostics
  
end function


Sub UpdateDebugOn(debugOn as boolean)
  
  m.debug = debugOn
  
end sub


Sub UpdateSystemLogDebugOn(systemLogDebug as boolean)
  
  m.systemLogDebug = systemLogDebug
  
  if systemLogDebug and type(m.systemLog) <> "roSystemLog" then
    m.systemLog = CreateObject("roSystemLog")
  end if
  
end sub


Sub PrintDebug(debugStr$ as string)

  if type(m) <> "roAssociativeArray" then stop
  
  if m.debug then
    
    print debugStr$
    
  end if
  
  if m.systemLogDebug then
    m.systemLog.SendLine(debugStr$)
  end if
  
  return
  
end sub


Sub PrintTimestamp()
  
  eventDateTime = m.systemTime.GetLocalDateTime()
  if m.debug then print eventDateTime.GetString()
  
  if m.systemLogDebug then
    m.systemLog.SendLine(eventDateTime.GetString())
  end if
  
  return
  
end sub


Sub SetSystemInfo(sysInfo as object, diagnosticCodes as object)
  
  m.autorunVersion$ = sysInfo.autorunVersion$
  m.customAutorunVersion$ = sysInfo.customAutorunVersion$
  m.firmwareVersion$ = sysInfo.deviceFWVersion$
  m.deviceUniqueID$ = sysInfo.deviceUniqueID$
  m.deviceModel$ = sysInfo.deviceModel$
  m.deviceFamily$ = sysInfo.deviceFamily$
  m.modelSupportsWifi = sysInfo.modelSupportsWifi
  
  m.enableLogDeletion = sysInfo.enableLogDeletion
  
  m.diagnosticCodes = diagnosticCodes
  
  return
  
end sub



Function GetHexColor(colorAttrs as object) as string
  
  ba = CreateObject("roByteArray")
  
  ba[0] = colorAttrs["a"]
  alpha$ = ba.ToHexString()
  
  ba[0] = colorAttrs["r"]
  red$ = ba.ToHexString()
  
  ba[0] = colorAttrs["g"]
  green$ = ba.ToHexString()
  
  ba[0] = colorAttrs["b"]
  blue$ = ba.ToHexString()
  
  return alpha$ + red$ + green$ + blue$
  
end function


Function ByteArraysMatch(baInput as object, baSpec as object) as boolean
  
  if baSpec.Count() > baInput.Count() then
    return false
  end if
  
  numBytesToMatch% = baSpec.Count()
  numBytesInInput% = baInput.Count()
  startByteInInput% = numBytesInInput% - numBytesToMatch%
  
  for i% = 0 to baSpec.Count() - 1
    if baInput[startByteInInput% + i%] <> baSpec[i%] then
      return false
    end if
  next
  
  return true
  
end function


Function StripLeadingSpaces(inputString$ as string) as string
  
  while true
    if left(inputString$, 1) <> " " then return inputString$
    inputString$ = right(inputString$, len(inputString$) - 1)
  end while
  
  return inputString$
  
end function


Function CopyDateTime(dateTimeIn as object) as object
  
  dateTimeOut = CreateObject("roDateTime")
  dateTimeOut.SetYear(dateTimeIn.GetYear())
  dateTimeOut.SetMonth(dateTimeIn.GetMonth())
  dateTimeOut.SetDay(dateTimeIn.GetDay())
  dateTimeOut.SetHour(dateTimeIn.GetHour())
  dateTimeOut.SetMinute(dateTimeIn.GetMinute())
  dateTimeOut.SetSecond(dateTimeIn.GetSecond())
  dateTimeOut.SetMillisecond(dateTimeIn.GetMillisecond())
  
  return dateTimeOut
  
end function


REM *******************************************************
REM *******************************************************
REM ***************                    ********************
REM *************** DIAGNOSTIC CODES   ********************
REM ***************                    ********************
REM *******************************************************
REM *******************************************************

Function newDiagnosticCodes() as object
  
  diagnosticCodes = { }
  
  diagnosticCodes.EVENT_STARTUP = "1000"
  diagnosticCodes.EVENT_SYNCSPEC_RECEIVED = "1001"
  diagnosticCodes.EVENT_DOWNLOAD_START = "1002"
  diagnosticCodes.EVENT_FILE_DOWNLOAD_START = "1003"
  diagnosticCodes.EVENT_FILE_DOWNLOAD_COMPLETE = "1004"
  diagnosticCodes.EVENT_DOWNLOAD_COMPLETE = "1005"
  diagnosticCodes.EVENT_READ_SYNCSPEC_FAILURE = "1006"
  diagnosticCodes.EVENT_RETRIEVE_SYNCSPEC_FAILURE = "1007"
  diagnosticCodes.EVENT_NO_SYNCSPEC_AVAILABLE = "1008"
  diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_IMMEDIATE_FAILURE = "1009"
  diagnosticCodes.EVENT_FILE_DOWNLOAD_FAILURE = "1010"
  diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_FAILURE = "1011"
  diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE = "1012"
  diagnosticCodes.EVENT_LOGFILE_UPLOAD_FAILURE = "1013"
  diagnosticCodes.EVENT_SYNC_ALREADY_ACTIVE = "1014"
  diagnosticCodes.EVENT_CHECK_CONTENT = "1015"
  diagnosticCodes.EVENT_FILE_DOWNLOAD_PROGRESS = "1016"
  diagnosticCodes.EVENT_FIRMWARE_DOWNLOAD = "1017"
  diagnosticCodes.EVENT_SCRIPT_DOWNLOAD = "1018"
  diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND = "1021"
  diagnosticCodes.EVENT_MEDIA_COUNTER_VARIABLE_NOT_FOUND = "1022"
  diagnosticCodes.EVENT_START_PRESENTATION = "1023"
  diagnosticCodes.EVENT_GPS_LOCATION = "1024"
  diagnosticCodes.EVENT_GPS_NOT_LOCKED = "1025"
  diagnosticCodes.EVENT_RETRIEVE_USER_VARIABLE_FEED = "1026"
  diagnosticCodes.EVENT_RETRIEVE_LIVE_TEXT_FEED = "1027"
  diagnosticCodes.EVENT_USER_VARIABLE_FEED_DOWNLOAD_FAILURE = "1028"
  diagnosticCodes.EVENT_LIVE_TEXT_FEED_DOWNLOAD_FAILURE = "1029"
  diagnosticCodes.EVENT_UNASSIGNED_LOCAL_PLAYLIST = "1030"
  diagnosticCodes.EVENT_UNASSIGNED_LOCAL_PLAYLIST_NO_NAVIGATION = "1031"
  diagnosticCodes.EVENT_REALIZE_FAILURE = "1032"
  diagnosticCodes.EVENT_LIVE_TEXT_PLUGIN_FAILURE = "1033"
  diagnosticCodes.EVENT_INVALID_DATE_TIME_SPEC = "1034"
  diagnosticCodes.EVENT_HTML5_LOAD_ERROR = "1035"
  diagnosticCodes.EVENT_USB_UPDATE_SECURITY_ERROR = "1036"
  diagnosticCodes.EVENT_SCRIPT_PLUGIN_FAILURE = "1041"
  diagnosticCodes.EVENT_DISK_ERROR = "1042"
  diagnosticCodes.EVENT_LIVE_MRSS_PLUGIN_FAILURE = "1043"
  diagnosticCodes.EVENT_EMPTY_MEDIA_PLAYLIST = "1044"
  diagnosticCodes.EVENT_CUSTOM_USER_AGENT_FAILURE = "1045"
  diagnosticCodes.EVENT_VARIABLE_REFERENCE_FAILURE = "1046"
  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_BEACON_START = "1300"
  diagnosticCodes.EVENT_BEACON_START_FAILED = "1301"
  diagnosticCodes.EVENT_BEACON_START_LIMIT_EXCEEDED = "1302"
  diagnosticCodes.EVENT_BTLE_START_FAILED = "1303"
  diagnosticCodes.EVENT_CONTROL_PORT_DISCONNECTED = "1304"
  diagnosticCodes.EVENT_BMAP_DISCONNECTED = "1305"
  
  return diagnosticCodes
  
end function

'endregion

'region GPS Functions
REM ==================================================
REM           GPS Functions
REM ==================================================
' Parse the NMEA GPRMC format and return the data in an object - http://www.gpsinformation.org/dale/nmea.htm
' The returned object contains the following fields
' .valid - boolean - is the sentence is correctly formed, has the correct checksum and has the correct GPRMC header
' .fixTime - contains the string from the sentence that is the time the sample was taken - no processing is done on this
' .fixActive - boolean - does the latitude and longitude contain real data
' .latitude - float - signed degrees of the latitude
' .longitude - float - signed degrees of the longitude
Sub ParseGPSdataGPRMCformat(NMEAsentence as string) as object
  gpsData = { }
  
  starLoc = instr(1, NMEAsentence, "*")
  if starLoc = 0 then
    gpsData.valid = false
  else if starLoc = len(NMEAsentence) - 2 then
    CalcChecksum = CalcChecksum (mid(NMEAsentence, 2, len(NMEAsentence) - 4))
    ba = CreateObject("roByteArray")
    ba.fromhexstring(mid(NMEAsentence, len(NMEAsentence) - 1, 2))
    CalcChecksum = ba[0]
    if (CalcChecksum <> CalcChecksum) then
      gpsData.valid = false
    else
      ' Strip off the beginning $ sign and the * + checksum
      strippedSentence = mid(NMEAsentence, 2, len(NMEAsentence) - 4)
      
      ' Get the identifier
      field = getNextGPSfield(strippedSentence, 1)
      gpsData.type = field.fieldString
      
      ' Make sure this is the right data format
      if (gpsData.type <> "GPRMC") then
        gpsData.valid = false
      else
        gpsData.valid = true
        
        ' Get the fix time
        field = getNextGPSfield(strippedSentence, field.nextFieldStart)
        gpsData.fixTime = field.fieldString
        
        ' Get the status of the fix: A=Active, V=Void time, convert to fixActive = true for A, false for V
        field = getNextGPSfield(strippedSentence, field.nextFieldStart)
        if (field.fieldString <> "A") then
          gpsData.fixActive = false
          gpsData.latitude = 0
          gpsData.longitude = 0
        else
          gpsData.fixActive = true
          
          ' Get the Latitude
          field = getNextGPSfield(strippedSentence, field.nextFieldStart)
          latDegrees = val(left(field.fieldString, 2))
          latMinutes = val(mid(field.fieldString, 3))
          latDegrees = latDegrees + (latMinutes / 60)
          
          ' Get the Latitude Direction
          field = getNextGPSfield(strippedSentence, field.nextFieldStart)
          
          ' Adjust the sign of the angle based on the direction
          gpsData.latitude = ConvertNSEWtoQuadrant(field.fieldString, latDegrees)
          
          ' Get the Longitude
          field = getNextGPSfield(strippedSentence, field.nextFieldStart)
          longDegrees = val(left(field.fieldString, 3))
          longMinutes = val(mid(field.fieldString, 4))
          longDegrees = longDegrees + (longMinutes / 60)
          
          ' Get the Longitude Direction
          field = getNextGPSfield(strippedSentence, field.nextFieldStart)
          
          ' Adjust the sign of the angle based on the direction
          gpsData.longitude = ConvertNSEWtoQuadrant(field.fieldString, longDegrees)
        end if
      end if
    end if
  end if
  
  return gpsData
end sub

' Parse and return the next NMEA field from the sentence
' returns an object with two members:
' .fieldString - contains the contents of the field, if nothing is in the field - returns ""
' .nextFieldStart - indicates the location in the string where the next field should start
Sub getNextGPSfield (NMEAsentence as string, startingIndex as integer) as object
  gpsField = { }
  ' Look for the next field seperator as a comma (this is the case except for the checksum which is a *)
  fieldEndLoc = instr(startingIndex, NMEAsentence, ",")
  if fieldEndLoc <> 0 then
    if fieldEndLoc > startingIndex then
      gpsField.fieldString = mid(NMEAsentence, startingIndex, fieldEndLoc - startingIndex)
    else
      gpsField.fieldString = ""
    end if
    gpsField.nextFieldStart = fieldEndLoc + 1
  else
    stringLen = len(NMEAsentence)
    if (stringLen >= startingIndex) then
      gpsField.fieldString = mid(NMEAsentence, startingIndex, stringLen - startingIndex + 1)
    else
      gpsField.fieldString = ""
    end if
    gpsField.nextFieldStart = stringLen + 1
  end if
  
  return (gpsField)
end sub


' Calculate the great circle distance of two gps points - points must be in radians
Sub CalcGPSDistance(lat1 as float, lon1 as float, lat2 as float, lon2 as float) as float
  
  radiusOfEarthInFeet# = 3963.1 * 5280.0
  
  ' Convert coodinate 1 to Cartesian coordinates
  x1# = radiusOfEarthInFeet# * cos(lon1) * sin(lat1)
  y1# = radiusOfEarthInFeet# * sin(lon1) * sin(lat1)
  z1# = radiusOfEarthInFeet# * sin(lat1)
  
  ' Convert coodinate 2 to Cartesian coordinates
  x2# = radiusOfEarthInFeet# * cos(lon2) * sin(lat2)
  y2# = radiusOfEarthInFeet# * sin(lon2) * sin(lat2)
  z2# = radiusOfEarthInFeet# * sin(lat2)
  
  ' Calc the distance based on Euclidean distance
  distance = sqr((x1# - x2#) * (x1# - x2#) + (y1# - y2#) * (y1# - y2#) + (z1# - z2#) * (z1# - z2#))
  
  return (distance)
end sub


' Calculate the checksum based on the NMEA stardard - http://www.gpsinformation.org/dale/nmea.htm
' the checksum is an XOR of all characters between the $ and * in the sentence
Sub CalcChecksum (theString as string) as integer
  checksum = 0
  
  theStringLen = len (theString)
  if (theStringLen >= 2) then
    a = asc(mid(theString, 1, 1))
    b = asc(mid(thestring, 2, 1))
    ' XOR the two first two characters in the string
    checksum = &HFF and ((a or b) and (not(a and b)))
  else if (theStringLen = 1) then
    ' If only one character is in the string, it is the checksum
    checksum = asc(mid(theString, 1, 1))
  end if
  if (theStringLen >= 3) then
    for i = 3 to theStringLen
      a = checksum
      b = asc(mid(thestring, i, 1))
      ' XOR the current checksum with the next character
      checksum = &HFF and ((a or b) and (not(a and b)))
    next
  end if
  
  return (checksum)
end sub


Sub ConvertDecimalDegtoRad(deg as float) as float
  pi = 3.14159265358979
  radians = deg * (pi / 180)
  
  return (radians)
end sub

Sub ConvertNSEWtoQuadrant(direction as string, angle as float) as float
  if (direction = "W") or (direction = "w") or (direction = "S") or (direction = "s") then
    angle = angle * -1
  end if
  
  return (angle)
end sub

'endregion

'region SIGNCHANNEL / MEDIARSS helpers
REM *******************************************************
REM *******************************************************
REM ***************                         ***************
REM *************** SIGNCHANNEL / MEDIARSS  ***************
REM ***************                         ***************
REM *******************************************************
REM *******************************************************

Function isImage(item as object) as boolean
  REM Default is the item is an image
  rv = TRUE
  
  if item.type = "video/mpeg" or item.type = "video/mp4" or item.type = "video/quicktime" or item.type = "video/x-matroska" or item.medium = "video" or item.type = "audio/mpeg" or item.medium = "audio" or item.type = "text/html" or item.type = "application/widget" or item.medium = "document" then
    rv = false
  end if
  
  return rv
end function


Function isAudio(item as object) as boolean
  REM Default is the item is an image
  rv = false
  
  if item.type = "audio/mpeg" or item.medium = "audio" then
    rv = true
  end if
  
  return rv
end function

Function isHtml(item as object) as boolean
  REM Default is the item is an image
  rv = false
  
  if item.type = "text/html" or item.type = "application/widget" or item.medium = "document" then
    rv = true
  end if
  
  return rv
end function

REM ================================================================
REM          helper_GetDuration
REM ================================================================
REM
REM Get duration attribute of a media:content sub element
REM of RSS Item element.  Duration is number of seconds for
REM image to be displayed on screen.
REM
REM If no duration found sets default of 15.
REM If duration < 5 seconds sets minimum of 5
REM

Function helper_GetDuration(contentElement as object) as integer
  
  duration = contentElement.GetAttributes()["duration"]
  
  if duration = invalid then
    return 15
  end if
  
  duration = Val(duration)
  
  '   if duration < 5 then
  '      duration = 5
  '   end if
  
  return duration
  
end function


Function helper_GetFileSize(contentElement as object) as integer
  
  size = contentElement.GetAttributes()["fileSize"]
  
  if size = invalid then
    return 0
  end if
  
  fileSize = Val(size)
  
  return fileSize
  
end function


Function helper_GetProbeData(contentElement as object) as string
  
  probe = contentElement.GetAttributes()["probe"]
  
  if probe = invalid then
    return ""
  end if
  
  return probe
  
end function

'endregion


'region Miscellaneous functions


Function FileExists(filePath$ as string) as boolean
  
  file = CreateObject("roReadFile", filePath$)
  if not type(file) = "roReadFile"
    return false
  end if
  
  file = invalid
  return true
  
end function

'endregion


'region MRSSDataFeed methods

' check for existence of the feed file associated with this feed
' if it exists, setup asset collection / assetPoolFiles objects for the feed (independent of whether or not the assets are actually on the card)
Sub ReadMRSSContent()
  
  feedFileName$ = "feed_cache/" + m.id$ + ".xml"
  
  m.isMRSSFeed = m.FeedIsMRSS(feedFileName$)
  if not m.isMRSSFeed and m.parser$ = "" then
    return
  end if
  
  m.bsp.diagnostics.PrintDebug("Read existing content for feed " + m.id$ + ".")
  
  file = CreateObject("roReadFile", feedFileName$)
  if type(file) <> "roReadFile" then
    return
  end if
  
  file = invalid
  
  ' parse the feed, building an asset collection and a list of file items
  m.assetCollection = CreateObject("roAssetCollection")
  
  m.ParseMRSSFeed(feedFileName$)
  
  for each item in m.feed.items
    ' Do not download content of type 'text/html' - this is accessed directly
    if item.type = invalid or item.type <> "text/html" then
      asset = { }
      asset.link = item.url
      ' SignChannel sizes appear to be inaccurate
      '			if item.size > 0 then
      '				asset.size = item.size
      '		endif
      asset.name = item.url
      if IsNonEmptyString(item.guid) then
        asset.change_hint = item.guid
      else if IsString(item.url) then
        asset.change_hint = item.url
      end if
      m.assetCollection.AddAsset(asset)
    end if
  next
  
  if not m.bsp.feedPool.ProtectAssets("display-" + m.id$, m.assetCollection) then
    m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, m.bsp.feedPool.GetFailureReason())
    m.bsp.logging.FlushLogFile()
    m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + m.bsp.feedPool.GetFailureReason())
    stop
  end if
  
  m.assetPoolFiles = CreateObject("roAssetPoolFiles", m.bsp.feedPool, m.assetCollection)
  
end sub


Function FeedIsMRSS(fileName$ as string)
  
  xml = ReadAsciiFile(fileName$)
  if len(xml) = 0 then
    return false
  end if
  
  feedXML = CreateObject("roXMLElement")
  if not feedXML.Parse(xml) then
    return false
  end if
  
  if feedXML.HasAttribute("xmlns:media") then
    attrs = feedXML.GetAttributes()
    if attrs["xmlns:media"] = "http://search.yahoo.com/mrss/" then
      return true
    end if
  end if
  
  return false
  
end function


Sub DownloadMRSSContent()

  m.bsp.diagnostics.PrintDebug("DownloadMRSSContent")
  
  if type(m.assetFetcher) = "roAssetFetcher" then
    return
  end if
  
  fileNameOnCard$ = "feed_cache/" + m.id$ + ".xml"
  
  ' write the mrss feed to the card
  CopyFile(m.rssFileName$, fileNameOnCard$)
  
  m.bsp.diagnostics.PrintDebug("Download new content for feed " + m.id$ + ".")
  m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_START_MRSS_FEED_CONTENT_DOWNLOAD, m.id$)
  
  ' parse the feed, building an asset collection and a list of file items
  m.assetCollection = CreateObject("roAssetCollection")
  
  m.ParseMRSSFeed(m.rssFileName$)
  
  m.feedContentFilesToDownload = { }
  
  for each item in m.feed.items
    ' Do not download content of type 'text/html' - this is accessed directly
    if item.type = invalid or item.type <> "text/html" then
      asset = { }
      asset.link = item.url
      '			if item.size > 0 then
      '				asset.size = item.size
      '			endif
      asset.name = item.url
      
      if IsNonEmptyString(item.guid) then
        asset.change_hint = item.guid
      else if IsString(item.url) then
        asset.change_hint = item.url
      end if
      
      m.assetCollection.AddAsset(asset)
    end if
    
    ' track feed content downloads
    fileToDownload = { }
    fileToDownload.name = item.title
    fileToDownload.size = item.size
    fileToDownload.hash = item.guid
    fileToDownload.currentFilePercentage$ = ""
    fileToDownload.status$ = ""
    
    if type(asset) = "roAssociativeArray" and type(asset.link) = "roString" then
      if type(m.assetPoolFiles) = "roAssetPoolFiles" then
        filePath = m.assetPoolFiles.GetPoolFilePath(asset.link)
        if filePath <> "" then
          fileToDownload.currentFilePercentage$ = "100"
          fileToDownload.status$ = "ok"
        end if
      end if
    end if
    
    if IsString(fileToDownload.hash) then
      m.feedContentFilesToDownload.AddReplace(fileToDownload.hash, fileToDownload)
    end if
    
  next
  
  if type(m.bsp.networkingHSM) = "roAssociativeArray" then
    m.bsp.networkingHSM.UploadDeviceDownloadProgressFileList()
    m.bsp.networkingHSM.FileListPendingUpload = false
  end if
  
  if not m.bsp.feedPool.ProtectAssets("download-" + m.id$, m.assetCollection) then
    m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_ASSETPOOL_PROTECT_FAILURE, m.bsp.feedPool.GetFailureReason())
    m.bsp.logging.FlushLogFile()
    m.bsp.diagnostics.PrintDebug("### ProtectFiles failed: " + m.bsp.feedPool.GetFailureReason())
    stop
  end if
  
  m.bsp.feedPool.ReserveMegabytes(50)
  
  m.assetFetcher = CreateObject("roAssetFetcher", m.bsp.feedPool)
  m.assetFetcher.SetPort(m.bsp.msgPort)
  
  currentSync = CreateObject("roSyncSpec")
  if type(currentSync) = "roSyncSpec" and currentSync.ReadFromFile("current-sync.json") then
    serverHeaders = CleanServerHeaders(currentSync.GetMetadata("server"))
    m.assetFetcher.SetHeaders(serverHeaders)
  end if
  
  m.assetFetcher.AddHeader("User-Agent", m.bsp.userAgent$)
  m.assetFetcher.AddHeader("DeviceID", m.bsp.sysInfo.deviceUniqueID$)
  m.assetFetcher.AddHeader("DeviceModel", m.bsp.sysInfo.deviceModel$)
  m.assetFetcher.AddHeader("DeviceFamily", m.bsp.sysInfo.deviceFamily$)
  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.duration) 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
        ' TEDTODO
        stop
      endif

      networkInterfacePriorityList = globalAA.networkInterfacePriorityLists.Lookup("mediaFeedsDownloadEnabled")
      m.bsp.mrssDataFeedsBindingPriorityIndex = m.bsp.mrssDataFeedsBindingPriorityIndex + 1
      if m.bsp.mrssDataFeedsBindingPriorityIndex >= networkInterfacePriorityList.count() then
        ' all network interfaces failed
        m.bsp.diagnostics.PrintDebug("### mrss data feed content download failed on all network interfaces")
        m.bsp.mrssDataFeedsBindingPriorityIndex = 0
      else
        ' try next network interface
        m.bsp.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
      if type(m.isTwitterFeed) = "roBoolean" and m.isTwitterFeed then
      text$ = jsonItem.full_text
    else
      text$ = jsonItem.text
    endif
    m.articles.Push(text$)
    m.articleTitles.Push(text$)
    m.articlesByTitle.AddReplace(text$, text$)

    numItemsAdded% = numItemsAdded% + 1

    if m.restrictNumberOfItems and (numItemsAdded% >= m.numberOfItemsToDisplay%) then
      exit for
    endif

  next

  if numItemsAdded% = 0 then
    success = false
  endif

  return success

End Function


Function ParseCustomContentFormat( filePath$ as String)

	success = true

	m.articles = CreateObject("roArray", 1, true)
	m.articleTitles = CreateObject("roArray", 1, true)
	m.articlesByTitle = CreateObject("roAssociativeArray")
	m.articleHashTypes = CreateObject("roArray", 1, true)
	m.articleHashes = CreateObject("roArray", 1, true)

	if m.parser$ <> "" then
  
    m.ParseRSSWithParserPlugin( filePath$)
	
  else
	
  	if type(m.isJSON) = "roBoolean" and m.isJSON then
  
      success = m.ParseJSONRSS(filePath$)
	
  	else

      xml = ReadAsciiFile( filePath$ )
      
      if len(xml) = 0 then
        return false
      endif

      mrssFeedXML = CreateObject("roXMLElement")
      if not mrssFeedXML.Parse(xml) stop

      items = []

      for each elt in mrssFeedXML.GetBody().Peek().GetBody()
        name = elt.GetName()
        if name = "item" then
          item = newCustomContentMRSSItem(elt)
          items.Push( item )
        end if
      next

      ' convert to format required for content feed
      m.articles = CreateObject("roArray", 1, true)
      m.articleTitles = CreateObject("roArray", 1, true)
      m.articlesByTitle = CreateObject("roAssociativeArray")
      m.articleMediaTypes = CreateObject("roArray", 1, true)

      for each item in items
        m.articles.push(item.url)
        m.articleTitles.push(item.title)
        m.articlesByTitle.AddReplace(item.title, item.url)
        m.articleMediaTypes.push(item.medium)
      next

    endif

  endif

	return success

End Function


Function IsFeatureSupported(featureName$ as string, fwVersion$ as string, featureMinRevs as object) as boolean
  
  featureExists = featureMinRevs.DoesExist(featureName$)
  if featureExists then
    featureMinFWRev = featureMinRevs[featureName$]
    featureMinFWRevVSFWVersion% = CompareFirmwareVersions(featureMinFWRev, fwVersion$)
    if featureMinFWRevVSFWVersion% <= 0 then
      return true
    end if
  end if
  
  return false
  
end function


Function CompareFirmwareVersions(a$ as string, b$ as string) as integer
  
  start_a% = 0
  start_b% = 0
  
  while true
    
    if start_a% >= len(a$) then
      if start_b% >= len(b$) then
        return 0
      else
        return -1
      end if
    else if start_b% >= len(b$) then
      return 1
    end if
    
    aChar$ = mid(a$, start_a% + 1, 1)
    a_digit = IsDigit(aChar$)
    
    bChar$ = mid(b$, start_b% + 1, 1)
    b_digit = IsDigit(bChar$)
    
    if a_digit and b_digit then
      
      ' Now we need to find the end of each of the sequences of digits.
      aa = { }
      aa.index = start_a%
      a_number% = ReadDigits(a$, aa)
      start_a% = aa.index
      
      bb = { }
      bb.index = start_b%
      b_number% = ReadDigits(b$, bb)
      start_b% = bb.index
      
      if a_number% < b_number% then
        return -1
      else if a_number% > b_number% then
        return 1
      end if
    else if a_digit then
      ' The first string has a digit but the second one has a
      ' non-digit so it must be greater.
      return 1
    else if b_digit then
      return -1
    else
      aChar$ = mid(a$, start_a% + 1, 1)
      bChar$ = mid(b$, start_b% + 1, 1)
      
      if asc(aChar$) < asc(bChar$) then
        return -1
      else if asc(aChar$) > asc(bChar$)
        return 1
      end if
      
      ' Otherwise we've dealt with this character
      start_a% = start_a% + 1
      start_b% = start_b% + 1
    end if
  end while
  
end function


Function IsDigit(a$ as string) as boolean
  
  if asc(a$) >= 48 and asc(a$) <= 57 then
    return true
  end if
  
  return false
  
end function


Function ReadDigits(s$ as string, aa as object) as integer
  
  value% = 0
  
  index% = aa.index
  
  sChar$ = mid(s$, index% + 1, 1)
  
  while index% < len(s$) and IsDigit(sChar$)
    
    new_value% = value% * 10 + asc(sChar$) - asc("0")
    index% = index% + 1
    
    value% = new_value%
    
    if len(s$) > index% then
      sChar$ = mid(s$, index% + 1, 1)
    end if
    
  end while
  
  aa.index = index%
  return value%
  
end function

'endregion

'region Bluetooth/Beacons

Function newBtManager() as object
  ' m is BSP
  btm = { bsp: m, btActive: false, beaconsSupported: false }
  btm.newBeacon = newBeacon
  btm.ParsePresentationBeacons = ParsePresentationBeacons
  btm.ResetPresentationBeacons = ResetPresentationBeacons
  btm.UpdatePersistentBeacons = UpdatePersistentBeacons
  btm.AddPersistentBeacon = AddPersistentBeacon
  btm.StartBeacon = StartBeacon
  btm.StopBeacon = StopBeacon
  btm.SetBtAdvertising = SetBtAdvertising
  btm.StartBtleClient = StartBtleClient
  btm.StopBtleClient = StopBtleClient
  btm.SetBtleStatus = SetBtleStatus
  btm.HandleEvent = btManager_HandleEvent
  
  btm.persistentBeacons = { }
  btm.presentationBeacons = { }
  
  btm.btleSupported = false
  btm.btleClientManager = invalid
  btm.btleClientServiceData = invalid
  btm.btleStatus% = 0
  
  btm.btManager = CreateObject("roBtManager")
  if type(btm.btManager) = "roBtManager" then
    btm.btManager.SetPort(m.msgPort)
    btm.UpdatePersistentBeacons()
  endif
  
  if m.btleSupported then
    btm.btleSupported = true
  end if
  return btm
end function

Function btManager_HandleEvent(event as object)
  btEventType = event.GetEvent()
  if btEventType = "add-adapter" then
    m.bsp.diagnostics.PrintDebug("-- Bluetooth adapter added")
    m.btActive = true
    m.SetBtAdvertising()
  else if btEventType = "remove-adapter" then
    m.bsp.diagnostics.PrintDebug("-- Bluetooth adapter removed")
    m.btActive = false
  end if
end function

Function newBeacon(beaconXml as object, persistent as boolean) as object
  beacon = invalid
  
  beaconName$ = beaconXml.name.GetText()
  beaconType$ = beaconXml.type.GetText()
  beaconData = invalid
  
  if beaconType$ = "IBeacon" then
    beaconId = beaconXml.beaconId.GetText()
    ' BeaconId must be specified
    if IsString(beaconId) and Len(beaconId) > 0 then
      beaconId = LCase(beaconId)
      major% = Val(beaconXml.data1.GetText())
      minor% = Val(beaconXml.data2.GetText())
      txp% = Val(beaconXml.txlevel.GetText())
      beaconData = { mode: "beacon", beacon_uuid: beaconId, beacon_major: major%, beacon_minor: minor%, beacon_level: txp%, persistent: persistent }
    end if
  else if beaconType$ = "EddystoneUrl" then
    url = beaconXml.beaconId.GetText()
    if IsString(url) then
      txp% = Val(beaconXml.txlevel.GetText())
      beaconData = { mode: "eddystone-url", url: url, tx_power: txp%, persistent: persistent }
    end if
  else if beaconType$ = "EddystoneUid" then
    txp% = Val(beaconXml.txlevel.GetText())
    beaconData = GetEddystoneUidBeaconData(beaconXml.beaconId.GetText(), beaconXml.data1.GetText(), txp%, persistent)
  end if
  
  if beaconData <> invalid then
    beacon = { name: beaconName$, type: beaconType$, data: beaconData, activate: true, isActive: false }
    if not persistent and beacon.autostart <> invalid then
      autostart = beaconXml.autostart.GetText()
      if not IsString(autostart) or LCase(autostart.Left(1)) <> "t" then
        beacon.activate = false
      end if
    end if
  end if
  
  return beacon
end function

Function ParsePresentationBeacons(beaconsXml as object) as boolean
  beacons = { }
  ' BACONTODO'
  
  ''	if type(beaconsXml) = "roXMLList" and beaconsXml.Count() >= 1 then
  ''		for each beaconXML in beaconsXml
  ''			beacon = m.newBeacon(beaconXML, false)
  ''			if beacon <> invalid then
  ''				beacons.AddReplace(beacon.name, beacon)
  ''			endif
  ''		next
  ''	endif
  m.presentationBeacons = beacons
  return not m.presentationBeacons.IsEmpty()
end function

Sub ResetPresentationBeacons()
  if not m.presentationBeacons.IsEmpty() then
    m.presentationBeacons = { }
    m.SetBtAdvertising()
  end if
end sub

Function GetEddystoneUidBeaconData(namespace as string, instance as string, txLevel% as integer, persistent as boolean) as object
  beaconData = invalid
  
  if Len(namespace) > 0 and Len(instance) > 0 then
    if LCase(namespace.Left(2)) = "0x" then
      namespace = namespace.mid(2)
    end if
    ns = CreateObject("roByteArray")
    ns.FromHexString(namespace)
    ' namespace string must have exactly ten bytes
    pad = 10 - ns.Count()
    for i = 1 to pad
      ns.Unshift(0)
    next
    
    if LCase(instance.Left(2)) = "0x" then
      instance = instance.mid(2)
    end if
    in = CreateObject("roByteArray")
    in.FromHexString(instance)
    ' instance string must have exactly six bytes
    pad = 6 - in.Count()
    for i = 1 to pad
      in.Unshift(0)
    next
    
    beaconData = { mode: "eddystone-uid", namespace: ns, instance: in, tx_power: txLevel%, persistent: persistent }
  end if
  return beaconData
end function

Sub UpdatePersistentBeacons()
  wasEmpty = m.persistentBeacons.IsEmpty()
  m.persistentBeacons = { }
  m.AddPersistentBeacon(GetGlobalAA().registrySettings.beacon1)
  m.AddPersistentBeacon(GetGlobalAA().registrySettings.beacon2)
  if not (m.persistentBeacons.IsEmpty() and wasEmpty) then
    ' Restart beacons to handle any persistent beacon changes
    m.SetBtAdvertising()
  end if
end sub

Function AddPersistentBeacon(beaconJson as string) as boolean
  success = false
  if IsString(beaconJson) and Len(beaconJson) > 0 then
    beaconInputData = ParseJson(beaconJson)
    if type(beaconInputData) = "roAssociativeArray" then
      beaconName$ = beaconInputData.Name
      beaconType% = beaconInputData.Type
      beaconType$ = ""
      beaconData = invalid
      
      if beaconType% = 0 then
        beaconId = LCase(beaconInputData.BeaconId)
        if Len(beaconId) > 0 then
          beaconType$ = "IBeacon"
          major% = Val(beaconInputData.Data1)
          minor% = Val(beaconInputData.Data2)
          txp% = beaconInputData.TxLevel
          beaconData = { mode: "beacon", beacon_uuid: beaconId, beacon_major: major%, beacon_minor: minor%, beacon_level: txp%, persistent: true }
        end if
      else if beaconType% = 1 then
        url = beaconInputData.BeaconId
        if Len(url) > 0 then
          beaconType$ = "EddystoneUrl"
          txp% = beaconInputData.TxLevel
          beaconData = { mode: "eddystone-url", url: url, tx_power: txp%, persistent: true }
        end if
      else if beaconType% = 2 then
        beaconType$ = "EddystoneUid"
        beaconData = GetEddystoneUidBeaconData(beaconInputData.BeaconId, beaconInputData.Data1, beaconInputData.TxLevel, true)
      end if
      
      if beaconData <> invalid then
        beacon = { name: beaconName$, type: beaconType$, data: beaconData, activate: true, isActive: false }
        m.persistentBeacons.AddReplace(beacon.name, beacon)
      end if
    end if
  end if
  return success
end function

Function StartBeacon(name as string) as boolean
  success = false
  beacon = m.presentationBeacons[name]
  if beacon <> invalid then
    beacon.activate = true
    success = m.SetBtAdvertising()
  end if
  return success
end function

Function StopBeacon(name as string) as boolean
  success = false
  beacon = m.presentationBeacons[name]
  if beacon <> invalid then
    beacon.activate = false
    success = m.SetBtAdvertising()
  end if
  return success
end function

Function StartBtleClient(clientParams as object, appId as string, txPower as integer) as boolean
  success = false
  if m.btleSupported then
    if m.btManager.GetAdapterList().Count() > 0 then
      m.bsp.diagnostics.PrintDebug("---- Starting BTLE Client manager")
      m.btleClientManager = CreateObject("roBtClientManager")
      m.btleClientManager.SetPort(m.bsp.msgPort)
      success = m.btleClientManager.Start(clientParams)
      if not success then
        m.bsp.diagnostics.PrintDebug("-- Start BTLE Client manager failed: " + m.btleClientManager.GetFailureReason())
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BTLE_START_FAILED, "Start BTLE Client manager failed: " + m.btleClientManager.GetFailureReason())
      else
        ' remember service ID
        m.btleServiceId = clientParams.service_uuid
        ' set up service data array
        m.btleClientServiceData = CreateObject("roByteArray")
        m.btleClientServiceData.FromHexString(appId)
        if m.btleClientServiceData.Count() < 4 then
          while m.btleClientServiceData.Count() < 4
            m.btleClientServiceData.Unshift(0)
          end while
        else if m.btleClientServiceData.Count() > 4 then
          while m.btleClientServiceData.Count() > 4
            m.btleClientServiceData.Pop()
          end while
        end if
        m.btleClientServiceData.push(txPower)
        m.btleClientServiceData.push(0)
        ' restart advertising to set connectable flag for all beacons
        success = m.SetBtAdvertising()
      end if
    else
      m.bsp.diagnostics.PrintDebug("-- Start BTLE Client manager failed - there is no bluetooth adapter")
      m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BTLE_START_FAILED, "Start BTLE Client manager failed - there is no bluetooth adapter")
    end if
  end if
  return success
end function

Function StopBtleClient(clientParams as object) as boolean
  success = false
  if m.btleClientManager then
    m.bsp.diagnostics.PrintDebug("---- Stopping BTLE Client manager")
    m.btleClientManager.Stop()
    m.btleClientManager = invalid
    m.btleClientServiceData = invalid
    m.btleServiceId = invalid
    ' restart advertising to reset connectable flag for all beacons
    success = m.SetBtAdvertising()
  end if
  return success
end function

Sub SetBtleStatus(status% as integer)
  newStatus% = status% and 255
  if newStatus% <> m.btleStatus% then
    m.btleStatus% = newStatus%
    m.SetBtAdvertising()
  end if
end sub

Function SetBtAdvertising()
  success = false
  if m.beaconsSupported then
    ' We must have active bluetooth hardware
    if m.btManager.GetAdapterList().Count() > 0 then
      ' If a btClientManager is active, we need to set 'connectable' flag in all beacons
      isConnectable = false
      sd = invalid
      if m.btleClientManager <> invalid and m.btleServiceId <> invalid then
        isConnectable = true
        sd = { }
        sd.uuid = m.BtleServiceId
        sd.data = m.btleClientServiceData
        sd.data.SetEntry(5, m.btleStatus%)
      end if
      ' Get array of beacon data that should be active now
      beaconDataArray = []
      for each beaconName in m.persistentBeacons
        beacon = m.persistentBeacons[beaconName]
        data = beacon.data
        data.connectable = isConnectable
        if sd <> invalid then
          sdlist = [sd]
          data.service_data = sdlist
        end if
        beaconDataArray.Push(data)
      next
      for each beaconName in m.presentationBeacons
        beacon = m.presentationBeacons[beaconName]
        if beacon.activate then
          if beaconDataArray.Count() < 5 then
            data = beacon.data
            data.connectable = isConnectable
            if sd <> invalid then
              sdlist = [sd]
              data.service_data = sdlist
            end if
            beaconDataArray.Push(data)
          else
            ' There is a limit of 5 beacons
            msg$ = "-- SetBtAdvertising - attempted to set more than 5 beacons - some beacons will not be started"
            m.bsp.diagnostics.PrintDebug(msg$)
            m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BEACON_START_LIMIT_EXCEEDED, msg$)
            exit for
          end if
        end if
      next
      ' If we have the BTLE client manager running, and there are no beacons defined, just advertise the client service
      if beaconDataArray.Count() = 0 and sd <> invalid then
        sdlist = [sd]
        data = { mode: "custom", connectable: true, service_data: sdlist }
        beaconDataArray.Push(data)
      end if
      for each beaconName in m.persistentBeacons
        m.persistentBeacons[beaconName].isActive = false
      next
      for each beaconName in m.presentationBeacons
        m.presentationBeacons[beaconName].isActive = false
      next
      if beaconDataArray.Count() > 0 then
        beaconMsgSpec$ = stri(beaconDataArray.Count()) + " beacons"
        if isConnectable then
          beaconMsgSpec$ = beaconMsgSpec$ + " (connectable, status =" + stri(m.btleStatus%) + ")"
        end if
        success = m.btManager.StartAdvertising(beaconDataArray)
        if success then
          m.bsp.diagnostics.PrintDebug("-- Set Bluetooth Advertising for" + beaconMsgSpec$)
          m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BEACON_START, "Set Bluetooth Advertising for" + beaconMsgSpec$)
          for each beaconName in m.persistentBeacons
            m.persistentBeacons[beaconName].isActive = true
          next
          for each beaconName in m.presentationBeacons
            beacon = m.presentationBeacons[beaconName]
            if beacon.activate then
              ' TODO - we shouldn't set active flag for beacons over the limit of 5
              beacon.isActive = true
            end if
          next
        else
          beaconMsgSpec$ = beaconMsgSpec$ + ", reason: " + m.btManager.GetFailureReason()
          m.bsp.diagnostics.PrintDebug("-- Set Bluetooth Advertising failed for" + beaconMsgSpec$)
          m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BEACON_START_FAILED, "Set Bluetooth Advertising failed for" + beaconMsgSpec$)
        end if
      else
        success = m.btManager.StopAdvertising()
        m.bsp.diagnostics.PrintDebug("-- Stop all Bluetooth Advertising")
        m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BEACON_START, "Stop all Bluetooth Advertising")
      end if
    else if not (m.persistentBeacons.IsEmpty() and m.presentationBeacons.IsEmpty()) then
      m.bsp.diagnostics.PrintDebug("-- Set Bluetooth Advertising failed - there is no bluetooth adapter")
      m.bsp.logging.WriteDiagnosticLogEntry(m.bsp.diagnosticCodes.EVENT_BEACON_START_FAILED, "Set Bluetooth Advertising failed - there is no bluetooth adapter")
    end if
  end if
  return success
end function


Function 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 ParseAutoplay(BrightAuthor as object, bsp as object)
  
  autoplay = { }
  meta = { }
  autoplay.meta = meta
  
  jsonParseAutoplay(BrightAuthor, meta, bsp)
  
  return autoplay
  
end function


Function ParseZones(bsp as object, BrightAuthor as object, Sign as object)
  
  return jsonParseZones(BrightAuthor, Sign)
  
end function


Function GetColor(colorAttrs as object) as integer
  
  alpha = colorAttrs["a"]
  red% = colorAttrs["r"]
  green% = colorAttrs["g"]
  blue% = colorAttrs["b"]
  
  color_spec% = (alpha * 256 * 256 * 256) + (red% * 256 * 256) + (green% * 256) + blue%
  return color_spec%
  
end function


' region Schedule parsing'
Sub GetStartingPresentation(schedule as object)
  
  ' get starting presentation
  schedule.GetActiveScheduledEvent = GetActiveScheduledEvent
  schedule.GetNextScheduledEventTime = GetNextScheduledEventTime
  
  schedule.activeScheduledEvent = schedule.GetActiveScheduledEvent(schedule.scheduledInterruptions)
  if type(schedule.activeScheduledEvent) <> "roAssociativeArray" then
    schedule.activeScheduledEvent = schedule.GetActiveScheduledEvent(schedule.scheduledEvents)
  end if
  
  if type(schedule.activeScheduledEvent) <> "roAssociativeArray" then
    schedule.nextScheduledEventTime = schedule.GetNextScheduledEventTime(schedule.allScheduledEvents)
  else
    ' determine when this scheduled event will end
    schedule.activeScheduledEventEndDateTime = invalid
    if schedule.activeScheduledEvent.interruption then
      schedule.activeScheduledEventEndDateTime = CopyDateTime(schedule.activeScheduledEvent.dateTime)
      schedule.activeScheduledEventEndDateTime.AddSeconds(schedule.activeScheduledEvent.duration% * 60)
    else
      endDateTime = invalid
      
      if not schedule.activeScheduledEvent.allDayEveryDay then
        endDateTime = CopyDateTime(schedule.activeScheduledEvent.dateTime)
        endDateTime.AddSeconds(schedule.activeScheduledEvent.duration% * 60)
      end if
      
      nextInterruptionStartTime = schedule.GetNextScheduledEventTime(schedule.scheduledInterruptions)
      
      if endDateTime = invalid then
        if nextInterruptionStartTime <> invalid then
          schedule.activeScheduledEventEndDateTime = nextInterruptionStartTime
        end if
      else
        if nextInterruptionStartTime = invalid then
          schedule.activeScheduledEventEndDateTime = endDateTime
        else
          if endDateTime.GetString() < nextInterruptionStartTime.GetString() then
            schedule.activeScheduledEventEndDateTime = endDateTime
          else
            schedule.activeScheduledEventEndDateTime = nextInterruptionStartTime
          end if
        end if
      end if
    end if
  end if
  
end sub


Function jsonAutoschedule(jsonFileName$ as string)
  
  autoScheduleJSON = ParseJSON(ReadAsciiFile(jsonFileName$))
  
  schedule = jsonNewSchedule(autoScheduleJSON)
  
  if type(schedule.activeScheduledEvent) = "roAssociativeArray" then
    
    presentation$ = schedule.activeScheduledEvent.presentationName$
    m.activePresentation$ = presentation$
    
    autoplayFileName$ = "autoplay-" + presentation$ + ".json"
    
    ' find the autoplay file in the pool folder    
    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)
  
  if IsBoolean(BrightAuthor.meta.disableSettingsHandler) then
    meta.enableSettingsHandler = not BrightAuthor.meta.disableSettingsHandler
  else
    meta.enableSettingsHandler = true
  endif

  meta.publishedModel = BrightAuthor.meta.model
  meta.name = BrightAuthor.meta.name
  meta.videoMode = BrightAuthor.meta.videoMode
  meta.forceResolution = BrightAuthor.meta.forceResolution
  meta.tenBitColorEnabled = BrightAuthor.meta.tenBitColorEnabled
  
  meta.dolbyVisionEnabled = BrightAuthor.meta.dolbyVisionEnabled
  meta.fullResGraphicsEnabled = BrightAuthor.meta.fullResGraphicsEnabled
  meta.stretchedVideoWall = []
  meta.audioAutoLevel = BrightAuthor.meta.audioAutoLevel
  
  meta.videoConnector = BrightAuthor.meta.videoConnector
  meta.monitorOrientation = lcase(BrightAuthor.meta.monitorOrientation)
  
  meta.deviceWebPageDisplay = BrightAuthor.meta.deviceWebPageDisplay
  meta.customDeviceWebPage = BrightAuthor.meta.customDeviceWebPage
  
  meta.alphabetizeVariableNames = BrightAuthor.meta.alphabetizeVariableNames
  
  meta.htmlEnableJavascriptConsole = BrightAuthor.meta.htmlEnableJavascriptConsole

  meta.htmlEnableChromiumVideoPlayback = BrightAuthor.meta.htmlEnableChromiumVideoPlayback
  
  meta.backgroundScreenColor = GetColor(BrightAuthor.meta.backgroundScreenColor)
  meta.dontChangePresentationUntilMediaEndEventReceived = BrightAuthor.meta.delayScheduleChangeUntilMediaEndEvent
  meta.delayScheduleChangeUntilMediaEndEvent = BrightAuthor.meta.delayScheduleChangeUntilMediaEndEvent
  meta.languageKey = BrightAuthor.meta.languageKey 'BACONTODO - check case'
  
  meta.bp900AConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp900a"].configureAutomatically
  meta.bp900BConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp900b"].configureAutomatically
  meta.bp900CConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp900c"].configureAutomatically
  meta.bp900DConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp900d"].configureAutomatically
  meta.bp200AConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp200a"].configureAutomatically
  meta.bp200BConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp200b"].configureAutomatically
  meta.bp200CConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp200c"].configureAutomatically
  meta.bp200DConfigureAutomatically = BrightAuthor.meta.buttonPanels["bp200d"].configureAutomatically
  
  meta.bp900AConfiguration% = BrightAuthor.meta.buttonPanels["bp900a"].configuration
  meta.bp900BConfiguration% = BrightAuthor.meta.buttonPanels["bp900b"].configuration
  meta.bp900CConfiguration% = BrightAuthor.meta.buttonPanels["bp900c"].configuration
  meta.bp900DConfiguration% = BrightAuthor.meta.buttonPanels["bp900d"].configuration
  meta.bp200AConfiguration% = BrightAuthor.meta.buttonPanels["bp200a"].configuration
  meta.bp200BConfiguration% = BrightAuthor.meta.buttonPanels["bp200b"].configuration
  meta.bp200CConfiguration% = BrightAuthor.meta.buttonPanels["bp200c"].configuration
  meta.bp200DConfiguration% = BrightAuthor.meta.buttonPanels["bp200d"].configuration
  
  ' remote configuration
  meta.irRemoteControl = BrightAuthor.meta.irRemote.irRemoteControl
  meta.irInConfiguration = BrightAuthor.meta.irRemote.irInConfiguration
  meta.irOutConfiguration = BrightAuthor.meta.irRemote.irOutConfiguration

  ' serial ports'
  meta.serialPortConfigurations = CreateObject("roArray", 1, true)
  
  for each serialPortConfigurationSpec in BrightAuthor.meta.serialPortConfigurations
    
    serialPortConfiguration = { }
    serialPortConfiguration.serialPortSpeed% = serialPortConfigurationSpec.baudRate
    serialPortConfiguration.protocol$ = serialPortConfigurationSpec.protocol
    serialPortConfiguration.sendEol$ = GetEolFromSpec(serialPortConfigurationSpec.sendEol)
    serialPortConfiguration.receiveEol$ = GetEolFromSpec(serialPortConfigurationSpec.receiveEol)
    serialPortConfiguration.invertSignals = serialPortConfigurationSpec.invertSignals
    
    dataBits$ = stri(serialPortConfigurationSpec.dataBits)
    parity$ = serialPortConfigurationSpec.parity
    stopBits$ = stri(serialPortConfigurationSpec.stopBits)
    serialPortConfiguration.serialPortMode = StripLeadingSpaces(dataBits$) + StripLeadingSpaces(parity$) + StripLeadingSpaces(stopBits$)
    port% = val(serialPortConfigurationSpec.port)
    serialPortConfiguration.port = port%
    
    connectedDevice$ = serialPortConfigurationSpec.connectedDevice
    if connectedDevice$ = "GPS" then
      serialPortConfiguration.gps = true
    else
      serialPortConfiguration.gps = false
    end if
    
    meta.serialPortConfigurations[port%] = serialPortConfiguration
  next
  
  ' parse parser plugins
  meta.parserPlugins = CreateObject("roArray", 1, true)
  for each parserPluginSpec in BrightAuthor.meta.parserPlugins
    parserPlugin = jsonParseParserPlugin(parserPluginSpec)
    meta.parserPlugins.push(parserPlugin)
  next
  
  ' first pass parse of user variables
  meta.userVariableSpecs = []
  
  userVariablesJson = BrightAuthor.meta.userVariables
  
  for each userVariableJson in userVariablesJson
    
    userVariableSpec = { }
    userVariableSpec.name = userVariableJson.name
    userVariableSpec.defaultValue = userVariableJson.defaultValue
    userVariableSpec.access = userVariableJSON.access
    userVariableSpec.systemVariable$ = userVariableJson.systemVariable
    
    ' record networked information - parse in 2nd pass
    userVariableSpec.url = ""
    userVariableSpec.liveDataFeedId = userVariableJson.liveDataFeedId
    
    url$ = userVariableJson.url
    if url$ <> "" then
      userVariableSpec.url$ = url$
    end if
    
    videoConnector$ = getVarFromObj(userVariableJson, "data.videoConnector", "String", "")
    if videoConnector$ <> "" userVariableSpec.videoConnector$ = videoConnector$
    
    meta.userVariableSpecs.push(userVariableSpec)
  next
  
  ' parse live data feeds
  meta.liveDataFeedDescriptions = []
  liveDataFeedsJson = BrightAuthor.meta.liveDataFeeds
  for each liveDataFeedJson in liveDataFeedsJson
    liveDataFeedDescription = jsonParseLiveDataFeed(liveDataFeedJson)
    meta.liveDataFeedDescriptions.push(liveDataFeedDescription)
  next
  meta.resetVariablesOnPresentationStart = BrightAuthor.meta.resetVariablesOnPresentationStart

  ' parse Node apps
  meta.nodeAppDescriptions = []
  for each nodeAppJson in BrightAuthor.meta.nodeApps
    nodeAppDescription = jsonParseNodeApp(bsp, nodeAppJson)
    meta.nodeAppDescriptions.push(nodeAppDescription)
  next

  ' parse HTML sites
  meta.htmlSiteDescriptions = []
  for each htmlSiteJson in BrightAuthor.meta.htmlSites
    htmlSiteDescription = jsonParseHtmlSite(bsp, htmlSiteJson)
    meta.htmlSiteDescriptions.push(htmlSiteDescription)
  next
  
  ' parse presentation identifiers
  meta.presentationIdentifiers = []
  if type(BrightAuthor.meta.presentationIdentifiers) = "roArray" then
    for each presentationIdentifier in BrightAuthor.meta.presentationIdentifiers
      meta.presentationIdentifiers.push(presentationIdentifier)
    next
  end if
  
  ' parse beacons
  meta.beacons = []
  
  ' get list of additional files to publish
  meta.additionalPublishedFiles = []
  if type(BrightAuthor.meta.auxiliaryFiles) = "roArray" then
    for each auxiliaryFile in BrightAuthor.meta.auxiliaryFiles
      meta.additionalPublishedFiles.push(auxiliaryFile)
    next
  end if
  
  meta.boseProducts = BrightAuthor.meta.partnerProducts
  meta.wssDeviceSpec = BrightAuthor.meta.wssDeviceSpec
  meta.bmapSpecAssetName = BrightAuthor.meta.bmapSpecAssetName

  meta.udpReceivePort = BrightAuthor.meta.udpReceiverPort
  meta.udpSendPort = BrightAuthor.meta.udpDestinationPort
  meta.udpAddressType = BrightAuthor.meta.udpDestinationAddressType 'BACONTODO - check case'
  
  if meta.udpAddressType = "" then meta.udpAddressType = "IPAddress"
  meta.udpAddress = BrightAuthor.meta.udpDestinationAddress
  
  meta.enableEnhancedSynchronization = false
  meta.deviceIsSyncMaster = false
  meta.ptpDomain$ = "0"
  if type(BrightAuthor.meta.enableEnhancedSynchronization) = "roAssociativeArray" and BrightAuthor.meta.enableEnhancedSynchronization.deviceIsSyncMaster <> invalid then
    meta.enableEnhancedSynchronization = true
    meta.deviceIsSyncMaster = BrightAuthor.meta.enableEnhancedSynchronization.deviceIsSyncMaster
    meta.ptpDomain$ = StripLeadingSpaces(stri(BrightAuthor.meta.enableEnhancedSynchronization.ptpDomain))
  else
  end if
  
  meta.flipCoordinates = BrightAuthor.meta.flipCoordinates
  meta.touchCursorDisplayMode = lcase(BrightAuthor.meta.touchCursorDisplayMode)
    
  meta.gpio = CreateObject("roArray", 8, true)
  for i% = 0 to 7
    meta.gpio[i%] = BrightAuthor.meta.gpio[i%]
  next
  
  meta.audioConfiguration = BrightAuthor.meta.audioConfiguration
  
  audioSignPropertyMap = BrightAuthor.meta.audioSignPropertyMap
  meta.audio1MinVolume = audioSignPropertyMap["analog1"].min
  meta.audio1MaxVolume = audioSignPropertyMap["analog1"].max
  meta.hdmiMinVolume = audioSignPropertyMap["hdmi"].min
  meta.hdmiMaxVolume = audioSignPropertyMap["hdmi"].max
  meta.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 = BrightAuthor.meta.inactivityTimeout
  meta.inactivityTime = BrightAuthor.meta.inactivityTime
  
  meta.graphicsZOrder = BrightAuthor.meta.graphicsZOrder 'BACONTODO - check case'
  
  meta.isMosaic = false
  
end sub


Function jsonParseZones(BrightAuthor as object, Sign as object)
  
  zoneList = BrightAuthor.zones
  numZones% = zoneList.Count()
  
  zoneDescriptions = CreateObject("roArray", numZones%, true)
  
  for each zoneSpec in zoneList
    zoneDescription = jsonParseZoneSpec(zoneSpec, Sign)
    zoneDescriptions.push(zoneDescription)
  next
  
  return zoneDescriptions
  
end function


' BACONTODO add support for AudioOnly'
Function jsonParseZoneSpec(zoneSpec as object, sign as object)
  
  zoneDescription = { }
  
  '' common zone parameters
  zoneDescription.name$ = zoneSpec.name
  
  zoneDescription.originalWidth% = zoneSpec.absolutePosition.width
  zoneDescription.originalHeight% = zoneSpec.absolutePosition.height
  
  zoneDescription.x = zoneSpec.absolutePosition.x
  zoneDescription.y = zoneSpec.absolutePosition.y
  zoneDescription.width = zoneSpec.absolutePosition.width
  zoneDescription.height = zoneSpec.absolutePosition.height
  
  zoneDescription.type$ = zoneSpec.type
  zoneDescription.id$ = zoneSpec.id
  
  ' VideoOrImagesZone'
  if zoneDescription.type$ = "VideoOrImages" then
    zoneDescription.imageMode% = GetImageModeValue(zoneSpec.zoneSpecificParameters.imageMode)
    zoneDescription.numImageItems% = 0
  end if
  
  ' Ticker '
  if lcase(zoneDescription.type$) = "ticker" then
    
    zoneDescription.numberOfLines% = zoneSpec.zoneSpecificParameters.textWidget.numberOfLines
    
    zoneDescription.delay% = zoneSpec.zoneSpecificParameters.textWidget.delay
    
    zoneDescription.rotation% = 0
    rotation$ = zoneSpec.zoneSpecificParameters.textWidget.rotation
    if rotation$ = "90" then
      zoneDescription.rotation% = 3
    else if rotation$ = "180" then
      zoneDescription.rotation% = 2
    else if rotation$ = "270" then
      zoneDescription.rotation% = 1
    end if
    
    alignment$ = lcase(zoneSpec.zoneSpecificParameters.textWidget.alignment)
    if alignment$ = "center" then
      zoneDescription.alignment% = 1
    else if alignment$ = "right" then
      zoneDescription.alignment% = 2
    else
      zoneDescription.alignment% = 0
    end if
    
    scrollingMethod$ = lcase(zoneSpec.zoneSpecificParameters.textWidget.scrollingMethod)
    zoneDescription.scrollingMethod% = 0
    if scrollingMethod$ = "statictext" then
      zoneDescription.scrollingMethod% = 1
    else if scrollingMethod$ = "scrolling" then
      zoneDescription.scrollingMethod% = 3
    end if
    
    zoneDescription.scrollSpeed% = zoneSpec.zoneSpecificParameters.scrollSpeed
    
    widget = zoneSpec.zoneSpecificParameters.widget
    
    foregroundTextColor = widget.foregroundTextColor
    backgroundTextColor = widget.backgroundTextColor
    
    zoneDescription.foregroundTextColor% = GetColor(foregroundTextColor)
    zoneDescription.backgroundTextColor% = GetColor(backgroundTextColor)
    
    zoneDescription.font$ = widget.font
    
    if type(widget.backgroundBitmap) = "roAssociativeArray" then
      zoneDescription.backgroundBitmapFile$ = widget.backgroundBitmap.file
      zoneDescription.stretch = widget.backgroundBitmap.stretch
    else
      zoneDescription.backgroundBitmapFile$ = ""
      zoneDescription.stretch = false
    end if
    
    safeTextRegion = widget.safeTextRegion
    if type(safeTextRegion) = "roAssociativeArray" then
      zoneDescription.safeTextRegionX% = safeTextRegion.x
      zoneDescription.safeTextRegionY% = safeTextRegion.y
      zoneDescription.safeTextRegionWidth% = safeTextRegion.width
      zoneDescription.safeTextRegionHeight% = safeTextRegion.height
    end if
    
  end if

 ' Clock '
  if lcase(zoneDescription.type$) = "clock" then

    rotation = zoneSpec.zoneSpecificParameters.rotation
    if rotation <> invalid and IsNonEmptyString(rotation) then
      zoneDescription.AddReplace("rotation", rotation)
    end if

    clockFormat = zoneSpec.zoneSpecificParameters.clockFormat
    if clockFormat <> invalid and IsNonEmptyString(clockFormat) then
      zoneDescription.AddReplace("clockFormat", clockFormat)
    end if
    
    widget = zoneSpec.zoneSpecificParameters.widget
    
    foregroundTextColor = widget.foregroundTextColor
    if type(foregroundTextColor) = "roAssociativeArray" then
      zoneDescription.AddReplace("foregroundTextColor", foregroundTextColor)
    end if

    backgroundTextColor = widget.backgroundTextColor
    if type(backgroundTextColor) = "roAssociativeArray" then
      zoneDescription.AddReplace("backgroundTextColor", backgroundTextColor)
    end if

    backgroundBitmapFileName = widget.backgroundBitmapFileName
    if backgroundBitmapFileName <> invalid and IsNonEmptyString(backgroundBitmapFileName) then
      filePath = GetPoolFilePath(m.bsp.assetPoolFiles, backgroundBitmapFileName)
      zoneDescription.AddReplace("backgroundBitmapFilePath", backgroundBitmapFileName)
      zoneDescription.AddReplace("stretchBitmapFile", widget.stretchBitmapFile)
    end if

    fontFileName = widget.fontFileName
    if fontFileName <> invalid and IsNonEmptyString(fontFileName) then
      filePath = GetPoolFilePath(m.bsp.assetPoolFiles, fontFileName)
      zoneDescription.AddReplace("fontFilePath", fontFileName)
    end if

    safeTextRegion = widget.safeTextRegion
    if type(safeTextRegion) = "roAssociativeArray" then
      zoneDescription.AddReplace("safeTextRegion", safeTextRegion)
    end if
    
  end if
  
  ' Video parameters'
  if zoneDescription.type$ = "VideoOrImages" or zoneDescription.type$ = "VideoOnly" then
    zoneDescription.viewMode% = GetViewModeValue(zoneSpec.zoneSpecificParameters.viewMode)
    zoneDescription.initialVideoVolume% = zoneSpec.zoneSpecificParameters.videoVolume
    zoneDescription.zOrderFront = zoneSpec.zoneSpecificParameters.zOrderFront
  end if

  ' Audio parameters'
  if zoneDescription.type$ = "VideoOrImages" or zoneDescription.type$ = "VideoOnly" or zoneDescription.type$ = "AudioOnly" or zoneDescription.type$ = "EnhancedAudio" then
    zoneDescription.audioMixMode$ = zoneSpec.zoneSpecificParameters.audioMixMode
    zoneDescription.analogOutput$ = zoneSpec.zoneSpecificParameters.analogOutput
    zoneDescription.hdmiOutput$ = zoneSpec.zoneSpecificParameters.hdmiOutput
    zoneDescription.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.localPlaylistItem) = "roAssociativeArray" then
    stateDescription.localPlaylistItem = stateSpec.localPlaylistItem
    stateDescription.localPlaylistItem.slideTransition% = 0
    stateDescription.type = "localPlaylist"
  else if type(stateSpec.backgroundImageItem) = "roAssociativeArray" then
    stateDescription.backgroundImageItem = stateSpec.backgroundImageItem
    stateDescription.type = "backgroundImage"
  else if type(stateSpec.textItem) = "roAssociativeArray" then
    stateDescription.textItem = stateSpec.textItem
    stateDescription.type = "textItem"
  else if type(stateSpec.mediaListItem) = "roAssociativeArray" then
    stateDescription.mediaListItem = stateSpec.mediaListItem
    stateDescription.type = "mediaList"
  else if type(stateSpec.playFileItem) = "roAssociativeArray" then
    stateDescription.playFileItem = stateSpec.playFileItem
    stateDescription.type = "playFile"
  else if type(stateSpec.templatePlaylistItem) = "roAssociativeArray" then
    stateDescription.templatePlaylistItem = stateSpec.templatePlaylistItem
    stateDescription.type = "template"
  else if type(stateSpec.twitterItem) = "roAssociativeArray" then
    stateDescription.twitterItem = stateSpec.twitterItem
    stateDescription.type = "twitter"
  else if type(stateSpec.eventHandlerItem) = "roAssociativeArray" then
    stateDescription.eventHandlerItem = stateSpec.eventHandlerItem
    stateDescription.type = "eventHandler"
  else if type(stateSpec.superStateItem) = "roAssociativeArray" then
    stateDescription.superStateItem = stateSpec.superStateItem
    stateDescription.type = "superState"
  else if type(stateSpec.userVariableInTickerItem) = "roAssociativeArray" then
    stateDescription.userVariableInTickerItem = stateSpec.userVariableInTickerItem
    stateDescription.type = "userVariableInTickerItem"
  end if
  
  stateDescription.brightSignCmd = stateSpec.entryCommands
  stateDescription.brightSignExitCommands = stateSpec.exitCommands
  
  return stateDescription
  
end function


Function jsonParseRSSItem(playlistItemJson as object) as object
  
  rssPlaylistItemDescription = { }
  rssPlaylistItemDescription.liveDataFeedId$ = playlistItemJson.liveDataFeedId
  return rssPlaylistItemDescription
  
end function


Function getStringFromJsonParameter(parameter as object) as string
  if parameter = invalid then
    return ""
  end if
  return parameter
end function


Function jsonParseTransition(transitionSpec as object)
  
  transitionDescription = { }
  
  transitionDescription.sourceMediaState = getStringFromJsonParameter(transitionSpec.sourceMediaState)
  transitionDescription.targetMediaState = getStringFromJsonParameter(transitionSpec.targetMediaState)
  
  transitionDescription.commands = transitionSpec.commands
  
  transitionDescription.assignInputToUserVariable = transitionSpec.assignInputToUserVariable
  if transitionSpec.assignInputToUserVariable then
    transitionDescription.variableToAssignFromInput$ = transitionSpec.variableToAssignFromInput
  else
    transitionDescription.variableToAssignFromInput$ = ""
  end if
  
  transitionDescription.assignWildcardToUserVariable = transitionSpec.assignWildcardToUserVariable
  if transitionSpec.assignWildcardToUserVariable then
    transitionDescription.variableToAssign$ = transitionSpec.variableToAssignFromWildcard
  else
    transitionDescription.variableToAssign$ = ""
  end if
  
  targetAction = GetTargetActionFromEventAction(transitionSpec.eventAction)
  transitionDescription.targetMediaStateIsPreviousState = targetAction.targetMediaStateIsPreviousState
  transitionDescription.remainOnCurrentStateActions = targetAction.remainOnCurrentStateActions
  
  transitionDescription.conditionalTransitions = transitionSpec.conditionalTransitions
  
  transitionDescription.userEvent = transitionSpec.userEvent
  
  eventName = transitionSpec.userEvent.name
  eventData = transitionSpec.userEvent.data
  
  if eventName = "rectangularTouchEvent" then
    
    eventData.x = eventData.region.x
    eventData.y = eventData.region.y
    eventData.width = eventData.region.width
    eventData.height = eventData.region.height
    
  else if eventName = "bp900AUserEvent" or eventName = "bp900BUserEvent" or eventName = "bp900CUserEvent" or eventName = "bp200AUserEvent" or eventName = "bp200BUserEvent" or eventName = "bp200CUserEvent" then
    
    bpIndex$ = eventData.bpIndex
    if bpIndex$ = "a" then
      eventData.buttonPanelIndex% = 0
    else if bpIndex$ = "b" then
      eventData.buttonPanelIndex% = 1
    else if bpIndex$ = "c" then
      eventData.buttonPanelIndex% = 2
    else
      eventData.buttonPanelIndex% = 3
    end if
    
    eventData.buttonNumber$ = StripLeadingSpaces(stri(eventData.buttonNumber))
    
    if type(eventData.pressContinuous) = "roAssociativeArray" then
      eventData.configuration$ = "pressContinuous"
      eventData.initialHoldoff$ = StripLeadingSpaces(stri(eventData.pressContinuous.initialHoldoff))
      eventData.repeatInterval$ = StripLeadingSpaces(stri(eventData.pressContinuous.repeatInterval))
    else
      eventData.configuration$ = "press"
    end if
    
  else if eventName = "gpioUserEvent" then
    
    eventData.buttonDirection$ = lcase(eventData.buttonDirection)
    
    eventData.buttonNumber$ = StripLeadingSpaces(stri(eventData.buttonNumber))
    
    if type(eventData.pressContinuous) = "roAssociativeArray" then
      eventData.configuration$ = "pressContinuous"
      eventData.initialHoldoff$ = StripLeadingSpaces(stri(eventData.pressContinuous.initialHoldoff))
      eventData.repeatInterval$ = StripLeadingSpaces(stri(eventData.pressContinuous.repeatInterval))
    else
      eventData.configuration$ = "press"
    end if
    
  end if
  
  return transitionDescription
  
end function


Function jsonParseParameterValue(parameterValueJson as object) as object
  
  parameterValueDescription = { }
  
  parameterValueItems = []
  for each parameterValueItemSpec in parameterValueJson
    parameterValueItem = { }
    parameterValueItem.type = parameterValueItemSpec.type
    parameterValueItem.value = parameterValueItemSpec.value
    parameterValueItems.push(parameterValueItemSpec)
  next
  
  parameterValueDescription.parameterValueItems = parameterValueItems
  
  return parameterValueDescription
  
end function


Function newParameterValue(bsp as object, parameterValueDescription as object) as object
  
  parameterValue = { }
  parameterValue.GetCurrentParameterValue = GetCurrentParameterValue
  parameterValue.GetVariableName = GetVariableName
  
  parameterValue.parameterValueItems = []
  if type(parameterValueDescription) = "roAssociativeArray" then
    
    parameterValueItems = parameterValueDescription.parameterValueItems
    if parameterValueItems.count() = 0 then
      parameterValue.parameterValueItems.push(newParameterValueItemText(""))
    else
      for each parameterValueItem in parameterValueItems
        if lcase(parameterValueItem.type) = "text" then
          parameterValue.parameterValueItems.push(newParameterValueItemText(parameterValueItem.value))
        else if lcase(parameterValueItem.type) = "uservariable" then
          parameterValue.parameterValueItems.push(jsonNewParameterValueItemUserVariable(bsp, parameterValueItem))
        end if
      next
    end if
  end if
  
  return parameterValue
  
end function


Function jsonNewParameterValueItemUserVariable(bsp as object, parameterValueItemJsonUserVariable) as object
  
  parameterValueItem = { }
  parameterValueItem.GetCurrentValue = GetCurrentUserVariableParameterValue
  
  parameterValueItem.type$ = "userVariable"
  
  userVariableName$ = parameterValueItemJsonUserVariable.value
  
  parameterValueItem.userVariable = bsp.GetUserVariable(userVariableName$)
  if type(parameterValueItem.userVariable) <> "roAssociativeArray" then
    bsp.diagnostics.PrintDebug("User variable " + userVariableName$ + " not found.")
    bsp.logging.WriteDiagnosticLogEntry(bsp.diagnosticCodes.EVENT_USER_VARIABLE_NOT_FOUND, userVariableName$)
  end if
  
  return parameterValueItem
  
end function


Function jsonGetTotalSpaceRequired(filesToCopy as object, deletionCandidates as object) as object
  
  filesToPublish$ = ReadAsciiFile("filesToPublish.json")
  if filesToPublish$ = "" then stop
  
  ' create the list of files that need to be copied. this is the list of files in filesToPublish that are not in listOfPoolFiles
  filesToPublish = ParseJson(filesToPublish$)
  
  ' determine total space required
  totalSpaceRequired! = 0
  
  for each file in filesToPublish.file
    fullFileName$ = file.fullFileName
    o = deletionCandidates.Lookup(fullFileName$)
    if not IsString(o) then ' file is not already on the card
    fileItem = { }
    fileItem.fileName$ = file.fileName
    fileItem.filePath$ = file.filePath
    fileItem.hashValue$ = file.hashValue
    fileItem.fileSize$ = file.fileSize
    
    filesToCopy.AddReplace(fullFileName$, fileItem) ' files that need to be copied to the card
    
    fileSize% = val(fileItem.fileSize$)
    totalSpaceRequired! = totalSpaceRequired! + fileSize%
  end if
next

filesToPublish = invalid

return totalSpaceRequired!

end function


Sub jsonGetFilesToDelete(deletionCandidates as object, oldLocationDeletionCandidates as object)
  
  stop ' not implemented yet
  filesToPublish$ = ReadAsciiFile("filesToPublish.json")
  stop
end sub

Function jsonParseLiveDataFeed(liveDataFeedJson)
  
  liveDataFeedDescription = { }
  liveDataFeedDescription.id$ = liveDataFeedJson.id
  
  liveDataFeedDescription.isLiveBSNDataFeed = false
  liveDataFeedDescription.isLiveMediaFeed = false
  liveDataFeedDescription.isDynamicPlaylist = false
  
  if liveDataFeedJson.type = "BSNDataFeed" then
    liveDataFeedDescription.isLiveBSNDataFeed = true
  else if liveDataFeedJson.type = "BSNMediaFeed" then
    liveDataFeedDescription.isLiveMediaFeed = true
  else if liveDataFeedJson.type = "BSNDynamicPlaylist" then
    liveDataFeedDescription.isDynamicPlaylist = true
  else if liveDataFeedJson.type = "BSNTaggedPlaylist" then
    liveDataFeedDescription.isLiveMediaFeed = true
  end if
  
  
  if IsString(liveDataFeedJson.url) then
    liveDataFeedDescription.urlPV = newTextParameterValue(liveDataFeedJson.url)
  else
    ' BACONTODO - review carefully'
    liveDataFeedDescription.urlPV = jsonParseParameterValue(liveDataFeedJson.url)
    ''    liveDataFeedDescription.urlPV = jsonParseParameterValue(liveDataFeedJson.url.params)
    ' liveDataFeedDescription.urlPV = liveDataFeedJson.url    ' xml saves it as urlSpec$'
  end if
  
  liveDataFeedDescription.parserPluginName = liveDataFeedJson.parserPluginName
  liveDataFeedDescription.uvParserPluginName = ""
  
  liveDataFeedDescription.updateInterval% = liveDataFeedJson.updateInterval
  liveDataFeedDescription.useHeadRequest = liveDataFeedJson.useHeadRequest
  liveDataFeedDescription.usage$ = lcase(liveDataFeedJson.usage)
  liveDataFeedDescription.autoGenerateUserVariables = liveDataFeedJson.autoGenerateUserVariables
  liveDataFeedDescription.userVariableAccess$ = lcase(liveDataFeedJson.userVariableAccess)
  return liveDataFeedDescription
  
end function


Function syncSpecValueTrue(syncSpecValue$) as boolean
  if lcase(syncSpecValue$) = "yes" or syncSpecValue$ = "1" or lcase(syncSpecValue$) = "true" then
    return true
  end if
  return false
end function

Function syncSpecValueFalse(syncSpecValue$) as boolean
  if lcase(syncSpecValue$) = "no" or syncSpecValue$ = "0" or lcase(syncSpecValue$) = "false" then
    return true
  end if
  return false
end function


Function GetSynchronizerFilesToTransfer(userData as object, e as object)
  
  mVar = userData.mVar
  
  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.HStateEventHandler = STDisplayingMediaListItemEventHandler
  state.PopulateMediaListFromLiveDataFeed = PopulateMediaListFromLiveDataFeed
  state.ShuffleMediaListContent = ShuffleMediaListContent
  state.ConfigureIntraStateEventHandlersButton = ConfigureIntraStateEventHandlersButton
  state.PrePlayAudio = PrePlayAudio
  state.PlayAudio = PlayAudio
  state.PostPlayAudio = PostPlayAudio
  state.PlayMixerAudio = PlayMixerAudio
  state.PrePlayVideo = PrePlayVideo
  state.PlayVideo = PlayVideo
  state.PostPlayVideo = PostPlayVideo
  state.PreDrawImage = PreDrawImage
  state.DrawImage = DrawImage
  state.PostDrawImage = PostDrawImage
  state.ClearVideo = ClearVideo
  state.StartInactivityTimer = StartInactivityTimer
  state.GetMatchingNavigationEvent = GetMatchingNavigationEvent
  state.HandleIntraStateEvent = HandleIntraStateEvent
  state.LaunchMediaListPlaybackItem = LaunchMediaListPlaybackItem
  state.GetTimeoutEvent = GetTimeoutEvent
  state.AdvanceMediaListPlayback = AdvanceMediaListPlayback
  state.RetreatMediaListPlayback = RetreatMediaListPlayback
  
  state.AddAudioTimeCodeEvent = AddAudioTimeCodeEvent
  state.SetAudioTimeCodeEvents = SetAudioTimeCodeEvents
  
  state.AddVideoTimeCodeEvent = AddVideoTimeCodeEvent
  state.SetVideoTimeCodeEvents = SetVideoTimeCodeEvents
  
  SetMediaItemEventHandlers(state)
  state.ExecuteTransition = ExecuteTransition
  state.MatchWssEvent = MatchWssEvent
  state.GetNextStateName = GetNextStateName
  state.UpdatePreviousCurrentStateNames = UpdatePreviousCurrentStateNames
  state.LaunchTimer = LaunchTimer
  state.PreloadItem = PreloadItem
  state.ConfigureBPButtons = ConfigureBPButtons
  state.ConfigureGPIOButtons = ConfigureGPIOButtons
  
  state.AtEndOfMediaList = AtEndOfMediaList
  
end sub


Function STDisplayingMediaListItemEventHandler(event as object, stateData as object) as object
  
  MEDIA_START = 3
  MEDIA_END = 8
  MEDIA_ERROR = 16
  
  VIDEO_TIME_CODE = 12
  
  stateData.nextState = invalid
  
  if type(event) = "roAssociativeArray" then ' internal message event
  
    if IsString(event["EventType"]) then

      if event["EventType"] = "ENTRY_SIGNAL" then
        
        m.bsp.diagnostics.PrintDebug(m.id$ + ": entry signal")
        
        ' if using a live data feed, populate items here
        if type(m.liveDataFeed) = "roAssociativeArray" then
        
        ' ensure that data feed content has been loaded
          if type(m.liveDataFeed.assetPoolFiles) = "roAssetPoolFiles" then
            m.PopulateMediaListFromLiveDataFeed()
          end if
        end if
        
        if m.inactivityTimeout then
          m.inactivityTimer = CreateObject("roTimer")
          m.inactivityTimer.SetPort(m.bsp.msgPort)
        endif

        m.ConfigureIntraStateEventHandlersButton(m.transitionToNextEventList)
        m.ConfigureIntraStateEventHandlersButton(m.transitionToPreviousEventList)
        
        m.firstItemDisplayed = false
        
        ' prevent start index from pointing beyond the number of items in the case where m.playFromBeginning is false
        if m.numItems% > 0 and m.startIndex% >= m.numItems% then
          m.startIndex% = 0
        end if
        
        ' reset playback index if appropriate
        if m.playFromBeginning then
          m.playbackIndex% = m.startIndex%
        end if
      
        if m.numItems% > 0 then
          
          m.playbackActive = true
          
          ' prevent start index from pointing beyond the number of items
          if m.playFromBeginning 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
      PostMediaEndEvent(m.bsp.msgPort)
    else if event.GetInt() = VIDEO_TIME_CODE then
      videoTimeCodeIndex$ = str(event.GetData())
      m.bsp.diagnostics.PrintDebug("Video TimeCode Event " + videoTimeCodeIndex$)
      if type(m.videoTimeCodeEvents) = "roAssociativeArray" then
        videoTimeCodeEvent = m.videoTimeCodeEvents[videoTimeCodeIndex$]
        if type(videoTimeCodeEvent) = "roAssociativeArray" then
          m.bsp.ExecuteTransitionCommands(m.stateMachine, videoTimeCodeEvent)
          m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "1")
          return "HANDLED"
        end if
      end if
      m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "videoTimeCode", "", "0")
    end if

  else if m.stateMachine.type$ = "EnhancedAudio" and type(event) = "roAudioEventMx" then
    m.bsp.diagnostics.PrintDebug("AudioMx Event" + stri(event.GetInt()))
    if event.GetInt() = MEDIA_START then
      if event.GetSourceIdentity() = m.stateMachine.audioPlayer.GetIdentity() then
        
        ' index of track that just started playing
        currentTrackIndex% = int(val(event.GetUserData()))
        
        'get index of track to queue
        
        m.playbackIndex% = currentTrackIndex% + 1
        if m.playbackIndex% >= m.numItems% then
          m.playbackIndex% = 0
        end if
        
        m.audioItem = m.items[m.playbackIndices[m.playbackIndex%]]
        
        'send zone message for the current track
        if m.sendZoneMessage then
          item = m.items[m.playbackIndices[currentTrackIndex%]]
          fileNameWithoutExtension$ = item.filename$
          
          'if the file name has an extension, remove it before sending
          ext = GetFileExtension(item.filename$)
          if type(ext) = "roString" then
            index = instr(1, item.filename$, ext)
            if index > 2 then
              fileNameWithoutExtension$ = mid(item.filename$, 1, index - 2)
            end if
          end if
          
          ' send ZoneMessage using the file name as the message
          zoneMessageCmd = { }
          zoneMessageCmd["EventType"] = "SEND_ZONE_MESSAGE"
          zoneMessageCmd["EventParameter"] = fileNameWithoutExtension$
          m.bsp.msgPort.PostMessage(zoneMessageCmd)
          
        end if
        
        
        if not(m.playbackIndex% = m.startIndex% and type(m.audioEndEvent) = "roAssociativeArray") then
          m.PlayMixerAudio(false, m.playbackIndex%, false)
        end if
        
        ' at this point, m.playbackIndex% points to both the item that is queued as well as the next item to play - the concept of
        ' "next item to play" is needed for NextNavigation, BackNavigation, and re-entering the state
        
        
        '				m.AdvanceMediaListPlayback(false)
        return "HANDLED"
      end if
      
    else if event.GetInt() = MEDIA_END then
    
      m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")
      if m.bsp.ProcessMediaEndEvent() then
        return "HANDLED"
      end if
      if type(m.audioEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.audioEndEvent, stateData, "")
      end if
      PostMediaEndEvent(m.bsp.msgPort)
      
    end if
    
  else if m.stateMachine.type$ <> "EnhancedAudio" and IsAudioEvent(m.stateMachine, event) then
    if event.GetInt() = MEDIA_END then
    
      m.bsp.diagnostics.PrintDebug("Audio Event" + stri(event.GetInt()))
      m.bsp.logging.WriteEventLogEntry(m.stateMachine, m.id$, "mediaEnd", "", "1")

      if m.bsp.ProcessMediaEndEvent() then
        return "HANDLED"
      end if
      
      if m.transitionToNextEventList.count() > 0 then
        for each transitionToNextEvent in m.transitionToNextEventList
          if transitionToNextEvent.eventName = "mediaEnd" then    ' equivalent to m.advanceOnMediaEnd in BrightAuthor
            if not(m.playbackIndex% = m.startIndex% and type(m.videoEndEvent) = "roAssociativeArray") then
              m.AdvanceMediaListPlayback(true, true)
              return "HANDLED"
            endif
          endif
        next
      endif

      if type(m.audioEndEvent) = "roAssociativeArray" then
        return m.ExecuteTransition(m.audioEndEvent, stateData, "")
      end if
      PostMediaEndEvent(m.bsp.msgPort)
    end if
    
  end if

  ' event received
  if m.transitionToNextEventList.count() > 0 then
    advance = m.HandleIntraStateEvent(event, m.transitionToNextEventList)
    if advance then
      m.AdvanceMediaListPlayback(true, true)
      return "HANDLED"
    end if
  end if

  if m.transitionToPreviousEventList.count() > 0 then
    retreat = m.HandleIntraStateEvent(event, m.transitionToPreviousEventList)
    if retreat then
      m.RetreatMediaListPlayback(true, true)
      return "HANDLED"
    end if
  end if

  return m.MediaItemEventHandler(event, stateData)

end function


Sub ConfigureMediaListStateIO(bsp as object, 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 = GetPassword(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 GetPassword(password$) as string

  ' No need to decrypt when not provided
  if password$ = "" then return ""

  hexKeyValue$ = "27ae0d9c871cc8b6eaabb1bdc7c53d66"
  hexIVValue$ = "6669fc502f92ba915164fafd7957ca70"
  
  key = CreateObject("roByteArray")
  iv = CreateObject("roByteArray")
  pw = CreateObject("roByteArray")
  key.FromHexString(hexKeyValue$)
  iv.FromHexString(hexIVValue$)
  pw.FromHexString(password$)
  
  c = CreateObject("roBlockCipher", { mode: "aes-128-cbc", padding: "pkcs7" })
  c.SetIV(iv)
  decrypted = c.Decrypt(key, pw)

  ' If the password failed to decrypt, return the original value
  if type(decrypted) <> "roByteArray" then return password$

  plainText = decrypted.toAsciiString()
  return plainText
  
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
    ' TEDTODO
    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 = {}
    ' Hardcode presentation type to "Regular" until BrightWall feature is in place
    presentation.AddReplace("type", "Regular")
    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)
  ' TEDTODO-Subban - for dev purposes only?
  if rc <> 200 then
    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


