前段时间接了个小项目,需要把一套 KNX 照明系统接进 .NET 后台做集中控制。原本以为跟普通 IoT 差不多,发个 HTTP 请求就完事,结果发现 KNX 的逻辑跟消费级智能家居完全不是一回事。折腾了几天,把原理和代码都跑通了,顺手记下来,给后面要踩坑的人省点时间。

这篇分两块:前半部分讲 KNX 照明到底怎么运作的,后半部分是 .NET 8 下的可运行示例。不保证面面俱到,但保证是我自己跑通的东西。


一、KNX 照明到底是怎么控制的

先纠正一个我最初的误解:KNX 的开关面板和灯之间没有直接的电线连接。面板只接在 KNX 总线上,灯接在执行器控制的强电回路上,两者靠“组地址”在逻辑上绑在一起。

物理层:一根总线,两套回路

KNX 最常用的是专用双绞线(KNX TP),总线同时传信号和供电,通常是 29V 直流。强电那边是另一套 220V 回路,由执行器里的继电器负责通断。所以布线的时候,KNX 总线可以走弱电桥架,跟强电彻底分开,抗干扰能力比想象中好。

组地址:广播 + 过滤

这是 KNX 最核心的概念,理解了这个,后面就顺了。

每个设备有一个物理地址,格式像 1.1.2,相当于身份证,调试和诊断用,日常运行不参与功能联动。真正决定“谁控制谁”的是组地址(Group Address,GA),一般写成 1/1/1 这种斜杠形式,本质上是个功能标签。

工作方式是广播:你按一下面板,面板往总线上发一条报文,里面带着某个组地址和一个值。总线上所有设备都能收到这条报文,但只有组地址跟它匹配的执行器才会响应,其他的直接忽略。

这就解释了为什么 KNX 系统里中控挂了,底层开关还能用——功能逻辑是设备之间通过组地址直接绑定的,不依赖中央控制器下命令。

传感器、执行器和 ETS

一个KNX照明系统里就三类角色:

  • 传感器:面板、人体感应器、光照度传感器,负责发起控制报文。
  • 执行器:开关执行器、调光执行器,直接控制强电回路,收到匹配的组地址就动作。
  • ETS:KNX 官方的工程配置软件,工程师用它把传感器和执行器绑到同一个组地址上。这个配置是一次性的,配完系统就能独立跑。

报文类型:开关、调光、场景

不同功能用的数据类型不一样:

  • 开关用 1-bit 报文,值 1 是开,0 是关,对应 DPT 1.001。
  • 相对调光用 4-bit 报文,值 1~7 调暗,9~15 调亮,0 停止。
  • 场景用 1 字节报文,数值对应场景编号,多个执行器绑同一个场景组地址就能一键切换氛围。

一次按键的完整流程

以开关一盏灯为例,数据流是这样的:

  1. 按下面板;
  2. 面板向总线广播报文:组地址 1/1/1,值 1;
  3. 总线上所有设备收到,只有监听 1/1/1 的开关执行器认领;
  4. 执行器闭合继电器,灯亮;
  5. 执行器再通过另一个组地址(比如 1/1/2)广播状态反馈,面板指示灯同步点亮。
flowchart LR subgraph CFG["ETS 配置阶段"] ETS["ETS 软件
绑定组地址 GA 1/1/1"] end subgraph BUS["KNX 总线侧(控制信号 + 供电)"] PWR["KNX 电源 + 扼流圈
约 29V DC"] BUSLINE(("KNX 总线")) PANEL["开关面板 / 传感器
物理地址 1.1.2"] ACT["开关执行器
物理地址 1.1.3
监听 GA 1/1/1"] end subgraph POWER["强电侧"] AC["220V 交流强电"] LAMP["灯具"] end ETS -. "配置绑定" .-> PANEL ETS -. "配置绑定" .-> ACT PWR --> BUSLINE PANEL -->|"① 广播报文
GA 1/1/1 = 1(开)"| BUSLINE BUSLINE -->|"② 所有设备收到,
仅匹配 GA 的执行器响应"| ACT AC --> ACT ACT -->|"③ 闭合继电器"| LAMP ACT -->|"④ 状态反馈
GA 1/1/2 = 1"| BUSLINE BUSLINE -->|"⑤ 反馈报文"| PANEL

二、.NET 8 下的 KNX 照明控制示例

这部分代码是我实际跑过的,环境是 .NET 8 控制台程序,库用的是 KNX.net。它支持 Routing 和 Tunneling 两种 KNXnet/IP 连接方式,API 不算复杂,够用。

建项目、装包

dotnet new console -n KnxLightingDemo -f net8.0
cd KnxLightingDemo
dotnet add package KNX.net

NuGet 里搜 KNX.net,作者是 lifeemotions 的那个就是。

完整代码

using KnxLib;

// 这两个组地址要按你 ETS 项目里的实际配置改
const string SwitchGroupAddress = "1/1/1";   // 开关控制
const string StatusGroupAddress = "1/1/2";   // 状态反馈

// Routing 模式,走 KNXnet/IP 多播
// 如果网关不支持 Routing,换成 KnxConnectionTunneling,下面会讲
var connection = new KnxConnectionRouting();

connection.KnxEventDelegate += OnKnxEvent;

Console.WriteLine("正在连接 KNX 总线...");
connection.Connect();
Console.WriteLine("连接成功。输入 on / off 控制灯具,输入 status 读取状态,输入 q 退出。");

// 启动时主动读一次当前状态
connection.RequestStatus(StatusGroupAddress);

while (true)
{
    Console.Write("> ");
    var input = Console.ReadLine()?.Trim().ToLower();

    if (input == "q") break;

    switch (input)
    {
        case "on":
            connection.Action(SwitchGroupAddress, true);
            Console.WriteLine($"已发送 ON 到 {SwitchGroupAddress}");
            break;

        case "off":
            connection.Action(SwitchGroupAddress, false);
            Console.WriteLine($"已发送 OFF 到 {SwitchGroupAddress}");
            break;

        case "status":
            connection.RequestStatus(StatusGroupAddress);
            Console.WriteLine($"已请求 {StatusGroupAddress} 的当前状态");
            break;

        default:
            Console.WriteLine("可用命令: on / off / status / q");
            break;
    }
}

connection.Disconnect();

// 事件处理:总线上收到的报文都会进这里
static void OnKnxEvent(string address, string state)
{
    if (address != "1/1/2") return;

    var isOn = ParseSwitchState(state);
    Console.WriteLine($"\n[状态反馈] {address} = {(isOn ? "ON" : "OFF")}  (原始值: {state})");
}

// 这里是个坑,下面单独说
static bool ParseSwitchState(string raw)
{
    if (string.IsNullOrWhiteSpace(raw)) return false;

    if (raw == "1" || raw.Equals("true", StringComparison.OrdinalIgnoreCase)
        || raw.Equals("on", StringComparison.OrdinalIgnoreCase))
        return true;

    return false;
}

几个实际调试时碰到的问题

1. Routing 还是 Tunneling,得看网关。

示例用的是 KnxConnectionRouting,多播方式。如果你的网关不支持 Routing,或者网络环境不允许多播,就得换 Tunneling:

var connection = new KnxConnectionTunneling(
    remoteIP: "192.168.1.100",   // KNX IP 接口地址
    remotePort: 3671,
    localIP: "192.168.1.50",     // 本机 IP
    localPort: 3671              // 本机监听端口
);

KNXnet/IP 标准端口是 3671。Tunneling 模式下本机的 localPort 需要对网关可达,Windows 防火墙记得放行入站,不然连上了也收不到事件。

2. 事件里的 state 格式不一定是你想要的。

KNX.net 的 KnxEventDelegate 给出来的 state 是字符串,不同版本或不同 DPT 下格式可能不一样,可能是 "1"、"True" 或者别的。我一开始直接用 bool.Parse,偶尔就炸了。所以代码里写了个 ParseSwitchState 做兼容。建议你第一次跑的时候先把原始 state 打出来看一眼,别急着解析。

3. RequestStatus 是主动读,不是等推送。

启动时和输入 status 时调 RequestStatus,它会往总线发 GroupValueRead,执行器收到后回 GroupValueResponse。如果不调这个,就只能等状态变化时被动收到报文。实际项目里建议在初始化阶段把关心的状态都读一遍,不然界面上的初始状态是空的。

4. 组地址和 DPT 要对上。

ETS 里确认一下:开关控制组地址用 DPT 1.001,状态反馈一般也是 1.001(或者 1.002)。如果反馈的 DPT 不是这个,解析逻辑就得改。KNX.net 本身有 FromDataPoint 之类的方法,但事件里拿到的字符串格式不太稳定,我最后还是用了显式字符串判断,简单粗暴但不容易出幺蛾子。

5. 控制台程序没有 UI 线程问题,但换 WinForms/WPF 要注意。

KNX.net 的事件回调不在 UI 线程上,如果要在界面上更新状态,必须 Control.Invoke 或 Dispatcher.Invoke 切回去,否则跨线程异常。这个坑跟 KNX 无关,但顺手提一句。


最后

KNX 这套东西,刚接触会觉得组地址、物理地址、ETS 一堆概念绕来绕去,但把“广播 + 组地址过滤”这个核心想明白之后,剩下的就是查 DPT 和调网络了。它跟消费级无线方案不是一个思路:KNX 追求的是确定性、长寿命和跨厂商互通,代价是布线成本和调试门槛。


原文地址: https://www.cveoy.top/t/topic/qHQ5 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录