简介:这是一套用Visual Basic编写的DMX512舞台灯光控制台程序源码包,适合灯光控制爱好者、活动执行人员以及有VB基础的程序员学习参考,帮助理解主从式DMX512协议、通道数据发送与可视化控制台的落地方法。压缩包共119个文件、约1.03MB,主要包含frm窗体与frx资源、bas业务模块(含ArtNet.bas网络扩展)、ctl控件、ocx/dll运行组件、chm/hhc联机帮助、htm命令语法说明、shw工程文件以及bmp/ico界面素材,目录结构清晰,便于按模块研读。已有152人学习下载。从StageConsole主控界面到command_examples实例、select_channels通道选择、default.shw默认布景,资源完整呈现了调光通道分配、0-255亮度映射、场景保存及ArtNet扩展通信等关键开发环节;源码注释配合帮助文档,还能帮助摆脱对特定硬件的依赖,快速移植到DMX USB或以太网灯光系统。
1. 在旧硬盘里翻出的 deskstage 工程,比现成 DMX512 调试助手更值得拆一遍
一个名为dmx_deskstage_dmx512_的目录里躺着一堆.bak文件和两个.bas模块。起初我以为只是老式 VB 窗体程序,打开Module1.bas和ArtNet.bas才发现,这是一个完整的 StageConsole 灯光控台源码。它没有依赖任何商业灯光 SDK,而是直接用 Visual Basic 把 DMX512 协议和 ArtNet 网络帧硬怼了一遍。对现在习惯在 Python 里调库的人来说,这种“从字节开始造轮子”的思路反而有参考价值。这个项目适合两类人:一是要维护十多年前灯光系统的工程师,二是在做自定义 dmx512 调试助手、不想被现成软件限制字段细节的开发者。它不负责帮你编排灯光秀,只负责把 0 到 255 的通道值按协议放进正确的时序。
2. DMX512 与 ArtNet 帧结构:deskstage 控制台的数据组织方式
deskstage 这个工程里,凡是跟硬件发送有关的部分都集中在两个模块:Module1.bas管主流程和通道缓冲,ArtNet.bas管网络封装。这意味着理解这个项目时,需要先把 DMX512 物理帧和 ArtNet 包结构拆开看。
2.1 从 0-255 的通道值到 512 个槽位的映射
DMX512 的基本单位是一个字节,取值 0 到 255,对应舞台上调光器的亮度、颜色盘位置或者其他功能参数。一条 DMX 链路最多承载 512 个这样的字节,所以“通道号”本质上是数组下标。协议规定在数据之前必须发送 BREAK(拉低线路至少 92 微秒)、MAB(Mark After Break,约 12 微秒)和一个起始码 0x00,然后才是通道数据。这个时序是由硬件适配器或串口状态机去处理的,软件侧只需要把 512 字节按顺序填好。
在Module1.bas里常见写法是定义一个 0 到 511 的全局字节数组。我一般在 UI 层加一层映射,把用户看到的“第 1 路”转成数组下标 0,这样既能保住直觉,又能避免到处减一:
' Module1.bas 中典型的通道值缓冲 Public DMXData(0 To 511) As Byte Public Sub SetChannel(ByVal uiChannel As Integer, ByVal value As Integer) Dim idx As Integer idx = uiChannel - 1 If idx >= 0 And idx <= 511 Then DMXData(idx) = value Mod 256 End If End Sub这段代码的逻辑很直接:uiChannel是用户看到的通道号,从 1 开始;idx是数组下标,从 0 开始。value Mod 256是为了防止 UI 上有人输入 255 以外的值,比如把调光值从 0-100 映射成 0-255 时,因为整数乘法偶尔会产生 256 这个越界值。实际使用时要注意value必须是整数类型,VB 里如果直接用浮点数,Mod运算符会先做四舍五入,容易引入偏差。
随后,灯光场景的“状态”就全部体现在这 512 个字节里。比如一个 RGB 三色灯占 3 个通道,我通常会让 UI 用三个滑块分别控制DMXData(0)、DMXData(1)和DMXData(2)。至于什么通道对应什么颜色,这是灯具厂商的说明书决定的,deskstage 工程里default.shw.bak这类场景快照,本质上就是把这一整块数组序列化保存。
2.2 ArtNet.bas 里如何把通道数组封装成 UDP 包
如果你只有一条串口线,直接发送上面的数组给 USB-DMX 适配器即可。但这个工程里出现ArtNet.bas,说明它走了 ArtNet 协议。ArtNet 是 Art-Net 标准定义的在以太网上传输 DMX512 数据的应用协议,默认 UDP 端口 6454。它把整帧 DMX512 数据包装成一个 ArtDmx 数据包,这样同一局域网里的解码器节点可以收到并转成物理 DMX 信号。
ArtDmx 包的格式很固定,我从实际抓包里整理了一份字段表:
| 字段 | 偏移 | 长度(字节) | 值/说明 |
|---|---|---|---|
| ID | 0 | 8 | 固定为Art-Net\0 |
| OpCode | 8 | 2 | 0x5000,表示 ArtDmx,低字节在前 |
| ProtVer | 10 | 2 | 0x000E(协议版本 14),低字节在前 |
| Sequence | 12 | 1 | 当前包的序列号,用来检测丢包 |
| Physical | 13 | 1 | 物理端口号,一般填 0 |
| Universe | 14 | 2 | 目标宇宙,0-32767,低字节在前 |
| Length | 16 | 2 | DMX 数据长度,高字节在前,512 即 0x0200 |
| DMX Data | 18 | n | 实际通道数据,最多 512 字节 |
注意 Length 字段和其他字段的字节序不一样,这是 Art-Net 规范里最容易错的地方。我见过有人把 Length 也按低字节在前写,结果节点接收到一个“反向长度”,拒绝转发。deskstage 的ArtNet.bas里构造包的时候,通常是把 512 字节复制到 offset 18 以后,再回填头部:
Public Sub BuildArtNetPacket(ByVal universe As Integer, _ ByRef data() As Byte, _ ByRef pkt() As Byte) Dim i As Integer ' 1. 固定 ID pkt(0) = Asc("A"): pkt(1) = Asc("r") pkt(2) = Asc("t"): pkt(3) = Asc("-") pkt(4) = Asc("N"): pkt(5) = Asc("e") pkt(6) = Asc("t"): pkt(7) = 0 ' 2. OpCode = 0x5000, 低字节在前 pkt(8) = 0: pkt(9) = &H50 ' 3. ProtVer = 14, 低字节在前 pkt(10) = 14: pkt(11) = 0 ' 4. Sequence / Physical pkt(12) = 0 pkt(13) = 0 ' 5. Universe, 低字节在前 pkt(14) = universe Mod 256 pkt(15) = (universe \ 256) Mod 256 ' 6. Length = 512, 高字节在前 pkt(16) = 2 pkt(17) = 0 ' 7. 复制通道数据 For i = 0 To 511 pkt(18 + i) = data(i) Next i End Sub解释一下参数:universe是 UI 里看到的宇宙号,如果用户选的是“宇宙 1”,这里要传 0,因为协议从 0 开始计数。pkt(16) = 2和pkt(17) = 0就是高字节在前地表示十进制的 512;如果你以后想发送更少的通道(比如只控制 128 通道),这里要改成pkt(16) = 0: pkt(17) = 128,并且在复制数据循环里只循环 0 到 127,否则多余的数据会被协议忽略。pkt(12)的序列号应该在每次发送前递增并限制在 0 到 255,否则接收端检测不到乱序。这个填充函数本身不做网络发送,真正的 UDP 发送是在Winsock1.SendData里完成的,后面第 4 章我会给出完整的调用方式。
这段代码的核心价值在于:它把“数据组织”和“传输”分离了。调试 DMX512 设备时,如果灯不亮,你可以先打印这个pkt数组看头部对不对,再谈网络问题。
3. 从 Module1.bas 到 StageConsole:事件驱动与命令解析
StageConsole.hhp是 VB 的帮助项目文件,command_syntax.htm.bak里记录了命令语法。这说明这个工程不是简单拉几个按钮,而是有完整的人机命令体系。理解这些,才能把老的 deskstage 改造为可用的调试助手。
3.1 滑块事件里只写缓冲,不要在事件里发数据
VB 的滑块(Slider)控件在拖动时会连续触发Change事件,频率比想象中高。如果在事件里直接调用 UDP 发送函数,UI 线程会被网络和控件刷新拖死。常见做法是:事件里只更新DMXData数组并标记一个“脏”标志,然后由一个定时器统一发送。这样无论 UI 以多快的速度触发,实际发送频率都由定时器控制。
Private Sub Slider1_Change() ' 滑块范围 0-100,映射到 0-255 DMXData(0) = Slider1.Value * 255 \ 100 DirtyFlag = True End Sub这段代码有两个细节值得说。第一,\是 VB 的整数除法,比/更适合这里,因为 DMX 通道值是整数,直接用浮点除会出现 99.9 这种到不了 255 的边界问题;整数除法可以保证 100 映射到 255。第二,DirtyFlag是一个模块级布尔变量,定时器事件里看到它为True就发送,发送完成后立即置回False。这比定时器每 25 毫秒无脑发一次要高效,尤其在做静态灯光场景时,能明显减少局域网中的垃圾包。
如果要做平滑亮度变化,不能依赖滑块事件本身。滑块停止后,最后的值会被写入数组,但“停在中间”的感觉不对。我一般会再加一个Timer做线性插值:把目标值保存到TargetValue,每次 Tick 时让当前值朝目标值走一小步,再把当前值写入数组。这样从 0 到 255 的过程是视觉上连续调光,而不是一下跳变。注意插值步长要根据发送帧率调整,例如 40fps 下发 30ms 完成一次全跨度变化,步长就是 255/(40*0.03),约等于 212。
3.2 command_syntax 文件与内置指令解析的取舍
工程里的command_syntax.htm.bak记录了类似“通道 1 到 5 设为 255”的命令语法。在 VB 里解析这种字符串,最简单的办法是用Split把命令按空格拆开,再根据第一个词分发到不同处理函数。表格是整理命令结构最好的方式:
| 命令示例 | 含义 | 解析后的动作 |
|---|---|---|
ch 1 @ 255 | 通道 1 亮度设为 255 | 调用SetChannel(1,255) |
ch 1-10 @ 128 | 通道 1 到 10 设为 128 | 循环调用SetChannel(i,128) |
cls | 清空所有通道 | 将整个DMXData数组归零 |
store 1 | 保存当前场景到 1 号快照 | 复制DMXData到Scene(1) |
解析函数可以写成下面这样,基本能覆盖 90% 的指令需求:
Public Sub ParseCommand(ByVal cmd As String) Dim parts() As String Dim i As Integer, ch As Integer parts = Split(Trim(cmd), " ") Select Case LCase(parts(0)) Case "ch" If InStr(parts(1), "-") > 0 Then Dim range() As String range = Split(parts(1), "-") For i = CInt(range(0)) To CInt(range(1)) SetChannel i, CInt(Replace(parts(3), "@", "")) Next i Else SetChannel CInt(parts(1)), CInt(parts(3)) End If Case "cls" For i = 0 To 511 DMXData(i) = 0 Next i Case "store" Call SaveScene(CInt(parts(1))) End Select DirtyFlag = True End Sub说明一下解析逻辑:Split(cmd," ")把ch 1 @ 255拆成四个字符串,parts(1)是通道号,parts(3)是值。命令字符串里@实际上只是装饰词,解析时直接忽略。为了兼容ch 1-10 @ 128这种连续通道写法,先检查parts(1)里是否包含-,包含就拆出起止通道循环赋值。LCase(parts(0))用于统一大小写,避免用户输入CH时找不到分支。所有分支结束后统一置DirtyFlag,等待定时器把数据发出去。
这种解析方式相比“每个命令写一个专用函数”更利于扩展到新的指令,比如fade 1-5 255 2000这种带时间的命令。关键点是不在解析函数里直接发送数据,而是把解析结果落进DMXData数组。否则一条ch 1-50 @ 128会把 50 个 UDP 包瞬间砸到网络上,接收节点会因为这个突发而丢失后续包。我调试时碰到过类似问题:灯多个通道跳变,原因不是协议错误,而是一条命令触发太多次发送。改成统一发送后,问题立刻消失。
这个逻辑同样适用于下拉框、复选按钮以及 scene 快照的加载。任何 UI 操作都只改数组和标志,发送动作留给定时器,这是从老式灯光台程序里学到的典型模式。
4. 搭建自己的 dmx512 调试助手:端口选择与发送节拍
把协议和事件模型理顺之后,就可以动手写一个自己的 dmx512 调试助手了。这个助手不追求复杂业务,只要做到“拖滑块、看灯亮、抓包能对上”。
4.1 选 USB-DMX 串口还是 ArtNet 网络口
deskstage 工程里只出现ArtNet.bas,没有看到串口 API,说明原作者默认走网络。但很多 DIY 项目用的是 USB-DMX 设备,比如 Enttec Open DMX,这类设备在 Windows 上会映射成 COM 口,需要用 MSComm 控件或调用 DLL 发送。两者选型差异看表格:
| 对比维度 | USB-DMX(串口) | ArtNet 网络口 |
|---|---|---|
| 硬件成本 | 低,兼容设备多 | 中,需要节点解码器 |
| 发送方式 | 按 DMX512 时序拉 BREAK 再送字节 | 把字节封装成 UDP 包 |
| 调试难度 | 需要示波器/逻辑分析仪看时序 | 用 Wireshark 抓包即可定位 |
| 最大距离 | 几十米内 | 受交换机限制,但方便跨房间 |
| 实时性 | 驱动控制精确到微秒 | 受网络抖动影响,现场需注意 |
如果是给剧场做走线,ArtNet 是更常见的选择,因为一条网线可以带多个 Universe。如果只是桌面上测试一个灯头,USB-DMX 更省钱。我一般建议新手用具 ArtNet 节点的方案,原因不是它更简单,而是抓包工具成熟,出了错能立刻看到包格式。如果你用串口,最怕的是时序参数(BREAK 宽度、MAB 宽度)不对,那些参数在软件里不可见,排查成本高。
4.2 用定时器控制发送频率,避免阻塞界面
选定 ArtNet 后,核心是一个 UDP 发送例程加一个定时器。VB6 里没有现成的 UDP Socket,通常用 Winsock 控件。Winsock1.Protocol设为sckUDPProtocol,RemoteHost填解码器的 IP,RemotePort填 6454。然后定时器每隔 25 毫秒检查脏标志并发送:
Private Sub Timer1_Timer() Dim artnetPkt() As Byte If Not DirtyFlag Then Exit Sub ReDim artnetPkt(0 To 529) Call BuildArtNetPacket(0, DMXData, artnetPkt) Winsock1.SendData artnetPkt DirtyFlag = False End Sub Private Sub Form_Load() Winsock1.Protocol = sckUDPProtocol Winsock1.RemoteHost = "192.168.1.200" Winsock1.RemotePort = 6454 Timer1.Interval = 25 End Sub这里BuildArtNetPacket对应第 2 章的函数,只是把pkt通过参数返回。Timer1.Interval = 25表示每秒最多 40 帧。Art-Net 标准推荐数据包发送速率不要超过每秒 40 帧,因为流明现场的灯具刷新率通常 25-40Hz,超过这个值只是在烧带宽。需要注意Winsock1.RemoteHost在 UDP 模式下可以每次发送前修改,所以想控制多个节点时,不用建多个控件,发送前改RemoteHost再SendData即可。但要注意两点:一是SendData是异步的,循环改 IP 时不会立即生效;二是如果一次要发到多个 Universe,推荐用多个 Winsock 控件,或者改用底层Winsock1.SendData在每次发包前重新指定地址,并确认节点端口的绑定不冲突。
发送节拍还要考虑 Timer 的精度。VB6 的 Timer 默认精度只有约 15.6 毫秒,把 Interval 设成 25 可能实际触发间隔在 15-30 毫秒之间波动。对舞台灯光的呼吸效果来说够用,但如果要做 DMX 波形回放(比如把录制的曲线送出去),建议把 Interval 设成 16 毫秒以下,并在发送前用timeGetTime做微调。另外,DirtyFlag方案意味着只有当滑块变化或命令执行时才发包,这在静态场景下很省网络,但等到你要做自动跑灯效果时,就不能依赖滑块事件了,需要定时器强制发包并更新数组。我通常会在定时器里加一个ContinuousMode As Boolean,为 True 时忽略DirtyFlag,每一帧都发送。
5. 老工程的兼容性坑与 ArtNet 抓包验证技巧
dmx_deskstage_dmx512_里的文件大多是.bak,这说明原工程经历过多次覆盖和备份。把这样一套 Visual Basic 6 工程重新跑起来,最大的麻烦不在代码,而在开发环境兼容性。VB6 的 IDE 在 Windows 10/11 上仍可以安装,但安装后要把MSComctlLib(Slider 控件、Toolbar)和MSWinsockLib(Winsock 控件)的引用勾选好。如果编译时提示“部件未注册”,用管理员身份打开 cmd,运行regsvr32 MSCOMCTL.OCX和regsvr32 MSWINSCK.OCX即可。注意 VB6 默认生成 32 位程序,在 64 位系统上能运行,但如果你的工程引用了 64 位专用 COM 组件,就会出现类型不匹配。
5.1 验证 ArtDmx 包是否合法
跑通后不要直接拿灯光设备试,先用 Wireshark 抓包。在过滤栏输入udp.port==6454,然后拖动滑块,你应该能看到目标 IP 端口 6454 的 UDP 包。点开第一个包,检查 Data 是否以41 72 74 2d 4e 65 74 00开头,这就是Art-Net\0。再往下翻,找到偏移 8 的00 50,也就是 ArtDmx 的 OpCode。如果抓到的包是50 00,说明字节序写反了。
另一个实用技巧是把Sequence字段固定为一个常数,比如 0x0D,发送几帧后再恢复递增。这样在大量相同数据包中能快速区分新旧包。因为 Art-Net 接收端只认 Sequence 变化,固定时有些节点会把重复包当成丢包后的重传处理,不会触发输出,所以平时不要长期固定,仅用来调试。
5.2 用 Python 做镜像接收端
如果你不想反复改 VB 代码,可以先用 Python 在另一台机器上监听 UDP 6454,把接收到的ArtDmx包解码成 512 个通道值,和 VB 界面上的滑块数值比对。下面是一段最小接收程序,用于验证通道映射:
import socket UDP_IP = "0.0.0.0" UDP_PORT = 6454 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((UDP_IP, UDP_PORT)) while True: data, addr = sock.recvfrom(1024) if data[:8] == b'Art-Net\x00': universe = data[14] + (data[15] << 8) length = (data[16] << 8) + data[17] dmx = data[18:18 + length] print(f"universe={universe}, len={length}, ch1={dmx[0]}")这段代码直接读取第 14、15 字节合成 Universe,第 16、17 字节合成 Length。注意 Python 端不需要再处理字节序,因为data[16] << 8已经模拟了高字节在前。如果这里打印出来的 universe 和你界面上设置的一致,就说明协议封装没问题。如果 ch1 值不对,重点检查 VB 端滑块事件里的映射公式。
在 Wireshark 里加一条udp.port==6454 && artnet.opcode==0x5000过滤器,能直接过滤出 ArtDmx 包,再结合上面的 Python 接收端,几乎所有的字节序错误都能在两步内暴露出来。如果这条过滤器里始终没有包,回头先确认 VB 的Winsock1.RemotePort是不是被某段初始化代码写成了 6453 之类的错误值。
本文还有配套的精品资源,点击获取