Library "setupCommon.brs"
Library "setupNetworkDiagnostics.brs"

REM
REM autorun-setup - setup file for simple file networking
REM Copyright (c) 2006-2019 BrightSign, LLC.
REM

Sub Main()
  
  setupVersion$ = "4.0.0.1"

  debugParams = EnableDebugging("current-sync.json")

  sysFlags = {}
  sysFlags.debugOn = debugParams.serialDebugOn
  sysFlags.systemLogDebugOn = debugParams.systemLogDebugOn

  diagnostics = newDiagnostics(sysFlags)

  diagnostics.printDebug("setup script version " + setupVersion$ + " started")
  
  CheckFirmwareVersion()
  
  CheckStorageDeviceIsWritable()
    
  diagnosticCodes = newDiagnosticCodes()
  
  RunSetup(setupVersion$, diagnostics, diagnosticCodes)
  
  stop
  
end sub


Sub RunSetup(setupVersion$ as string, diagnostics as object, diagnosticCodes as object)
  
  Setup = newSetup(diagnostics)
  
  modelObject = CreateObject("roDeviceInfo")
  sysInfo = CreateObject("roAssociativeArray")
  sysInfo.deviceUniqueID$ = modelObject.GetDeviceUniqueId()
  sysInfo.deviceFWVersion$ = modelObject.GetVersion()
  sysInfo.deviceModel$ = modelObject.GetModel()
  sysInfo.deviceFamily$ = modelObject.GetFamily()
  sysInfo.setupVersion$ = setupVersion$
  sysInfo.deviceFWVersionNumber% = modelObject.GetVersionNumber()
  
  ' create pool directory
  ok = CreateDirectory("pool")
  
  Setup.SetSystemInfo(sysInfo, diagnosticCodes)
  
  currentSync = CreateObject("roSyncSpec")
  if not currentSync.ReadFromFile("current-sync.json") then
    Setup.diagnostics.PrintDebug("### No current sync state available")
    stop
  end if
  
  setupParams = ParseAutoplay(currentSync)
  
  ' initialize logging parameters
  loggingParameters = SetLogging(setupParams, invalid)
  
  ' initialize networking
  Setup.networkingActive = Setup.networking.InitializeNetworkDownloads(setupParams)
  
  ' setup logging
  Setup.logging.InitializeLogging(false, false, false, setupParams.diagnosticLoggingEnabled, setupParams.variableLoggingEnabled, setupParams.uploadLogFilesAtBoot, setupParams.uploadLogFilesAtSpecificTime, setupParams.uploadLogFilesTime%)
  
  Setup.logging.WriteDiagnosticLogEntry(diagnosticCodes.EVENT_STARTUP, sysInfo.deviceFWVersion$ + chr(9) + sysInfo.setupVersion$ + chr(9) + "")
  
  Setup.EventLoop()
    
end sub


Function newSetup(diagnostics as object) as object
  
  Setup = {}
  
  Setup.diagnostics = diagnostics
  
  Setup.systemTime = CreateObject("roSystemTime")
  
  Setup.msgPort = CreateObject("roMessagePort")
  
  Setup.newLogging = newLogging
  Setup.logging = Setup.newLogging()
  Setup.newNetworking = newNetworking
  Setup.networking = Setup.newNetworking(Setup)
  Setup.logging.networking = Setup.networking
  
  Setup.SetSystemInfo = SetupSetSystemInfo
  Setup.EventLoop = EventLoop
  
  return Setup
  
end function


Sub SetupSetSystemInfo(sysInfo as object, diagnosticCodes as object)
  
  m.diagnostics.SetSystemInfo(sysInfo, diagnosticCodes)
  m.networking.SetSystemInfo(sysInfo, diagnosticCodes)
  m.logging.SetSystemInfo(sysInfo, diagnosticCodes)
  
end sub


Sub EventLoop()
  
  while true
    
    msg = wait(0, m.msgPort)
    
    if (type(msg) = "roUrlEvent") then
      
      m.networking.URLEvent(msg)
      
    else if (type(msg) = "roSyncPoolEvent") then
      
      m.networking.PoolEvent(msg)
      
    else if (type(msg) = "roTimerEvent") then

      if type(m.networking.checkAlarm) = "roTimer" and stri(msg.GetSourceIdentity()) = stri(m.networking.checkAlarm.GetIdentity()) then
        m.networking.StartSync()
      else if type(m.networking.registrationResponseTimer) = "roTimer" and stri(msg.GetSourceIdentity()) = stri(m.networking.registrationResponseTimer.GetIdentity()) then
        ' If downloads has finished, move forward. If not, set the proceedAfterRegistrationProcessComplete flag to true
        if m.networking.contentDownloadComplete then
          m.networking.deviceSetupSplashScreen = invalid
          m.logging.FlushLogFile()
          m.logging.registrySection.Delete("registration_in_progress")
          m.logging.registrySection.Flush()
          a = RestartApplication()
          stop
        else
          m.networking.proceedAfterRegistrationProcessComplete = true
        end if
      end if
      
      ' see if the timer is for Logging
      if type(m.logging) = "roAssociativeArray" then
        if type(m.logging.cutoverTimer) = "roTimer" then
          if msg.GetSourceIdentity() = m.logging.cutoverTimer.GetIdentity() then
            ' indicate that event was for logging
            m.logging.HandleTimerEvent(msg)
          end if
        end if
      end if
      
    else if (type(msg) = "roSyncPoolProgressEvent") then
      
      m.networking.SyncPoolProgressEvent(msg)

    else if (type(msg) = "roDatagramEvent") and IsString(msg.GetUserData()) and msg.GetUserData() = "bootstrap" then

      if msg.GetSourcePort() = m.networking.wsUdpSocketPort% then
        payload = ParseJson(msg.GetString())
        if payload <> invalid and payload.message <> invalid and payload.message = "sync.registration.complete" then
          ' If downloads has finished, move forward. If not, set the proceedAfterRegistrationProcessComplete flag to true
          if m.networking.contentDownloadComplete then
            m.networking.deviceSetupSplashScreen = invalid
            m.logging.FlushLogFile()
            m.logging.registrySection.Delete("registration_in_progress")
            m.logging.registrySection.Flush()
            a = RestartApplication()
            stop
          else
            m.networking.proceedAfterRegistrationProcessComplete = true
          end if
        end if
      end if
      
    else if (type(msg) = "roControlCloudMessageEvent") and IsString(msg.GetUserData()) and msg.GetUserData() = "bootstrap" then
      jsonobject = ParseJson(msg.GetData())
      if jsonobject <> invalid and jsonobject.message <> invalid and jsonobject.message = "sync.registration.complete" then
        ' If downloads has finished, move forward. If not, set the proceedAfterRegistrationProcessComplete flag to true
          if m.networking.contentDownloadComplete then
            m.networking.deviceSetupSplashScreen = invalid
            m.logging.FlushLogFile()
            m.logging.registrySection.Delete("registration_in_progress")
            m.logging.registrySection.Flush()
            a = RestartApplication()
            stop
          else
            m.networking.proceedAfterRegistrationProcessComplete = true
          end if
      end if
    end if
    
  end while
  
  return
  
end sub



REM *******************************************************
REM *******************************************************
REM ***************                    ********************
REM *************** NETWORKING OBJECT  ********************
REM ***************                    ********************
REM *******************************************************
REM *******************************************************

REM
REM construct a new networking BrightScript object
REM
Function newNetworking(Setup as object) as object
  
  networking = CreateObject("roAssociativeArray")
  
  networking.systemTime = m.systemTime
  networking.diagnostics = m.diagnostics
  networking.logging = m.logging
  networking.msgPort = m.msgPort
  
  networking.InitializeNetworkDownloads = InitializeNetworkDownloads
  networking.StartSync = StartSync
  networking.URLEvent = URLEvent
  networking.PoolEvent = PoolEvent
  networking.SyncPoolProgressEvent = SyncPoolProgressEvent
  networking.GetPoolFilePath = GetPoolFilePath
  
  networking.SetPoolSizes = SetPoolSizes
  
  networking.AddUploadHeaders = AddUploadHeaders
  
  networking.SendError = SendError
  networking.SendErrorCommon = SendErrorCommon
  networking.SendEvent = SendEvent
  networking.SendEventCommon = SendEventCommon
  networking.SetSystemInfo = SetSystemInfo
  
  ' logging
  networking.UploadLogFiles = UploadLogFiles
  networking.UploadLogFileHandler = UploadLogFileHandler
  networking.uploadLogFileURLXfer = CreateObject("roUrlTransfer")
  networking.uploadLogFileURLXfer.SetPort(networking.msgPort)
  networking.uploadLogFileURL$ = ""
  networking.uploadLogFolder = "logs"
  networking.uploadLogArchiveFolder = "archivedLogs"
  networking.uploadLogFailedFolder = "failedLogs"
  networking.enableLogDeletion = true
  
  networking.POOL_EVENT_FILE_DOWNLOADED = 1
  networking.POOL_EVENT_FILE_FAILED = -1
  networking.POOL_EVENT_ALL_DOWNLOADED = 2
  networking.POOL_EVENT_ALL_FAILED = -2
  
  networking.EVENT_REALIZE_SUCCESS = 101
  
  networking.SYNC_ERROR_CANCELLED = -10001
  networking.SYNC_ERROR_CHECKSUM_MISMATCH = -10002
  networking.SYNC_ERROR_EXCEPTION = -10003
  networking.SYNC_ERROR_DISK_ERROR = -10004
  networking.SYNC_ERROR_POOL_UNSATISFIED = -10005
  
  networking.URL_EVENT_COMPLETE = 1
  
  du = CreateObject("roStorageInfo", "./")
  networking.cardSizeInMB = du.GetSizeInMegabytes()
  du = 0
  
  networking.assetPool = CreateObject("roAssetPool", "pool")
  
  return networking
  
end function


Function InitializeNetworkDownloads(setupParams as object) as boolean

  ' create setup splash screen
  ' only set splash screen if device supports video
  videoMode = CreateObject("roVideoMode")
	if type(videoMode) = "roVideoMode" then
    m.deviceSetupSplashScreen = SetDeviceSplashScreenMessageByType(invalid, "sfn_initialize", m.msgPort)
  end if

  ' Load up the current sync specification so we have it ready
  m.currentSync = CreateObject("roSyncSpec")
  if not m.currentSync.ReadFromFile("current-sync.json") then
    m.diagnostics.PrintDebug("### No current sync state available")
    stop
  end if
  
  m.nextURL$ = setupParams.nextURL$
  m.user$ = setupParams.user$
  m.password$ = GetPassword(setupParams.password$)
  m.enableBasicAuthentication = setupParams.enableBasicAuthentication
  m.uploadLogFileURL$ = setupParams.uploadLogFileURL$
  
  registrySection = CreateObject("roRegistrySection", "networking")
  
  supervisorRegistrySection = CreateObject("roRegistrySection", "!supervisor.brightsignnetwork.com")
  if type(supervisorRegistrySection) <> "roRegistrySection" then
    m.diagnostics.PrintDebug("Error: Unable to create supervisorRegistrySection roRegistrySection"): stop
  end if

' Create application connection to wait for bootstrap messages
  CreateApplicationConnection(setupParams, m.msgPort, registrySection)
  m.wsUdpSocketPort% = setupParams.wsUdpSocketPort%

  SendStatusPayload(setupParams, m.setupVersion$)

  m.proceedAfterRegistrationProcessComplete = false
  registrationFlag = registrySection.Read("registered_with_bsn")
  if IsTruthy(registrationFlag) = true then
    m.proceedAfterRegistrationProcessComplete = true
  end if

  m.contentDownloadComplete = false

  ' This timer will be triggered in function PoolEvent
  m.registrationResponseTimer = CreateObject("roTimer")
  m.registrationResponseTimer.SetPort(m.msgPort)
  m.registrationResponseTimer.SetElapsed(60, 0)
  
  if setupParams.user$ <> "" and setupParams.password$ <> "" then
    m.setUserAndPassword = true
  else
    m.setUserAndPassword = false
  end if
  
  oldBsnce = supervisorRegistrySection.Read("bsnce")
  ClearBsnce(supervisorRegistrySection, setupParams.bsnCloudEnabled)
  ClearRegistryKeys(registrySection)

  ' BSN.cloud
  SetBsnCloudParameters(setupParams, registrySection)

  performLegacySetup = true

  supervisorSupportsConfigureNetwork = GetSupervisorSupportsConfigureNetwork(m.msgPort)

  if supervisorSupportsConfigureNetwork then
    registrationRequestResult = TriggerRegistrationRequest(registrySection, m.msgPort)
    if registrationRequestResult = "networkSetupSuccess" then
      performLegacySetup = false
    else if registrationRequestResult = "networkSetupFailure" then
      if type(videoMode) = "roVideoMode" then
        deviceSetupSplashScreen = SetDeviceSplashScreenMessageByType(invalid, "network_setup_failure", m.msgPort)
      endif
      registrySection.Delete("registration_in_progress")
      registrySection.Flush()
      while true
        wait(0, m.msgPort)
      end while
    endif
  else
    ' always perform legacy setup if the supervisor doesn't support configure network
    registrationRequired = RegistrationReset(registrySection)
    if registrationRequired then
      registrationRequestResult = TriggerRegistrationRequest(registrySection, m.msgPort)
      ' TEDTODO - splash screen here as well?
    end if
  endif

' Update BSN.cloud in case it’s deleted by force registration reset
  SetBsnCloudParameters(setupParams, registrySection)

  ' retrieve and parse featureMinRevs.json
  featureMinRevs = ParseFeatureMinRevs()
  
  modelSupportsWifi = GetModelSupportsWifi()

  registrySection.Write("sut", "SFN")

  if performLegacySetup then

    ClearNetworkRegistryKeys(registrySection, setupParams.inheritNetworkProperties)

    if setupParams.inheritNetworkProperties then
      registrySection.Write("inp", "yes")
    endif

    WriteRegistryVersion(registrySection, setupParams.version)

    if setupParams.inheritNetworkProperties then

      WriteTimeServer(setupParams, registrySection)
      GetProxy(setupParams, registrySection)
      registrySection.Write("inp", "yes")
    
      checkINPWifiConnectionTimerCount = 0

      ' Wireless parameters
      wirelessConnectionStatus = GetWirelessParametersInheritNetworkProperties(setupParams, registrySection, modelSupportsWifi, checkINPWifiConnectionTimerCount) 

      if wirelessConnectionStatus = "wifiConnectionPending" then
        ' This timer will be triggered when a wifi setup was configured previous to the
        ' current inheritNetworkProperties setup, but is slow to receive a link and ip address
        connectionTimerMsgPort = CreateObject("roMessagePort")

        checkINPWifiConnectionTimerInterval = 15
        checkINPWifiConnectionTimer = CreateObject("roTimer")
        checkINPWifiConnectionTimer.SetPort(connectionTimerMsgPort)

        while checkINPWifiConnectionTimerCount < 3
          if wirelessConnectionStatus = "wifiConnectionPending" then
            ' Update wifi check count, interval, and start timer again
            ' Intervals will be 15, 30, 90 seconds
            checkINPWifiConnectionTimerInterval = checkINPWifiConnectionTimerInterval * (checkINPWifiConnectionTimerCount + 1)
            checkINPWifiConnectionTimerCount = checkINPWifiConnectionTimerCount + 1
            checkINPWifiConnectionTimer.SetElapsed(checkINPWifiConnectionTimerInterval, 0)

            m.diagnostics.PrintDebug("### Attempting to inherit current Wifi configuration and establish a connection, retry count:" + strI(checkINPWifiConnectionTimerCount))

            checkINPWifiConnectionTimer.Start()

            ' wait for next timer event from checkINPWifiConnectionTimer
            receivedINPWifiConnectionTimerEvent = false
            while receivedINPWifiConnectionTimerEvent = false
              msg = wait(0, connectionTimerMsgPort)

              ' check wifi connection again for a link and ipv4 address
              if (type(msg) = "roTimerEvent") and type(checkINPWifiConnectionTimer) = "roTimer" and stri(msg.GetSourceIdentity()) = stri(checkINPWifiConnectionTimer.GetIdentity()) then 
                receivedINPWifiConnectionTimerEvent = true
                wirelessConnectionStatus = GetWirelessParametersInheritNetworkProperties(setupParams, registrySection, modelSupportsWifi, checkINPWifiConnectionTimerCount)
              end if
            end while
          else
            exit while
          end if
        end while

        ' Free variables no longer needed
        connectionTimerMsgPort = invalid
        checkINPWifiConnectionTimer = invalid
      end if

      ' initialize useWireless from retrieved wifi status
      if wirelessConnectionStatus = "wifiConnected" then
        useWireless = true
      else
        useWireless = false
      end if

      ' Wired parameters
      SetWiredParameters(setupParams, registrySection, useWireless)

      ' Network configurations
      SetNetworkConfigurationInterfaces(setupParams, registrySection, useWireless, modelSupportsWifi)          

    else
      ' Hostname
      SetHostname(setupParams.specifyHostname, setupParams.hostName$)
      
      ' Wireless parameters
      useWireless = SetWirelessParameters(setupParams, registrySection, modelSupportsWifi)

      ' Wired parameters
      SetWiredParameters(setupParams, registrySection, useWireless)

      ' Network configurations
      networkConfigurationInterfaces = SetNetworkConfigurationInterfaces(setupParams, registrySection, useWireless, modelSupportsWifi)

      ' network configuration parameters. read from setupParams, set roNetworkConfiguration, write to registry
      proxySpec$ = GetProxy(setupParams, registrySection)
      bypassProxyHosts = GetBypassProxyHosts(proxySpec$, setupParams)

      'Timer server
      WriteTimeServer(setupParams, registrySection)

      ' Network connection priorities
      networkConnectionPriorityWired% = setupParams.networkConnectionPriorityWired%
      networkConnectionPriorityWireless% = setupParams.networkConnectionPriorityWireless%
      
      ' configure ethernet
      ConfigureEthernet(networkConfigurationInterfaces.ethernetNetworkingParameters, networkConnectionPriorityWired%, setupParams.timeServer$, proxySpec$, bypassProxyHosts, featureMinRevs)
      
      ' configure wifi if specified and device supports wifi
      if useWireless
        ConfigureWifi(networkConfigurationInterfaces.wifiNetworkingParameters, setupParams.ssid$, setupParams.passphrase$, networkConnectionPriorityWireless%, setupParams.timeServer$, proxySpec$, bypassProxyHosts, featureMinRevs)
      end if
      
      ' if a device is setup to not use wireless, ensure that wireless is not used (for wireless model only)
      if not useWireless and modelSupportsWifi then
        DisableWireless()
      end if
    end if
  
  else

    ' Network configurations
    networkConfigurationInterfaces = SetNetworkConfigurationInterfaces(setupParams, registrySection, setupParams.useWireless, modelSupportsWifi)

    ConfigureEthernetWPA(networkConfigurationInterfaces.ethernetNetworkingParameters)

    if setupParams.useWireless then
      ConfigureWifiWPA(networkConfigurationInterfaces.wifiNetworkingParameters)
    endif

  endif

  ' determine bindings
  
  m.contentXfersBinding% = GetBinding(setupParams.contentDataTypeEnabledWired, setupParams.contentDataTypeEnabledWireless)
  m.logUploadsXfersBinding% = GetBinding(setupParams.logUploadsXfersEnabledWired, setupParams.logUploadsXfersEnabledWireless)
  
  ' net connect parameters. read from setupParams, write to registry
  if type(registrySection) <> "roRegistrySection" then print "Error: Unable to create roRegistrySection": stop
  registrySection.Write("tbnc", GetNumericStringFromNumber(setupParams.timeBetweenNetConnects%))
  
  registrySection.Write("cdr", GetYesNoFromBoolean(setupParams.contentDownloadsRestricted))
  registrySection.Write("cdrs", GetNumericStringFromNumber(setupParams.contentDownloadRangeStart%))
  registrySection.Write("cdrl", GetNumericStringFromNumber(setupParams.contentDownloadRangeLength%))
  
  ' diagnostic web server
  SetDWS(setupParams, registrySection)
  
  ' remote snapshot
  SetRemoteSnapshot(setupParams, registrySection)
  
  ' idle screen color
  SetIdleColor(setupParams, registrySection)
  
  ' custom splash screen
  SetCustomSplashScreen(setupParams, registrySection, featureMinRevs)
  
  ' local web server
  SetLWS(setupParams, registrySection)
  
  ' Recovery
  SetRecoveryHandlerUrl(setupParams, registrySection)

  ' unit name parameters. read from setupParams, write to registry
  registrySection.Write("tz", setupParams.timezone$)
  registrySection.Write("un", setupParams.unitName$)
  registrySection.Write("unm", setupParams.unitNamingMethod$)
  registrySection.Write("ud", setupParams.unitDescription$)
  
  ' bsnCloudEnabled === False, bsnce === False then DISABLE_BSN_CLOUD (Danbert) = true, set bsnce and restart
  ' bsnCloudEnabled === undefined, bsnce === undefined, restart to clear bsnce
  ' This variable will never be set to true. Only will ever be undefined or false, set in device setup files. Bootstrap will look for bsnce reference
  ' Will put a RestartApplication() at the end of setup if restartRequired = true.
  if (setupParams.restartRequired = true) then m.restartRequired = true
  if IsTruthy(setupParams.bsnCloudEnabled) <> invalid and IsTruthy(setupParams.bsnCloudEnabled) = false and supervisorRegistrySection.Read("bsnce") <> "False" then
    supervisorRegistrySection.Write("bsnce", "False")
    supervisorRegistrySection.Flush()
    m.restartRequired = true
  else if IsTruthy(setupParams.bsnCloudEnabled) = invalid and oldBsnce = "False" then ' bsnce registry no longer requested to be false from setup files
    m.restartRequired = true
  end if
  
  ' registry writes complete - flush it
  registrySection.Flush()
  
  ' perform network diagnostics if enabled
  if setupParams.networkDiagnosticsEnabled then
    PerformNetworkDiagnostics(setupParams.testEthernetEnabled, setupParams.testWirelessEnabled, setupParams.testInternetEnabled)
  end if
  
  m.diagnostics.PrintTimestamp()
  m.diagnostics.PrintDebug("### Currently active sync list suggests next URL of " + m.nextURL$)
  
  if m.nextURL$ = "" then stop
  
  ' Check for updates every minute
  m.checkAlarm = CreateObject("roTimer")
  m.checkAlarm.SetPort(m.msgPort)
  m.checkAlarm.SetDate( - 1, - 1, - 1)
  m.checkAlarm.SetTime( - 1, - 1, 0, 0)
  if not m.checkAlarm.Start() then stop
  
  return true
  
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)
  plainText = decrypted.toAsciiString()
  return plainText
  
end function

  
Sub StartSync()
  
  ' Call when you want to start a sync operation
  
  m.diagnostics.PrintTimestamp()
  m.diagnostics.PrintDebug("### start_sync")
  
  if type(m.syncPool) = "roSyncPool" 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.diagnostics.PrintDebug("### sync already active so we'll let it continue")
    m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_SYNC_ALREADY_ACTIVE, "")
    return
  end if
  
  m.xfer = CreateObject("roUrlTransfer")
  m.xfer.SetPort(m.msgPort)
  
  ' 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.
  
  m.diagnostics.PrintDebug("### Looking for new sync list from " + m.nextURL$)
  m.xfer.SetUrl(m.nextURL$)
  
  if m.setUserAndPassword then m.xfer.SetUserAndPassword(m.user$, m.password$)
  m.xfer.EnableUnsafeAuthentication(m.enableBasicAuthentication)
  headers = m.currentSync.GetMetadata("server")
  CleanServerHeaders(headers)
  m.xfer.SetHeaders(headers)

  ' Add device unique identifier, timezone
  m.xfer.AddHeader("DeviceID", m.deviceUniqueID$)
  m.xfer.AddHeader("DeviceFWVersion", m.deviceFWVersion$)
  m.xfer.AddHeader("DeviceSWVersion", "autorun-setup.brs " + m.setupVersion$)
  m.xfer.AddHeader("timezone", m.systemTime.GetTimeZone())
  
  ' Add card size
  m.xfer.AddHeader("storage-size", str(m.cardSizeInMB))
  
  m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_CHECK_CONTENT, m.nextURL$)
  
  m.diagnostics.PrintDebug("binding for StartSync is " + stri(m.contentXfersBinding%))
  ok = m.xfer.BindToInterface(m.contentXfersBinding%)
  if not ok then stop
  if not m.xfer.AsyncGetToFile("tmp:new-sync.json") then stop
  
  return
  
end sub


Sub SetPoolSizes(syncSpec as object) as object
  limitStorageSpace = syncSpecValueTrue(syncSpec.LookupMetadata("client", "limitStorageSpace"))
  if limitStorageSpace then
    spaceLimitedByAbsoluteSize = syncSpecValueTrue(syncSpec.LookupMetadata("client", "spaceLimitedByAbsoluteSize"))
    publishedDataSizeLimitMB = syncSpec.LookupMetadata("client", "publishedDataSizeLimitMB")
    publishedDataSizeLimitPercentage = syncSpec.LookupMetadata("client", "publishedDataSizeLimitPercentage")
  end if
  
  if limitStorageSpace then
    
    if spaceLimitedByAbsoluteSize then
      
      ' convert from percentage settings to absolute settings
      du = CreateObject("roStorageInfo", "./")
      totalCardSizeMB% = du.GetSizeInMegabytes()
      
      ' pool size for published data
      publishedDataSizeLimitPercentage% = int(val(publishedDataSizeLimitPercentage))
      maximumPublishedDataPoolSizeMB% = publishedDataSizeLimitPercentage% * totalCardSizeMB% / 100
      
    else
      
      maximumPublishedDataPoolSizeMB% = int(val(publishedDataSizeLimitMB))
      
    end if
    
    ok = m.assetPool.SetMaximumPoolSizeMegabytes(maximumPublishedDataPoolSizeMB%)
    ' if not ok ??
    
  else
    ' clear prior settings
    ok = m.assetPool.SetMaximumPoolSizeMegabytes( - 1)
    ' if not ok ??
    
  end if
  
end sub


' Call when we get a URL event
Sub URLEvent(msg as object)
  
  m.diagnostics.PrintTimestamp()
  m.diagnostics.PrintDebug("### url_event")
  
  if type (m.xfer) <> "roUrlTransfer" then return
  if msg.GetSourceIdentity() = m.xfer.GetIdentity() then
    if msg.GetInt() = m.URL_EVENT_COMPLETE then
      xferInUse = false
      if msg.GetResponseCode() = 200 then
        m.newSync = CreateObject("roSyncSpec")
        if m.newSync.ReadFromFile("tmp:new-sync.json") then
          m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_SYNCSPEC_RECEIVED, "YES")
          m.diagnostics.PrintDebug("### Server gave us spec: " + m.newSync.GetName())
          readySync = CreateObject("roSyncSpec")
          if readySync.ReadFromFile("ready-sync.xml") then
            if m.newSync.EqualTo(readySync) then
              m.diagnostics.PrintDebug("### Server has given us a spec that matches ready-sync. Nothing more to do.")
              DeleteFile("tmp:new-sync.xml")
              readySync = 0
              m.newSync = 0
              return
            end if
          end if
          
          ' Anything the server has given us supersedes ready-sync.xml so we'd better delete it and cancel its alarm
          DeleteFile("ready-sync.xml")
          
          ' Log the start of sync list download
          m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_DOWNLOAD_START, "")
          m.SendEvent("StartSyncListDownload", m.newSync.GetName(), "")
          
          m.SetPoolSizes(m.newSync)
          
          m.syncPool = CreateObject("roSyncPool", "pool")
          m.syncPool.SetPort(m.msgPort)
          m.syncPool.SetMinimumTransferRate(1000, 900)
          m.syncPool.SetFileProgressIntervalSeconds(15)
          if m.setUserAndPassword then m.syncPool.SetUserAndPassword(m.user$, m.password$)
          m.syncPool.EnableUnsafeAuthentication(m.enableBasicAuthentication)
          headers = m.newSync.GetMetadata("server")
          CleanServerHeaders(headers)
          m.syncPool.SetHeaders(headers)

          m.syncPool.AddHeader("DeviceID", m.deviceUniqueID$)
          
          m.diagnostics.PrintDebug("binding for syncPool is " + stri(m.contentXfersBinding%))
          ok = m.syncPool.BindToInterface(m.contentXfersBinding%)
          if not ok then stop
          
          ' implies dodgy XML, or something is already running. could happen if server sends down bad xml.
          if not m.syncPool.AsyncDownload(m.newSync) then
            m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_IMMEDIATE_FAILURE, m.syncPool.GetFailureReason())
            m.diagnostics.PrintTimestamp()
            m.diagnostics.PrintDebug("### AsyncDownload failed: " + m.syncPool.GetFailureReason())
            m.SendError("AsyncDownloadFailure", m.syncPool.GetFailureReason(), "", m.newSync.GetName())
            m.newSync = 0
          end if
          ' implies dodgy XML, or something is already running. could happen if server sends down bad xml.
        else
          m.diagnostics.PrintDebug("### Failed to read new-sync.xml")
          m.SendError("Failed to read new-sync.xml", "", "", m.newSync.GetName())
          m.newSync = 0
        end if
      else if msg.GetResponseCode() = 404 then
        m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_NO_SYNCSPEC_AVAILABLE, "404")
        m.diagnostics.PrintDebug("### Server has no sync list for us: " + str(msg.GetResponseCode()))
        RestartIfNecessary(m, false)
        ' only set splash screen if device supports video
        videoMode = CreateObject("roVideoMode")
        if type(videoMode) = "roVideoMode" then
          m.apf = CreateObject("roAssetPoolFiles", m.assetPool, m.currentSync)
          autoscheduleFilePath = m.apf.GetPoolFilePath("autoschedule.json")
          if autoscheduleFilePath = "" then
            m.diagnostics.PrintDebug("### Set the setup splash screen for SFN")
            m.deviceSetupSplashScreen = SetDeviceSplashScreenMessageByType(m.deviceSetupSplashScreen, "sfn_complete", m.msgPort)
          end if
        end if
        ' The server has no new sync for us. That means if we have one lined up then we should destroy it.
        DeleteFile("ready-sync.xml")
      else
        ' retry - server returned something other than a 200 or 404
        m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_RETRIEVE_SYNCSPEC_FAILURE, str(msg.GetResponseCode()))
        m.diagnostics.PrintDebug("### Failed to download sync list: " + stri(msg.GetResponseCode()))
        m.SendError("Failed to download sync list", "", str(msg.GetResponseCode()), "")
        RestartIfNecessary(m, false)
        ' only set splash screen if device supports video
        videoMode = CreateObject("roVideoMode")
        if type(videoMode) = "roVideoMode" then
          m.deviceSetupSplashScreen = SetDeviceSplashScreenMessageByType(m.deviceSetupSplashScreen, "sfn_error", m.msgPort)
        end if
      end if
    else
      m.diagnostics.PrintDebug("### Progress URL event - we don't know about those.")
    end if
    
  else
    m.diagnostics.PrintDebug("### url_event from beyond this world: " + str(msg.GetSourceIdentity()) + ", " + str(msg.GetResponseCode()) + ", " + str(msg.GetInt()))
    m.SendError("url_event from beyond this world", "", "", str(msg.GetSourceIdentity()))
  end if
  
  return
  
end sub


Sub SyncPoolProgressEvent(msg as object)
  
  m.diagnostics.PrintDebug("### File download progress " + msg.GetFileName() + str(msg.GetCurrentFilePercentage()))
  
  m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_FILE_DOWNLOAD_PROGRESS, msg.GetFileName() + chr(9) + str(msg.GetCurrentFilePercentage()))
  
end sub


' Call when we get a sync event
Sub PoolEvent(msg as object)
  m.diagnostics.PrintTimestamp()
  m.diagnostics.PrintDebug("### pool_event")
  if type(m.syncPool) <> "roSyncPool" then
    m.diagnostics.PrintDebug("### pool_event but we have no object")
    return
  end if
  if msg.GetSourceIdentity() = m.syncPool.GetIdentity() then
    if (msg.GetEvent() = m.POOL_EVENT_FILE_DOWNLOADED) then
      m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_FILE_DOWNLOAD_COMPLETE, msg.GetName())
      m.diagnostics.PrintDebug("### File downloaded " + msg.GetName())
    else if (msg.GetEvent() = m.POOL_EVENT_FILE_FAILED) then
      m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_FILE_DOWNLOAD_FAILURE, msg.GetName() + chr(9) + msg.GetFailureReason())
      m.diagnostics.PrintDebug("### File failed " + msg.GetName() + ": " + msg.GetFailureReason())
      m.SendError("FileDownloadFailure", msg.GetFailureReason(), str(msg.GetResponseCode()), msg.GetName())
    else if (msg.GetEvent() = m.POOL_EVENT_ALL_DOWNLOADED) then
      m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_DOWNLOAD_COMPLETE, "")
      m.diagnostics.PrintDebug("### All downloaded for " + m.newSync.GetName())
      
      ' Log the end of sync list download
      m.SendEvent("EndSyncListDownload", m.newSync.GetName(), str(msg.GetResponseCode()))
      
      ' Save to current-sync.xml then do cleanup
      ''		    if not m.newSync.WriteToFile("current-sync.xml") then stop
      
      jsonSyncSpec$ = m.newSync.WriteToString({ format : "json" })
      ok = WriteAsciiFile("current-sync.json", jsonSyncSpec$)
      if not ok then stop
      
      timezone = m.newSync.LookupMetadata("client", "timezone")
      if timezone <> "" then
        m.systemTime.SetTimeZone(timezone)
      end if
      
      m.diagnostics.PrintDebug("### DOWNLOAD COMPLETE")
      
      m.spf = CreateObject("roSyncPoolFiles", "pool", m.newSync)
      
      newSyncSpecScriptsOnly = m.newSync.FilterFiles("download", { group: "script" })
      event = m.syncPool.Realize(newSyncSpecScriptsOnly, "/")
      if event.GetEvent() <> m.EVENT_REALIZE_SUCCESS then
        m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_REALIZE_FAILURE, stri(event.GetEvent()) + chr(9) + event.GetName() + chr(9) + event.GetFailureReason())
        m.diagnostics.PrintDebug("### Realize failed " + stri(event.GetEvent()) + chr(9) + event.GetName() + chr(9) + event.GetFailureReason())
        m.SendError("RealizeFailure", event.GetName(), event.GetFailureReason(), str(event.GetEvent()))
      else
        ' Entering here means finished downloading files and ready to restart.
        ' If proceedAfterRegistrationProcessComplete flag is set to true, then device has registered. -> restart to move forward.

        ' Registration process may be complete by:
        ' 1. a successful registration(flag is set or get udp message from supervisor) 
        ' 2. timeout occurring prior to receiving a successful message from the supervisor
        if m.proceedAfterRegistrationProcessComplete then
          m.deviceSetupSplashScreen = invalid
          m.logging.FlushLogFile()
          m.logging.registrySection.Delete("registration_in_progress")
          m.logging.registrySection.Flush()
          a = RestartApplication()
          stop
        end if
        ' If flag is false, then set contentDownloadComplete flag and wait for bootstrap udp message for a min -> Eventloop will both wait for a timer and a udp message.
        m.contentDownloadComplete = true
        m.registrationResponseTimer.Start()
        return
      end if
      
      DeleteFile("tmp:new-sync.xml")
      m.newSync = invalid
      m.syncPool = invalid
    else if (msg.GetEvent() = m.POOL_EVENT_ALL_FAILED) then
      m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_FAILURE, msg.GetFailureReason())
      m.diagnostics.PrintDebug("### Sync failed: " + msg.GetFailureReason())
      m.SendError("SyncFailure", msg.GetFailureReason(), str(msg.GetResponseCode()), "")
      m.newSync = invalid
      m.syncPool = invalid
    end if
  else
    m.diagnostics.PrintDebug("### pool_event from beyond this world: " + str(msg.GetSourceIdentity()))
  end if
  return
  
end sub


Sub UploadLogFiles()
  
  if m.uploadLogFileURL$ = "" then return
  
  ' 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
  
  ' upload the first file
  for each file in listOfLogFiles
    m.diagnostics.PrintDebug("### UploadLogFiles " + file + " to " + m.uploadLogFileURL$)
    fullFilePath = m.uploadLogFolder + "/" + file
    
    contentDisposition$ = GetContentDisposition(file)
    m.AddUploadHeaders(m.uploadLogFileURLXfer, contentDisposition$)
    
    m.diagnostics.PrintDebug("binding for UploadLogFiles is " + stri(m.logUploadsXfersBinding%))
    ok = m.uploadLogFileURLXfer.BindToInterface(m.logUploadsXfersBinding%)
    if not ok then stop
    
    ok = m.uploadLogFileURLXfer.AsyncPostFromFile(fullFilePath)
    if not ok then stop
    m.logFileUpload = fullFilePath
    m.logFile$ = file
    return
  next
  
end sub


Sub UploadLogFileHandler(msg as object)
  
  if msg.GetResponseCode() = 200 then
    
    if type(m.logFileUpload) = "roString" then
      m.diagnostics.PrintDebug("###  UploadLogFile XferEvent - successfully uploaded " + m.logFileUpload)
      if m.enableLogDeletion then
        DeleteFile(m.logFileUpload)
      else
        target$ = m.uploadLogArchiveFolder + "/" + m.logFile$
        ok = MoveFile(m.logFileUpload, target$)
      end if
      m.logFileUpload = invalid
    end if
    
  else
    
    if type(m.logFileUpload) = "roString" then
      m.diagnostics.PrintDebug("### Failed to upload log file " + m.logFileUpload + ", error code = " + str(msg.GetResponseCode()))
      
      ' move file so that the script doesn't try to upload it again immediately
      target$ = m.uploadLogFailedFolder + "/" + m.logFile$
      ok = MoveFile(m.logFileUpload, target$)
      
    end if
    
    m.logging.WriteDiagnosticLogEntry(m.diagnosticCodes.EVENT_LOGFILE_UPLOAD_FAILURE, str(msg.GetResponseCode()))
    
  end if
  
  m.UploadLogFiles()
  
end sub


Sub CleanServerHeaders(serverHeaders as object)
  serverHeaders.delete("user") 
  serverHeaders.delete("password")
end sub


Sub AddUploadHeaders(url as object, contentDisposition$)
  
  headers = m.currentSync.GetMetadata("server")
  CleanServerHeaders(headers)
  url.SetHeaders(headers)

  ' Add device unique identifier, timezone
  url.AddHeader("DeviceID", m.deviceUniqueID$)
  
  url.AddHeader("DeviceModel", m.deviceModel$)
  url.AddHeader("DeviceFamily", m.deviceFamily$)
  url.AddHeader("DeviceFWVersion", m.deviceFWVersion$)
  url.AddHeader("DeviceSWVersion", m.setupVersion$)
  
  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


Function GetPoolFilePath(fileName$ as string) as object
  
  return m.spf.GetPoolFilePath(fileName$)
  
end function


Function SendEventCommon(eventURL as object, eventType$ as string, eventData$ as string, eventResponseCode$ as string) as string
  
  m.diagnostics.PrintDebug("### send_event")
  
  eventURL.SetUrl(m.event_url$)
  eventURL.AddHeader("account", m.account$)
  eventURL.AddHeader("group", m.group$)
  eventURL.AddHeader("DeviceID", m.deviceUniqueID$)
  eventURL.AddHeader("DeviceFWVersion", m.deviceFWVersion$)
  eventURL.AddHeader("DeviceSWVersion", "recovery_runsetup.brs " + m.setupVersion$)
  eventStr$ = "EventType=" + eventType$ + "&EventData=" + eventData$ + "&ResponseCode=" + eventResponseCode$
  
  return eventStr$
  
end function


Sub SendEvent(eventType$ as string, eventData$ as string, eventResponseCode$ as string)
  
  return
  
  eventURL = CreateObject("roUrlTransfer")
  
  eventStr$ = m.SendEventCommon(eventURL, eventType$, eventData$, eventResponseCode$)
  
  eventURL.AsyncPostFromString(eventStr$)
  
  m.diagnostics.WriteToLog(eventType$, eventData$, eventResponseCode$, m.account$)
  
  return
  
end sub


Function SendErrorCommon(errorURL as object, errorType$ as string, errorReason$ as string, errorResponseCode$ as string, errorData$ as string) as string
  
  m.diagnostics.PrintDebug("### send_error")
  
  errorURL = CreateObject("roUrlTransfer")
  errorURL.SetUrl(m.error_url$)
  errorURL.AddHeader("account", m.account$)
  errorURL.AddHeader("group", m.group$)
  errorURL.AddHeader("DeviceID", m.deviceUniqueID$)
  errorURL.AddHeader("DeviceFWVersion", m.deviceFWVersion$)
  errorURL.AddHeader("DeviceSWVersion", "recovery_runsetup.bas " + m.setupVersion$)
  errorStr$ = "ErrorType=" + errorType$ + "&FailureReason=" + errorReason$ + "&ResponseCode=" + errorResponseCode$ + "&ErrorData=" + errorData$
  
  return errorStr$
  
end function


Sub SendError(errorType$ as string, errorReason$ as string, errorResponseCode$ as string, errorData$ as string)
  
  return
  
  errorURL = CreateObject("roUrlTransfer")
  
  errorStr$ = m.SendErrorCommon(errorURL, errorType$, errorReason$, errorResponseCode$, errorData$)
  
  if not errorURL.AsyncPostFromString(errorStr$) then stop
  
  return
  
end sub


REM *******************************************************
REM *******************************************************
REM ***************                    ********************
REM *************** LOGGING OBJECT     ********************
REM ***************                    ********************
REM *******************************************************
REM *******************************************************

REM
REM construct a new logging BrightScript object
REM
Function newLogging() as object
  
  logging = CreateObject("roAssociativeArray")
  
  logging.logging = true

  logging.msgPort = m.msgPort
  logging.systemTime = m.systemTime
  logging.diagnostics = m.diagnostics
  
  logging.SetSystemInfo = SetSystemInfo
  
  logging.registrySection = CreateObject("roRegistrySection", "networking")
  if type(logging.registrySection) <> "roRegistrySection" then print "Error: Unable to create roRegistrySection": stop
  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.WriteDiagnosticLogEntry = WriteDiagnosticLogEntry
  logging.PushLogFile = PushLogFile
  logging.CutoverLogFile = CutoverLogFile
  logging.HandleTimerEvent = HandleLoggingTimerEvent
  logging.PushLogFilesOnBoot = PushLogFilesOnBoot
  logging.OpenOrCreateCurrentLog = OpenOrCreateCurrentLog
  logging.DeleteExpiredFiles = DeleteExpiredFiles
  logging.DeleteOlderFiles = DeleteOlderFiles
  logging.FlushLogFile = FlushLogFile
  logging.logFile = invalid
  
  logging.uploadLogFolder = "logs"
  logging.uploadLogArchiveFolder = "archivedLogs"
  logging.uploadLogFailedFolder = "failedLogs"
  logging.logFileUpload = invalid
  
  logging.playbackLoggingEnabled = false
  logging.eventLoggingEnabled = false
  logging.stateLoggingEnabled = false
  logging.diagnosticLoggingEnabled = false
  logging.variableLoggingEnabled = false
  logging.uploadLogFilesAtBoot = false
  logging.uploadLogFilesAtSpecificTime = false
  logging.uploadLogFilesTime% = 0
  
  logging.OpenLogFile = OpenLogFile
  logging.CloseLogFile = CloseLogFile
  logging.WriteToLog = WriteToLog
  logging.RotateLogFiles = RotateLogFiles

  CreateDirectory("logs")
  CreateDirectory("currentLog")
  CreateDirectory("archivedLogs")
  CreateDirectory("failedLogs")

  logging.OpenLogFile()

  return logging
  
end function


Function CreateLogFile(logDateKey$ as string, logCounterKey$ as string) as object
  
  dtLocal = m.systemTime.GetLocalDateTime()
  year$ = Right(stri(dtLocal.GetYear()), 2)
  month$ = StripLeadingSpaces(stri(dtLocal.GetMonth()))
  if len(month$) = 1 then
    month$ = "0" + month$
  end if
  day$ = StripLeadingSpaces(stri(dtLocal.GetDay()))
  if len(day$) = 1 then
    day$ = "0" + day$
  end if
  dateString$ = year$ + month$ + day$
  
  logDate$ = m.registrySection.Read(logDateKey$)
  logCounter$ = m.registrySection.Read(logCounterKey$)
  
  if logDate$ = "" or logCounter$ = "" then
    logCounter$ = "000"
  else if logDate$ <> dateString$ then
    logCounter$ = "000"
  end if
  logDate$ = dateString$
  
  localFileName$ = "BrightSign" + "Log." + m.deviceUniqueID$ + "-" + dateString$ + logCounter$ + ".log"
  
  ' at a later date, move this code to the point where the file has been uploaded successfully
  m.registrySection.Write(logDateKey$, logDate$)
  
  logCounter% = val(logCounter$)
  logCounter% = logCounter% + 1
  if logCounter% > 999 then
    logCounter% = 0
  end if
  logCounter$ = StripLeadingSpaces(stri(logCounter%))
  if len(logCounter$) = 1 then
    logCounter$ = "00" + logCounter$
  else if len(logCounter$) = 2 then
    logCounter$ = "0" + logCounter$
  end if
  m.registrySection.Write(logCounterKey$, logCounter$)
  
  fileName$ = "currentLog/" + localFileName$
  logFile = CreateObject("roCreateFile", fileName$)
  m.diagnostics.PrintDebug("Create new log file " + localFileName$)
  
  t$ = chr(9)
  
  ' version
  header$ = "BrightSignLogVersion" + t$ + "1"
  logFile.SendLine(header$)
  
  ' serial number
  header$ = "SerialNumber" + t$ + m.deviceUniqueID$
  logFile.SendLine(header$)
  
  ' group id
  if type(m.networking.currentSync) = "roSyncSpec" then
    header$ = "Account" + t$ + m.networking.currentSync.LookupMetadata("server", "account")
    logFile.SendLine(header$)
    header$ = "Group" + t$ + m.networking.currentSync.LookupMetadata("server", "group")
    logFile.SendLine(header$)
  end if
  
  ' 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.deviceFWVersion$
  logFile.SendLine(header$)
  
  ' script version
  header$ = "ScriptVersion" + t$ + m.setupVersion$
  logFile.SendLine(header$)
  
  ' custom script version
  header$ = "CustomScriptVersion" + t$ + ""
  logFile.SendLine(header$)
  
  ' model
  header$ = "Model" + t$ + m.deviceModel$
  logFile.SendLine(header$)
  
  logFile.AsyncFlush()
  
  return logFile
  
end function


Sub MoveExpiredCurrentLog()
  
  dtLocal = m.systemTime.GetLocalDateTime()
  currentDate$ = StripLeadingSpaces(stri(dtLocal.GetDay()))
  if len(currentDate$) = 1 then
    currentDate$ = "0" + currentDate$
  end if
  
  listOfPendingLogFiles = MatchFiles("/currentLog", "*")
  
  for each file in listOfPendingLogFiles
    
    logFileDate$ = left(right(file, 9), 2)
    
    if logFileDate$ <> currentDate$ then
      sourceFilePath$ = "currentLog/" + file
      destinationFilePath$ = "logs/" + file
      CopyFile(sourceFilePath$, destinationFilePath$)
      DeleteFile(sourceFilePath$)
    end if
    
  next
  
end sub


Sub MoveCurrentLog()
  
  listOfPendingLogFiles = MatchFiles("/currentLog", "*")
  for each file in listOfPendingLogFiles
    sourceFilePath$ = "currentLog/" + file
    destinationFilePath$ = "logs/" + file
    CopyFile(sourceFilePath$, destinationFilePath$)
    DeleteFile(sourceFilePath$)
  next
  
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.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.loggingEnabled = playbackLoggingEnabled or eventLoggingEnabled or stateLoggingEnabled or diagnosticLoggingEnabled or variableLoggingEnabled
  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
  
  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.loggingEnabled = playbackLoggingEnabled or eventLoggingEnabled or stateLoggingEnabled or diagnosticLoggingEnabled or variableLoggingEnabled
  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 m.uploadLogFilesAtSpecificTime then
    hour% = m.uploadLogFilesTime% / 60
    minute% = m.uploadLogFilesTime% - (hour% * 60)
  else if not m.uploadLogsEnabled then
    hour% = 0
    minute% = 0
  end if
  
  if m.uploadLogFilesAtSpecificTime or not m.uploadLogsEnabled then
    m.cutoverTimer = CreateObject("roTimer")
    m.cutoverTimer.SetPort(m.msgPort)
    m.cutoverTimer.SetDate( - 1, - 1, - 1)
    m.cutoverTimer.SetTime(hour%, minute%, 0, 0)
    m.cutoverTimer.Start()
  end if
  
end sub


Sub DeleteExpiredFiles()
  
  ' delete any files that are more than 10 days old
  
  dtExpired = m.systemTime.GetLocalDateTime()
  dtExpired.SubtractSeconds(60 * 60 * 24 * 10)
  
  ' look in the following folders
  '   logs
  '   failedLogs
  '   archivedLogs
  
  m.DeleteOlderFiles("logs", dtExpired)
  m.DeleteOlderFiles("failedLogs", dtExpired)
  m.DeleteOlderFiles("archivedLogs", dtExpired)
  
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 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 WriteEventLogEntry(zoneName$ as string, timestamp$ as string, eventType$ as string, eventData$ as string)
  
  if not m.eventLoggingEnabled then return
  
  if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return
  
  t$ = chr(9)
  m.logFile.SendLine("L=e" + t$ + "Z=" + zoneName$ + t$ + "T=" + timestamp$ + t$ + "E=" + eventType$ + t$ + "D=" + eventData$)
  m.logFile.AsyncFlush()
  
end sub


Sub WriteDiagnosticLogEntry(eventId$ as string, eventData$ as string)
  
  if not m.diagnosticLoggingEnabled then return
  
  if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return
  
  timestamp$ = m.systemTime.GetLocalDateTime().GetString()
  
  t$ = chr(9)
  m.logFile.SendLine("L=d" + t$ + "T=" + timestamp$ + t$ + "I=" + eventId$ + t$ + "D=" + eventData$)
  m.logFile.AsyncFlush()
  
end sub


Sub PushLogFile()
  
  if not m.uploadLogsEnabled then return
  
  ' files that failed to upload in the past were moved to a different folder. move them back to the appropriate folder so that the script can attempt to upload them again
  listOfFailedLogFiles = MatchFiles("/" + m.uploadLogFailedFolder, "*.log")
  for each file in listOfFailedLogFiles
    target$ = m.uploadLogFolder + "/" + file
    fullFilePath$ = m.uploadLogFailedFolder + "/" + file
    ok = MoveFile(fullFilePath$, target$)
  next
  
  m.networking.UploadLogFiles()
  
end sub


Sub PushLogFilesOnBoot()
  
  m.MoveCurrentLog()
  m.PushLogFile()
  
end sub


Sub HandleLoggingTimerEvent(msg as object)
  
  m.CutoverLogFile()
  
  m.cutoverTimer.Start()
  
end sub


Sub CutoverLogFile()
  
  if type(m.logFile) <> "roCreateFile" and type(m.logFile) <> "roAppendFile" then return
  
  m.logFile.Flush()
  m.MoveCurrentLog()
  m.logFile = m.CreateLogFile("ld", "lc")
  
  m.PushLogFile()
  
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("ld", "lc")
  
end sub


Sub OpenLogFile()
  
  m.logFile = 0
  
  if not m.logging then return
  
  m.logFileLength = 0
  
  m.logFile = CreateObject("roReadFile", "log.txt")
  if type(m.logFile) = "roReadFile" then
    m.logFile.SeekToEnd()
    m.logFileLength = m.logFile.CurrentPosition()
    m.logFile = 0
  end if
  
  m.logFile = CreateObject("roAppendFile", "log.txt")
  if type(m.logFile) <> "roAppendFile" then
    print "unable to open log.txt"
    stop
  end if
  
  return
  
end sub


Sub CloseLogFile()
  
  if not m.logging then return
  
  m.logFile.Flush()
  m.logFile = 0
  
  return
  
end sub


Sub FlushLogFile()
  
  if not m.logging then return
  
  if m.logFileLength > 1000000 then
    print "### - Rotate Log Files - ###"
    m.logFile.SendLine("### - Rotate Log Files - ###")
  end if
  
  m.logFile.Flush()
  
  if m.logFileLength > 1000000 then
    m.RotateLogFiles()
  end if
  
  return
  
end sub


Sub WriteToLog(eventType$ as string, eventData$ as string, eventResponseCode$ as string, accountName$ as string)
  
  if not m.logging then return
  
  if m.debug then print "### write_event"
  
  ' write out the following info
  '   Timestamp, Device ID, Account Name, Event Type, Event Data, Response Code, Software Version, Firmware Version
  eventDateTime = m.systemTime.GetLocalDateTime()
  eventDataStr$ = eventDateTime + " " + accountName$ + " " + eventType$ + " " + eventData$ + " " + eventResponseCode$ + " recovery_runsetup.brs " + m.setupVersion$ + " " + m.deviceFWVersion$
  if m.debug then print "eventDataStr$ = ";eventDataStr$
  m.logFile.SendLine(eventDataStr$)
  
  m.logFileLength = m.logFileLength + len(eventDataStr$) + 14
  
  m.FlushLogFile()
  
  return
  
end sub


Sub RotateLogFiles()
  
  log3 = CreateObject("roReadFile", "log_3.txt")
  if type(log3) = "roReadFile" then
    log3 = 0
    DeleteFile("log_3.txt")
  end if
  
  log2 = CreateObject("roReadFile", "log_2.txt")
  if type(log2) = "roReadFile" then
    log2 = 0
    MoveFile("log_2.txt", "log_3.txt")
  end if
  
  m.logFile = 0
  MoveFile("log.txt", "log_2.txt")
  
  m.OpenLogFile()
  
  return
  
end sub


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

Function newDiagnosticCodes() as object
  
  diagnosticCodes = CreateObject("roAssociativeArray")
  
  diagnosticCodes.EVENT_STARTUP = "1000"
  diagnosticCodes.EVENT_SYNCSPEC_RECEIVED = "1001"
  diagnosticCodes.EVENT_DOWNLOAD_START = "1002"
  diagnosticCodes.EVENT_FILE_DOWNLOAD_START = "1003"
  diagnosticCodes.EVENT_FILE_DOWNLOAD_COMPLETE = "1004"
  diagnosticCodes.EVENT_DOWNLOAD_COMPLETE = "1005"
  diagnosticCodes.EVENT_READ_SYNCSPEC_FAILURE = "1006"
  diagnosticCodes.EVENT_RETRIEVE_SYNCSPEC_FAILURE = "1007"
  diagnosticCodes.EVENT_NO_SYNCSPEC_AVAILABLE = "1008"
  diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_IMMEDIATE_FAILURE = "1009"
  diagnosticCodes.EVENT_FILE_DOWNLOAD_FAILURE = "1010"
  diagnosticCodes.EVENT_SYNCSPEC_DOWNLOAD_FAILURE = "1011"
  diagnosticCodes.EVENT_SYNCPOOL_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_REALIZE_FAILURE = "1032"
  
  
  return diagnosticCodes
  
end function


' function StripLeadingSpaces moved to setupCommon.brs


Function ParseAutoplay(setupSync as object) as object
  
  setupParams = { }
  
  baseURL$ = setupSync.LookupMetadata("client", "base")
  setupParams.nextURL$ = baseURL$ + setupSync.LookupMetadata("client", "next")
  setupParams.user$ = setupSync.LookupMetadata("server", "user")
  setupParams.password$ = setupSync.LookupMetadata("server", "password")

  setupParams.enableBasicAuthentication = GetBoolFromNumericString(setupSync.LookupMetadata("server", "enableBasicAuthentication"))

  clientMetadata = setupSync.GetMetadata("client")
  uploadlogs$ = clientMetadata.LookupCi("uploadLogs")
  setupParams.uploadLogFileURL$ = baseURL$ + uploadlogs$

  setupParams.timeBetweenNetConnects% = GetNumberFromNumericString(setupSync.LookupMetadata("client", "timeBetweenNetConnects"))
  setupParams.contentDownloadsRestricted = setupSync.LookupMetadata("client", "contentDownloadsRestricted")
  setupParams.contentDownloadRangeStart% = GetNumberFromNumericString(setupSync.LookupMetadata("client", "contentDownloadRangeStart"))
  setupParams.contentDownloadRangeLength% = GetNumberFromNumericString(setupSync.LookupMetadata("client", "contentDownloadRangeLength"))
  
  ParseAutoplayCommon(setupParams, setupSync)
  
  return setupParams
  
end function

