# ============================================================================== # lf_mri_platform -- Start services and GUI # Usage: # .\start.ps1 -- plug mode: Docker + GUI # .\start.ps1 -Mode real -- real mode: Docker + spectrometer + GUI # .\start.ps1 -GuiOnly -- GUI only (services already running) # .\start.ps1 -ServicesOnly -- Docker + spectrometer, no GUI # .\start.ps1 -SkipInstall -- skip venv check # .\start.ps1 -SkipSpectrometer -- skip native spectrometer even in real mode # ============================================================================== param( [ValidateSet("plug", "real")] [string]$Mode = "plug", [switch]$GuiOnly, [switch]$ServicesOnly, [switch]$SkipInstall, [switch]$SkipSpectrometer ) $ErrorActionPreference = "Stop" $Root = $PSScriptRoot $GuiDir = Join-Path $Root "apps\gui" $VenvPython = Join-Path $GuiDir ".venv\Scripts\python.exe" $AppScript = Join-Path $Root "apps\gui\app.py" $EnvFile = Join-Path $Root ".env" $EnvExample = Join-Path $Root ".env.example" $LogFile = Join-Path $Root "start_log.txt" # Write all output to log file so errors are readable even if window closes Start-Transcript -Path $LogFile -Append | Out-Null Write-Host "=== start.ps1 $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Mode=$Mode ===" function Write-Step($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan } function Write-OK($msg) { Write-Host " [OK] $msg" -ForegroundColor Green } function Write-Warn($msg) { Write-Host " [!!] $msg" -ForegroundColor Yellow } function Write-Fail($msg) { Write-Host "`n[FAIL] $msg" -ForegroundColor Red Write-Host " Log: $LogFile" -ForegroundColor DarkGray try { Stop-Transcript | Out-Null } catch {} Write-Host "`n Press Enter to close..." -ForegroundColor DarkGray $null = Read-Host exit 1 } # Keep window open on any unhandled error trap { Write-Host "`n[ERROR] $_" -ForegroundColor Red Write-Host " Log: $LogFile" -ForegroundColor DarkGray try { Stop-Transcript | Out-Null } catch {} Write-Host "`n Press Enter to close..." -ForegroundColor DarkGray $null = Read-Host exit 1 } function Get-EnvPort($key, $default) { if (Test-Path $EnvFile) { $line = Get-Content $EnvFile | Select-String "^$key=(\d+)" if ($line) { return $line.Matches[0].Groups[1].Value } } return $default } # Returns $true if the TCP port is accepting connections. # Using TCP instead of HTTP so HTTP 4xx/auth errors don't cause false negatives. function Test-ServiceUp($svc) { try { $uri = [System.Uri]$svc.Url $port = if ($uri.Port -gt 0) { $uri.Port } else { 80 } $tcp = New-Object System.Net.Sockets.TcpClient $ar = $tcp.BeginConnect($uri.Host, $port, $null, $null) $ok = $ar.AsyncWaitHandle.WaitOne(1500, $false) try { $tcp.Close() } catch {} return $ok } catch { return $false } } # -- 1. Check / install GUI venv ----------------------------------------------- if (-not $GuiOnly -and -not $SkipInstall) { Write-Step "Checking GUI environment" $needsInstall = $false if (-not (Test-Path $VenvPython)) { Write-Warn "Virtual environment not found -- running install" $needsInstall = $true } else { & $VenvPython -c "import PySide6" 2>$null if ($LASTEXITCODE -ne 0) { Write-Warn "GUI packages missing -- running install" $needsInstall = $true } else { Write-OK "Virtual environment ready" } } if ($needsInstall) { $installScript = Join-Path $Root "install.ps1" if (-not (Test-Path $installScript)) { Write-Fail "install.ps1 not found" } & powershell.exe -ExecutionPolicy Bypass -File $installScript if ($LASTEXITCODE -ne 0) { Write-Fail "install.ps1 failed" } } } # -- 2. Docker ----------------------------------------------------------------- if (-not $GuiOnly) { Write-Step "Checking Docker" $dockerOk = $false try { & docker info *>$null; $dockerOk = $true } catch {} if (-not $dockerOk) { Write-Warn "Docker not responding -- starting Docker Desktop..." $dockerExe = "C:\Program Files\Docker\Docker\Docker Desktop.exe" if (Test-Path $dockerExe) { Start-Process $dockerExe } else { Write-Fail "Docker Desktop not found -- install from https://docker.com" } $waited = 0 while ($waited -lt 60) { Start-Sleep 5 $waited += 5 try { & docker info *>$null; $dockerOk = $true; break } catch {} Write-Host " ... $waited/60 s" -ForegroundColor DarkGray } if (-not $dockerOk) { Write-Fail "Docker did not start in time" } } Write-OK "Docker is running" # -- 3. .env --------------------------------------------------------------- if (-not (Test-Path $EnvFile)) { if (Test-Path $EnvExample) { Copy-Item $EnvExample $EnvFile Write-Warn ".env created from .env.example" } else { Write-Warn ".env missing -- using Compose defaults" } } # -- 4. Start containers --------------------------------------------------- Write-Step "Starting Docker services (mode: $Mode)" $env:ORCHESTRATOR_MODE = $Mode $envArgs = if (Test-Path $EnvFile) { @("--env-file", $EnvFile) } else { @() } & docker compose @envArgs up -d if ($LASTEXITCODE -ne 0) { Write-Fail "docker compose up failed" } Write-OK "Containers started" # -- 5. Native spectrometer ------------------------------------------------ if (-not $SkipSpectrometer) { Write-Step "Starting native spectrometer" $SpecDir = Join-Path $Root "services\spectrometer" $SpecVenv = Join-Path $SpecDir "mvenv\Scripts\python.exe" $PicoExe = Join-Path $SpecDir "bin\pico-tcp.exe" $oldPico = Get-Process -Name "pico-tcp" -ErrorAction SilentlyContinue if ($oldPico) { $oldPico | Stop-Process -Force; Write-Warn "Killed stale pico-tcp.exe" } if (-not (Test-Path $SpecVenv)) { Write-Warn "Spectrometer venv not found -- creating..." Push-Location $SpecDir & python -m venv mvenv if ($LASTEXITCODE -ne 0) { Write-Fail "Failed to create spectrometer venv" } Pop-Location Write-OK "Spectrometer venv created" } else { Write-OK "Spectrometer venv found" } # Always sync packages (catches new deps after git pull) Write-Host " Installing/updating spectrometer packages..." -ForegroundColor DarkGray Push-Location $SpecDir & $SpecVenv -m pip install -q --upgrade pip $pipOut = & $SpecVenv -m pip install -q -r requirements.txt 2>&1 $pipFailed = ($LASTEXITCODE -ne 0) Pop-Location if ($pipFailed) { Write-Warn "pip install had issues:`n$pipOut" } else { Write-OK "Packages up to date" } # Migrations -- show output so errors are visible Write-Host " Running migrations..." -ForegroundColor DarkGray Push-Location $SpecDir $migrateOut = & $SpecVenv manage.py migrate --noinput 2>&1 $migrateFailed = ($LASTEXITCODE -ne 0) Pop-Location if ($migrateFailed) { Write-Warn "Migration warnings:`n$migrateOut" } else { Write-OK "Migrations applied" } # pico-tcp.exe if (Test-Path $PicoExe) { Start-Process $PicoExe -WindowStyle Normal Write-OK "pico-tcp.exe started (visible window)" } else { Write-Warn "bin\pico-tcp.exe not found -- ADC proxy not started" } # Django runserver -- run via "cmd /k" so window stays open on error $specPort = Get-EnvPort "SPECTROMETER_PORT" "8000" $alreadyUp = $false try { $r = Invoke-WebRequest "http://localhost:$specPort/api/" ` -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop $alreadyUp = ($r.StatusCode -lt 400) } catch {} if ($alreadyUp) { Write-OK "Spectrometer already responding on port $specPort -- checking devices..." # Fall through to device registration check below } else { # Title makes the window easy to find in the taskbar. # /k keeps the window open even if Django crashes at startup. # --noreload disables Django's file-watcher subprocess (cleaner in cmd). $runCmd = "`"$SpecVenv`" manage.py runserver 0.0.0.0:$specPort --noreload" Start-Process "cmd.exe" ` -ArgumentList "/k title LF-MRI Spectrometer && $runCmd" ` -WorkingDirectory $SpecDir ` -WindowStyle Normal Write-OK "Spectrometer started (window: 'LF-MRI Spectrometer', port $specPort)" } # -- Auto-register hardware devices if DB is empty -------------------- Write-Host " Waiting for spectrometer API..." -ForegroundColor DarkGray $apiReady = $false $apiWait = 0 while ($apiWait -lt 20) { if (Test-ServiceUp @{ Url = "http://localhost:$specPort/api/" }) { $apiReady = $true; break } Start-Sleep 1; $apiWait++ } if ($apiReady) { try { $devResp = Invoke-WebRequest "http://localhost:$specPort/api/devices/" ` -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop $devJson = $devResp.Content | ConvertFrom-Json $devCount = if ($devJson.PSObject.Properties['results']) { $devJson.results.Count } else { @($devJson).Count } if ($devCount -eq 0) { Write-Warn "No devices in DB -- registering hardware..." $base = "http://localhost:$specPort/api/devices/" $hdr = @{ "Content-Type" = "application/json" } $devs = @( '{"device_type":"ADC", "brend":"Picoscope", "serial_model":"PS4000A", "proto":"adc_default", "proto_interface":"TCP"}', '{"device_type":"SDR", "brend":"HackRF", "serial_model":"HackRF", "proto":"sdr_default", "proto_interface":"USB"}', '{"device_type":"SYNC", "brend":"Arduino", "serial_model":"DuePP", "proto":"sync_default", "proto_interface":"USB"}', '{"device_type":"GRA", "brend":"ITMO", "serial_model":"GRU", "proto":"gra_default", "proto_interface":"UDP"}' ) foreach ($body in $devs) { try { Invoke-WebRequest $base -Method Post -Headers $hdr ` -Body $body -UseBasicParsing -TimeoutSec 5 | Out-Null $name = ($body | Select-String '"serial_model":"([^"]+)"').Matches[0].Groups[1].Value Write-Host " Registered: $name" -ForegroundColor DarkGray } catch { Write-Warn " Failed to register device: $_" } } Write-OK "Hardware devices registered" } else { Write-OK "Devices already in DB ($devCount found)" } } catch { Write-Warn "Could not check/register devices: $_" } } else { Write-Warn "Spectrometer API not ready in 20 s -- skipping device registration" } } # -- 6. Health check ------------------------------------------------------- Write-Step "Waiting for services to become healthy" $checkSpec = -not $SkipSpectrometer $services = @( @{ Name = "Orchestrator"; Url = "http://localhost:$(Get-EnvPort 'ORCHESTRATOR_PORT' '1717')/health"; Required = $true }, @{ Name = "Seq-Interp"; Url = "http://localhost:$(Get-EnvPort 'SEQ_INTERP_PORT' '7475')/health"; Required = $true }, @{ Name = "Reconstructor"; Url = "http://localhost:$(Get-EnvPort 'RECONSTRUCTOR_PORT' '8081')/health"; Required = $true }, @{ Name = "Spectroscopy"; Url = "http://localhost:$(Get-EnvPort 'SPECTROSCOPY_PORT' '8002')/health"; Required = $true }, @{ Name = "Spectrometer"; Url = "http://localhost:$(Get-EnvPort 'SPECTROMETER_PORT' '8000')/api/"; Required = $checkSpec; AllowedCodes = @(200,301,302,401,403) } ) $maxWait = 120; $interval = 3; $elapsed = 0 while ($elapsed -lt $maxWait) { $pending = @() foreach ($svc in $services) { if (-not $svc.Required) { continue } if (-not (Test-ServiceUp $svc)) { $pending += $svc.Name } } if ($pending.Count -eq 0) { Write-OK "All required services healthy"; break } Write-Host (" ... {0}/{1} s waiting: {2}" -f $elapsed, $maxWait, ($pending -join ", ")) -ForegroundColor DarkGray Start-Sleep $interval $elapsed += $interval } if ($elapsed -ge $maxWait) { Write-Warn "Some services did not respond in time -- continuing anyway" } Write-Host "" foreach ($svc in $services) { $ok = Test-ServiceUp $svc $icon = if ($ok) { "[OK]" } else { "[--]" } $color = if ($ok) { "Green" } elseif ($svc.Required) { "Yellow" } else { "DarkGray" } $suffix = if (-not $svc.Required -and -not $ok) { " (native -- start manually)" } else { "" } Write-Host (" {0,-6} {1,-16} {2}{3}" -f $icon, $svc.Name, $svc.Url, $suffix) -ForegroundColor $color } # -- 7. Write server_config.json ------------------------------------------- Write-Step "Configuring GUI" $orchPort = Get-EnvPort "ORCHESTRATOR_PORT" "1717" $seqPort = Get-EnvPort "SEQ_INTERP_PORT" "7475" $spectPort = Get-EnvPort "SPECTROSCOPY_PORT" "8002" $cfgPath = Join-Path $Root "apps\gui\cfg\server_config.json" $cfgObj = @{ srv_name = "srv_interp" log_dir = "log" upload_dir = "data/input" output_dir = "data/output" server_host = "0.0.0.0" server_port = [int]$seqPort orchestrator_url = "http://localhost:$orchPort" seq_interp_url = "http://localhost:$seqPort" spectroscopy_url = "http://localhost:$spectPort" mode = $Mode } $cfgObj | ConvertTo-Json -Depth 3 | Set-Content $cfgPath -Encoding utf8 Write-OK "server_config.json updated (mode=$Mode orch=:$orchPort seq=:$seqPort spectro=:$spectPort)" } # -- 8. Launch GUI ------------------------------------------------------------- if (-not $ServicesOnly) { Write-Step "Launching GUI" if (-not (Test-Path $VenvPython)) { Write-Warn "Venv not found -- falling back to system Python" $VenvPython = "python" } if (-not (Test-Path $AppScript)) { Write-Fail "GUI entry point not found: $AppScript" } Start-Process $VenvPython -ArgumentList "`"$AppScript`"" -WorkingDirectory $Root -WindowStyle Normal Write-OK "GUI launched" } # -- Summary ------------------------------------------------------------------- Write-Host "" Write-Host "============================================================" -ForegroundColor Green Write-Host (" LF-MRI platform is running [{0} mode]" -f $Mode.ToUpper()) -ForegroundColor Green Write-Host "============================================================" -ForegroundColor Green Write-Host "" if (-not $SkipSpectrometer) { Write-Host " Stop spectrometer: close the spectrometer terminal window" Write-Host " Stop pico-tcp: services\spectrometer\autokill.bat" } Write-Host " Stop all: .\stop.ps1" Write-Host " Update code: .\update.bat" Write-Host "" Write-Host " Log saved to: $LogFile" -ForegroundColor DarkGray try { Stop-Transcript | Out-Null } catch {} Write-Host " Press Enter to close this window..." -ForegroundColor DarkGray $null = Read-Host