WLED 进阶玩法

前面一篇讲了怎么使用 WLED 驱动一个灯条,我们往往可以使用单独的控制器为一两个互相没有关系的物体产生需要的效果。可是有没有一些更为炫酷的做法?我们先从照明的协议 DMX 谈起。

DMX-512

早期的舞台灯光需要同步展示某些效果,于是产生了基于 RS-485 的通信协议 Digital Multiplex(DMX)。谈到 RS-485 可以简单的理解成为长程版的 RS-232(计算机上古早的串口通信通常使用这种接头),在 WLED 的官网上也提到了基于 RS-485 解决数据线较长产生的问题。给个基本的概念,RS-485 连接个 40-50m 的距离是没什么问题的,但是一般我们连接的数据线可能也就 5m 左右,一个好一点的 level shifter(主要是数据大了之后频率较高,I2C 的 level shifter 可能支持不了)。

DMX 当初设计的时候定义了这么几个概念

  • universe:一组灯光的集合,
  • 每一个灯称为一个 channel,一个 universe 里面最多 512 个 channel

那么我们控制灯光就只需要发送 universe X 后面跟一串 bytes,一个 byte 0-255 可以表示该灯的亮度。是不是完美?DMX-512 使用的 XLR 5 pin 的接头,是不是跟专业 mic 线(一般是 3 pin)一路的?

到了网络时代,DMX-512 并没有被摒弃,而是依托于现在的网络提出了 sACN(streaming ACN,Architect for Control Networks),我们可以在 LAN 上(包括有线、无线,甚至有 DMX 转 ethernet 的转接设备)轻松的实现,它使用 UDP 进行通信,可以点播也可以广播。而这也被规范为 ANSI E1.31,所以某些地方看到的是 E1.31 这个名号。

末了,给个 E1.31 的参考文档吧!

WLED 的相关配置

我们可以在 WLED 的 Config -> Sync Interface 中找到,默认情况下打开的就是 sACN / E1.31 同步模式。从这个角度来看 WLED 实现了一个 local 的 DMX 控制器,因此任何使用该协议的程序/控制端都可以根据自己的需求来实现更为复杂的效果、同步关系,等等。我们拿 sound reactive(声控)WLED 这个问题举个例子

  • 我们可以在控制器的片子上增加一个声音的传感器,基于这个的读数的变化,我们就可以计算出来一套效果,这样一来
    • 我们可以节省另一个设备,整合的更好,由于本地获得效果无需传输,延迟也比较低
    • 对控制器芯片的计算能力有要求,比如 ESP8266 就不适合做大量的运算,因而无法实现一些特殊的效果;但是我们也可以使用 ESP-32,比如这个项目就可以在 ESP-32 上通过 FFT 实现更为复杂的效果
    • 如果音源离控制器比较远,可能不容易获得声音信号
  • 我们也可以在另外的设备上获得声音信号,并进行转换,仅仅将转换出来的亮度信息通过 sACN / E1.31 协议传递给 WLED
    • 我们需要额外的设备;但好处是其计算能力不那么受到控制器的限制,并且比较容易扩展到其他的数据源(如视频、图片等等)
    • 需要通过网络通信,有可能受到干扰,还可能产生较大的延迟

当然每个方法都有自己的优缺点,我们需要根据实际情况进行取舍。为了方便外加的控制 WLED 提供了 mapping 功能(详细参考这里),比如说 WS2812b 由于是一根数据线通信,如果我们将其组装成一个矩阵,为了避免额外的走线,我们会从一头走到另一头,然后换一个垂直方向走一步,然后反向(贪吃蛇路径),但是“数据逻辑”比如图片在存储的时候不会考虑到显示设备这边为了节省产生的“格式不匹配”,因此我们可以通过 mapping 关系简化 driver 一侧任务的复杂性。比如,我们拿一个比较常见的 16×16 的面板为例,可以使用如下的映射

{"mapping": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 127, 126, 125, 124, 123, 122, 121, 120, 119, 118, 117, 116, 115, 114, 113, 112, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 159, 158, 157, 156, 155, 154, 153, 152, 151, 150, 149, 148, 147, 146, 145, 144, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 191, 190, 189, 188, 187, 186, 185, 184, 183, 182, 181, 180, 179, 178, 177, 176, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 223, 222, 221, 220, 219, 218, 217, 216, 215, 214, 213, 212, 211, 210, 209, 208, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240]}

将它保存为 ledmap.json 之后我们就可以获得和直觉对应的样式。

控制程序

我们如果需要实现一个简单的控制程序,当然最简单的莫过于 python,其实很多你能在市面上找到的控制程序也许也是用 python 实现的(比如 WLED 上也强烈推荐的 LedFx,提供声控效果,值得注意的是它为 Windows 提供了个安装包而已,其实可以直接用 pip 装一个的,那么 Linux / MacOS 乃至 30$ 左右的 Raspberry Pi 都能用)。

我们这里拿这个 16×16 的面板作为例子。首先我们需要确认对应的控制器相关的配置

网络配置

这里点击下面 E1.31 info 可以看到有几点,但是语焉不详:

  • 每个 universe 最多只能有 512 个 channel,而 WLED 限制每个 universe 里最多 170 颗 WS2812b,这是因为每颗 LED 有 3 个颜色 RGB,这样占用了 510 个 channel(需要 510 bytes)
  • 但是我们知道,一根数据线上的 LED 没必要被这个值(170)限制,比如我们的 16×16 面板,这就有 256 颗 LED,那么按照这个描述,前面的 170 颗就归为 universe 1(Start universe 那行配置),剩余的 86 颗就自动落到 universe 2 里面。如果更大的面板,可能就会引入更多的 universe,比如拿两块 16×16 拼接起来变成 16×32 的面板,那这 2×256=512 颗 LED 就会分到 4 个 universe 里面,尽管第二个 universe 控制的 LED 其实会两个 panel 上各有一部分
  • 这里单根数据线的上限大约是 9 个 universe 1500 颗 LED,这主要是 WLED 的限制,但是如果要流畅的话建议最多 3 个 universe。不过这已经很强了,传统的 universe 不也就对应一个 DMX 控制器吗,这一个 WLED 当三个不香吗?

那我们怎么把数据发出来试一试呢?我们可以使用 sacn 这个库,下面我们给一段 sample code 展示大概的逻辑:我们读取一个目录里面 16×16 的 png icon,一张一张的显示出来

import math
import os
from PIL import Image
import sacn
import time

class Animator:
    def __init__(self, dst=IP):
        self.sender = sacn.sACNsender()
        self.dst = IP
        self.num_universes = 2
        self.data_ranges = [[0, 510], [510, 768]]

    def play(self, frames, loop=False):
        sender = self.sender
        sender.start()
        sender.manual_flush = True
        for i in range(self.num_universes):
            sender.activate_output(i+1)
            sender[i+1].destination = self.dst
        while True:
            for frame in frames:
                for i in range(self.num_universes):
                    r = self.data_ranges[i]
                    sender[i+1].dmx_data = frame[r[0]:r[1]]
                time.sleep(.25) # let the sender initialize itself
                sender.flush()
            if not loop:
                break
        sender.manual_flush = False
        sender.stop()

def get_pngs(directory):
    filenames = os.listdir(directory)
    return [os.path.join(directory, f) for f in filenames if f.endswith('png')]

def get_pixels(path):
    img = Image.open(path)
    if img.size != (16, 16):
        print(f'File at {path} is not 16x16')
        return None
    data = []
    pix = img.load()
    for i in range(16):
        for j in range(16):
            cv = pix[i, j]
            if len(cv) == 3:
                v = cv
            elif len(cv) == 4:
                if cv[3] == 0:
                    v = [0, 0, 0]
                else:
                    v = cv[0:3]
            else:
                print(f'File at {path} might not have 3 or 4 channels')
                return None
            data.extend(v)
    return data

if __name__ == '__main__':
    d = 'images/'
    png_files = get_pngs(d)
    print(f"Found {len(png_files)} in {d}")
    frames = [get_pixels(f) for f in png_files]
    frames = [f for f in frames if f is not None]
    print(f"Read all {len(frames)} png files into memory, start animation ...")
    animator.play(frames)

我们这里直接 hard code 了每帧 16×16 图像需要截成两个 universe 的代码,当然可以弄得更为智能一点,这里纯粹演示一下。pillow 也用的比较一般,轻拍。有了这个基础我们玩其他的程序就会相对来说容易一点。

网上能找到不少类似的程序,我看到的有 jinx!OpenRGB,但是后者实在太 buggy 了,没啥好说的。前者是个 Windows 程序,似乎也不开源,跑起来有点尴尬,特别是某一步(我就后面再吐槽了)

  • 首先需要设置 Setup -> Matrix Options,这里很简单 16×16 设置 width / height 即可
  • 然后是 Setup -> Output Devices,这里我就没搞明白,一开始以为我可以自己定义,比如我当有两块 16×8 不就简单了嘛,实际是你可以这么设,但是 wled 并不屌你,你必须按照 510 这样一个一个的添加,为啥不好使?后面继续说哈,比如第一个设备要 universe 1,channels 510,第二个设备别的都一样但是是 universe 2 channels 258
  • 最后就是 Setup -> Output Patch 了,其实 jinx 可能为了方便大家看,把每个 LED 画了出来,让人在上面填 channel 编号。256 个要手填吗?哦它还提供了个工具 Fast Patch,但是他没说这个玩意其实就是个 rectangular fill,那我 170 个 LED 不是 16×10 还有 10 颗吗,那不是个矩形哎,后面那部分更诡异,后来明白过来你得一小块一小块的填(先填 1×6,再来 5×16),它不会自动的帮你来的

之后就可以开始特效了。不过既然上面的代码都能播放 png 序列了,不能通过别的什么方式先生成图片来搞了吗 =.= 而且高度可定制啊。

效果示意

这里我们用了 adafruit 提供的 3d 建模,可惜散射片自己打印了一个效果不是太好,下次换个颜色跟 pattern。

红外遥控器

有了 WLED,其实已经有了手机、网页、HA 这好几个方式来控制其开关效果,但是其实 WLED 还支持红外遥控器。之前在网上买了条非常便宜的 analog LED strip,它自带了 IR / BT(通过 apollo lighting app 控制),后来觉得没法与 HA 整合换了个 zigbee 的控制器,于是原厂的控制器就闲置了下来。于是撬开安装盒,里面似乎是一颗 KY-022 红外配备了一个 24 键的红外遥控器,把它从原厂的电路板上焊掉,转接到 wemos D1 mini,红黑蓝分别接入 5V/GND/D2,然后在 WLED 里面开启遥控,就这么容易搞定了。

WLED 进阶玩法

一个有关“WLED 进阶玩法”的想法

留下评论