前段时间接了个小项目,需要把一套 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/1/1,值1; - 总线上所有设备收到,只有监听
1/1/1的开关执行器认领; - 执行器闭合继电器,灯亮;
- 执行器再通过另一个组地址(比如
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 -->|"① 广播报文<br/>GA 1/1/1 = 1(开)"| BUSLINE BUSLINE -->|"② 所有设备收到,<br/>仅匹配 GA 的执行器响应"| ACT AC --> ACT ACT -->|"③ 闭合继电器"| LAMP ACT -->|"④ 状态反馈<br/>GA 1/1/2 = 1"| BUSLINE BUSLINE -->|"⑤ 反馈报文"| PANEL
二、.NET 8 下的 KNX 照明控制示例
这部分代码是我实际跑过的,环境是 .NET 8 控制台程序,库用的是 KNX.net。它支持 Routing 和 Tunneling 两种 KNXnet/IP 连接方式,API 不算复杂,够用。
建项目、装包
bash
dotnet new console -n KnxLightingDemo -f net8.0
cd KnxLightingDemo
dotnet add package KNX.net
NuGet 里搜 KNX.net,作者是 lifeemotions 的那个就是。
完整代码
csharp
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:
csharp
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 追求的是确定性、长寿命和跨厂商互通,代价是布线成本和调试门槛。