- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base) - 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base) - 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、 东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base) - 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面 - 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、 J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder 开关(route_base、remote_route.ps1);剩 37 个网待手工补 - 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base); 已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查) - 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
167 行
8.2 KiB
PowerShell
167 行
8.2 KiB
PowerShell
param(
|
|
[string]$Server = '10.5.5.13',
|
|
[int]$Passes = 1,
|
|
[switch]$PriorityCache,
|
|
[switch]$DpOnly,
|
|
# 服务器并行试算:每个元素是一组额外参数(如 '--repair --bot-pen-w 1'),
|
|
# 各自在独立副本里跑,取未完成最少的那个走下面同一套校验 / 接受逻辑。
|
|
[string[]]$Variants = @(),
|
|
[int]$PollSec = 60,
|
|
# 摆位 / 布线模型改过之后,旧缓存的未完成数没有可比性:用这个开关无条件接受新结果
|
|
[switch]$AcceptAny,
|
|
# 服务器上跑布线的解释器;PyPy 见 PROGRESS(纯 Python 的 A*,JIT 通常快数倍)
|
|
[string]$Interp = 'python3'
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$Here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
$Repo = Resolve-Path (Join-Path $Here '..\..')
|
|
$Python = 'C:\Python314\python.exe'
|
|
$Ssh = 'C:\Windows\System32\OpenSSH\ssh.exe'
|
|
$Scp = 'C:\Windows\System32\OpenSSH\scp.exe'
|
|
# 临时文件带 PID:允许同时起两批(快的单轮变体 + 慢的拆线重布),互不覆盖
|
|
$Archive = Join-Path ([System.IO.Path]::GetTempPath()) "dcdc-remote-route-$PID.tgz"
|
|
$RunScript = Join-Path ([System.IO.Path]::GetTempPath()) "dcdc-remote-run-$PID.sh"
|
|
$RemoteArchive = Split-Path -Leaf $Archive
|
|
$Incoming = Join-Path $Here "_base_routes.server.$PID.json"
|
|
$Cache = Join-Path $Here '_base_routes.json'
|
|
$Backup = Join-Path $Here '_base_routes.pre-server.json'
|
|
$Rejected = Join-Path $Here '_base_routes.rejected.json'
|
|
$remoteRoot = $null
|
|
|
|
if ($Passes -lt 1) {
|
|
throw 'Passes must be at least 1.'
|
|
}
|
|
if ($DpOnly -and $Variants.Count -gt 0) {
|
|
throw '-DpOnly cannot be combined with -Variants.'
|
|
}
|
|
|
|
Push-Location $Repo
|
|
try {
|
|
& $Python '.claude\r2\route_base.py' --snapshot-vias
|
|
if ($LASTEXITCODE -ne 0) { throw 'Failed to refresh board-via snapshot.' }
|
|
|
|
$remoteRoot = (& $Ssh $Server 'mktemp -d /tmp/dcdc-route.XXXXXX').Trim()
|
|
if ($LASTEXITCODE -ne 0 -or $remoteRoot -notmatch '^/tmp/dcdc-route\.[A-Za-z0-9]+$') {
|
|
throw "Unexpected remote work directory: $remoteRoot"
|
|
}
|
|
|
|
& tar.exe --exclude=*.png --exclude=*.bak --exclude=__pycache__ `
|
|
--exclude=_remote_route.tgz -czf $Archive `
|
|
'.claude\r2' '.claude\eda.py' '.claude\pcb_pour.py' '.claude\eda-parts.json'
|
|
if ($LASTEXITCODE -ne 0) { throw 'Failed to build routing bundle.' }
|
|
|
|
& $Scp $Archive "${Server}:${remoteRoot}/"
|
|
if ($LASTEXITCODE -ne 0) { throw 'Failed to upload routing bundle.' }
|
|
& $Ssh $Server "tar -xzf ${remoteRoot}/${RemoteArchive} -C ${remoteRoot}"
|
|
if ($LASTEXITCODE -ne 0) { throw 'Failed to extract routing bundle.' }
|
|
|
|
$priority = if ($PriorityCache) { ' --priority-cache' } else { '' }
|
|
$scope = if ($DpOnly) { ' --dp-only' } else { '' }
|
|
$timeFmt = "/usr/bin/time -f 'REMOTE_TIME=%e sec CPU=%P RSS=%M KB'"
|
|
$remoteResult = "${remoteRoot}/.claude/r2/_base_routes.json"
|
|
|
|
if ($Variants.Count -eq 0) {
|
|
$command = "DCDC_ROUTE_SERVER=1 $timeFmt $Interp " +
|
|
"${remoteRoot}/.claude/r2/route_base.py --offline --replan --passes $Passes$priority$scope"
|
|
& $Ssh $Server $command
|
|
if ($LASTEXITCODE -ne 0) { throw 'Remote routing failed.' }
|
|
}
|
|
else {
|
|
# 变体各自一份副本(缓存文件同名,不能共用目录),脱离 ssh 会话在后台跑:
|
|
# 拆线重布可能要跑很久,ssh 断线不能把计算带走。
|
|
$lines = @('#!/bin/bash', "cd ${remoteRoot}")
|
|
for ($k = 0; $k -lt $Variants.Count; $k++) {
|
|
$v = $Variants[$k]
|
|
$lines += "mkdir -p v$k && cp -r .claude v$k/"
|
|
$lines += "( DCDC_ROUTE_SERVER=1 $timeFmt $Interp ${remoteRoot}/v$k/.claude/r2/route_base.py " +
|
|
"--offline --replan --passes $Passes$priority $v > v$k.log 2>&1; echo `$? > v$k.rc ) &"
|
|
}
|
|
$lines += 'wait'
|
|
$lines += 'touch done'
|
|
[System.IO.File]::WriteAllText($RunScript, (($lines -join "`n") + "`n"))
|
|
& $Scp $RunScript "${Server}:${remoteRoot}/run.sh"
|
|
if ($LASTEXITCODE -ne 0) { throw 'Failed to upload run script.' }
|
|
& $Ssh $Server "setsid nohup bash ${remoteRoot}/run.sh > /dev/null 2>&1 < /dev/null &"
|
|
if ($LASTEXITCODE -ne 0) { throw 'Failed to start remote variants.' }
|
|
for ($k = 0; $k -lt $Variants.Count; $k++) { Write-Host "v${k}: $($Variants[$k])" }
|
|
|
|
$start = Get-Date
|
|
$poll = 0
|
|
while ($true) {
|
|
Start-Sleep -Seconds $PollSec
|
|
$poll++
|
|
$state = (& $Ssh $Server "test -f ${remoteRoot}/done && echo DONE || echo RUN").Trim()
|
|
if ($state -eq 'DONE') { break }
|
|
if ($poll % 5 -eq 0) {
|
|
$mins = [int]((Get-Date) - $start).TotalMinutes
|
|
Write-Host "--- $mins min ---"
|
|
& $Ssh $Server "cd ${remoteRoot} && for f in v*.log; do echo `"`$f: `$(tail -n 1 `$f)`"; done"
|
|
}
|
|
}
|
|
|
|
$summary = & $Ssh $Server ("cd ${remoteRoot} && for k in `$(seq 0 $($Variants.Count - 1)); do " +
|
|
"echo `"v`$k rc=`$(cat v`$k.rc) `$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); " +
|
|
"print(len(d[\`"logs\`"]), len(d[\`"segs\`"]), len(d[\`"vias\`"]))' " +
|
|
"v`$k/.claude/r2/_base_routes.json 2>/dev/null || echo NA)`"; done")
|
|
$best = $null
|
|
foreach ($row in $summary) {
|
|
Write-Host $row
|
|
if ($row -match '^v(\d+) rc=0 (\d+) (\d+) (\d+)$') {
|
|
$cand = @{ K = [int]$Matches[1]; Logs = [int]$Matches[2]; Vias = [int]$Matches[4] }
|
|
if (-not $best -or $cand.Logs -lt $best.Logs -or
|
|
($cand.Logs -eq $best.Logs -and $cand.Vias -lt $best.Vias)) {
|
|
$best = $cand
|
|
}
|
|
}
|
|
}
|
|
for ($k = 0; $k -lt $Variants.Count; $k++) {
|
|
Write-Host "===== v${k}: $($Variants[$k]) ====="
|
|
& $Ssh $Server "grep -E '拆线重布|第 [0-9]+ 轮|REMOTE_TIME|Traceback|Error' ${remoteRoot}/v$k.log | tail -n 25"
|
|
}
|
|
if (-not $best) { throw 'No remote variant finished successfully.' }
|
|
Write-Host "Best variant: v$($best.K) ($($Variants[$best.K])) with $($best.Logs) unfinished."
|
|
$remoteResult = "${remoteRoot}/v$($best.K)/.claude/r2/_base_routes.json"
|
|
}
|
|
|
|
if ($DpOnly) {
|
|
Write-Host 'Server-side D+/- diagnostic completed; local route cache was not changed.'
|
|
}
|
|
else {
|
|
& $Scp "${Server}:${remoteResult}" $Incoming
|
|
if ($LASTEXITCODE -ne 0) { throw 'Failed to download routing result.' }
|
|
$result = Get-Content -LiteralPath $Incoming -Raw | ConvertFrom-Json
|
|
# schema 8 = 4 层版(D15);7 是 2 层版,必须被拒
|
|
if ($result.schema -ne 8 -or $result.layers -ne 4 -or $result.slot_rot -ne 0 -or
|
|
$result.usb_slot_ref.'1' -ne 'J34' -or $result.usb_slot_ref.'5' -ne 'J38') {
|
|
throw 'Downloaded routing cache failed topology validation.'
|
|
}
|
|
$current = if (Test-Path -LiteralPath $Cache) {
|
|
Get-Content -LiteralPath $Cache -Raw | ConvertFrom-Json
|
|
} else { $null }
|
|
if ($current -and -not $AcceptAny -and $result.logs.Count -gt $current.logs.Count) {
|
|
Move-Item -LiteralPath $Incoming -Destination $Rejected -Force
|
|
Write-Host "Rejected worse server result: $($result.logs.Count) unfinished > current $($current.logs.Count)."
|
|
}
|
|
else {
|
|
if ($current) {
|
|
Copy-Item -LiteralPath $Cache -Destination $Backup -Force
|
|
}
|
|
Move-Item -LiteralPath $Incoming -Destination $Cache -Force
|
|
Write-Host "Accepted server result: $($result.segs.Count) segments, $($result.vias.Count) vias, $($result.logs.Count) unfinished."
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
foreach ($f in @($Archive, $RunScript)) {
|
|
if (Test-Path -LiteralPath $f) { Remove-Item -LiteralPath $f -Force }
|
|
}
|
|
if ($remoteRoot -and $remoteRoot -match '^/tmp/dcdc-route\.[A-Za-z0-9]+$') {
|
|
# ⛔ 模式里的 `(v[0-9]+/)?[.]claude` 是故意的:直接写目录串的话,执行这条命令的
|
|
# 远程 shell 命令行里也含这个串,pkill 先把自己杀了(ssh 返回 255),后面的 rm
|
|
# 根本不执行。按路径匹配而不是按 `python3`,换成 pypy3 也杀得到。
|
|
& $Ssh $Server "pkill -f '${remoteRoot}/(v[0-9]+/)?[.]claude/r2/route_base' ; rm -rf -- '${remoteRoot}'"
|
|
}
|
|
}
|