AMCLite—LabVIEW 轻量级异步消息通信框架

写在前面:一个用了很多年的"老伙计"

做 LabVIEW 开发这些年,大大小小的项目做过不少——有只有一个前面板、几个按钮的小工具,也有十几个功能模块、跨多个 VI 并行跑的产线测试系统,更有行业产品级的软件系统。

框架换来换去,最后沉淀下来一直在用的,就是这套自己改出来的 AMCLite

它脱胎于 NI 官方的 AMC(Asynchronous Message Communication)参考库。原生 AMC 很经典,但用久了就会发现几个绕不开的痛点:消息值是平化字符串,传个数组、簇、波形都得先 Flatten 再 Unflatten,写一堆胶水代码;为了支持跨网络通信引入了 UDP 和 Dispatcher,而 90% 的项目其实根本不需要跨机器通信,白白背着一堆用不上的依赖;想跨 VI 发消息,还得先把队列引用一层层传过去。

于是我把它做了减法,也做了加法:

  • 减法:砍掉 UDP / Dispatcher / 网络相关的全部内容,只保留最核心的本地异步消息机制,整个库变得非常轻;

  • 加法:消息值由平化字符串改为 Variant,任意 LabVIEW 数据类型直接扔进去,取出来强制转换即可;

  • 加法:新增按队列名寻址的全局消息发送,不需要传递队列引用,任意 VI 之间都能直接对话。

改完之后,从简单的单循环状态机,到多线程并行的大型系统,这一套都能兼容着往下用。小项目用它不觉得重,大项目用它不觉得不够——这大概就是我一直没换的原因。

这篇文章把 AMCLite 的功能完整梳理一遍。如果你还不了解原生 AMC 的设计思想,建议先看我之前那篇 《LabVIEW教程——AMC异步通信框架详解》,本文会默认你对 QMH(队列消息处理器)设计模式有基本概念。

AMCLite工具包已打包好,博主主页简介提供AMCLite工具包下载。


一、AMCLite 是什么

一句话概括:AMCLite 是一个基于 LabVIEW 原生队列的、以 Variant 为消息载体的轻量级本地异步消息通信框架。

它解决的核心问题是:让程序中各个独立运行的部分(线程)之间,能够低耦合地互相传递命令、状态和数据。

这里说的"线程",指的是程序里那些持续运行的部分,通常表现为一个 While 循环:

  • 用户界面(UI)循环

  • 数据采集 / IO 处理循环

  • 仪器通信循环

  • 数据记录 / 存盘循环

  • 状态机控制循环

这些模块彼此需要交流:UI 告诉采集循环"开始测试",采集循环告诉记录循环"这一帧数据拿去存",任何一个模块出错要能通知主控循环"我挂了,赶紧退出"。

AMCLite 就是它们之间的"邮政系统"。

与原生 AMC 的差异对照

对比项原生 AMCAMCLite
消息值数据类型平化字符串(Flattened String)Variant
传递复杂数据需手动 Flatten / Unflatten直接连线,取出时强制转换
网络通信支持,基于 UDP + Dispatcher已移除
跨 VI 通信需传递队列引用,或依赖 Dispatcher按队列名直接寻址(Global 系列 VI)
依赖复杂度较重(含网络、注册表、Dispatcher 等)极简,只依赖 LabVIEW 原生队列
适用场景本地 + 分布式单机本地(同一 LabVIEW 应用实例内)

需要明确说明的是:AMCLite 不做跨网络通信。如果你的项目确实需要跨设备传消息,请用原生 AMC,或者用 Network Stream、STM 等更合适的方案。AMCLite 的定位就是把"单机内部的异步通信"这一件事做到足够顺手。

什么时候用它

AMCLite 的适用面很广,下面这些场景都可以放心用:

  • 低频、不定时产生的事件:用户点击、设备报警、状态变更、阶段性结果等;

  • 命令下发与状态回传:模块之间发指令、报状态、传结果;

  • 模块之间的解耦通信:不想让模块互相持有引用、又要能通信时;

  • 高频、定时产生的数据流:由于底层直接基于 LabVIEW 原生队列,且消息值改用 Variant 传递(避免了平化字符串的序列化开销),AMCLite 完全有能力扛住连续采集、实时数据这类高速场景,可以直接用它在采集循环和处理/存盘循环之间搬运数据。

一句话:从小工具到大型多循环系统,从低频事件到高速数据流,AMCLite 都能兼容着用。 这也是我这些年一直没换框架的原因——不用为不同项目在不同框架之间反复切换。


二、安装

AMCLite 以 VIPM 包(.vip 文件)形式分发,安装非常简单。

图片

图1:下载得到的 VIPM 安装包

下载后得到如上图所示的文件,双击即可安装(前提是已安装 VI Package Manager)。按提示一路 Next 即可完成。

安装完成后,库文件位于:

National Instruments\LabVIEW 2023\vi.lib\AMCLite

路径中的 LabVIEW 2023 会随你实际安装的 LabVIEW 版本变化。

安装完成后重启 LabVIEW,在程序框图的函数选板中,依次展开 Addons ---> AMCLite,即可找到整个工具包:

图片

图2:AMCLite 函数选板位置

选板中提供了两类内容:常规使用的功能 VI 和 可直接拖入使用的模板 VI,下面分别展开介绍。


三、核心概念:Local 与 Global

在讲具体 VI 之前,必须先讲清楚 AMCLite 最重要的一个设计——消息发送分为 Local 和 Global 两套。理解了这一点,整个框架就通了。

Local:靠队列引用寻址

Local 系列 面向的是"我手上已经有这个队列的引用"的场景。

典型场景就是经典的 QMH:在同一个 VI 里,左边的消息产生器循环(UI 事件结构)和右边的消息处理器循环,共享同一个队列引用。产生器把消息塞进队列,处理器取出来处理。

这种方式的特点是快、直接、不会出错——引用是编译期就连好线的,不存在名字写错找不到队列的问题。

Global:靠队列名寻址

Global 系列 是 AMCLite 相对原生 AMC 最重要的增强。

它的思路很简单:每个消息队列在创建时都有一个唯一的名字,发送方只要知道这个名字,就能给它发消息,完全不需要拿到队列引用。

这带来的好处是巨大的:

  • 跨 VI 通信零成本。子 VI 想给主界面发个状态更新?直接 Send Global Message,队列名填 "UI" 就行。不用把队列引用作为参数一层层往下传,也不用搞全局变量存引用。

  • 模块彻底解耦。发送方只需要知道"收件人叫什么名字",完全不需要知道对方是谁、在哪、怎么创建的。新增一个模块不需要改动任何已有代码的接线。

  • 动态调用友好。异步动态调用起来的 VI,天然拿不到主 VI 的引用,用 Global 方式就毫无障碍。

在实际项目里,我的习惯是给每个功能模块的队列起一个语义清晰的名字,比如 "UI""DAQ""Logger""Instrument"。整个系统的通信关系一目了然,看名字就知道消息发给谁。


四、功能 VI 详解

选板中提供了五个常规 VI:Send Local MessageSend Local MessagesSend Global MessageSend Global MessagesFlush Message Queue

4.1 Send Local Message

图片

图3:Send Local Message

向指定队列引用发送单条消息。

主要输入参数:

  • Message Queue:目标消息队列的引用;

  • Message:消息名(字符串),用来标识这条消息的类型,通常直接作为消息处理器中 Case 结构的选择器;

  • Message Value(Variant)这里就是 AMCLite 的核心改动。消息值类型为 Variant,你可以把任意 LabVIEW 数据类型直接连上去——数值、数组、簇、波形、路径、甚至引用句柄,不需要任何转换;

  • Priority Message:优先级标志。默认为 False,消息追加到队列末尾,按 FIFO 顺序处理;设为 True 时,消息会插入到队列最前端,下一次读取时被优先处理。

关于 Priority Message,最典型的用法是错误处理。当某个模块检测到严重错误,需要立刻中止当前流程时,就用优先级消息插队,避免错误消息排在一堆待处理消息后面迟迟得不到响应。

Variant 带来的实际差别有多大? 举个例子,假设你要把一个采集到的波形数组连同配置簇一起发给记录模块:

  • 原生 AMC 的写法:先把数据 Flatten To String,接收端再 Unflatten From String,还得小心两端的类型定义完全一致,改一个字段就得两边同步改,出错了还没有明确报错,只会得到一堆乱码或者莫名其妙的默认值;

  • AMCLite 的写法:数据直接连到 Message Value 端子,接收端一个 Variant To Data 搞定,类型不匹配时会直接报错,问题一眼看得见。

代码量少了,出错概率也小了。

4.2 Send Local Messages

图片

图4:Send Local Messages

向指定队列引用一次性发送多条消息。

功能与 Send Local Message 一致,区别是消息以数组形式批量传入,会按数组顺序依次入队。

这个 VI 在初始化流程编排时特别好用。比如一个测试流程需要依次执行"上电 → 自检 → 加载配置 → 开始测试",直接用一个数组常量一次性把四条消息压进队列,消息处理器会按顺序逐条执行,流程逻辑清晰可读,比在多个 Case 里互相跳转要好维护得多。

4.3 Send Global Message

图片

图5:Send Global Message

按队列名发送单条消息。

与 Local 版本的唯一区别:不再输入队列引用,而是输入目标队列的名称字符串。VI 内部会根据名字查找对应的队列并投递消息。

这是 AMCLite 用得最多的一个 VI。任意一个 VI、任意一个循环、甚至一个被动态调用起来的子 VI,只要知道目标队列名,一行代码就能把消息送到。

举几个我在项目里的实际用法:

  • 子 VI 向主界面回报进度:一个耗时的算法子 VI,在计算过程中不断 Send Global Message 到 "UI" 队列,消息名 "Update Progress",值为进度百分比。主界面收到就刷新进度条。子 VI 完全不需要任何界面引用;

  • 异常统一上报:所有模块遇到错误,一律 Send Global Message 到 "Main" 队列,消息名 "Error",值为错误簇。主控循环集中处理,要弹窗、要写日志、要安全停机,全在一处决定;

  • 跨模块联动:仪器通信模块拿到测量结果,直接发给 "Logger" 存盘、发给 "UI" 显示,两个接收方互不知情,各改各的。

注意:Global 发送依赖目标队列已经创建。如果队列名对应的队列还不存在(比如目标模块尚未启动,或者已经退出销毁了队列),发送会失败。在实际工程中,建议在系统启动流程里明确各模块队列的创建顺序,或者对发送结果做必要的错误处理。

4.4 Send Global Messages

图片

图6:Send Global Messages

按队列名一次性发送多条消息。

Send Global Message 的批量版本,消息以数组形式传入并按序入队。用法参照 4.2 节,不再赘述。

4.5 Flush Message Queue

图片

图7:Flush Message Queue

清空指定消息队列中所有待处理的消息。

它会一次性移除队列里所有还没被读取的消息,而不需要逐条读出来丢掉。

典型使用场景:

  • 中止操作:用户点了"停止"按钮,此时队列里可能还积压着一堆待执行的测试步骤消息。先 Flush 清空队列,再压入 "Stop" 消息,才能真正做到立即停止,而不是把剩下的步骤全跑完再停;

  • 状态切换:从"自动模式"切到"手动模式"时,清掉自动模式遗留的待处理指令,避免模式混乱;

  • 异常恢复:出错后重置流程,把脏数据清干净再重新开始。

Flush + 优先级消息是一对黄金搭档:先清空、再插队,能保证关键指令得到最快响应。


五、模板 VI

除了功能 VI,AMCLite 还内置了三个开箱即用的模板Single Loop SMDouble Loop SMPure Message SM

图片

图8:AMCLite 模板选板

这三个模板可以直接从选板拖入空白 VI 的程序框图,自动生成完整的骨架代码——队列创建、消息读取循环、Case 结构、退出处理、队列销毁全都写好了,你只需要往里填业务逻辑。

这是我认为 AMCLite 最省时间的部分。 新开一个 VI,拖一个模板进来,五秒钟就有了一个结构规范、可扩展、能正确退出的程序骨架。团队里所有人写出来的代码结构都一致,接手别人的项目也不用重新理解架构。

5.1 Single Loop SM(单循环状态机)

图片

图9:Single Loop SM 模板

单循环架构:一个 While 循环 + 一个 Case 结构,事件结构内嵌在其中——熟悉 JKI State Machine 的朋友对这个结构会很有亲切感。

它把"消息驱动"和"事件响应"合并进了同一个循环,运行逻辑是这样的:

  • 循环从队列读取消息,消息名作为 Case 选择器,进入对应分支执行;

  • 其中有一个专门的 "Idle"(空闲)分支,里面放着事件结构。当队列里没有待处理消息、循环进入空闲状态时,就停在事件结构上等待用户操作;

  • 一旦捕获到前面板事件(按钮点击、控件值改变等),就把对应的消息压入队列,循环随即取出并跳转到相应分支执行;

  • Case 分支内部同样可以继续往队列里压消息,驱动状态一步步流转。

这样一来,用一个循环就同时搞定了 UI 事件响应和状态流转,不需要开第二个循环,结构比双循环更紧凑、数据在循环内直接用移位寄存器传递、也没有两个循环之间同步退出的握手负担。

适用场景:

  • 中小型带界面的 VI,UI 逻辑不算特别复杂

  • 流程/状态明确、以顺序流转为主的应用

  • 想要单循环、结构紧凑、维护简单的场合

  • 熟悉并偏好 JKI 风格状态机的开发者

一个需要留意的点:因为事件响应和业务执行在同一个循环里,如果某个 Case 分支执行时间很长,循环就无法回到 Idle 分支的事件结构,这段时间内前面板的操作不会被立即响应。若你的应用里存在耗时较长、又不能阻塞界面的操作,就应该改用下一节的 Double Loop SM,把界面和业务彻底拆到两个循环里。

5.2 Double Loop SM(双循环状态机)

图片

图10:Double Loop SM 模板

经典的 QMH 生产者-消费者结构:左边消息产生器循环(Event Structure),右边消息处理器循环(While + Case)。

  • 左侧循环:事件结构捕获前面板的用户操作(按钮点击、控件值改变、面板关闭等),把每个操作打包成消息投入队列;

  • 右侧循环:从队列取消息,在 Case 结构中执行对应的业务逻辑。

两个循环并行独立运行,这样做的关键价值在于:界面响应和业务执行彻底分离。即使某个业务操作耗时几秒钟,UI 循环依然在正常响应用户点击,界面不会卡死。

适用场景:

  • 需要在响应界面的同时执行耗时较长后台任务的 VI

  • UI 交互复杂、业务逻辑较重的应用

  • 中大型应用的主程序面板

它和 5.1 Single Loop 的分工可以这样把握:界面简单、没有长耗时阻塞操作时,用 Single Loop 更紧凑;一旦某些操作耗时较长、又必须保证界面随时可响应,就用 Double Loop 把界面和业务彻底拆到两个循环里。实际项目中,稍具规模的主面板我基本都用它。

关于两个循环的退出:模板中已经处理好了。用户点击退出时,UI 循环发送退出消息给处理器循环,同时自身条件端子置 True 退出;处理器循环收到退出消息后,执行清理动作、销毁队列,然后退出。这个握手过程是 QMH 里最容易写错的地方,模板帮你写好了,直接用就行。

5.3 Pure Message SM(纯消息状态机)

图片

图11:Pure Message SM 模板

去掉界面元素,只保留纯粹的消息驱动结构。

它不包含 UI 事件处理,所有的驱动力都来自外部通过 Send Global Message 发进来的消息。VI 启动后创建自己的命名队列,然后就安静地待在那里等消息,收到什么执行什么。

适用场景:

  • 通过异步动态调用启动的后台服务型模块

  • 数据记录服务、日志服务

  • 仪器通信代理

  • 任何"常驻后台、被别人调用"的功能模块

这个模板和 Global 消息机制是天生一对:把一个 Pure Message SM 动态调用起来跑在后台,系统里任何地方需要它干活,直接按队列名发消息就行。这基本上就是用 LabVIEW 实现的一个轻量级"服务"。

在稍大一点的项目里,我通常会这样组织:

  • 主界面用 Double Loop SM

  • 每个后台功能模块(采集、存盘、通信、报警)各用一个 Pure Message SM,异步动态调用启动

  • 模块之间全部通过 Send Global Message 按名字通信

整个系统的结构非常清晰:一个主界面 + N 个后台服务,靠消息串起来。加模块只需要新增一个 VI 和一个队列名,不用动任何已有代码。


六、使用范例

AMCLite 附带了一个可直接运行的示例,位于选板的 Example 子面板中,名为 Queued Message Handler Fundamentals

图片

图12:Example 示例

示例代码如下:

图片

图13:示例代码

建议直接打开这个范例跑一遍,配合断点和高亮执行,观察消息是如何从产生器进入队列、再被处理器逐条取出处理的。看十遍文档不如自己跑一遍代码。


七、实战建议

最后分享几条这些年用下来总结的经验,都是踩过坑之后留下的。

1. 消息名直接用字符串即可

消息名和队列名直接用字符串就好,简单直观,不建议特意套一层 TypeDef 枚举或者常量子 VI——那样反而增加维护成本,也失去了字符串消息"随手加一条新消息"的灵活性。

唯一要提醒的是:字符串是手打的,注意别拼错。消息名或队列名一旦写错,消息可能悄无声息地进了错误的分支、或者发到一个不存在的队列,不会有明显报错。所以敲字符串时留个心,两端对照着写,出问题时也优先检查一下是不是拼写不一致。

2. Variant 取值时做好类型校验

Variant 灵活是灵活,但也把类型检查从编译期推到了运行期。Variant To Data 的错误输出不要直接丢弃,接上错误处理,这样类型不匹配的时候能立刻定位到是哪条消息出了问题。

一个更稳妥的做法:对于结构复杂、字段多的消息,用 TypeDef 簇作为消息值的类型,两端都引用同一个 TypeDef。这样改字段的时候两端自动同步,不会出现单边遗漏。

3. 队列的创建与销毁要配对

每个线程创建的队列,退出时必须销毁。忘记销毁会导致队列引用泄漏,长时间运行的程序上这个问题会逐渐累积。模板里已经处理好了配对逻辑,不要随意删掉模板里的销毁代码

同时也要注意销毁的顺序:如果 A 模块还在给 B 模块发消息,就不能先把 B 的队列销毁掉。退出流程建议由主控模块统一编排,先通知所有子模块退出,等它们全部确认退出后,主控再退出。

4. 善用优先级消息,但别滥用

优先级消息插队处理,适合错误、急停、中止这类必须立刻响应的场景。但如果什么消息都设优先级,队列的 FIFO 语义就被破坏了,执行顺序会变得不可预测,反而更难调试。优先级消息应该是少数派。


八、总结

AMCLite 做的事情其实不复杂,就是在原生 AMC 的基础上做了三件事:

  1. 消息值改为 Variant —— 任意数据类型直接传,告别 Flatten/Unflatten 的胶水代码;

  2. 移除 UDP 网络通信 —— 砍掉单机项目用不到的部分,框架回归轻量;

  3. 新增按队列名寻址的 Global 消息 —— 跨 VI 通信零成本,模块彻底解耦。

再加上三个开箱即用的模板,让"新建一个结构规范的 VI"这件事变成五秒钟的事。

这套东西谈不上多高深,但胜在足够简单、足够顺手、足够稳定。小项目用它不觉得重,大项目用它不觉得不够,从几年前的老项目到现在的新项目一路兼容下来,没有为了框架本身返工过。

对于日常做 LabVIEW 开发的工程师来说,有一套自己用得顺手、且能长期沿用的架构底座,价值可能比学十个新特性都要大。

如果这篇文章对你有帮助,欢迎点赞收藏。有任何问题或者建议,也欢迎在评论区交流。
博主主页简介提供AMCLite工具包下载。


版权与免责声明

AMCLite 基于 NI 开源的 AMC 参考库修改而来,原库版权归 National Instruments 所有,依据 Apache License 2.0 授权,安装包内已随附许可证与 NOTICE 文件。
AMCLite 为独立第三方修改版本,与 NI 无隶属或背书关系。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未央LabVIEW

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值