Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copilot Instructions

## 目录结构/版本管理

- **临时文件目录**:生成的临时文件放在 `.tmp/` 目录下, 不提交到版本控制系统
- **Wiki 目录**:`.wiki/` 是 GitHub Wiki 仓库(`NEVSTOP-LAB/LabVIEW-QuickDrops-Manager.wiki.git`)的 submodule,wiki 文档的修改在该目录内提交并推送
- **pr留言**:每完成一个阶段(一批提交)后,用 `gh pr status` 检查当前分支是否有关联的 open PR;若有关联,用 `gh pr comment` 留言本阶段修改的背景、内容与关键决策,并用 `gh pr edit --body-file` 总结 PR 修改描述
- **issue创建**:创建issue时,添加适当的标签以便分类和跟踪; 复杂问题中,需要有checkbox列表跟踪任务完成情况
- **issue留言**:如果上下文了解到是在处理issue,每完成一个阶段,在 issue 中留言相关的背景、内容与关键决策
- **修改前同步认知**:在进行修改前,确认是否完全理解需求,如果不清楚,采访用户直到理解全部的细节

## 文档

- **中英文一致**:README 与 wiki 的单个 QuickDrop 页均为「英文在前、一行 `-----`、中文在后」的单文件双语;中文是英文的逐节对应翻译,章节与要点必须一一对应,不允许一边多一边少,改动任一半后必须同步另一半(删除中文副本文档时同时清理 `.vipb` 的 `<Exclusions>`)。README 顶部保留 `[English](#…) | [中文](#…)` 语言导航,链接指向文中 `## English Description` / `## 中文描述` 两个语言分节,两种语言共用文件顶部唯一的 H1 标题。
- **单页版式**:wiki 的 QuickDrop 单页把 `Default Shortcut` 代码块放在页面最前面(结构:快捷键 → `-----` → 英文 → `-----` → 中文);导航页 `_Sidebar.md`、`_Footer.md`、`Home.md` 不带中文。
56 changes: 56 additions & 0 deletions .github/skills/labview-quickdrop-plugin/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
name: labview-quickdrop-plugin
description: "创建、修改或调试 LabVIEW QuickDrop 插件与配套 subVI。Use when: 需要按选区改造框图(连线、节点、簇、Bundle/Unbundle 等), 需要用 labview-mcp 生成/校验/运行 VI, 需要判断生成的 VI 能否作为 subVI 接线, 需要设计可无头验证的插件 VI, 需要定位 labview-mcp 通讯卡死。"
argument-hint: '[插件功能或目标 VI,留空则从当前上下文提取]'
---

# LabVIEW QuickDrop 插件

## 硬性门禁:先起模态框看门狗

使用 labview-mcp 期间若 LabVIEW 弹出模态对话框,整个 AI gRPC 服务会被冻结:所有 `lvai_*` 调用一律超时(DeadlineExceeded),而 LabVIEW 界面本身看起来正常。因此:

- 动手前先在后台常驻 `scripts/lv-dialog-guard.ps1`:它发现属于 LabVIEW 的对话框(`#32770`)或小窗口就置前并回车,命中记录写入日志。
- 启动:`pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/lv-dialog-guard.ps1`
- 已经卡住时先用该脚本解除;仍不通则按「服务未起」处理:用 `lvai_status` 探测,`lvai_ensure_labview` 只能拉起 IDE(gRPC 服务随 IDE 里的 NIGEL 一起启动,没开就不会监听任何端口)。

## 工具能力边界(决定方案形态)

- AIXML 只能**新建** VI;`ApplyAIXMLToVI` 对第三方客户端恒失败,因此**改不了现有 VI 的框图**——要改就得重新生成一个新文件。
- 生成的 VI 必须自包含:不能 Call 工程内/库内的 subVI,只能调调色板可达的 VI。
- AIXML 不能表达坐标与版面,节点位置由生成器决定。
- 要接线的端子**必须**在 AIXML 里写 `conIdx`;不写则成品一个连接器板端子都没有,无法作为 subVI 接线。生成后必须用 `lvai_connector_pane` 复测:pattern 由生成器挑,换 pattern 会移动全部编号。
- 保存目录需先存在;同名 VI 还在内存中会报 1051 / 1357。
- 先 `ValidateAIXML`(廉价失败路径)再 `ConvertAIXMLToVI`;生成后自检「被消费但从未产生」的连线名。
- `lvai_run_vi_and_read_values` 只能写入字符串控件;数组/簇以扁平 XML 返回;VI 被改动过要先保存再释放引用,否则改动丢失。

语法、节点、VI Scripting 的完整事实清单见 [references/aixml-and-scripting-facts.md](./references/aixml-and-scripting-facts.md)。

## 保存方决定 VI 版本(本仓库硬约束)

- CI(`Check_Broken_VIs`)在 **LabVIEW 2017** 上加载工作区内所有 VI;任何被更高版本保存过的 VI 都会报 `VI version (26.0) is newer than LabVIEW version (17.0)` 并让 CI 变红。改动现有 VI 前先确认保存它的是哪个 LabVIEW。
- `LabVIEW-QuickDrops-Manager.lvproj` 带 `NI.LV.All.SaveVersion = 17.0`:**该工程处于活动状态时**生成的 VI 才是 17.0 格式。会话开始前确认工程已激活,否则会产出 26.0 的 VI。
- `ConvertVIToAIXML` / `ConvertVIsToAIXML` **不是纯读**:对保存版本高于工程 SaveVersion 的 VI,它会按工程版本重写文件(实测 4 个 `NEVSTOP_QuickDrop/__QDMgr.vi` 因此在磁盘上被改成 17.0)。批量导出后要 `git status` 核对。
- AIXML 只能新建 VI、`ApplyAIXMLToVI` 对第三方客户端不可用,因此**改现有 VI 的描述**要另找保存方:生成一个 helper VI,用 `Open Application Reference` + `Open VI Reference`(`application reference (local)`)指向 2017 实例的 VI Server,写 `{LV.VI}` 的 `VI Description` 属性后 `Save:Instrument`,保存方即 2017,文件版本不变。helper 由 `RunVIAsTopLevel` 驱动时,**只有字符串指示器能回传**(布尔/数值静默变成空串,错误信息只看字符串型 `source`)。
- 内置原语(如 `Open Application Reference`、`Select`)的节点名与端子名不要猜:用 `SearchInfoCache` + `LookupInfoCacheItems` 取现成原型,端子名照抄(`machine name ("": open local reference)`、`Select` 的输出 `s? t\3Af`)。移位寄存器的线名要用左右端子自身的 uid,不是 ShiftReg 的 uid。

## 插件契约

- 插件放在 QuickDrop 的 `plugins/` 下,一插件一目录;用插件模板新建以取得标准连接器板:`error in` 8、`QD Launch VI Ref` 11、`Shift Pressed?` 7、`Variant in` 6、`QD Combo Box Ref` 10、`error out` 0、`Variant out` 4、`Undo Name` 无 conIdx。
- 选区来自「启动 QuickDrop 的那个 VI」:`QD Launch VI Ref` → `Block Diagram` → `Selection List[]`,再逐个 `To More Specific Class` 筛出需要的类型。
- Undo 事务由调用方(模板 / 管理器)负责:插件 VI 自己不开事务,出错时应让事务失败。
- NEVSTOP QuickDrops Manager:目录名 `QD_<名字>`;快捷键取自插件 VI 描述里的 `Default Shortcut - [X]` 行。
- 交付形态二选一:把 subVI 交给使用者接入模板(已验证可行);或脚本化模板(复制模板 → 放 subVI → 连线 → 保存,未验证)。

## 可测试性设计(先定接口再动手)

- 不要依赖 UI 选区:`Selection List[]` 只读,且无头环境不可达。
- 固定套路:加一个「诊断路径」字符串输入(空=按选区;给路径=打开该 VI、对整图同类对象操作并保存)+ 一个 `changes made?` 布尔输出。每个功能因此都能无头跑真实数据。
- 收尾必须落到证据:跑一次读回值、导出目标 VI(`ConvertVIToAIXML`)核对连接关系、`lvai_connector_pane` 核对端子;要断言版面顺序时截图比对。

## 实施流程

1. **探针先行**:用最小探针 VI 证实不确定的 API 事实(类名、属性/方法是否存在、返回类型、是否可写),再写正式 VI。
2. **写 AIXML**:按参考清单的语法要点,`ValidateAIXML` 通过后生成到新文件名。
3. **端到端验证**:无头运行 + 导出目标 VI + 连接器板复测,逐条对齐验收标准;每修一处重新验证。
4. **交付**:附使用说明(端子与 conIdx、接入步骤、限制、验证记录),并写明 Undo 由谁负责。
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# AIXML 与 VI Scripting 事实清单

## AIXML 语法

- 每个元素都要 `uid`;顶层元素加 `uid_parent="root"`。
- `<Control>` / `<Indicator>` 必须带 `value`,缺了校验报 `Error -2628 解析文档失败`(症状像「节点不支持」,容易误判)。
- 类引用常量写 `ref{LV.X}`;只写 `{LV.X}` 报 "Unrecognized or unsupported attribute set in Constant"。
- 字符串 / 路径常量里的反斜杠要双写;属性值里的 `>` 写 `&gt;`,同一个网名在产生端与消费端都要写成同样的转义形式。
- 连线名是「网」:同名即同一根线;扇出靠重复同一个网名。
- 跨结构边界(case / loop)必须显式写 `<Tunnel>`;case 的每个帧都要声明全部 tunnel,未用的写空 net。
- `For Loop`:N 用 `maxin` 指定;`count="<loopUid>.value"` 指循环自己的 `i`;`ShiftReg` 的 `Left` 是循环输出侧、`Right` 是输入侧,初值必须来自循环外(写进循环内会报环)。
- uid 用数字或字母开头的串,不要重复。
- 结构节点:`<Structure _name="Case Structure">` + `<CaseFrame selector="...">`;布尔 selector 用 `True` / `False`,错误簇用 `No Error` / `Error`。判断空字符串的 case(`Empty String/Path?`)True 帧是「空」,容易写反。
- `Constant` 的数组字面量形如 `[0,3,8,10,11]`,簇形如 `[false,0,]`。
- 属性节点用 `fields="read+X"` / `write+X`,并必须用 `type="{LV.类名}"` 声明所操作的类;属性名要照抄类文档(如 `Input Terminals` 无方括号、`Output Terminals[]` 有方括号)。
- Invoke 节点写成 `_name="Invoke Node"` + `target="方法名"` + `type="{LV.类名}"`;把 `_name` 写成方法名会报 "Unrecognized node type"。

## 生成与保存

- 先 `ValidateAIXML`;`ConvertAIXMLToVI` 可以覆盖已存在的文件(前提:同名 VI 不在内存中),并能顺带 `openVI` 打开。
- 保存目录必须先存在(LabVIEW 不建目录,报 `Error 7`)。
- 生成的 VI 默认**没有**连接器板端子:要在 `<Control>` / `<Indicator>` 上写 `conIdx`;pattern 由生成器按 conIdx 选择,不可预测。
- 每个新 VI 用新文件名,避免 1051 / 1357;生成后若无人持引用会自行卸载,因此同一路径可反复再生成。

## VI Scripting 要点

- 常用节点:`New VI Object`(`owner refnum` / `style` / `position,next to` / `error in` / `vi object class` / `auto wire? (F)` / `path` / `bounds`,终端要列全)、`To More Specific Class`、`Connect Wire`(`Wire Source` / `Auto Wire? (T)` / `Auto Route? (F)`)、`Delete`、`AddInputAfter`、`Save.Instrument`。
- 常用属性/方法:`Wires[]`、`Selection List[]`、`All Objects[]`(不含 wire)、`Terminals[]`、`Master Bounds Rect`、`Is Source?`、`Input Count` / `Output Count`、`Class Name`、`Style`。
- `New VI Object` 的 `style` 是 Ring,值 = 调色板 item ID 减两个字符:Bundle = 2049、Unbundle = 2048、Build Array = 2041。创建 Bundle/Unbundle 时 `vi object class` 传 `ref{LV.GrowableFunction}`,再用 `To More Specific Class` 转成 `ref{LV.Bundler}` / `ref{LV.Unbundler}`。
- Bundler 读 `Input Terminals`(有序,与元素顺序一致);Unbundler 读 `Output Terminals[]`。
- `AddInputAfter(Index := Input Count - 1)` 只在节点已定型(已接源)后成功;在未接任何源的 Bundle 上运行时报 1055。正确顺序:先把前两个源接进 input 0 / 1 让它定型,再增长。
- 分叉的线在对象模型里是**一个** Wire(`Terminals[]` 有 3 个以上端子);「每根线只有一个下游端子」的判定就是 `Array Size(Wire.Terminals[]) == 2`。
- `Selection List[]` 只读,既不能写也不能在无头环境设置;UI 选区无法脚本化。
- `Open VI Reference` 的 `vi path` 必须是 path 类型:直接接 string 会在运行时报 `Error 1004`(提示 path 未接),前面要加 `String To Path`。
- 前面板对象:`{LV.VI} Front Panel` → `{LV.Panel} All Objects[]`(顺序是声明顺序的反序);`{LV.Control}` 可读 `Class Name`(如 `Cluster` / `Boolean` / `String` / `VIRefNum`)、`Indicator`、`Is On Connector Pane`(只读)。
- 连接器板对象:`{LV.ConnectorPane}` 有 `AssignCtrlToTerm(Control, TermIdx)`;它的 `Controls[]` 读出是 `1D array of void`,不可用;`{LV.VI} Connector Pane:Reference` 读出来不能喂给 `To More Specific Class`(报 bad terminal)。
- 用 `lvai_connector_pane` 测量连接器板:给出 pattern id、slot map、每个端子的位置与风格判定。**不同 pattern 的编号完全不同**(例如 `error in` 在 4815 上是 8,在 4833 上是 11),写 conIdx 前必须先测。

## 验证手段

- 无头运行:`lvai_run_vi_and_read_values`(只能写字符串控件;`errorCode` 是 helper 的,目标 VI 自己的错误要看它返回的 `error out`)。
- 读回代码:`lvai_convert_vi_to_aixml` 导出目标 VI 的 AIXML,核对节点与连接关系(`_name` 取文件名,与生成时的 `_name` 无关)。
- 断言版面/顺序:用 Win32 截图(`EnumWindows` 找窗口 → `ShowWindow(9)` + `SetForegroundWindow` → `PrintWindow(hwnd, hdc, 2)` 存 PNG)。用 `powershell.exe -NoProfile -ExecutionPolicy Bypass -File x.ps1` 跑,脚本写成文件再执行,别塞进 `-Command`。
150 changes: 150 additions & 0 deletions .github/skills/labview-quickdrop-plugin/scripts/lv-dialog-guard.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#requires -Version 5.1
<#
lv-dialog-guard.ps1 — 监视 LabVIEW 的模态对话框并自动置前 + 回车。

背景:LabVIEW 弹出模态对话框时,labview-mcp 的 AI gRPC 服务会被整块冻结,
所有 lvai_* 调用一律超时(DeadlineExceeded),但 LabVIEW 界面看起来正常。
本脚本常驻后台,检测到对话框就自动点掉,并把命中记录写进日志。

用法:
pwsh -NoProfile -ExecutionPolicy Bypass -File lv-dialog-guard.ps1
pwsh -NoProfile -ExecutionPolicy Bypass -File lv-dialog-guard.ps1 -Once
pwsh -NoProfile -ExecutionPolicy Bypass -File lv-dialog-guard.ps1 -DryRun -MaxWidth 700 -MaxHeight 500

判定规则:
属于目标进程(默认 LabVIEW)的可见顶层窗口,满足任一即为对话框:
1. 窗口类名为 #32770(标准对话框);
2. 窗口尺寸不超过 -MaxWidth x -MaxHeight 且有标题(LabVIEW 自绘的小提示窗走这条)。
#>
param(
[double]$IntervalSeconds = 1,
[int]$MaxWidth = 700,
[int]$MaxHeight = 500,
[int]$CooldownSeconds = 3,
[string]$ProcessName = 'LabVIEW',
[string]$LogPath = (Join-Path $env:TEMP 'lv-dialog-guard.log'),
[switch]$Once,
[switch]$DryRun
)

Add-Type -AssemblyName System.Windows.Forms
Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Text;
public class LVGuard {
public delegate bool EnumProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr lParam);
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowTextW(IntPtr hWnd, StringBuilder s, int n);
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetClassNameW(IntPtr hWnd, StringBuilder s, int n);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")] public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int pid);
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int cmd);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
public struct RECT { public int Left, Top, Right, Bottom; }
}
"@

$script:logEnc = New-Object System.Text.UTF8Encoding($false)

function Write-GuardLog {
param([string]$Message)
$line = '{0} {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
Write-Host $line
try { [System.IO.File]::AppendAllText($LogPath, $line + [Environment]::NewLine, $script:logEnc) } catch { }
}

function Get-TargetWindows {
param([int[]]$Pids)
$found = New-Object System.Collections.ArrayList
[LVGuard]::EnumWindows({
param($hWnd, $lParam)
if (-not [LVGuard]::IsWindowVisible($hWnd)) { return $true }

$procId = 0
[void][LVGuard]::GetWindowThreadProcessId($hWnd, [ref]$procId)
if ($Pids -notcontains $procId) { return $true }

$titleSb = New-Object System.Text.StringBuilder 512
[void][LVGuard]::GetWindowTextW($hWnd, $titleSb, 512)
$title = $titleSb.ToString().Replace("`r", ' ').Replace("`n", ' ')
if ([string]::IsNullOrWhiteSpace($title)) { return $true }

$classSb = New-Object System.Text.StringBuilder 256
[void][LVGuard]::GetClassNameW($hWnd, $classSb, 256)

$rect = New-Object LVGuard+RECT
[void][LVGuard]::GetWindowRect($hWnd, [ref]$rect)
$w = $rect.Right - $rect.Left
$h = $rect.Bottom - $rect.Top
if ($w -le 0 -or $h -le 0) { return $true }

$isStdDialog = ($classSb.ToString() -eq '#32770')
$isSmallPopup = ($w -le $MaxWidth -and $h -le $MaxHeight)

# 排除 LabVIEW 正常的窗口(小尺寸的前面板/框图等),避免误按回车
$isNormalWindow = $title -match '(?i)front panel|block diagram|project explorer|getting started|icon editor'

if (-not $isNormalWindow -and ($isStdDialog -or $isSmallPopup)) {
[void]$found.Add([pscustomobject]@{
Handle = $hWnd
Pid = $procId
Class = $classSb.ToString()
Title = $title
Width = $w
Height = $h
Kind = $(if ($isStdDialog) { 'dialog(#32770)' } else { 'small-window' })
})
}
return $true
}, [IntPtr]::Zero) | Out-Null
return $found
}

Write-GuardLog ("guard started: process={0} interval={1}s maxsize={2}x{3} cooldown={4}s dryrun={5} log={6}" -f $ProcessName, $IntervalSeconds, $MaxWidth, $MaxHeight, $CooldownSeconds, $DryRun, $LogPath)

$cooldown = @{}
$round = 0

while ($true) {
$round++
$procs = @(Get-Process -Name $ProcessName -ErrorAction SilentlyContinue)
if ($procs.Count -eq 0) {
if ($round % 30 -eq 1) { Write-GuardLog ("no {0} process; waiting" -f $ProcessName) }
}
else {
$pids = @($procs | ForEach-Object { $_.Id })
foreach ($win in (Get-TargetWindows -Pids $pids)) {
$key = [string]$win.Handle
if ($cooldown.ContainsKey($key)) {
if (((Get-Date) - $cooldown[$key]).TotalSeconds -lt $CooldownSeconds) { continue }
}
$cooldown[$key] = Get-Date

$desc = '[{0}] hwnd=0x{1:X} {2}x{3} "{4}"' -f $win.Kind, [int64]$win.Handle, $win.Width, $win.Height, $win.Title
if ($DryRun) {
Write-GuardLog ('would dismiss ' + $desc)
continue
}

[void][LVGuard]::ShowWindow($win.Handle, 9)
[void][LVGuard]::SetForegroundWindow($win.Handle)
Start-Sleep -Milliseconds 300
try {
[System.Windows.Forms.SendKeys]::SendWait('{ENTER}')
Write-GuardLog ('dismissed ' + $desc)
}
catch {
Write-GuardLog ('sendkeys failed ' + $desc + ' : ' + $_.Exception.Message)
}
Start-Sleep -Milliseconds 300
}
if ($round % 60 -eq 1) { Write-GuardLog ('watching {0} window(s) of {1} process(es)' -f (Get-TargetWindows -Pids $pids).Count, $procs.Count) }
}

if ($Once) { break }
Start-Sleep -Milliseconds ([int]($IntervalSeconds * 1000))
}

Write-GuardLog 'guard stopped'
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# LabVIEW Ignore Files
*.lvlps
*.aliases
*.UserState/

# VIP Compiled Folder
/vip/*
.tmp/

# Ignore Folder
/_Ignore
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule ".wiki"]
path = .wiki
url = https://github.com/NEVSTOP-LAB/LabVIEW-QuickDrops-Manager.wiki.git
Loading
Loading