View PS1

Plain-text view of pwi4_connect_enable_home.ps1 for the PWI4 helper.

PowerShell Script Content

This script waits for the local PWI4 API, connects the mount if needed, enables RA/Azimuth first, enables DEC/Altitude second, uses up to three total attempts through disconnect/connect recovery if an axis does not become enabled and optionally starts homing.

param(
    [string]$BaseUrl = "http://localhost:8220",
    [int]$TimeoutSeconds = 90,
    [int]$HomeSettleSeconds = 3,
    [int]$AxisTimeoutSeconds = 20,
    [int]$AxisRetryCount = 2,
    [int]$AxisEnableSettleSeconds = 5,
    [int]$AxisRetryDelaySeconds = 10,
    [int]$ReconnectSettleSeconds = 8,
    [int]$StatusLogIntervalSeconds = 5,
    [bool]$DetailedLog = $true,
    [string]$LogDirectory = "",
    [switch]$SkipHome,
    [switch]$DryRun
)

$ScriptVersion = "2026-05-26.1"
$ErrorActionPreference = "Stop"

$script:RunStartedAt = Get-Date
$script:RunId = "{0}_pid{1}" -f $script:RunStartedAt.ToString("yyyyMMdd_HHmmss_fff"), $PID
$script:ScriptRoot = if ([string]::IsNullOrWhiteSpace($PSScriptRoot)) { (Get-Location).Path } else { $PSScriptRoot }
if ([string]::IsNullOrWhiteSpace($LogDirectory)) {
    $LogDirectory = Join-Path $script:ScriptRoot "logs"
}
elseif (-not [System.IO.Path]::IsPathRooted($LogDirectory)) {
    $LogDirectory = Join-Path $script:ScriptRoot $LogDirectory
}

$script:LogDirectory = $LogDirectory
$script:LatestLogPath = Join-Path $script:ScriptRoot "pwi4_connect_enable_home.log"
$script:DetailLogPath = Join-Path $script:LogDirectory ("pwi4_connect_enable_home_{0}.log" -f $script:RunId)
$script:LogTargets = @()

function Add-AsciiLogLines {
    param(
        [string]$Path,
        [string[]]$Lines,
        [switch]$Overwrite
    )

    $fileMode = if ($Overwrite) { [System.IO.FileMode]::Create } else { [System.IO.FileMode]::Append }
    $stream = $null
    $writer = $null
    try {
        $stream = New-Object System.IO.FileStream($Path, $fileMode, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite)
        $writer = New-Object System.IO.StreamWriter($stream, [System.Text.Encoding]::ASCII)
        foreach ($line in $Lines) {
            $writer.WriteLine($line)
        }
        $writer.Flush()
    }
    finally {
        if ($null -ne $writer) {
            $writer.Dispose()
        }
        elseif ($null -ne $stream) {
            $stream.Dispose()
        }
    }
}

function Initialize-LogFiles {
    $usableTargets = @()

    $latestHeader = @(
        "------------------------------------------------------------",
        "PWI4 Connect Helper diagnostic log",
        ("RunId={0}" -f $script:RunId),
        ("Started={0}" -f $script:RunStartedAt.ToString("yyyy-MM-dd HH:mm:ss.fff zzz")),
        ("ScriptVersion={0}" -f $ScriptVersion),
        ""
    )
    try {
        Add-AsciiLogLines -Path $script:LatestLogPath -Lines $latestHeader
        $usableTargets += $script:LatestLogPath
    }
    catch {
        Write-Host ("Log file could not be initialized at {0}: {1}" -f $script:LatestLogPath, $_.Exception.Message)
    }

    try {
        if (-not (Test-Path -LiteralPath $script:LogDirectory)) {
            New-Item -ItemType Directory -Path $script:LogDirectory -Force | Out-Null
        }

        $detailHeader = @(
            "PWI4 Connect Helper diagnostic log",
            ("RunId={0}" -f $script:RunId),
            ("Started={0}" -f $script:RunStartedAt.ToString("yyyy-MM-dd HH:mm:ss.fff zzz")),
            ("ScriptVersion={0}" -f $ScriptVersion),
            ""
        )
        Add-AsciiLogLines -Path $script:DetailLogPath -Lines $detailHeader -Overwrite
        $usableTargets += $script:DetailLogPath
    }
    catch {
        Write-Host ("Log file could not be initialized at {0}: {1}" -f $script:DetailLogPath, $_.Exception.Message)
    }

    if ($usableTargets.Count -eq 0) {
        $fallbackDir = Join-Path ([System.IO.Path]::GetTempPath()) "PWI4ConnectLogs"
        $fallbackPath = Join-Path $fallbackDir ("pwi4_connect_enable_home_{0}.log" -f $script:RunId)
        try {
            New-Item -ItemType Directory -Path $fallbackDir -Force | Out-Null
            Add-AsciiLogLines -Path $fallbackPath -Lines @(
                "PWI4 Connect Helper diagnostic log",
                ("RunId={0}" -f $script:RunId),
                ("Started={0}" -f $script:RunStartedAt.ToString("yyyy-MM-dd HH:mm:ss.fff zzz")),
                ""
            ) -Overwrite
            $usableTargets += $fallbackPath
            $script:DetailLogPath = $fallbackPath
        }
        catch {
            Write-Host ("Fallback log file could not be initialized at {0}: {1}" -f $fallbackPath, $_.Exception.Message)
        }
    }

    $script:LogTargets = $usableTargets
}

function Write-Log {
    param(
        [string]$Message,
        [string]$Level = "INFO"
    )

    $line = "[{0}] [{1}] [PID:{2}] {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"), $Level, $PID, $Message
    Write-Host $line
    foreach ($target in $script:LogTargets) {
        for ($attempt = 1; $attempt -le 5; $attempt++) {
            try {
                Add-AsciiLogLines -Path $target -Lines @($line)
                break
            }
            catch {
                if ($attempt -ge 5) {
                    Write-Host ("Log write failed for {0}: {1}" -f $target, $_.Exception.Message)
                }
                else {
                    Start-Sleep -Milliseconds 200
                }
            }
        }
    }
}

function Write-DiagnosticHeader {
    param([string]$Mode)

    Write-Log ("Start PWI4 {0}, Version={1}, BaseUrl={2}, Timeout={3}s, AxisTimeout={4}s, AxisRetries={5}, AxisAttempts={6}" -f $Mode, $ScriptVersion, $BaseUrl, $TimeoutSeconds, $AxisTimeoutSeconds, $AxisRetryCount, ([Math]::Max(1, $AxisRetryCount + 1)))
    Write-Log ("RunId={0}" -f $script:RunId)
    Write-Log ("LatestLog={0}" -f $script:LatestLogPath)
    Write-Log ("DetailLog={0}" -f $script:DetailLogPath)
    Write-Log ("DetailedLog={0}; StatusLogIntervalSeconds={1}" -f $DetailedLog, $StatusLogIntervalSeconds)
    Write-Log ("ScriptRoot={0}" -f $script:ScriptRoot)
    Write-Log ("ScriptPath={0}" -f $PSCommandPath)
    Write-Log ("WorkingDirectory={0}" -f (Get-Location).Path)
    Write-Log ("CommandLine={0}" -f [Environment]::CommandLine)
    Write-Log ("User={0}\{1}; Computer={2}" -f $env:USERDOMAIN, $env:USERNAME, $env:COMPUTERNAME)
    Write-Log ("OS={0}; 64BitOS={1}; 64BitProcess={2}" -f [Environment]::OSVersion.VersionString, [Environment]::Is64BitOperatingSystem, [Environment]::Is64BitProcess)
    Write-Log ("PowerShellVersion={0}; CLRVersion={1}" -f $PSVersionTable.PSVersion, $PSVersionTable.CLRVersion)
    Write-Log ("EffectiveParameters: SkipHome={0}; DryRun={1}; HomeSettleSeconds={2}; AxisEnableSettleSeconds={3}; AxisRetryDelaySeconds={4}; ReconnectSettleSeconds={5}" -f $SkipHome, $DryRun, $HomeSettleSeconds, $AxisEnableSettleSeconds, $AxisRetryDelaySeconds, $ReconnectSettleSeconds)
}

function Write-RawResponseLog {
    param(
        [string]$Label,
        [string]$Content
    )

    if (-not $DetailedLog) {
        return
    }

    if ($null -eq $Content) {
        Write-Log ("RAW {0}: <null>" -f $Label) "DEBUG"
        return
    }

    Write-Log ("RAW {0}: begin" -f $Label) "DEBUG"
    $lines = $Content -split "`r?`n"
    if ($lines.Count -eq 0) {
        Write-Log ("RAW {0}: <empty>" -f $Label) "DEBUG"
    }
    else {
        for ($index = 0; $index -lt $lines.Count; $index++) {
            Write-Log ("RAW {0}: {1:D3}: {2}" -f $Label, ($index + 1), $lines[$index]) "DEBUG"
        }
    }
    Write-Log ("RAW {0}: end" -f $Label) "DEBUG"
}

function Write-ExceptionDetail {
    param([System.Management.Automation.ErrorRecord]$ErrorRecord)

    if ($null -eq $ErrorRecord) {
        return
    }

    Write-Log ("ERROR_DETAIL Category={0}; FullyQualifiedErrorId={1}" -f $ErrorRecord.CategoryInfo, $ErrorRecord.FullyQualifiedErrorId) "ERROR"
    if (-not [string]::IsNullOrWhiteSpace($ErrorRecord.ScriptStackTrace)) {
        Write-Log ("ERROR_DETAIL ScriptStackTrace={0}" -f ($ErrorRecord.ScriptStackTrace -replace "`r?`n", " | ")) "ERROR"
    }

    $exception = $ErrorRecord.Exception
    $depth = 0
    while ($null -ne $exception) {
        Write-Log ("ERROR_DETAIL Exception[{0}] Type={1}; Message={2}" -f $depth, $exception.GetType().FullName, $exception.Message) "ERROR"
        $exception = $exception.InnerException
        $depth++
    }
}

function Convert-PwiStatus {
    param([string]$Content)

    $status = @{}
    foreach ($line in ($Content -split "`r?`n")) {
        if ($line -match "^([^=]+)=(.*)$") {
            $status[$matches[1]] = $matches[2]
        }
    }
    return $status
}

function Get-PwiBool {
    param(
        [hashtable]$Status,
        [string]$Key
    )

    if (-not $Status.ContainsKey($Key)) {
        return $false
    }
    $value = [string]$Status[$Key]
    return (
        $value.Equals("true", [System.StringComparison]::OrdinalIgnoreCase) -or
        $value.Equals("yes", [System.StringComparison]::OrdinalIgnoreCase) -or
        $value.Equals("enabled", [System.StringComparison]::OrdinalIgnoreCase) -or
        $value.Equals("1", [System.StringComparison]::OrdinalIgnoreCase)
    )
}

function Get-PwiAnyBool {
    param(
        [hashtable]$Status,
        [string[]]$Keys
    )

    foreach ($key in $Keys) {
        if (Get-PwiBool -Status $Status -Key $key) {
            return $true
        }
    }
    return $false
}

function Test-PwiHasAnyKey {
    param(
        [hashtable]$Status,
        [string[]]$Keys
    )

    foreach ($key in $Keys) {
        if ($Status.ContainsKey($key)) {
            return $true
        }
    }
    return $false
}

function Format-PwiStatusValues {
    param(
        [hashtable]$Status,
        [string[]]$Keys
    )

    if ($null -eq $Status) {
        return "<no status>"
    }

    $parts = @()
    foreach ($key in $Keys) {
        if ($Status.ContainsKey($key)) {
            $parts += ("{0}={1}" -f $key, $Status[$key])
        }
        else {
            $parts += ("{0}=<missing>" -f $key)
        }
    }
    return ($parts -join "; ")
}

function Write-PwiStatusSummary {
    param(
        [string]$Label,
        [hashtable]$Status,
        [string[]]$Keys = @()
    )

    if ($null -eq $Status) {
        Write-Log ("STATUS {0}: <no status>" -f $Label) "DEBUG"
        return
    }

    Write-Log ("STATUS {0}: parsed_key_count={1}" -f $Label, $Status.Count) "DEBUG"
    if ($Keys.Count -gt 0) {
        Write-Log ("STATUS {0}: selected={1}" -f $Label, (Format-PwiStatusValues -Status $Status -Keys $Keys)) "DEBUG"
    }

    if ($DetailedLog) {
        foreach ($key in ($Status.Keys | Sort-Object)) {
            Write-Log ("STATUS {0}: {1}={2}" -f $Label, $key, $Status[$key]) "DEBUG"
        }
    }
}

function Read-ErrorResponseBody {
    param($Response)

    if ($null -eq $Response) {
        return ""
    }

    try {
        $stream = $Response.GetResponseStream()
        if ($null -eq $stream) {
            return ""
        }
        $reader = New-Object System.IO.StreamReader($stream)
        try {
            return $reader.ReadToEnd()
        }
        finally {
            $reader.Dispose()
        }
    }
    catch {
        Write-Log ("Could not read error response body: {0}" -f $_.Exception.Message) "WARN"
        return ""
    }
}

function Invoke-Pwi {
    param(
        [string]$Path,
        [switch]$Quiet,
        [switch]$TransientFailure,
        [int]$RequestTimeoutSeconds = 10
    )

    $uri = $BaseUrl.TrimEnd("/") + $Path
    if ((-not $Quiet) -or $DetailedLog) {
        Write-Log ("PWI4 API request: path={0}; uri={1}" -f $Path, $uri)
    }

    if ($DryRun) {
        Write-Log ("DryRun: skipped PWI4 API request path={0}" -f $Path) "DEBUG"
        return @{}
    }

    $started = Get-Date
    $effectiveRequestTimeout = [Math]::Max(1, $RequestTimeoutSeconds)
    try {
        $response = Invoke-WebRequest -UseBasicParsing -Uri $uri -TimeoutSec $effectiveRequestTimeout
    }
    catch {
        $failureLevel = if ($TransientFailure) { "WARN" } else { "ERROR" }
        $elapsedMs = [Math]::Round(((Get-Date) - $started).TotalMilliseconds)
        Write-Log ("PWI4 API failed: path={0}; uri={1}; elapsed_ms={2}; message={3}" -f $Path, $uri, $elapsedMs, $_.Exception.Message) $failureLevel

        $errorResponse = $_.Exception.Response
        if ($null -ne $errorResponse) {
            $statusCode = ""
            $statusDescription = ""
            try {
                $statusCode = [int]$errorResponse.StatusCode
                $statusDescription = $errorResponse.StatusDescription
            }
            catch {
                $statusCode = "<unknown>"
                $statusDescription = "<unknown>"
            }
            Write-Log ("PWI4 API error response: status={0}; description={1}; content_type={2}" -f $statusCode, $statusDescription, $errorResponse.ContentType) $failureLevel
            $body = Read-ErrorResponseBody -Response $errorResponse
            if (-not [string]::IsNullOrEmpty($body)) {
                Write-RawResponseLog -Label ("ERROR {0}" -f $Path) -Content $body
            }
        }

        throw ("PWI4 API is not reachable or returned an error at {0}: {1}" -f $uri, $_.Exception.Message)
    }

    $elapsed = (Get-Date) - $started
    $content = [string]$response.Content
    $byteCount = [System.Text.Encoding]::UTF8.GetByteCount($content)
    Write-Log ("PWI4 API result: path={0}; status={1} {2}; elapsed_ms={3}; chars={4}; utf8_bytes={5}" -f $Path, $response.StatusCode, $response.StatusDescription, [Math]::Round($elapsed.TotalMilliseconds), $content.Length, $byteCount) "DEBUG"
    Write-RawResponseLog -Label $Path -Content $content

    $status = Convert-PwiStatus -Content $content
    if ($Path -eq "/status") {
        Write-PwiStatusSummary -Label $Path -Status $status -Keys @("mount.is_connected", "mount.is_slewing", "mount.axis0.is_enabled", "mount.axis1.is_enabled", "mount.ra.is_enabled", "mount.dec.is_enabled")
    }
    elseif ($DetailedLog -and $status.Count -gt 0) {
        Write-PwiStatusSummary -Label $Path -Status $status
    }

    return $status
}

function Get-PwiStatus {
    param(
        [switch]$Quiet,
        [switch]$TransientFailure,
        [int]$RequestTimeoutSeconds = 10
    )

    return (Invoke-Pwi -Path "/status" -Quiet:$Quiet -TransientFailure:$TransientFailure -RequestTimeoutSeconds $RequestTimeoutSeconds)
}

function Wait-PwiApiReady {
    param([int]$WaitSeconds = 0)

    if ($WaitSeconds -le 0) {
        $WaitSeconds = $TimeoutSeconds
    }

    $statusUri = $BaseUrl.TrimEnd("/") + "/status"
    $progressInterval = [Math]::Max(1, $StatusLogIntervalSeconds)
    $deadline = (Get-Date).AddSeconds($WaitSeconds)
    $nextProgressLog = Get-Date
    $lastError = $null
    $requestTimeout = [Math]::Min(5, [Math]::Max(1, $WaitSeconds))

    Write-Log ("Waiting for PWI4 API readiness at {0}. Timeout up to {1}s." -f $statusUri, $WaitSeconds)
    do {
        try {
            $status = Get-PwiStatus -Quiet -TransientFailure -RequestTimeoutSeconds $requestTimeout
            Write-Log ("OK: PWI4 API is reachable at {0}." -f $statusUri)
            return $status
        }
        catch {
            $lastError = $_
            $now = Get-Date
            if ($now -ge $nextProgressLog) {
                $remaining = [Math]::Max(0, [Math]::Round(($deadline - $now).TotalSeconds))
                Write-Log ("PWI4 API is not reachable yet. Remaining up to {0}s. Last error: {1}" -f $remaining, $_.Exception.Message) "WARN"
                $nextProgressLog = $now.AddSeconds($progressInterval)
            }
            if ((Get-Date) -lt $deadline) {
                Start-Sleep -Seconds 1
            }
        }
    } while ((Get-Date) -lt $deadline)

    $lastMessage = if ($null -eq $lastError) { "no response received" } else { $lastError.Exception.Message }
    throw ("Timeout while waiting for PWI4 API at {0}. Last error: {1}" -f $statusUri, $lastMessage)
}

function Wait-PwiStatus {
    param(
        [string]$Description,
        [scriptblock]$Predicate,
        [string[]]$DiagnosticKeys = @(),
        [int]$WaitSeconds = 0
    )

    if ($WaitSeconds -le 0) {
        $WaitSeconds = $TimeoutSeconds
    }

    $progressInterval = [Math]::Max(1, $StatusLogIntervalSeconds)
    $deadline = (Get-Date).AddSeconds($WaitSeconds)
    $nextProgressLog = Get-Date
    $status = $null
    $lastError = $null
    $requestTimeout = [Math]::Min(5, [Math]::Max(1, $WaitSeconds))
    do {
        try {
            $status = Get-PwiStatus -Quiet -TransientFailure -RequestTimeoutSeconds $requestTimeout
            $lastError = $null
        }
        catch {
            $lastError = $_
            $now = Get-Date
            if ($now -ge $nextProgressLog) {
                $remaining = [Math]::Max(0, [Math]::Round(($deadline - $now).TotalSeconds))
                Write-Log ("Waiting for {0}. PWI4 status API is temporarily not reachable. Remaining up to {1}s. Last error: {2}" -f $Description, $remaining, $_.Exception.Message) "WARN"
                $nextProgressLog = $now.AddSeconds($progressInterval)
            }
            if ((Get-Date) -lt $deadline) {
                Start-Sleep -Seconds 1
            }
            continue
        }

        if (& $Predicate $status) {
            Write-Log ("OK: {0}" -f $Description)
            if ($DiagnosticKeys.Count -gt 0) {
                Write-Log ("OK status: {0}" -f (Format-PwiStatusValues -Status $status -Keys $DiagnosticKeys)) "DEBUG"
            }
            return $status
        }
        $now = Get-Date
        if ($now -ge $nextProgressLog) {
            $remaining = [Math]::Max(0, [Math]::Round(($deadline - $now).TotalSeconds))
            Write-Log ("Waiting for {0}. Remaining up to {1}s." -f $Description, $remaining)
            if ($DiagnosticKeys.Count -gt 0) {
                Write-Log ("Current status: {0}" -f (Format-PwiStatusValues -Status $status -Keys $DiagnosticKeys)) "DEBUG"
            }
            $nextProgressLog = $now.AddSeconds($progressInterval)
        }
        Start-Sleep -Seconds 1
    } while ((Get-Date) -lt $deadline)

    if ($DiagnosticKeys.Count -gt 0) {
        Write-Log ("Last status: {0}" -f (Format-PwiStatusValues -Status $status -Keys $DiagnosticKeys)) "ERROR"
    }

    if ($null -ne $lastError) {
        Write-Log ("Last PWI4 status API error while waiting for {0}: {1}" -f $Description, $lastError.Exception.Message) "ERROR"
        throw ("Timeout while waiting for: {0}. Last PWI4 API error: {1}" -f $Description, $lastError.Exception.Message)
    }

    throw ("Timeout while waiting for: {0}" -f $Description)
}

function Connect-PwiMount {
    param([string]$Description = "mount.is_connected=true")

    Invoke-Pwi -Path "/mount/connect" | Out-Null
    return (Wait-PwiStatus -Description $Description -DiagnosticKeys @("mount.is_connected") -Predicate {
        param($s)
        Get-PwiBool -Status $s -Key "mount.is_connected"
    })
}

function Reconnect-PwiMount {
    param([string]$Reason)

    Write-Log ("Recovery: {0}" -f $Reason)
    Write-Log "Recovery: disconnecting mount."
    Invoke-Pwi -Path "/mount/disconnect" | Out-Null
    Write-Log ("Recovery: waiting {0}s after disconnect." -f $ReconnectSettleSeconds)
    Start-Sleep -Seconds $ReconnectSettleSeconds

    Write-Log "Recovery: reconnecting mount."
    $status = Connect-PwiMount -Description "mount.is_connected=true after recovery reconnect"
    Write-Log ("Recovery: waiting {0}s before retrying axis enable sequence." -f $AxisRetryDelaySeconds)
    Start-Sleep -Seconds $AxisRetryDelaySeconds
    return $status
}

function Enable-PwiAxis {
    param(
        [int]$Axis,
        [string]$Name,
        [string[]]$Keys
    )

    Write-Log ("Enable {0} axis {1}." -f $Name, $Axis)
    Invoke-Pwi -Path ("/mount/enable?axis={0}" -f $Axis) | Out-Null
    Write-Log ("Waiting {0}s after enabling {1} axis {2}." -f $AxisEnableSettleSeconds, $Name, $Axis)
    Start-Sleep -Seconds $AxisEnableSettleSeconds

    $status = Get-PwiStatus -Quiet
    if (-not (Test-PwiHasAnyKey -Status $status -Keys $Keys)) {
        Write-Log ("Axis status keys missing after enabling {0}. Expected one of: {1}" -f $Name, ($Keys -join ", ")) "ERROR"
        throw ("PWI4 /status does not expose enabled state for {0}. Cannot safely continue. Expected one of: {1}" -f $Name, ($Keys -join ", "))
    }

    $axisKeys = $Keys
    return (Wait-PwiStatus -Description ("{0} axis {1} enabled" -f $Name, $Axis) -DiagnosticKeys $axisKeys -WaitSeconds $AxisTimeoutSeconds -Predicate {
        param($s)
        Get-PwiAnyBool -Status $s -Keys $axisKeys
    }.GetNewClosure())
}

function Enable-PwiAxesWithRecovery {
    param(
        [string[]]$Axis0Keys,
        [string[]]$Axis1Keys
    )

    $maxAttempts = [Math]::Max(1, $AxisRetryCount + 1)
    for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
        if ($attempt -gt 1) {
            Write-Log ("Axis enable retry attempt {0}/{1}." -f $attempt, $maxAttempts)
        }

        try {
            $status = Enable-PwiAxis -Axis 0 -Name "RA/Azimuth" -Keys $Axis0Keys
            $status = Enable-PwiAxis -Axis 1 -Name "DEC/Altitude" -Keys $Axis1Keys
            return $status
        }
        catch {
            $message = $_.Exception.Message
            if ($attempt -ge $maxAttempts) {
                throw ("Axes did not become enabled after {0} attempt(s). Last error: {1}" -f $maxAttempts, $message)
            }

            Write-Log ("Axis enable attempt {0}/{1} failed: {2}" -f $attempt, $maxAttempts, $message)
            Reconnect-PwiMount -Reason "axis did not become enabled; repeating complete RA/DEC enable sequence" | Out-Null
        }
    }
}

function Test-PwiAxesEnabled {
    param(
        [hashtable]$Status,
        [string[]]$Axis0Keys,
        [string[]]$Axis1Keys
    )

    return (
        (Get-PwiAnyBool -Status $Status -Keys $Axis0Keys) -and
        (Get-PwiAnyBool -Status $Status -Keys $Axis1Keys)
    )
}

Initialize-LogFiles

try {
    $mode = if ($SkipHome) { "connect/enable" } else { "connect/enable/home" }
    Write-DiagnosticHeader -Mode $mode

    if ($DryRun) {
        Write-Log "DryRun: no PWI4 commands are executed."
        Write-Log ("Startup script completed successfully. DetailLog={0}" -f $script:DetailLogPath)
        exit 0
    }

    $axis0Keys = @("mount.axis0.is_enabled", "mount.axis0.enabled", "mount.axis0.is_initialized", "mount.axis0.is_servo_on", "mount.axis0.is_servo_enabled", "mount.ra.is_enabled", "mount.ra.enabled")
    $axis1Keys = @("mount.axis1.is_enabled", "mount.axis1.enabled", "mount.axis1.is_initialized", "mount.axis1.is_servo_on", "mount.axis1.is_servo_enabled", "mount.dec.is_enabled", "mount.dec.enabled")
    Write-Log ("Axis0Keys={0}" -f ($axis0Keys -join ", ")) "DEBUG"
    Write-Log ("Axis1Keys={0}" -f ($axis1Keys -join ", ")) "DEBUG"

    $status = Wait-PwiApiReady
    if (Get-PwiBool -Status $status -Key "mount.is_connected") {
        $hasAxisStatus = (
            (Test-PwiHasAnyKey -Status $status -Keys $axis0Keys) -and
            (Test-PwiHasAnyKey -Status $status -Keys $axis1Keys)
        )

        if ($hasAxisStatus -and (Test-PwiAxesEnabled -Status $status -Axis0Keys $axis0Keys -Axis1Keys $axis1Keys)) {
            Write-Log "PWI4 mount is already connected and both axes are enabled. No action, no home."
            Write-Log ("Startup script completed successfully. DetailLog={0}" -f $script:DetailLogPath)
            exit 0
        }

        if (-not $hasAxisStatus) {
            Write-Log "PWI4 mount is already connected, but expected axis status fields are not available. Running axis enable sequence so the diagnostic log can capture the PWI4 response."
        }
        else {
            Write-Log "PWI4 mount is already connected, but axes are not both enabled. Running axis enable recovery sequence."
        }
    }
    else {
        Write-Log "PWI4 mount is not connected. Connecting mount."
        $status = Connect-PwiMount
    }

    $status = Enable-PwiAxesWithRecovery -Axis0Keys $axis0Keys -Axis1Keys $axis1Keys

    if ($SkipHome) {
        Write-Log "SkipHome is set. Homing was not started."
        Write-Log ("Startup script completed successfully. DetailLog={0}" -f $script:DetailLogPath)
        exit 0
    }

    Write-Log "Starting mount home."
    Invoke-Pwi -Path "/mount/find_home" | Out-Null

    Start-Sleep -Seconds $HomeSettleSeconds
    $status = Wait-PwiStatus -Description "home finished / mount is not slewing" -DiagnosticKeys @("mount.is_slewing") -Predicate {
        param($s)
        if (-not $s.ContainsKey("mount.is_slewing")) {
            return $true
        }
        return (-not (Get-PwiBool -Status $s -Key "mount.is_slewing"))
    }

    Write-Log ("Startup script completed successfully. DetailLog={0}" -f $script:DetailLogPath)
    exit 0
}
catch {
    Write-Log ("ERROR: {0}" -f $_.Exception.Message) "ERROR"
    Write-ExceptionDetail -ErrorRecord $_
    Write-Log ("Startup script failed. LatestLog={0}; DetailLog={1}" -f $script:LatestLogPath, $script:DetailLogPath) "ERROR"
    exit 1
}