ads-discover.ps1 — Beckhoff ADS Broadcast 搜尋工具
直接用 PowerShell 發 ADS discovery broadcast(UDP 48899)找同網段的 Beckhoff TwinCAT 裝置,不需要安裝 TwinCAT,也不依賴 Npcap。適合在 TwinCAT Broadcast Search 失敗時做為獨立驗證工具。
前提
- Windows + PowerShell 5.1 或 7+
- 電腦與 PLC 在同一個 L2 網段(直連或同一台 switch)
- PLC 已開機、網路孔指示燈亮
- 本機網卡有 IPv4(固定 IP 或 APIPA
169.254.x.x都可)
腳本本身不需要系統管理員權限。
腳本內容
將以下內容存成 ads-discover.ps1:
# ADS Broadcast Discovery for Beckhoff TwinCAT devices
# Sends UDP discovery packet to 255.255.255.255:48899 and listens for responses.
#
# Usage:
# .\ads-discover.ps1 # auto-detect adapter
# .\ads-discover.ps1 -AdapterName '*Realtek*' # pick by adapter description (wildcard OK)
# .\ads-discover.ps1 -LocalIP 169.254.188.1 # pin to specific IP
# .\ads-discover.ps1 -TimeoutMs 8000 # listen longer
# .\ads-discover.ps1 -ShowHex # dump response bytes
param(
[string]$LocalIP,
[string]$AdapterName,
[int]$TimeoutMs = 4000,
[int]$DiscoveryPort = 48899,
[switch]$ShowHex
)
$ErrorActionPreference = 'Stop'
# Resolve LocalIP from adapter name if given (more stable than IP — APIPA can change)
if (-not $LocalIP -and $AdapterName) {
$ad = Get-NetAdapter | Where-Object {
$_.Status -eq 'Up' -and ($_.Name -like $AdapterName -or $_.InterfaceDescription -like $AdapterName)
} | Select-Object -First 1
if (-not $ad) { throw "No Up adapter matches '$AdapterName'." }
$LocalIP = (Get-NetIPAddress -InterfaceIndex $ad.ifIndex -AddressFamily IPv4 -AddressState Preferred).IPAddress
}
# Full auto-detect: first Up IPv4 adapter, non-loopback
if (-not $LocalIP) {
$cand = Get-NetIPAddress -AddressFamily IPv4 -AddressState Preferred -ErrorAction SilentlyContinue |
Where-Object { $_.IPAddress -ne '127.0.0.1' -and (Get-NetAdapter -InterfaceIndex $_.InterfaceIndex).Status -eq 'Up' } |
Select-Object -First 1
if (-not $cand) { throw "No active IPv4 adapter found." }
$LocalIP = $cand.IPAddress
}
Write-Host "[*] Using local IP: $LocalIP"
# Sender AmsNetId = local IP + .1.1 (Beckhoff convention for unroutted senders)
$ipBytes = ([System.Net.IPAddress]::Parse($LocalIP)).GetAddressBytes()
$netId = [byte[]]($ipBytes + @(1,1))
$senderPort = 10000
# 24-byte ADS discovery request
$packet = [byte[]]::new(24)
$packet[0]=0x03; $packet[1]=0x66; $packet[2]=0x14; $packet[3]=0x71 # magic
# bytes 4..7 = request type 0x00000000 (discover)
for ($i=0; $i -lt 6; $i++) { $packet[8+$i] = $netId[$i] }
$packet[14] = [byte]( $senderPort -band 0xFF)
$packet[15] = [byte](($senderPort -shr 8) -band 0xFF)
$packet[16] = 1
# UDP socket bound to chosen adapter
$udp = New-Object System.Net.Sockets.UdpClient
$udp.Client.SetSocketOption(
[System.Net.Sockets.SocketOptionLevel]::Socket,
[System.Net.Sockets.SocketOptionName]::ReuseAddress, $true)
$udp.Client.Bind([System.Net.IPEndPoint]::new([System.Net.IPAddress]::Parse($LocalIP), 0))
$udp.EnableBroadcast = $true
$dest = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Broadcast, $DiscoveryPort)
[void]$udp.Send($packet, $packet.Length, $dest)
Write-Host ("[+] Broadcast sent to 255.255.255.255:{0} ({1} bytes)" -f $DiscoveryPort, $packet.Length)
Write-Host ("[*] Listening for {0} ms..." -f $TimeoutMs)
$udp.Client.ReceiveTimeout = $TimeoutMs
$results = @()
try {
while ($true) {
$remote = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0)
$data = $udp.Receive([ref]$remote)
$nid = if ($data.Length -ge 14) { ($data[8..13]) -join '.' } else { '?' }
$hex = ($data | ForEach-Object { $_.ToString('X2') }) -join ' '
$results += [PSCustomObject]@{
IP = $remote.Address.ToString()
AmsNetId = $nid
Bytes = $data.Length
Hex = $hex
}
}
} catch {
# ReceiveTimeout -> normal exit
}
$udp.Close()
if ($results.Count -eq 0) {
Write-Host "[-] No responses."
Write-Host " Check: adapter link, Windows Firewall, PLC boot state."
} else {
if ($ShowHex) {
$results | Format-Table IP, AmsNetId, Bytes, Hex -AutoSize -Wrap
} else {
$results | Format-Table IP, AmsNetId, Bytes -AutoSize
}
Write-Host ("[+] {0} device(s) found." -f $results.Count)
}
用法
# 1. 自動挑第一張 Up 的網卡
.\ads-discover.ps1
# 2. 指定網卡(建議做法 — 比 IP 穩定,APIPA 會漂移)
.\ads-discover.ps1 -AdapterName '*Realtek*PCIe*GbE*'
# 3. 指定來源 IP(僅當該 IP 是本機某張 Up 網卡的當前 IP 時才有效)
.\ads-discover.ps1 -LocalIP 169.254.188.1
# 4. 聽久一點(PLC 剛開機或裝置多時)
.\ads-discover.ps1 -AdapterName '*Realtek*' -TimeoutMs 8000
# 5. 顯示完整回應 hex(除錯用)
.\ads-discover.ps1 -AdapterName '*Realtek*' -ShowHex
若執行時被 ExecutionPolicy 擋:
powershell -ExecutionPolicy Bypass -File .\ads-discover.ps1 -AdapterName '*Realtek*'
參數
| 參數 | 型別 | 預設 | 說明 |
|---|---|---|---|
-LocalIP | string | — | 從哪個本機 IP 送 broadcast。省略則走 -AdapterName 或自動偵測。 |
-AdapterName | string | — | 匹配 Name 或 InterfaceDescription,支援萬用字元。最穩定的指定方式。 |
-TimeoutMs | int | 4000 | 送出後的收封包時間窗(毫秒)。 |
-DiscoveryPort | int | 48899 | ADS discovery UDP port,非必要不要改。 |
-ShowHex | switch | off | 印出每個回應的完整 bytes(hex)。 |
輸出欄位
IP AmsNetId Bytes
-- -------- -----
169.254.188.13 169.254.188.129.5.124 24
| 欄位 | 含義 |
|---|---|
IP | 可靠。 回應封包的 UDP 來源 IP,就是 PLC 的 IP。 |
AmsNetId | 僅供參考,不要當真。 這是回應 bytes 8..13 解讀出來的值。實測會隨本機送出 NetId 而變動,不是 PLC 真正的 AmsNetId。 |
Bytes | 回應封包長度。一般 ≥ 24。 |
關於 AmsNetId:本腳本不解析完整的 ADS discovery TLV 區段。真正的 PLC AmsNetId 請在 TwinCAT Router 的 Add Route Dialog 裡填入 IP 後按 Address Info 讓它向 PLC 即時查詢。
典型流程(找到 PLC → 建立 TwinCAT Route)
1. 接上網路線、PLC 開機
2. .\ads-discover.ps1 -AdapterName '*Realtek*'
→ 取得 PLC IP(例如 169.254.188.13)
3. ping 169.254.188.13 # 確認 L3 通
4. Test-NetConnection ... -Port 48898 # 確認 ADS TCP 通
5. 打開 TwinCAT tray icon → Router → Edit Routes → Add
- Advanced Settings
- IP Address: 169.254.188.13
- 按 Address Info → 自動帶出 AmsNetId
- Transport: TCP/IP
- Remote User / Password: CX 系列新機預設 Administrator / 1
按 Add Route
故障排除
| 症狀 | 可能原因 | 處理 |
|---|---|---|
Bind 失敗:The requested address is not valid in its context | -LocalIP 帶的 IP 已經不是該網卡當前的 IP(APIPA 重配) | 改用 -AdapterName '*Realtek*' 或先用 Get-NetIPAddress -InterfaceIndex <idx> 看當前 IP |
No responses 但 PLC 指示燈正常 | 1. Windows 防火牆把 UDP 48899 回應擋掉 2. 不同 VLAN / L3 網段 | 1. 暫時關防火牆測試,或放行 PowerShell 2. 確認兩邊同網段 |
No responses,PLC 剛開機 | CX 系列冷開機 30 秒 ~ 2 分鐘才起得來 | 等指示燈穩定再跑一次,或 -TimeoutMs 10000 |
| 找到裝置但 TwinCAT 仍連不上 | TwinCAT Router 的 AmsNetId 沒對到,或尚未 Add Route | 走上面的「典型流程」手動 Add Route |
| 多張網卡都自動偵測到錯的那張 | 自動偵測只取第一張 Up 的 | 明確指定 -AdapterName |
背後原理(簡述)
ADS discovery 封包是 24 bytes 的 UDP broadcast:
offset 0..3 : 03 66 14 71 magic
offset 4..7 : 00 00 00 00 request type = discover
offset 8..13 : <sender AmsNetId> 6 bytes,本腳本用「本機 IP + 1.1」
offset 14..15 : <sender port LE> 0x2710 = 10000
offset 16 : 01 option flag
offset 17..23 : 00 reserved
發到 255.255.255.255:48899。同網段的 Beckhoff 裝置會以 UDP 回覆到來源 IP:port。本腳本取 UDP 來源 IP 當 PLC IP(可靠),並顯示回應 bytes 8..13(僅供觀察,非真正 AmsNetId)。
本協定未公開文件化,格式來自社群逆向工程(pyads、ads-discover 等開源工具)。可能隨 TwinCAT 版本變化,目前在 TC2/TC3 都能工作。