From 3a26aea0821a48d9322b5f1eb52c37c7941bbace Mon Sep 17 00:00:00 2001 From: Tony Cao Date: Tue, 28 Jul 2026 00:11:34 +0800 Subject: [PATCH] 2kHz spin-lock mode: RTT halved to 2ms, per-byte XOR proof at 500us cycle - ticker: --spin mode polls FPRD until payload changes (phase-locks processing to TwinCAT frame arrival instead of free-running ticker), timeout at 1.5 periods keeps loop alive on master stop - verified: 238k cycles, 0 errors, exec=245us (FPWR-bound), TwinCAT PLC task confirmed at 2000.8 Hz (all three tasks 0.5ms; base tick was already 50us - no base-time change needed) - docs: 500us latency plan; scripts: task-rate ADS probe, 2kHz deploy, UI automation experiments (pywinauto) for task cycle inspection --- docs/500us-latency-plan.md | 132 +++++++++++++++++++++++++++++++ scripts/tc-ads-rtt.ps1 | 31 ++++++++ scripts/tc-build-diag-2khz.ps1 | 45 +++++++++++ scripts/tc-build-output-2khz.ps1 | 36 +++++++++ scripts/tc-deploy-retry-2khz.ps1 | 61 ++++++++++++++ scripts/tc-dump-tirr.ps1 | 20 +++++ scripts/tc-dump-tirt.ps1 | 22 ++++++ scripts/tc-enum-cfgmgr.ps1 | 16 ++++ scripts/tc-enum-root.ps1 | 23 ++++++ scripts/tc-find-basetime.ps1 | 36 +++++++++ scripts/tc-find-commands.ps1 | 14 ++++ scripts/tc-find-system-node.ps1 | 21 +++++ scripts/tc-rt-node.ps1 | 26 ++++++ scripts/tc-task-rate.ps1 | 12 +++ scripts/tc-walk-tree.ps1 | 18 +++++ src/main.rs | 121 ++++++++++++++++++++-------- uia_edits.py | 19 +++++ uia_explore.py | 35 ++++++++ uia_open_rt.py | 53 +++++++++++++ uia_tasks.py | 46 +++++++++++ uia_tree.py | 29 +++++++ 21 files changed, 784 insertions(+), 32 deletions(-) create mode 100644 docs/500us-latency-plan.md create mode 100644 scripts/tc-ads-rtt.ps1 create mode 100644 scripts/tc-build-diag-2khz.ps1 create mode 100644 scripts/tc-build-output-2khz.ps1 create mode 100644 scripts/tc-deploy-retry-2khz.ps1 create mode 100644 scripts/tc-dump-tirr.ps1 create mode 100644 scripts/tc-dump-tirt.ps1 create mode 100644 scripts/tc-enum-cfgmgr.ps1 create mode 100644 scripts/tc-enum-root.ps1 create mode 100644 scripts/tc-find-basetime.ps1 create mode 100644 scripts/tc-find-commands.ps1 create mode 100644 scripts/tc-find-system-node.ps1 create mode 100644 scripts/tc-rt-node.ps1 create mode 100644 scripts/tc-task-rate.ps1 create mode 100644 scripts/tc-walk-tree.ps1 create mode 100644 uia_edits.py create mode 100644 uia_explore.py create mode 100644 uia_open_rt.py create mode 100644 uia_tasks.py create mode 100644 uia_tree.py diff --git a/docs/500us-latency-plan.md b/docs/500us-latency-plan.md new file mode 100644 index 0000000..fd01a67 --- /dev/null +++ b/docs/500us-latency-plan.md @@ -0,0 +1,132 @@ +# EL6695 端到端 500µs 延迟优化 — 计划文档 + +> **版本**: 1.0 +> **日期**: 2026-07-27 +> **状态**: 待执行 +> **前置文档**: `8b-bridge-design.md`、`8b-verification-methodology.md`、`8b-test-report.md` + +--- + +## 1. 目标 + +| 目标 | 定义 | 优先级 | +|---|---|---| +| G1(主目标) | **单向**端到端延迟 ≤ 500µs(TwinCAT 写出 → J1900 可读,或 J1900 写出 → TwinCAT 可读) | 必须 | +| G2(Stretch) | **往返**延迟(aOut1 变化 → aIn1 可见对应回传)≤ 500µs | 力争 | + +验收方式与 8B 验证体系一致:逐字节图案 + 历史环匹配测延迟(`nMatchLag` 换算为 µs)+ J1900 周期统计 + 连续 10 分钟零错误。 + +--- + +## 2. 现状基线(实测) + +当前两侧均为 1kHz 自由运行(未同步),RTT 恒为 4 个 PLC 周期(≈4ms),单向 ≈2ms。 + +| 构成 | 实测值 | 性质 | 可压缩性 | +|---|---|---|---| +| TwinCAT 任务量化(PLC 写 → 帧发出) | ≤1000µs | 调度 | 提频 / DC 锁相 | +| EtherCAT 帧传输(9 端子, 8B) | ~15µs | 硬件 | 不可压缩(下限) | +| EL6695 桥内部转发 | 微秒级(推定) | 固件 | 待实测确认 | +| J1900 采样量化(自由 ticker) | ≤1000µs | 调度 | 提频 / DC 锁相 | +| J1900 处理(exec) | 313µs 均值 / 359µs 峰值 | 软件+事务 | 优化写路径 | +| ├ FPRD(读 SM) | 71µs 均值 / 84µs p99 | | | +| └ FPWR(写 SM) | **242µs 均值 / 246µs p99** | | **主要瓶颈** | +| 返回路径量化 | ≤1000µs | 调度 | 提频 / DC 锁相 | + +**关键结论**: 4ms 中约 3ms 是两侧 1kHz 自由运行的相位量化,~0.3ms 是 J1900 写事务,硬件传输仅 ~15µs 级。延迟优化的本质是**消除量化 + 压缩 J1900 写耗时**。 + +--- + +## 3. 阶段计划 + +### 阶段 0:精确测量现状分解(0.5 天) + +| # | 任务 | 方法 | 产出 | +|---|---|---|---| +| 0.1 | 测量桥纯转发延迟 | J1900 侧环回测试(`ecat_el6695_loopback_test` 已有)或对同一字节做"写后立即读"测 DPRAM 镜像时间 | 桥转发延迟(预期 <50µs) | +| 0.2 | 定位 FPWR 242µs 根因 | 对比 FPRD/FPWR 在不同 payload(8B/64B/1KB)下的耗时;检查 ethercrab PDU 等待逻辑;逻辑分析仪或帧时间戳 | 结论:固件写处理慢 vs 软件等待方式 | +| 0.3 | J1900 高频空跑 | ticker 以 2kHz/4kHz/10kHz 空跑(不读写),测周期抖动与 CPU 占用 | J1900 可达到的稳定频率上限 | + +**决策点 D0**: 若 FPWR 是固件特性(写路径必慢),则单向 J1900→TwinCAT 方向延迟受此下限约束,G1 双向需重新评估;若可用 LRW 合并读写(exec → ~100µs),进入阶段 1。 + +### 阶段 1:J1900 写路径优化(1 天) + +| # | 任务 | 预期收益 | +|---|---|---| +| 1.1 | 试验 LRW(单帧逻辑读写)替代 FPRD+FPWR,利用已有 `ecat_el6695_mem_reflect_lrw` 程序验证 | exec 313µs → ~150µs | +| 1.2 | 若 LRW 不可用:FPWR 异步化(发出不等响应,下周期校验 WKC) | exec → ~120µs | +| 1.3 | RT 调优复核:核隔离、IRQ 亲和、禁用 eth1 节能 | 抖动 7µs → <5µs | + +**验收**: 1kHz 下 exec p99 < 200µs,10 分钟零错误。 + +### 阶段 2:双侧提频(0.5 天) + +| # | 任务 | 备注 | +|---|---|---| +| 2.1 | TwinCAT PlcTask 周期 1000µs → 500µs(2kHz) | XAE 直接改;确认 TcRTime 在该硬件支持 500µs 基频 | +| 2.2 | J1900 ticker `--cycle 250`(4kHz) | 需阶段 1 完成(exec < 250µs) | +| 2.3 | PLC 历史环匹配窗口适配新周期(16 槽仍覆盖 4ms) | 仅改注释/常量 | + +**预期延迟**: RTT 4 阶段 × 250µs = 1ms;单向 ≈ 500µs(G1 达标,余量小)。 +**风险**: 自由运行相位最差时单向可能到 750µs → 需要阶段 3。 + +### 阶段 3:DC 分布式时钟同步(1–2 天) + +| # | 任务 | 备注 | +|---|---|---| +| 3.1 | TwinCAT 侧启用 EL6695 DC(SYNC0),设为参考时钟或跟随现有 DC 主 | 参考工程 `TwinCATProject1_DC_Reference` 已有 DC 配置可借用 | +| 3.2 | J1900 读取 EL6695 DC 系统时间,ticker 锁相 SYNC0(ethercrab 支持 DC 读;或轮询 0x0910 寄存器对齐) | 消除 J1900 采样量化 | +| 3.3 | 相位标定:调整 J1900 唤醒相位,使"帧到达 → 立即处理 → 帧返回前写回"在单周期内完成 | 目标:单向 = 帧传输 + 桥转发 + exec ≈ 200–300µs | + +**预期延迟(2kHz + DC)**: +- 单向: ~200–350µs ✅ G1 有余量 +- 往返: ~500–700µs,G2 边缘 + +### 阶段 4:G2 冲刺(可选,视阶段 3 结果) + +仅当 G2 明确需要时:两侧 4kHz(250µs 周期)+ DC + exec < 150µs。往返 = 帧 + 桥 + exec + 帧 ≈ 400–500µs。余量极小,需 EL6695 固件最小周期(待阶段 0 实测)允许 250µs。 + +### 阶段 5:验证与文档(0.5 天) + +- 按 `8b-verification-methodology.md` 体系做 10 分钟连续验证(逐字节图案 + lag 测量); +- 更新设计/验证/测试三份文档,新增延迟分解表; +- git 提交。 + +--- + +## 4. 里程碑与决策点 + +| 里程碑 | 判据 | 决策 | +|---|---|---| +| D0(阶段 0 末) | FPWR 根因明确 | 固件瓶颈→重谈目标;软件可优化→继续 | +| M1(阶段 1 末) | exec p99 < 200µs | 不达标→LRW/异步化二选一深挖 | +| M2(阶段 2 末) | 2kHz 稳定,单向 ≈500µs | G1 名义达标 | +| M3(阶段 3 末) | DC 锁相,单向 <350µs | G1 稳妥达标;评估是否冲 G2 | +| M4(阶段 5 末) | 10 分钟零错误 + 文档提交 | 收尾 | + +--- + +## 5. 风险登记 + +| # | 风险 | 概率 | 影响 | 缓解 | +|---|---|---|---|---| +| R1 | EL6695 固件最小周期 > 250µs,无法 4kHz | 中 | G2 不可达 | 阶段 0 实测;G1 不受影响 | +| R2 | FPWR 慢是固件特性 | 中 | J1900→TwinCAT 方向下限 ~250µs | LRW 合并;或接受单向非对称 | +| R3 | TcRTime 在 500µs 基频下抖动大 | 低 | 量化消除不彻底 | 实测 TwinCAT 任务抖动;必要时 1kHz+DC | +| R4 | ethercrab 不支持 DC 时间读取/锁相 | 低 | 阶段 3 受阻 | 轮询 DC 寄存器 0x0910 软件锁相 | +| R5 | 提频后 E-bus 上其他端子(EL6851/EL6022 串口模块)不支持 | 中 | 2kHz 激活失败 | 串口模块 PDO 移出高速任务或降低其更新率 | + +--- + +## 6. 资源与依赖 + +- 硬件现状即可,无需新增; +- 依赖阶段 0 的三个测量结论再细化后续工时; +- J1900 已有实验程序可复用:`ecat_el6695_loopback_test`、`ecat_el6695_mem_reflect_lrw`、`ecat_el6695_mem_reflect_dc`; +- TwinCAT 侧 DC 配置参考: `C:\Users\tonycao\work\free\TwinCATProject1_DC_Reference`。 + +## 7. 修订历史 + +| 日期 | 内容 | +|---|---| +| 2026-07-27 | 初版计划 | diff --git a/scripts/tc-ads-rtt.ps1 b/scripts/tc-ads-rtt.ps1 new file mode 100644 index 0000000..9627786 --- /dev/null +++ b/scripts/tc-ads-rtt.ps1 @@ -0,0 +1,31 @@ +#Requires -RunAsAdministrator +#Requires -Version 5.1 +$ErrorActionPreference = 'Continue' +$adsDll = 'C:\Program Files (x86)\Beckhoff\TwinCAT\3.1\Components\Base\v170\TwinCAT.Ads.dll' +Add-Type -Path $adsDll -ErrorAction Stop +$c = New-Object TwinCAT.Ads.TcAdsClient +$c.Connect('169.254.176.217.1.1', 851) +function ReadSym($name, $type) { + try { + $h = $c.CreateVariableHandle($name) + $v = $c.ReadAny($h, $type) + $c.DeleteVariableHandle($h) + return $v + } catch { return "ERR" } +} +Write-Host "nCycles = $(ReadSym 'GVL_Sync.nCycles' ([uint64]))" +Write-Host "bSync = $(ReadSym 'GVL_Sync.bSync' ([bool]))" +Write-Host "nMatchLag = $(ReadSym 'GVL_Sync.nMatchLag' ([int])) (bridge round-trip, PLC cycles)" +Write-Host "nMismatch = $(ReadSym 'GVL_Sync.nMismatch' ([int]))" +Write-Host "--- RTT (PLC cycles, 1 cycle = 1 ms) ---" +Write-Host "nRttCycles = $(ReadSym 'GVL_Sync.nRttCycles' ([int]))" +Write-Host "nRttMin = $(ReadSym 'GVL_Sync.nRttMin' ([int]))" +Write-Host "nRttMax = $(ReadSym 'GVL_Sync.nRttMax' ([int]))" +Write-Host "nRttSum = $(ReadSym 'GVL_Sync.nRttSum' ([int64]))" +Write-Host "nRttCount = $(ReadSym 'GVL_Sync.nRttCount' ([uint64]))" +Write-Host "--- EL1252 DC latch path delay ---" +Write-Host "nLatchCount = $(ReadSym 'GVL_Sync.nLatchCount' ([uint64]))" +Write-Host "nLatchDelayMin = $(ReadSym 'GVL_Sync.nLatchDelayMin' ([int])) us" +Write-Host "nLatchDelayMax = $(ReadSym 'GVL_Sync.nLatchDelayMax' ([int])) us" +Write-Host "nLatchDelaySum = $(ReadSym 'GVL_Sync.nLatchDelaySum' ([int64])) ns" +$c.Dispose() diff --git a/scripts/tc-build-diag-2khz.ps1 b/scripts/tc-build-diag-2khz.ps1 new file mode 100644 index 0000000..d63fd73 --- /dev/null +++ b/scripts/tc-build-diag-2khz.ps1 @@ -0,0 +1,45 @@ +#Requires -RunAsAdministrator +$ErrorActionPreference = 'Continue' +function Invoke-Com { + param([scriptblock]$Fn, [int]$Retries = 5, [int]$DelayMs = 2000) + for ($i = 0; $i -lt $Retries; $i++) { + try { return & $Fn } catch { + if ($_.Exception.HResult -eq -2147418111) { Start-Sleep -Milliseconds $DelayMs; continue } + throw + } + } + return $null +} + +$dte = New-Object -ComObject TcXaeShell.DTE.17.0 +$dte.MainWindow.Visible = $true +try { $dte.SuppressUI = $false } catch {} +$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln') +Write-Host "open issued, waiting 30s..." +Start-Sleep 30 + +Write-Host "BuildState pre-build: $(Invoke-Com { $dte.Solution.SolutionBuild.BuildState })" + +Write-Host "issuing build..." +Invoke-Com { $dte.ExecuteCommand('Build.BuildSolution') } | Out-Null +for ($i = 0; $i -lt 30; $i++) { + $bs = Invoke-Com { $dte.Solution.SolutionBuild.BuildState } -Retries 2 -DelayMs 1000 + Write-Host " [$($i*3)s] BuildState=$bs" + if ($bs -eq 3) { break } + Start-Sleep 3 +} + +Write-Host "--- output window (Build pane) ---" +try { + $ow = $dte.ToolWindows.OutputWindow + foreach ($pane in $ow.OutputWindowPanes) { + if ($pane.Name -match 'Build|����') { + $td = $pane.TextDocument + $txt = $td.StartPoint.CreateEditPoint() + $content = $txt.GetText($td.EndPoint) + $lines = $content -split "`r?`n" + Write-Host "pane '$($pane.Name)': $($lines.Count) lines, last 25:" + $lines | Select-Object -Last 25 | ForEach-Object { Write-Host " $_" } + } + } +} catch { Write-Host "output window err: $_" } diff --git a/scripts/tc-build-output-2khz.ps1 b/scripts/tc-build-output-2khz.ps1 new file mode 100644 index 0000000..f5ed86c --- /dev/null +++ b/scripts/tc-build-output-2khz.ps1 @@ -0,0 +1,36 @@ +#Requires -RunAsAdministrator +$ErrorActionPreference = 'Continue' +$dte = New-Object -ComObject TcXaeShell.DTE.17.0 +$dte.MainWindow.Visible = $true +try { $dte.SuppressUI = $false } catch {} +$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln') +Start-Sleep 30 +try { $dte.ExecuteCommand('Build.BuildSolution') } catch {} +Start-Sleep 15 +for ($i = 0; $i -lt 30; $i++) { + $bs = $null + try { $bs = $dte.Solution.SolutionBuild.BuildState } catch {} + if ($bs -eq 3) { break } + Start-Sleep 2 +} +Write-Host "BuildState=$($dte.Solution.SolutionBuild.BuildState) LastBuildInfo=$($dte.Solution.SolutionBuild.LastBuildInfo)" +try { + $el = $dte.ToolWindows.ErrorList + $items = $el.ErrorItems + Write-Host "error items: $($items.Count)" + for ($i = 1; $i -le [Math]::Min($items.Count, 30); $i++) { + $it = $items.Item($i) + Write-Host " [$($it.ErrorLevel)] $($it.Description) ($($it.FileName):$($it.Line))" + } +} catch { Write-Host "errorlist err: $_" } +try { + $ow = $dte.ToolWindows.OutputWindow + foreach ($pane in $ow.OutputWindowPanes) { + $td = $pane.TextDocument + $ep = $td.StartPoint.CreateEditPoint() + $content = $ep.GetText($td.EndPoint) + $lines = ($content -split "`r?`n") | Where-Object { $_.Trim() -ne '' } + Write-Host "=== pane '$($pane.Name)': $($lines.Count) lines ===" + $lines | Select-Object -Last 30 | ForEach-Object { Write-Host " $_" } + } +} catch { Write-Host "ow err: $_" } diff --git a/scripts/tc-deploy-retry-2khz.ps1 b/scripts/tc-deploy-retry-2khz.ps1 new file mode 100644 index 0000000..50dc862 --- /dev/null +++ b/scripts/tc-deploy-retry-2khz.ps1 @@ -0,0 +1,61 @@ +#Requires -RunAsAdministrator +#Requires -Version 5.1 +$ErrorActionPreference = 'Continue' + +function Invoke-Com { + param([scriptblock]$Fn, [int]$Retries = 30, [int]$DelayMs = 2000) + for ($i = 0; $i -lt $Retries; $i++) { + try { return & $Fn } catch { + $hr = $_.Exception.HResult + if ($hr -eq -2147418111) { Start-Sleep -Milliseconds $DelayMs; continue } + throw + } + } + throw "COM call rejected $Retries times (RPC_E_CALL_REJECTED)" +} + +Write-Host "=== Deploy 8B with RPC retry ===" +$dte = Invoke-Com { New-Object -ComObject TcXaeShell.DTE.17.0 } +Invoke-Com { $dte.MainWindow.Visible = $true } | Out-Null +try { Invoke-Com { $dte.SuppressUI = $false } | Out-Null } catch {} +Invoke-Com { $dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln') } | Out-Null +Write-Host "solution opened" + +$sm = $null +for ($i = 0; $i -lt 40; $i++) { + try { $sm = Invoke-Com { $dte.Solution.Projects.Item(1).Object } -Retries 3 -DelayMs 3000; if ($sm) { break } } catch { Write-Host " waiting sysmgr..." } +} +if (-not $sm) { Write-Host "FATAL: no sysmgr"; exit 1 } +Write-Host "sysmgr OK" +Invoke-Com { $sm.SetTargetNetId('169.254.176.217.1.1') } | Out-Null + +Write-Host "building..." +try { Invoke-Com { $dte.ExecuteCommand('Build.BuildSolution') } -Retries 10 | Out-Null } catch { Write-Host "build cmd: $_" } +# wait for build done (BuildState 3 = done) +$done = $false +for ($i = 0; $i -lt 90; $i++) { + try { + $bs = Invoke-Com { $dte.Solution.SolutionBuild.BuildState } -Retries 5 -DelayMs 3000 + $bi = Invoke-Com { $dte.Solution.SolutionBuild.LastBuildInfo } -Retries 5 -DelayMs 1000 + if ($bs -eq 3) { Write-Host "build done (LastBuildInfo=$bi) after $($i*2)s"; $done = $true; break } + } catch { Write-Host " build poll err: $_" } + Start-Sleep -Seconds 2 +} +if (-not $done) { Write-Host "WARN: build state never reached done" } + +# dump build errors if any +try { + $ew = $dte.ToolWindows.ErrorList + Write-Host "error list check skipped" +} catch {} + +Write-Host "activating..." +Invoke-Com { $sm.ActivateConfiguration() } -Retries 30 -DelayMs 3000 | Out-Null +Write-Host "activated" +Start-Sleep 10 +Write-Host "restarting runtime..." +Invoke-Com { $sm.StartRestartTwinCAT() } -Retries 30 -DelayMs 3000 | Out-Null +Write-Host "restart issued" +Start-Sleep 12 +try { Write-Host "IsTwinCATStarted=$($sm.IsTwinCATStarted)" } catch {} +Write-Host "=== done ===" diff --git a/scripts/tc-dump-tirr.ps1 b/scripts/tc-dump-tirr.ps1 new file mode 100644 index 0000000..82a497f --- /dev/null +++ b/scripts/tc-dump-tirr.ps1 @@ -0,0 +1,20 @@ +#Requires -RunAsAdministrator +$ErrorActionPreference = 'Continue' +$dte = New-Object -ComObject TcXaeShell.DTE.17.0 +$dte.MainWindow.Visible = $true +try { $dte.SuppressUI = $false } catch {} +$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln') +Start-Sleep 25 +$proj = $dte.Solution.Projects.Item(1) +try { Write-Host "loaded tsproj: $($proj.Properties.Item('ProjectFilePath').Value)" } catch {} +$sm = $null +for ($i = 0; $i -lt 20; $i++) { + try { $sm = $proj.Object; if ($sm) { break } } catch {} + Start-Sleep 3 +} +if (-not $sm) { Write-Host 'no sysmgr'; exit 1 } +$node = $sm.LookupTreeItem('TIRR') +Write-Host "TIRR node: $($node.Name) cc=$($node.ChildCount)" +$xml = $node.ProduceXml($false) +[System.IO.File]::WriteAllText('C:\Users\tonycao\work\ethercat-linux\tmp_tirr.xml', $xml, [System.Text.Encoding]::UTF8) +Write-Host "saved ($($xml.Length) bytes)" diff --git a/scripts/tc-dump-tirt.ps1 b/scripts/tc-dump-tirt.ps1 new file mode 100644 index 0000000..746512b --- /dev/null +++ b/scripts/tc-dump-tirt.ps1 @@ -0,0 +1,22 @@ +#Requires -RunAsAdministrator +$ErrorActionPreference = 'Continue' +$dte = New-Object -ComObject TcXaeShell.DTE.17.0 +$dte.MainWindow.Visible = $true +try { $dte.SuppressUI = $false } catch {} +$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary\TwinCATProject1.sln') +Start-Sleep 25 +$sm = $null +for ($i = 0; $i -lt 20; $i++) { + try { $sm = $dte.Solution.Projects.Item(1).Object; if ($sm) { break } } catch {} + Start-Sleep 3 +} +if (-not $sm) { Write-Host 'no sysmgr'; exit 1 } +$rt = $sm.LookupTreeItem('TIRT') +$xml = $rt.ProduceXml($false) +[System.IO.File]::WriteAllText('C:\Users\tonycao\work\ethercat-linux\tmp_tirt.xml', $xml, [System.Text.Encoding]::UTF8) +Write-Host "TIRT xml saved ($($xml.Length) bytes)" +try { $sm.SetTargetNetId('169.254.176.217.1.1') } catch {} +Start-Sleep 2 +$xml2 = $rt.ProduceXml($true) +[System.IO.File]::WriteAllText('C:\Users\tonycao\work\ethercat-linux\tmp_tirt_online.xml', $xml2, [System.Text.Encoding]::UTF8) +Write-Host "TIRT online xml saved ($($xml2.Length) bytes)" diff --git a/scripts/tc-enum-cfgmgr.ps1 b/scripts/tc-enum-cfgmgr.ps1 new file mode 100644 index 0000000..65e66e1 --- /dev/null +++ b/scripts/tc-enum-cfgmgr.ps1 @@ -0,0 +1,16 @@ +#Requires -RunAsAdministrator +$ErrorActionPreference = 'Continue' +$dte = New-Object -ComObject TcXaeShell.DTE.17.0 +$dte.MainWindow.Visible = $true +try { $dte.SuppressUI = $false } catch {} +$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln') +Start-Sleep 25 +$sm = $null +for ($i = 0; $i -lt 20; $i++) { + try { $sm = $dte.Solution.Projects.Item(1).Object; if ($sm) { break } } catch {} + Start-Sleep 3 +} +if (-not $sm) { Write-Host 'no sysmgr'; exit 1 } +$cm = $sm.ConfigurationManager +Write-Host '--- ConfigurationManager members ---' +$cm | Get-Member | ForEach-Object { Write-Host "$($_.MemberType) $($_.Name)" } diff --git a/scripts/tc-enum-root.ps1 b/scripts/tc-enum-root.ps1 new file mode 100644 index 0000000..cc30190 --- /dev/null +++ b/scripts/tc-enum-root.ps1 @@ -0,0 +1,23 @@ +#Requires -RunAsAdministrator +$ErrorActionPreference = 'Continue' +$dte = New-Object -ComObject TcXaeShell.DTE.17.0 +$dte.MainWindow.Visible = $true +try { $dte.SuppressUI = $false } catch {} +$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln') +Start-Sleep 25 +$sm = $null +for ($i = 0; $i -lt 20; $i++) { + try { $sm = $dte.Solution.Projects.Item(1).Object; if ($sm) { break } } catch {} + Start-Sleep 3 +} +if (-not $sm) { Write-Host 'no sysmgr'; exit 1 } +$root = $sm.QueryTreeItems('') +Write-Host "root: '$($root.Name)' path='$($root.PathName)' cc=$($root.ChildCount)" +for ($j = 1; $j -le [int]$root.ChildCount; $j++) { + $ch = $root.Child($j) + Write-Host "[$j] '$($ch.Name)' path='$($ch.PathName)' cc=$($ch.ChildCount)" + for ($k = 1; $k -le [int]$ch.ChildCount; $k++) { + $g = $ch.Child($k) + Write-Host " - '$($g.Name)' path='$($g.PathName)' cc=$($g.ChildCount)" + } +} diff --git a/scripts/tc-find-basetime.ps1 b/scripts/tc-find-basetime.ps1 new file mode 100644 index 0000000..c87265a --- /dev/null +++ b/scripts/tc-find-basetime.ps1 @@ -0,0 +1,36 @@ +#Requires -RunAsAdministrator +$ErrorActionPreference = 'Continue' +$dte = New-Object -ComObject TcXaeShell.DTE.17.0 +$dte.MainWindow.Visible = $true +try { $dte.SuppressUI = $false } catch {} +$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln') +Start-Sleep 25 +$proj = $dte.Solution.Projects.Item(1) +Write-Host '--- project properties ---' +try { + $props = $proj.Properties + Write-Host "count: $($props.Count)" + for ($i = 1; $i -le $props.Count; $i++) { + $p = $props.Item($i) + Write-Host " $($p.Name) = $($p.Value)" + } +} catch { Write-Host "props err: $_" } +Write-Host '--- try sysmgr prefixes ---' +$sm = $null +for ($i = 0; $i -lt 20; $i++) { + try { $sm = $proj.Object; if ($sm) { break } } catch {} + Start-Sleep 3 +} +if ($sm) { + foreach ($prefix in @('TISY','TISE','TISC','TISN','TISG','TIRL','TIRE','TIRR','TIS2','TIS3','TIMO','TICP','TIMA')) { + try { + $it = $sm.LookupTreeItem($prefix) + if ($it) { Write-Host "$prefix => $($it.Name) cc=$($it.ChildCount)" } + } catch {} + } + Write-Host '--- QueryTreeItems ---' + try { + $q = $sm.QueryTreeItems('') + Write-Host ($q | Out-String) + } catch { Write-Host "query err: $_" } +} diff --git a/scripts/tc-find-commands.ps1 b/scripts/tc-find-commands.ps1 new file mode 100644 index 0000000..28ffd75 --- /dev/null +++ b/scripts/tc-find-commands.ps1 @@ -0,0 +1,14 @@ +#Requires -RunAsAdministrator +$ErrorActionPreference = 'Continue' +$dte = New-Object -ComObject TcXaeShell.DTE.17.0 +$dte.MainWindow.Visible = $true +try { $dte.SuppressUI = $false } catch {} +Start-Sleep 10 +Write-Host '--- DTE commands matching time/base/real/tick ---' +$cmds = $dte.Commands +$count = 0 +foreach ($c in $cmds) { + $count++ + if ($c.Name -match 'time|base|real|tick|cpu') { Write-Host " $($c.Name)" } +} +Write-Host "total commands: $count" diff --git a/scripts/tc-find-system-node.ps1 b/scripts/tc-find-system-node.ps1 new file mode 100644 index 0000000..fe6afbd --- /dev/null +++ b/scripts/tc-find-system-node.ps1 @@ -0,0 +1,21 @@ +#Requires -RunAsAdministrator +$ErrorActionPreference = 'Continue' +$dte = New-Object -ComObject TcXaeShell.DTE.17.0 +$dte.MainWindow.Visible = $true +try { $dte.SuppressUI = $false } catch {} +$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary_2kHz\TwinCATProject1.sln') +Start-Sleep 25 +$sm = $null +for ($i = 0; $i -lt 20; $i++) { + try { $sm = $dte.Solution.Projects.Item(1).Object; if ($sm) { break } } catch {} + Start-Sleep 3 +} +if (-not $sm) { Write-Host 'no sysmgr'; exit 1 } +$cands = @('TISA','TISB','TISD','TISF','TISH','TISJ','TISK','TISL','TISO','TISP','TISQ','TISR','TIST','TISU','TISV','TISW','TISX','TISZ','TIAN','TIR1','TIR2','TIR3','TIR4','TIR5','TIR6','TIR7','TIR8','TIR9','TIRE') +foreach ($p in $cands) { + try { + $it = $sm.LookupTreeItem($p) + if ($it) { Write-Host "$p => '$($it.Name)' cc=$($it.ChildCount) subtype=$($it.ItemSubType)" } + } catch {} +} +Write-Host 'done' diff --git a/scripts/tc-rt-node.ps1 b/scripts/tc-rt-node.ps1 new file mode 100644 index 0000000..88c2c6d --- /dev/null +++ b/scripts/tc-rt-node.ps1 @@ -0,0 +1,26 @@ +#Requires -RunAsAdministrator +$ErrorActionPreference = 'Continue' +$dte = New-Object -ComObject TcXaeShell.DTE.17.0 +$dte.MainWindow.Visible = $true +try { $dte.SuppressUI = $false } catch {} +$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary\TwinCATProject1.sln') +Start-Sleep 25 +$sm = $null +for ($i = 0; $i -lt 20; $i++) { + try { $sm = $dte.Solution.Projects.Item(1).Object; if ($sm) { break } } catch {} + Start-Sleep 3 +} +if (-not $sm) { Write-Host 'no sysmgr'; exit 1 } + +foreach ($prefix in @('TIRT', 'TISM', 'TISY', 'TICC', 'TIDK')) { + try { + $it = $sm.LookupTreeItem($prefix) + if ($it) { + Write-Host "=== $prefix : $($it.Name) childcount=$($it.ChildCount) ===" + for ($j = 1; $j -le [int]$it.ChildCount; $j++) { + $ch = $it.Child($j) + Write-Host " [$j] $($ch.Name) subtype=$($ch.ItemSubType)" + } + } + } catch { Write-Host "$prefix : not found" } +} diff --git a/scripts/tc-task-rate.ps1 b/scripts/tc-task-rate.ps1 new file mode 100644 index 0000000..94a6946 --- /dev/null +++ b/scripts/tc-task-rate.ps1 @@ -0,0 +1,12 @@ +#Requires -RunAsAdministrator +Add-Type -Path 'C:\Program Files (x86)\Beckhoff\TwinCAT\3.1\Components\Base\v170\TwinCAT.Ads.dll' +$c = New-Object TwinCAT.Ads.TcAdsClient +$c.Connect('169.254.176.217.1.1', 851) +$h = $c.CreateVariableHandle('GVL_Sync.nCycles') +$n1 = $c.ReadAny($h, [uint64]) +Start-Sleep -Milliseconds 5000 +$n2 = $c.ReadAny($h, [uint64]) +$c.DeleteVariableHandle($h) +$rate = ($n2 - $n1) / 5.0 +Write-Host ("nCycles rate = {0} Hz (n1={1} n2={2})" -f [math]::Round($rate,1), $n1, $n2) +$c.Dispose() diff --git a/scripts/tc-walk-tree.ps1 b/scripts/tc-walk-tree.ps1 new file mode 100644 index 0000000..6e3fa40 --- /dev/null +++ b/scripts/tc-walk-tree.ps1 @@ -0,0 +1,18 @@ +#Requires -RunAsAdministrator +$ErrorActionPreference = 'Continue' +$dte = New-Object -ComObject TcXaeShell.DTE.17.0 +$dte.MainWindow.Visible = $true +try { $dte.SuppressUI = $false } catch {} +$dte.Solution.Open('C:\Users\tonycao\work\twincat3-auto-cli\TwinCAT_EL6695_Primary\TwinCATProject1.sln') +Start-Sleep 25 +$proj = $dte.Solution.Projects.Item(1) +$n = $proj.ProjectItems.Count +Write-Host "top-level items: $n" +for ($i = 1; $i -le $n; $i++) { + $it = $proj.ProjectItems.Item($i) + Write-Host "[$i] $($it.Name)" + try { + $m = $it.ProjectItems.Count + for ($j = 1; $j -le $m; $j++) { Write-Host " - $($it.ProjectItems.Item($j).Name)" } + } catch {} +} diff --git a/src/main.rs b/src/main.rs index b16f5b3..167bb2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,6 +94,10 @@ struct Config { /// with other per-cycle frames; edges are latched so nothing is lost /// beyond time resolution). el1252_every: u32, + /// Spin-lock mode: instead of a free-running ticker, poll FPRD until the + /// payload changes (new TwinCAT frame), then process immediately. + /// Phase-locks J1900 processing to the primary master's frame arrival. + spin: bool, } fn parse_args() -> Result { @@ -111,6 +115,7 @@ fn parse_args() -> Result { el1252: false, el2262: false, el1252_every: 1, + spin: false, }; let mut args = std::env::args().skip(1).peekable(); if let Some(a) = args.peek() { @@ -140,6 +145,7 @@ fn parse_args() -> Result { "--core" => cfg.core = val("--core")?.parse().map_err(|_| "bad --core")?, "--prio" => cfg.prio = val("--prio")?.parse().map_err(|_| "bad --prio")?, "--quiet" => cfg.quiet = true, + "--spin" => cfg.spin = true, "--el2202" => cfg.el2202 = true, "--el2202-dual" => { cfg.el2202 = true; @@ -746,6 +752,8 @@ async fn run(cfg: &Config) -> Result<(), Box> { let mut comp_err: u64 = 0; // internal computation errors let mut payload_checked: u64 = 0; // J1900 payload verification cycles let mut payload_mismatch: u64 = 0; // J1900 payload mismatch bytes accumulated + let mut prev_rx8 = [0u8; 8]; // spin-lock: previous payload (change detect) + let mut last_fresh: Option = None; // spin-lock: last fresh-data instant let mut aborted: Option = None; @@ -758,45 +766,94 @@ async fn run(cfg: &Config) -> Result<(), Box> { break; } - let jitter_ns = ticker.wait_next_period(); + let mut rx_ok = true; + let spin_mode = cfg.spin && cfg.mode == Mode::Ticker; + let mut jitter_ns: i64 = 0; + + if spin_mode { + // ---- Spin-lock: poll FPRD until the payload changes ---- + // Phase-locks processing to TwinCAT's frame arrival instead of a + // free-running ticker. Timeout at 1.5 periods keeps the loop + // alive (counted as stale) if the primary master stops. + let spin_t0 = Instant::now(); + let timeout_ns = cycle_ns + cycle_ns / 2; + let mut fresh = false; + loop { + match ethercrab::Command::fprd(station, SM_RX_ADDR) + .receive_slice(&maindevice, SM_RX_LEN as u16) + .await + { + Ok(pdu) => { + let raw: &[u8] = &pdu; + let n = raw.len().min(SM_RX_LEN); + if n > RX_PREFIX { + let copy_len = (n - RX_PREFIX).min(SM_RX_PLD_LEN); + rx_buf[..copy_len].copy_from_slice(&raw[RX_PREFIX..RX_PREFIX + copy_len]); + } + if rx_buf[..8] != prev_rx8 { + prev_rx8.copy_from_slice(&rx_buf[..8]); + fresh = true; + break; + } + } + Err(_) => { + rx_err += 1; + } + } + if spin_t0.elapsed().as_nanos() as u64 > timeout_ns { + break; + } + } + if !fresh { + rx_ok = false; + rx_stale += 1; + } + // jitter = deviation of fresh-data arrival interval from nominal + let now = Instant::now(); + if let Some(prev) = last_fresh { + let dt = now.duration_since(prev).as_nanos() as i64 - cycle_ns as i64; + jitter_st.push(dt); + } + last_fresh = Some(now); + } else { + jitter_ns = ticker.wait_next_period(); + jitter_st.push(jitter_ns); + } + let t0 = Instant::now(); cyc += 1; - jitter_st.push(jitter_ns); - // 1. Read SM3 (TwinCAT data via bridge) - let t_rx0 = Instant::now(); - let mut rx_ok = true; - match ethercrab::Command::fprd(station, SM_RX_ADDR) - .receive_slice(&maindevice, SM_RX_LEN as u16) - .await - { - Ok(pdu) => { - let raw: &[u8] = &pdu; - // Strip 2-byte diagnostic word prefix (0x1A01) from plain mode - let n = raw.len().min(SM_RX_LEN); - if n > RX_PREFIX { - let copy_len = (n - RX_PREFIX).min(SM_RX_PLD_LEN); - rx_buf[..copy_len].copy_from_slice(&raw[RX_PREFIX..RX_PREFIX + copy_len]); + if !spin_mode { + // 1. Read SM3 (TwinCAT data via bridge) + let t_rx0 = Instant::now(); + match ethercrab::Command::fprd(station, SM_RX_ADDR) + .receive_slice(&maindevice, SM_RX_LEN as u16) + .await + { + Ok(pdu) => { + let raw: &[u8] = &pdu; + // Strip 2-byte diagnostic word prefix (0x1A01) from plain mode + let n = raw.len().min(SM_RX_LEN); + if n > RX_PREFIX { + let copy_len = (n - RX_PREFIX).min(SM_RX_PLD_LEN); + rx_buf[..copy_len].copy_from_slice(&raw[RX_PREFIX..RX_PREFIX + copy_len]); + } } - if cyc <= 3 { - let live: u32 = rx_buf[..SM_RX_PLD_LEN.min(256)].iter().map(|&b| if b != 0 { 1 } else { 0 }).sum(); - let show = SM_RX_LEN.min(64); - println!(" rx_buf[0..{}]: {} non-zero (of {}) at cycle {}", show, live, SM_RX_PLD_LEN.min(256), cyc); - print!(" rx_buf[..{}] hex: ", show); - for b in rx_buf[..show].iter() { print!("{:02X}", b); } - println!(); - print!(" raw[..{}] hex: ", show); - let raw_show = show.min(raw.len()); - for b in raw[..raw_show].iter() { print!("{:02X}", b); } - println!(); + Err(_) => { + rx_ok = false; + rx_err += 1; } } - Err(_) => { - rx_ok = false; - rx_err += 1; - } + rx_rtt_st.push(t_rx0.elapsed().as_nanos() as i64); + } + if cyc <= 3 && rx_ok { + let live: u32 = rx_buf[..SM_RX_PLD_LEN.min(256)].iter().map(|&b| if b != 0 { 1 } else { 0 }).sum(); + let show = SM_RX_LEN.min(64); + println!(" rx_buf[0..{}]: {} non-zero (of {}) at cycle {}", show, live, SM_RX_PLD_LEN.min(256), cyc); + print!(" rx_buf[..{}] hex: ", show); + for b in rx_buf[..show].iter() { print!("{:02X}", b); } + println!(); } - rx_rtt_st.push(t_rx0.elapsed().as_nanos() as i64); // 2. In ticker mode: prove J1900 processing by transforming TwinCAT's // tc_seq instead of blind echo. The function f(x)=x*7+3 is a diff --git a/uia_edits.py b/uia_edits.py new file mode 100644 index 0000000..918bb39 --- /dev/null +++ b/uia_edits.py @@ -0,0 +1,19 @@ +import sys, time +sys.stdout.reconfigure(encoding='utf-8', errors='replace') +from pywinauto import Application + +app = Application(backend='uia').connect(process=1456, timeout=10) +win = app.window(title_re='.*TcXaeShell.*') +win.set_focus() +for e in win.descendants(control_type='Edit'): + try: + r = e.rectangle() + aid = e.automation_id() + txt = e.window_text() + try: + val = e.get_value() + except Exception: + val = None + print(f"aid='{aid}' text='{txt}' value='{val}' rect=({r.left},{r.top},{r.right},{r.bottom})") + except Exception as ex: + print('err', ex) diff --git a/uia_explore.py b/uia_explore.py new file mode 100644 index 0000000..e2e242b --- /dev/null +++ b/uia_explore.py @@ -0,0 +1,35 @@ +import sys, time +sys.stdout.reconfigure(encoding='utf-8', errors='replace') +from pywinauto import Application + +app = Application(backend='uia').connect(process=1456, timeout=10) +win = app.window(title_re='.*TcXaeShell.*') +win.set_focus() +print('window:', win.window_text()) + +def dump(el, depth=0, maxdepth=4): + if depth > maxdepth: + return + try: + ct = el.friendly_class_name() + nm = el.window_text() + if nm or ct in ('TreeView', 'TreeItem', 'ComboBox', 'Edit', 'Button', 'Tab', 'TabItem', 'DataGrid', 'ListItem', 'ListView', 'Document'): + # print everything actually, tree is big; filter noise + pass + print(' ' * depth + f"[{ct}] '{nm}'") + except Exception: + return + try: + for ch in el.children(): + dump(ch, depth + 1, maxdepth) + except Exception: + pass + +# find all TreeViews in the window +trees = win.descendants(control_type='Tree') +print(f'found {len(trees)} tree controls') +for t in trees: + try: + print('tree:', t.friendly_class_name(), '| items:', [i.window_text() for i in t.children()][:10]) + except Exception as e: + print('tree err:', e) diff --git a/uia_open_rt.py b/uia_open_rt.py new file mode 100644 index 0000000..c05a395 --- /dev/null +++ b/uia_open_rt.py @@ -0,0 +1,53 @@ +import sys, time +sys.stdout.reconfigure(encoding='utf-8', errors='replace') +from pywinauto import Application +from pywinauto.mouse import double_click + +app = Application(backend='uia').connect(process=1456, timeout=10) +win = app.window(title_re='.*TcXaeShell.*') +tree = win.descendants(control_type='Tree')[0] + +def find_item(item, name, depth=0): + try: + item.expand(); time.sleep(0.25) + except Exception: + pass + try: + for ch in item.children(): + if ch.window_text() == name: + return ch + r = find_item(ch, name, depth + 1) + if r is not None: + return r + except Exception: + pass + return None + +rt = None +for root in tree.children(): + rt = find_item(root, '实时') + if rt: + break +if not rt: + print('实时 node not found'); sys.exit(1) +print('found 实时, double-clicking...') +rt.iface_expand_collapse.Expand() +time.sleep(0.5) +r = rt.rectangle() +double_click(coords=(int((r.left + r.right) / 2), int((r.top + r.bottom) / 2))) +time.sleep(3) + +# dump new document/dialog contents +win.set_focus() +tabs = win.descendants(control_type='TabItem') +print('tab items:', [t.window_text() for t in tabs]) +combos = win.descendants(control_type='ComboBox') +print('combos:', [(c.window_text()) for c in combos]) +edits = win.descendants(control_type='Edit') +print('edits:', [(e.window_text()) for e in edits][:20]) +buttons = win.descendants(control_type='Button') +print('buttons:', [b.window_text() for b in buttons][:20]) +# text labels containing Base +texts = win.descendants(control_type='Text') +base_related = [t.window_text() for t in texts if t.window_text() and ('Base' in t.window_text() or '基' in t.window_text() or 'ms' in t.window_text() or 'CPU' in t.window_text())] +print('base-related texts:', base_related[:30]) diff --git a/uia_tasks.py b/uia_tasks.py new file mode 100644 index 0000000..1948bbd --- /dev/null +++ b/uia_tasks.py @@ -0,0 +1,46 @@ +import sys, time +sys.stdout.reconfigure(encoding='utf-8', errors='replace') +from pywinauto import Application +from pywinauto.mouse import click + +app = Application(backend='uia').connect(process=1456, timeout=10) +win = app.window(title_re='.*TcXaeShell.*') +tree = win.descendants(control_type='Tree')[0] + +def find_item(item, name): + try: + item.expand(); time.sleep(0.25) + except Exception: + pass + try: + for ch in item.children(): + if ch.window_text() == name: + return ch + r = find_item(ch, name) + if r is not None: + return r + except Exception: + pass + return None + +for task_name in ['PlcTask', 'PlcTask1', 'PlcTask2']: + node = None + for root in tree.children(): + node = find_item(root, task_name) + if node: + break + if not node: + print(f'{task_name}: NOT FOUND'); continue + node.iface_expand_collapse.Expand() + r = node.rectangle() + click(coords=(int((r.left + r.right) / 2), int((r.top + r.bottom) / 2))) + time.sleep(1.5) + vals = {} + for e in win.descendants(control_type='Edit'): + try: + aid = e.automation_id() + if aid in ('1054', '1058', '1055', '1057', '1516', '1061'): + vals[aid] = e.get_value() + except Exception: + pass + print(f"{task_name}: name={vals.get('1054')} port={vals.get('1058')} prio={vals.get('1055')} ticks={vals.get('1057')} ms={vals.get('1516')}") diff --git a/uia_tree.py b/uia_tree.py new file mode 100644 index 0000000..ef41b2c --- /dev/null +++ b/uia_tree.py @@ -0,0 +1,29 @@ +import sys, time +sys.stdout.reconfigure(encoding='utf-8', errors='replace') +from pywinauto import Application + +app = Application(backend='uia').connect(process=1456, timeout=10) +win = app.window(title_re='.*TcXaeShell.*') +tree = win.descendants(control_type='Tree')[0] + +def expand_and_print(item, depth=0, maxdepth=4): + try: + name = item.window_text() + except Exception: + name = '?' + print(' ' * depth + name) + if depth >= maxdepth: + return + try: + item.expand() + time.sleep(0.3) + except Exception: + pass + try: + for ch in item.children(): + expand_and_print(ch, depth + 1, maxdepth) + except Exception: + pass + +for root in tree.children(): + expand_and_print(root)