#Requires -Version 5.1
<#
.SYNOPSIS
Deploy Primary-side TwinCAT project - scan hardware, fix PDO, link PLC, activate, start runtime.
#>
param(
[string]$Solution = 'C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary\TwinCATProject1.sln',
[string]$Tsproj = 'C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary\TwinCATProject1.tsproj',
[string]$TargetNetId = '169.254.176.217.1.1',
[string]$LogDir = 'C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary\Logs',
[switch]$SkipScan
)
$ErrorActionPreference = 'Continue'
$LogFile = Join-Path $LogDir "deploy-primary.log"
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
function Log($m) {
$line = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + " $m"
Add-Content -Path $LogFile -Value $line -Encoding utf8 -ErrorAction SilentlyContinue
Write-Host $line
}
function LogSection($m) { Log ''; Log ('='*65); Log " $m"; Log ('='*65) }
Log '=== tc-deploy-primary start ==='
# ---- Step 1: Fix PDO in tsproj ----
LogSection 'Step 1/5: Fix PDO in tsproj'
$content = [System.IO.File]::ReadAllText($Tsproj, [System.Text.Encoding]::UTF8)
$oldPdoIn = ''
$newPdoIn = @"
ARRAY [0..1023] OF BYTE
"@
if ($content -match [regex]::Escape($oldPdoIn)) { $content = $content -replace [regex]::Escape($oldPdoIn), $newPdoIn; Log ' IO Inputs PDO fixed' }
$oldPdoOut = ''
$newPdoOut = @"
ARRAY [0..1023] OF BYTE
"@
if ($content -match [regex]::Escape($oldPdoOut)) { $content = $content -replace [regex]::Escape($oldPdoOut), $newPdoOut; Log ' IO Outputs PDO fixed' }
# Fix SM lengths for EL6695
$content = $content -replace '001c00006400000003000000000000000000001c64010000', '001c00040000000003000000000000000000001c64010000'
$content = $content -replace '008e02002000010004000000000000000200008e20010000', '008e02040000010004000000000000000200008e20010000'
[System.IO.File]::WriteAllText($Tsproj, $content, [System.Text.Encoding]::UTF8)
Log ' tsproj PDO fix saved'
# ---- Step 2: Find adapter and write Pnp binding ----
LogSection 'Step 2/5: Discover network adapter + Pnp binding'
$adapters = Get-NetAdapter -Physical | Where-Object { $_.Status -eq 'Up' }
$sel = $adapters | Where-Object { $_.InterfaceDescription -like '*TwinCAT*' } | Select-Object -First 1
if (-not $sel) { $sel = $adapters | Where-Object { $_.InterfaceDescription -like '*Intel*' } | Select-Object -First 1 }
if (-not $sel) { $sel = $adapters[0] }
$adapterMac = ($sel.MacAddress -replace '-', '').ToLower()
$adapterName = $sel.Name
Log " Adapter: $adapterName MAC=$adapterMac"
if ($content -match '.*?') { $content = $content -replace '.*?', '' }
$addrBlock = @"
$adapterName (TwinCAT-Intel PCI Ethernet Adapter - $($sel.InterfaceDescription))
\DEVICE\$adapterMac
$adapterMac
"@
$content = $content -replace '(?s)([^<]+\s*)', "`$1`t`t`t`t$addrBlock`n`t`t`t`t"
[System.IO.File]::WriteAllText($Tsproj, $content, [System.Text.Encoding]::UTF8)
Log ' Pnp binding written'
if ($SkipScan) {
Log ' -SkipScan: skipping hardware scan, using tsproj as-is'
} else {
# ---- Step 3: Open DTE, reload devices, activate ----
LogSection 'Step 3/5: Open DTE + reload devices'
try { $dte = New-Object -ComObject TcXaeShell.DTE.17.0 } catch { $dte = New-Object -ComObject TcXaeShell.DTE.15.0 }
$dte.MainWindow.Visible = $true
try { $dte.SuppressUI = $false } catch { }
$dte.Solution.Open($Solution)
for ($i = 1; $i -le 120; $i++) { Start-Sleep -Seconds 1; if ($dte.Solution.IsOpen) { Log "IsOpen after ${i}s"; break } }
$sm = $null
for ($i = 1; $i -le 30; $i++) { try { $sm = $dte.Solution.Projects.Item(1).Object; if ($sm) { Log 'SysManager ready'; break } } catch {}; Start-Sleep -Seconds 3 }
Start-Sleep -Seconds 15
try { $sm.SetTargetNetId($TargetNetId); Log 'SetTargetNetId OK' } catch { Log "SetTargetNetId err: $_" }
# Activate to go online (needed before reload)
try { $dte.SuppressUI = $true } catch { }
try { $sm.ActivateConfiguration(); Log 'ActivateConfiguration OK' } catch { Log "Activate err: $_" }
Start-Sleep -Seconds 10
# Reload devices to scan actual hardware topology
$bytes = @(0xE9,0x87,0x8D,0xE6,0x96,0xB0,0xE5,0x8A,0xA0,0xE8,0xBD,0xBD,0xE8,0xAE,0xBE,0xE5,0xA4,0x87)
$reloadCmd = 'TwinCAT.' + [System.Text.Encoding]::UTF8.GetString($bytes)
try { $dte.ExecuteCommand($reloadCmd); Log "Reload devices OK" } catch { Log "Reload err: $_" }
Start-Sleep -Seconds 10
# ---- Step 4: Link PLC variables to IO ----
LogSection 'Step 4/5: Link PLC variables to IO'
try {
# Find PLC instance tree item
$plcInstance = $sm.LookupTreeItem('TIPC^PlcSync^PlcSync Instance')
if ($plcInstance) {
Log "PLC instance found: $($plcInstance.Name)"
# Find Outputs and Inputs sub-items
for ($j = 1; $j -le [int]$plcInstance.ChildCount; $j++) {
try {
$child = $plcInstance.Child($j)
Log " Child: $($child.Name)"
for ($k = 1; $k -le [int]$child.ChildCount; $k++) {
try { $var = $child.Child($k); Log " Var: $($var.Name) type=$($var.ItemType)" } catch {}
}
} catch {}
}
} else { Log ' PLC instance not found in tree' }
} catch { Log " Tree lookup err: $($_.Exception.Message)" }
# ---- Step 5: Activate and start ----
LogSection 'Step 5/5: Activate + start runtime'
try { $dte.SuppressUI = $true } catch { }
try { $sm.ActivateConfiguration(); Log 'ActivateConfiguration OK' } catch { Log "Activate err: $_" }
Start-Sleep -Seconds 10
try { $sm.StartRestartTwinCAT(); Log 'StartRestartTwinCAT executed' } catch { Log "StartRestart err: $_" }
$adsDll = 'C:\Program Files (x86)\Beckhoff\TwinCAT\3.1\Components\Base\v170\TwinCAT.Ads.dll'
Add-Type -Path $adsDll -ErrorAction SilentlyContinue
function Get-TcState {
try { $c = New-Object TwinCAT.Ads.TcAdsClient; $c.Connect($TargetNetId, 10000); $s = $c.ReadState(); $c.Dispose(); return "$($s.AdsState)" }
catch { return "ERR" }
}
for ($i = 1; $i -le 12; $i++) {
Start-Sleep -Seconds 5; $st = Get-TcState
Log "state +$(($i)*5)s: $st"
if ($st -eq 'Run') { break }
}
try { $dte.Quit() } catch {}
Log "Final state: $(Get-TcState)"
}
LogSection 'Deploy complete'
Log '=== tc-deploy-primary end ==='