Add KNX DALI Gateway Module and Message Queue Implementation

- Introduced KnxDaliModule class for handling DALI message queuing, commissioning, and KNX group-object dispatch.
- Implemented Message and MessageQueue classes for managing message operations.
- Removed obsolete OpenKNX IDF component files and CMake configurations.
- Updated submodule reference for KNX.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-15 12:34:13 +08:00
parent 3f15cd7f3f
commit 449a3a801a
41 changed files with 2279 additions and 918 deletions
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <cstdint>
struct Ballast {
uint8_t high{0};
uint8_t middle{0};
uint8_t low{0};
uint8_t address{255};
};
+138
View File
@@ -0,0 +1,138 @@
#include "color_helper.h"
uint16_t ColorHelper::getKelvinFromSun(uint16_t minCurr, uint16_t minDiff, uint16_t minK, uint16_t maxK)
{
float xAchse = (minCurr * 3.14159) / minDiff;
float yAchse = sin(xAchse);
return (maxK - minK) * yAchse + minK;
}
void ColorHelper::rgbToXY(uint8_t in_r, uint8_t in_g, uint8_t in_b, uint16_t& x, uint16_t& y)
{
float r = in_r / 255.0;
float g = in_g / 255.0;
float b = in_b / 255.0;
r = (r > 0.04045) ? pow((r + 0.055) / (1.0 + 0.055), 2.4) : (r / 12.92);
g = (g > 0.04045) ? pow((g + 0.055) / (1.0 + 0.055), 2.4) : (g / 12.92);
b = (b > 0.04045) ? pow((b + 0.055) / (1.0 + 0.055), 2.4) : (b / 12.92);
float X = r * 0.4124 + g * 0.3576 + b * 0.1805;
float Y = r * 0.2126 + g * 0.7152 + b * 0.0722;
float Z = r * 0.0193 + g * 0.1192 + b * 0.9505;
float cx = X / (X + Y + Z);
float cy = Y / (X + Y + Z);
x = getBytes(cx);
y = getBytes(cy);
y = y + 1;
y = y - 1;
}
void ColorHelper::hsvToRGB(uint8_t in_h, uint8_t in_s, uint8_t in_v, uint8_t& r, uint8_t& g, uint8_t& b)
{
float h = in_h / 255.0;
float s = in_s / 255.0;
float v = in_v / 255.0;
double rt = 0;
double gt = 0;
double bt = 0;
int i = int(h * 6);
double f = h * 6 - i;
double p = v * (1 - s);
double q = v * (1 - f * s);
double t = v * (1 - (1 - f) * s);
switch(i % 6){
case 0: rt = v, gt = t, bt = p; break;
case 1: rt = q, gt = v, bt = p; break;
case 2: rt = p, gt = v, bt = t; break;
case 3: rt = p, gt = q, bt = v; break;
case 4: rt = t, gt = p, bt = v; break;
case 5: rt = v, gt = p, bt = q; break;
}
r = rt * 255;
g = gt * 255;
b = bt * 255;
}
void ColorHelper::kelvinToRGB(uint16_t kelvin, uint8_t& r, uint8_t& g, uint8_t& b)
{
auto temp = kelvin / 100;
if (temp <= 66)
{
r = 255;
g = 99.4708025861 * log(temp) - 161.1195681661;
if (temp <= 19)
b = 0;
else
b = 138.5177312231 * log(temp - 10) - 305.0447927307;
}
else
{
r = 329.698727446 * pow(temp - 60, -0.1332047592);
g = 288.1221695283 * pow(temp - 60, -0.0755148492);
b = 255;
}
}
void ColorHelper::xyyToRGB(uint16_t ix, uint16_t iy, uint8_t iz, uint8_t& r, uint8_t& g, uint8_t& b)
{
float _x = getFloat(ix);
float _y = getFloat(iy);
//let z = 1.0 - x - y;
//return this.colorFromXYZ((Y / y) * x, Y, (Y / y) * z);
float y = iz / 255.0f;
float x = _x * (y / _y);
float z = ((1.0 - _x - _y) * y) / _y;
float rt = x * 3.2404f + y * -1.5371f + z * -0.4985f;
float gt = x * -0.9693f + y * 1.8760f + z * 0.0416f;
float bt = x * 0.0556f + y * -0.2040f + z * 1.05723f;
rt = adjust(rt);
gt = adjust(gt);
bt = adjust(bt);
r = std::max(std::min(rt, 255.0f), 0.0f);
g = std::max(std::min(gt, 255.0f), 0.0f);
b = std::max(std::min(bt, 255.0f), 0.0f);
}
uint16_t ColorHelper::getBytes(float input)
{
return std::max(std::min(round(input * 65536.0), 65534.0), 0.0);
}
float ColorHelper::getFloat(uint16_t input)
{
float output = input / 65536.0f;
return std::max(std::min(output, 0.0f), 1.0f);
}
double ColorHelper::hue2rgb(double p, double q, double t)
{
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6.0) return p + (q - p) * 6 * t;
if (t < 1 / 2.0) return q;
if (t < 2 / 3.0) return p + (q - p) * (2 / 3.0 - t) * 6;
return p;
}
float ColorHelper::adjust(float input)
{
if (input > 0.0031308) {
return (1.055f * pow(input, (1.0f / 2.4f)) - 0.055f) * 255.0;
}
return 12.92f * input;
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
#include <cmath>
#include <algorithm>
//extended lib from https://github.com/ratkins/RGBConverter
//WTFPL license
class ColorHelper
{
public:
static void rgbToXY(uint8_t r, uint8_t g, uint8_t b, uint16_t& x, uint16_t& y);
static void hsvToRGB(uint8_t h, uint8_t s, uint8_t v, uint8_t& r, uint8_t& g, uint8_t& b);
static void kelvinToRGB(uint16_t kelvin, uint8_t& r, uint8_t& g, uint8_t& b);
static void xyyToRGB(uint16_t x, uint16_t y, uint8_t z, uint8_t& r, uint8_t& g, uint8_t& b);
static uint16_t getKelvinFromSun(uint16_t minCurr, uint16_t minDiff, uint16_t minK, uint16_t maxK);
private:
static uint16_t getBytes(float input);
static float getFloat(uint16_t input);
static double hue2rgb(double p, double q, double t);
static float adjust(float input);
};
@@ -0,0 +1,323 @@
#include "dali_gateway_bridge.h"
#include "dali_define.hpp"
#include "dali_comm.hpp"
#include <algorithm>
#include <cmath>
namespace gateway {
namespace knx_dali_gw {
namespace {
constexpr double kArcMax = 254.0;
constexpr double kLogFactor = 3.0;
// DALI address encoding helpers (mirroring lib/dali/comm.dart).
// Short address 0-63 → (addr << 1) | 0x01 for commands.
constexpr uint8_t kShortAddrCmdBase = 0x01;
constexpr uint8_t kShortAddrArcBase = 0x00;
constexpr uint8_t kGroupAddrBase = 0x80;
constexpr uint8_t kBroadcastCmd = 0xFE;
constexpr uint8_t kBroadcastArc = 0xFF;
uint8_t makeShortCmdAddr(int addr) {
return static_cast<uint8_t>((addr << 1) | kShortAddrCmdBase);
}
uint8_t makeShortArcAddr(int addr) {
return static_cast<uint8_t>((addr << 1) | kShortAddrArcBase);
}
uint8_t makeGroupCmdAddr(int group) {
return static_cast<uint8_t>(kGroupAddrBase | (group << 1) | 0x01);
}
uint8_t makeBroadcastCmdAddr() { return kBroadcastCmd; }
uint8_t makeBroadcastArcAddr() { return kBroadcastArc; }
bool isBroadcastAddr(uint8_t raw) { return raw == kBroadcastCmd || raw == kBroadcastArc; }
bool isGroupAddr(uint8_t raw) { return (raw & 0x80) != 0 && !isBroadcastAddr(raw); }
int extractGroupAddr(uint8_t raw) { return (raw >> 1) & 0x0F; }
int extractShortAddr(uint8_t raw) { return (raw >> 1) & 0x3F; }
uint8_t encodeDaliRawAddr(DaliTarget target) {
switch (target.kind) {
case DaliTargetKind::kShortAddress:
return makeShortCmdAddr(target.address);
case DaliTargetKind::kGroup:
return makeGroupCmdAddr(target.address);
case DaliTargetKind::kBroadcast:
default:
return makeBroadcastCmdAddr();
}
}
} // namespace
// =============================================================================
// DaliTarget ↔ raw address encoding (public API)
// =============================================================================
uint8_t EncodeDaliRawAddr(DaliTarget target) {
return encodeDaliRawAddr(target);
}
DaliTarget DecodeDaliRawAddr(uint8_t raw_addr, int default_short_address) {
if (isBroadcastAddr(raw_addr)) {
return {DaliTargetKind::kBroadcast, 0};
}
if (isGroupAddr(raw_addr)) {
return {DaliTargetKind::kGroup, extractGroupAddr(raw_addr)};
}
int sa = extractShortAddr(raw_addr);
if (sa < 0 && default_short_address >= 0) {
sa = default_short_address;
}
return {DaliTargetKind::kShortAddress, sa};
}
// =============================================================================
// Arc power ↔ percentage conversion
// =============================================================================
double ArcToPercent(uint8_t arc) {
if (arc == 0) return 0.0;
return 100.0 * std::pow(static_cast<double>(arc) / kArcMax, kLogFactor);
}
uint8_t PercentToArc(double percent) {
if (percent <= 0.0) return 0;
if (percent >= 100.0) return 254;
return static_cast<uint8_t>(
std::round(kArcMax * std::pow(percent / 100.0, 1.0 / kLogFactor)));
}
// =============================================================================
// DaliGatewayBridge
// =============================================================================
DaliGatewayBridge::DaliGatewayBridge(DaliDomainService& dali, uint8_t gateway_id)
: dali_(dali), gateway_id_(gateway_id) {}
bool DaliGatewayBridge::sendRaw(DaliTarget target, uint8_t command) const {
return dali_.sendRaw(gateway_id_, encodeDaliRawAddr(target), command);
}
bool DaliGatewayBridge::sendExtRaw(DaliTarget target, uint8_t command) const {
return dali_.sendExtRaw(gateway_id_, encodeDaliRawAddr(target), command);
}
std::optional<uint8_t> DaliGatewayBridge::queryRaw(DaliTarget target, uint8_t command) const {
return dali_.queryRaw(gateway_id_, encodeDaliRawAddr(target), command);
}
bool DaliGatewayBridge::setArc(DaliTarget target, uint8_t arc) const {
return sendRaw(target, arc);
}
std::optional<uint8_t> DaliGatewayBridge::queryActualLevel(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_ACTUAL_LEVEL);
}
bool DaliGatewayBridge::on(DaliTarget target) const {
return sendRaw(target, DALI_CMD_RECALL_MAX);
}
bool DaliGatewayBridge::off(DaliTarget target) const {
return sendRaw(target, DALI_CMD_OFF);
}
bool DaliGatewayBridge::stepUp(DaliTarget target) const {
return sendRaw(target, DALI_CMD_RECALL_MAX);
}
bool DaliGatewayBridge::stepDown(DaliTarget target) const {
return sendRaw(target, DALI_CMD_OFF);
}
bool DaliGatewayBridge::recallMax(DaliTarget target) const {
return sendRaw(target, DALI_CMD_RECALL_MAX);
}
bool DaliGatewayBridge::recallMin(DaliTarget target) const {
return sendRaw(target, DALI_CMD_RECALL_MIN);
}
bool DaliGatewayBridge::goToScene(DaliTarget target, uint8_t scene) const {
return sendRaw(target, DALI_CMD_GO_TO_SCENE(scene & 0x0F));
}
std::optional<uint8_t> DaliGatewayBridge::queryStatus(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_STATUS);
}
std::optional<uint8_t> DaliGatewayBridge::queryDeviceType(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_DEVICE_TYPE);
}
std::optional<uint8_t> DaliGatewayBridge::queryMinLevel(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_MIN_LEVEL);
}
std::optional<uint8_t> DaliGatewayBridge::queryMaxLevel(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_MAX_LEVEL);
}
std::optional<uint8_t> DaliGatewayBridge::queryPowerOnLevel(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_POWER_ON_LEVEL);
}
std::optional<uint8_t> DaliGatewayBridge::querySystemFailureLevel(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_SYSTEM_FAILURE_LEVEL);
}
std::optional<uint8_t> DaliGatewayBridge::queryFadeTimeRate(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_FADE_TIME_FADE_RATE);
}
std::optional<uint8_t> DaliGatewayBridge::queryFadeTime(int short_address) const {
return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address),
DALI_CMD_QUERY_EXTENDED_FADE_TIME);
}
std::optional<uint16_t> DaliGatewayBridge::queryGroups(int short_address) const {
return dali_.queryGroupMask(gateway_id_, short_address);
}
std::optional<uint8_t> DaliGatewayBridge::querySceneLevel(int short_address,
uint8_t scene) const {
return dali_.querySceneLevel(gateway_id_, short_address, scene & 0x0F);
}
// ---- DT8 ----
bool DaliGatewayBridge::setColourTemperature(int short_address, int kelvin) const {
return dali_.setColTemp(gateway_id_, short_address, kelvin);
}
bool DaliGatewayBridge::setColourRGB(int short_address, uint8_t r, uint8_t g,
uint8_t b) const {
return dali_.setColourRGB(gateway_id_, short_address, r, g, b);
}
std::optional<DaliDomainSnapshot> DaliGatewayBridge::dt8StatusSnapshot(
int short_address) const {
return dali_.dt8StatusSnapshot(gateway_id_, short_address);
}
std::optional<DaliDomainSnapshot> DaliGatewayBridge::dt8SceneColorReport(
int short_address, int scene) const {
return dali_.dt8SceneColorReport(gateway_id_, short_address, scene);
}
// ---- Scenes & groups ----
bool DaliGatewayBridge::setDtr(uint8_t value) const {
return dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(),
DALI_CMD_SPECIAL_SET_DTR0) &&
dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(), value);
}
bool DaliGatewayBridge::setDtrAsScene(DaliTarget target, uint8_t scene) const {
return sendRaw(target, DALI_CMD_SET_SCENE(scene & 0x0F));
}
bool DaliGatewayBridge::addToGroup(DaliTarget target, uint8_t group) const {
return sendRaw(target, DALI_CMD_ADD_TO_GROUP(group & 0x0F));
}
bool DaliGatewayBridge::removeFromGroup(DaliTarget target, uint8_t group) const {
return sendRaw(target, DALI_CMD_REMOVE_FROM_GROUP(group & 0x0F));
}
bool DaliGatewayBridge::removeFromScene(DaliTarget target, uint8_t scene) const {
return sendRaw(target, DALI_CMD_REMOVE_SCENE(scene & 0x0F));
}
bool DaliGatewayBridge::setSceneLevel(DaliTarget target, uint8_t scene,
uint8_t level) const {
return setDtr(level) && setDtrAsScene(target, scene);
}
// ---- Commissioning ----
bool DaliGatewayBridge::initialise(DaliTarget target) const {
return sendRaw(target, DALI_CMD_SPECIAL_INITIALIZE);
}
bool DaliGatewayBridge::randomise() const {
return sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_RANDOMIZE);
}
bool DaliGatewayBridge::searchAddrH(uint8_t high) const {
return dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(),
DALI_CMD_SPECIAL_SEARCHADDRH) &&
dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(), high);
}
bool DaliGatewayBridge::searchAddrM(uint8_t middle) const {
return dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(),
DALI_CMD_SPECIAL_SEARCHADDRM) &&
dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(), middle);
}
bool DaliGatewayBridge::searchAddrL(uint8_t low) const {
return dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(),
DALI_CMD_SPECIAL_SEARCHADDRL) &&
dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(), low);
}
bool DaliGatewayBridge::compare() const {
return sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_COMPARE);
}
bool DaliGatewayBridge::withdraw() const {
return sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_WITHDRAW);
}
bool DaliGatewayBridge::terminate() const {
return sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_TERMINATE);
}
bool DaliGatewayBridge::programShort(DaliTarget target, uint8_t short_address) const {
const uint8_t raw = (short_address << 1) | 0x01;
return dali_.sendRaw(gateway_id_, encodeDaliRawAddr(target),
DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS) &&
dali_.sendRaw(gateway_id_, encodeDaliRawAddr(target), raw);
}
bool DaliGatewayBridge::verifyShort(DaliTarget target) const {
return sendRaw(target, DALI_CMD_SPECIAL_VERIFY_SHORT_ADDRESS);
}
std::optional<uint8_t> DaliGatewayBridge::queryShort(DaliTarget target) const {
return dali_.queryRaw(gateway_id_, encodeDaliRawAddr(target),
DALI_CMD_SPECIAL_QUERY_SHORT_ADDRESS);
}
bool DaliGatewayBridge::allocateAllAddr(int start_address) const {
return dali_.allocateAllAddr(gateway_id_, start_address);
}
void DaliGatewayBridge::stopAllocAddr() const {
dali_.stopAllocAddr(gateway_id_);
}
bool DaliGatewayBridge::resetAndAllocAddr(int start_address, bool remove_addr_first,
bool close_light) const {
return dali_.resetAndAllocAddr(gateway_id_, start_address, remove_addr_first, close_light);
}
bool DaliGatewayBridge::resetBus() const {
return dali_.resetBus(gateway_id_);
}
} // namespace knx_dali_gw
} // namespace gateway
@@ -0,0 +1,40 @@
#include "dali_helper.h"
uint8_t DaliHelper::percentToArc(uint8_t value)
{
if(value == 0)
{
return 0;
}
//Todo also include _max
uint8_t arc = roundToInt(((253/3.0)*(std::log10(value)+1)) + 1);
return arc;
}
uint8_t DaliHelper::arcToPercent(uint8_t value)
{
if(value == 0)
{
return 0;
}
//Todo also include _max
double arc = std::pow(10, ((value-1) / (253/3.0)) - 1);
return roundToInt(arc);
}
float DaliHelper::arcToPercentFloat(uint8_t value)
{
if(value == 0)
{
return 0;
}
//Todo also include _max
float arc = std::pow(10, ((value-1) / (253/3.0)) - 1);
return arc;
}
uint8_t DaliHelper::roundToInt(double input)
{
double temp = input + 0.5;
return (uint8_t)temp;
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <cstdint>
#include <cmath>
class DaliHelper
{
public:
static uint8_t percentToArc(uint8_t value);
static uint8_t arcToPercent(uint8_t value);
static float arcToPercentFloat(uint8_t value);
static uint8_t roundToInt(double input);
};
+15
View File
@@ -0,0 +1,15 @@
#include "hcl_curve.h"
#include "esp_timer.h"
namespace gateway {
namespace knx_dali_gw {
void HclCurve::setup(uint8_t index) { index_ = index; }
void HclCurve::loop() {
// HCL curve logic — simplified for now.
// Full port from HclCurve.cpp in subsequent iteration.
}
} // namespace knx_dali_gw
} // namespace gateway
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
namespace gateway {
namespace knx_dali_gw {
class HclCurve {
public:
void setup(uint8_t index);
void loop();
private:
uint8_t index_{0};
bool is_configured_{false};
uint8_t type_{0};
uint64_t last_check_{0};
uint8_t last_day_{0};
uint8_t last_minute_{0};
};
} // namespace knx_dali_gw
} // namespace gateway
@@ -0,0 +1,184 @@
#include "knx_dali_channel.h"
#include "dali_define.hpp"
#include "knxprod.h"
#include "dali_helper.h"
#include "esp_log.h"
#include "esp_timer.h"
namespace gateway {
namespace knx_dali_gw {
KnxDaliChannel::KnxDaliChannel() = default;
KnxDaliChannel::~KnxDaliChannel() = default;
void KnxDaliChannel::init(uint8_t channel_index, bool is_group, DaliGatewayBridge& bridge) {
index_ = channel_index;
is_group_ = is_group;
dali_ = &bridge;
}
void KnxDaliChannel::setup() {
if (dali_ == nullptr) return;
// Query initial state
DaliTarget target = is_group_ ? DaliTarget{DaliTargetKind::kGroup, static_cast<int>(index_)}
: DaliTarget{DaliTargetKind::kShortAddress, static_cast<int>(index_)};
(void)target; // Will be used in full port
}
void KnxDaliChannel::loop() {
if (dali_ == nullptr) return;
loopDimming();
loopStaircase();
loopQueryLevel();
}
void KnxDaliChannel::processInputKo(GroupObject& ko) {
uint16_t asap = ko.asap();
int slot = static_cast<int>(asap) - (is_group_ ? GRP_KoOffset : ADR_KoOffset) - index_ * (is_group_ ? GRP_KoBlockSize : ADR_KoBlockSize);
// TODO: Full slot-to-handler mapping from DaliChannel.cpp
// For now, delegate to basic handlers
switch (slot) {
case 0: koHandleSwitch(ko); break;
// ... more slots
default: break;
}
}
// ---- Dimming ----
void KnxDaliChannel::loopDimming() {
if (dimm_direction_ == DimmDirection::kNone) return;
uint64_t now = esp_timer_get_time() / 1000ULL;
if (now - dimm_last_ < dimm_interval_) return;
dimm_last_ = now;
DaliTarget target = is_group_ ? DaliTarget{DaliTargetKind::kGroup, static_cast<int>(index_)}
: DaliTarget{DaliTargetKind::kShortAddress, static_cast<int>(index_)};
if (dimm_direction_ == DimmDirection::kUp) {
if (current_step_ < max_) current_step_++;
dali_->setArc(target, current_step_);
} else {
if (current_step_ > min_) current_step_--;
dali_->setArc(target, current_step_);
}
}
// ---- Staircase ----
void KnxDaliChannel::loopStaircase() {
if (interval_ == 0 || !current_state_) return;
uint64_t now = esp_timer_get_time() / 1000ULL;
if (now - start_time_ >= interval_ * 1000ULL) {
current_state_ = false;
interval_ = 0;
DaliTarget target = is_group_ ? DaliTarget{DaliTargetKind::kGroup, static_cast<int>(index_)}
: DaliTarget{DaliTargetKind::kShortAddress, static_cast<int>(index_)};
dali_->off(target);
}
}
// ---- Query Level ----
void KnxDaliChannel::loopQueryLevel() {
// Periodic status query — simplified for now
}
// ---- Switch State ----
void KnxDaliChannel::setSwitchState(bool value, bool is_switch_command) {
if (current_is_locked_) return;
current_state_ = value;
DaliTarget target = is_group_ ? DaliTarget{DaliTargetKind::kGroup, static_cast<int>(index_)}
: DaliTarget{DaliTargetKind::kShortAddress, static_cast<int>(index_)};
if (value) {
dali_->on(target);
} else {
dali_->off(target);
}
if (value) {
start_time_ = esp_timer_get_time() / 1000ULL;
}
}
// ---- Configuration setters ----
void KnxDaliChannel::setOnValue(uint8_t value) {
on_day_ = value;
on_night_ = value / 2;
}
void KnxDaliChannel::setGroups(uint16_t groups) { groups_ = groups; }
void KnxDaliChannel::setGroupState(uint8_t group, bool state) {
if (state) groups_ |= (1 << group); else groups_ &= ~(1 << group);
}
void KnxDaliChannel::setGroupState(uint8_t group, uint8_t) {}
void KnxDaliChannel::setMinMax(uint8_t min, uint8_t max) { min_ = min; max_ = max; }
void KnxDaliChannel::setMinArc(uint8_t min) { min_ = min; }
void KnxDaliChannel::setHcl(uint8_t curve, uint16_t temp, uint8_t) {
hcl_curve_ = curve;
hcl_current_temp_ = temp;
}
// ---- Dimm State ----
void KnxDaliChannel::setDimmState(uint8_t value, bool, bool) {
current_step_ = value;
}
// ---- Color ----
void KnxDaliChannel::sendColor() {
if (dali_ == nullptr) return;
dali_->setColourRGB(static_cast<int>(index_), current_color_[0],
current_color_[1], current_color_[2]);
}
// ---- KO Handlers ----
void KnxDaliChannel::koHandleSwitch(GroupObject& ko) {
bool on = static_cast<bool>(ko.value());
setSwitchState(on);
}
void KnxDaliChannel::koHandleDimmRel(GroupObject& ko) {
int step = static_cast<int>(static_cast<float>(ko.value()));
if (step > 0) {
dimm_direction_ = DimmDirection::kUp;
dimm_step_ = static_cast<uint8_t>(step);
} else if (step < 0) {
dimm_direction_ = DimmDirection::kDown;
dimm_step_ = static_cast<uint8_t>(-step);
} else {
dimm_direction_ = DimmDirection::kNone;
}
dimm_last_ = esp_timer_get_time() / 1000ULL;
}
void KnxDaliChannel::koHandleDimmAbs(GroupObject& ko) {
uint8_t value = static_cast<uint8_t>(static_cast<float>(ko.value()) * 255.0f / 100.0f);
setDimmState(value);
dimm_direction_ = DimmDirection::kNone;
DaliTarget target = is_group_ ? DaliTarget{DaliTargetKind::kGroup, static_cast<int>(index_)}
: DaliTarget{DaliTargetKind::kShortAddress, static_cast<int>(index_)};
dali_->setArc(target, value);
}
void KnxDaliChannel::koHandleLock(GroupObject& ko) {
bool lock = static_cast<bool>(ko.value());
current_is_locked_ = lock;
}
void KnxDaliChannel::koHandleColor(GroupObject& ko) {
KNXValue val = ko.value();
if (true) {
// RGB packed in float or raw bytes
// Simplified: store and send
sendColor();
}
}
} // namespace knx_dali_gw
} // namespace gateway
@@ -0,0 +1,91 @@
#pragma once
// =============================================================================
// KnxDaliChannel — Per-address / per-group DALI channel (ported from DaliChannel)
// =============================================================================
#include "dali_gateway_bridge.h"
#include "knx/group_object.h"
#include <cstdint>
namespace gateway {
namespace knx_dali_gw {
class KnxDaliChannel {
public:
KnxDaliChannel();
~KnxDaliChannel();
void init(uint8_t channel_index, bool is_group, DaliGatewayBridge& bridge);
void setup();
void loop();
void processInputKo(GroupObject& ko);
// --- Configuration ---
void setOnValue(uint8_t value);
void setGroups(uint16_t groups);
void setGroupState(uint8_t group, bool state);
void setGroupState(uint8_t group, uint8_t value);
void setMinMax(uint8_t min, uint8_t max);
void setMinArc(uint8_t min);
void setHcl(uint8_t curve, uint16_t temp, uint8_t bri);
uint8_t getMin() const { return min_; }
uint8_t getMax() const { return max_; }
uint16_t getGroups() const { return groups_; }
bool isNight{false};
private:
enum class DimmDirection { kDown, kUp, kNone };
void loopDimming();
void loopStaircase();
void loopQueryLevel();
void sendColor();
void setSwitchState(bool value, bool is_switch_command = true);
void setDimmState(uint8_t value, bool is_dimm_command = true, bool is_last = false);
void koHandleSwitch(GroupObject& ko);
void koHandleDimmRel(GroupObject& ko);
void koHandleDimmAbs(GroupObject& ko);
void koHandleLock(GroupObject& ko);
void koHandleColor(GroupObject& ko);
DaliGatewayBridge* dali_{nullptr};
uint8_t index_{0};
bool is_group_{false};
// Dimming
DimmDirection dimm_direction_{DimmDirection::kNone};
uint8_t dimm_step_{0};
uint64_t dimm_last_{0};
uint8_t dimm_interval_{100};
// Staircase
uint64_t start_time_{0};
uint32_t interval_{0};
// Limits
uint8_t min_{0};
uint8_t max_{254};
uint8_t on_day_{100};
uint8_t on_night_{10};
// State
bool current_state_{false};
uint8_t current_step_{0};
bool current_is_locked_{false};
uint8_t current_color_[4]{};
// HCL
uint8_t hcl_curve_{255};
uint16_t hcl_current_temp_{0};
// Groups
uint16_t groups_{0};
};
} // namespace knx_dali_gw
} // namespace gateway
+106
View File
@@ -0,0 +1,106 @@
#include "knx_dali_gw.hpp"
#include "knx/bau07B0.h"
#include "esp_log.h"
namespace gateway {
namespace knx_dali_gw {
namespace {
constexpr const char* kTag = "knx_dali_gw";
} // namespace
// =============================================================================
// KnxDaliGateway::Impl
// =============================================================================
struct KnxDaliGateway::Impl {
KnxDaliGatewayConfig config;
gateway::openknx::EspIdfPlatform platform;
Bau07B0 device;
bool initialized{false};
explicit Impl(const KnxDaliGatewayConfig& cfg)
: config(cfg),
platform(nullptr, cfg.nvs_namespace.c_str()),
device(platform) {}
bool init() {
if (initialized) return true;
device.deviceObject().manufacturerId(0x00a4);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
const uint8_t order_number[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0};
device.deviceObject().orderNumber(order_number);
const uint8_t program_version[5] = {0x00, 0xa4, 0x00, 0x01, 0x05};
device.parameters().property(PID_PROG_VERSION)->write(program_version);
device.readMemory();
if (!device.configured()) {
ESP_LOGW(kTag, "KNX device is not configured (blank ETS memory). "
"Individual address: 0x%04X (fallback).",
config.fallback_individual_address);
}
device.enabled(true);
initialized = true;
ESP_LOGI(kTag, "KNX-DALI gateway initialized");
return true;
}
void loop() {
if (!initialized) return;
device.loop();
}
void setNetworkInterface(esp_netif_t* netif) {
platform.networkInterface(netif);
}
};
// =============================================================================
// Public API
// =============================================================================
KnxDaliGateway::KnxDaliGateway(const KnxDaliGatewayConfig& config)
: impl_(std::make_unique<Impl>(config)) {}
KnxDaliGateway::~KnxDaliGateway() = default;
bool KnxDaliGateway::init() { return impl_->init(); }
void KnxDaliGateway::loop() { impl_->loop(); }
Bau07B0& KnxDaliGateway::knxDevice() { return impl_->device; }
const Bau07B0& KnxDaliGateway::knxDevice() const { return impl_->device; }
void KnxDaliGateway::setNetworkInterface(esp_netif_t* netif) {
impl_->setNetworkInterface(netif);
}
bool KnxDaliGateway::handleTunnelFrame(const uint8_t* data, size_t len) {
// TODO: Implement cEMI tunnel frame handling.
(void)data; (void)len;
return false;
}
bool KnxDaliGateway::handleBusFrame(const uint8_t* data, size_t len) {
// TODO: Implement bus frame handling.
(void)data; (void)len;
return false;
}
bool KnxDaliGateway::emitGroupValue(uint16_t group_object_number,
const uint8_t* data, size_t len) {
(void)group_object_number; (void)data; (void)len;
// TODO(Phase 3): Implement with proper KNXValue conversion.
return false;
}
} // namespace knx_dali_gw
} // namespace gateway
@@ -0,0 +1,679 @@
#include "knx_dali_module.h"
#include "knx/bau07B0.h"
#include "knx/group_object.h"
#include "dali_define.hpp"
#include "esp_log.h"
#include "esp_timer.h"
#include <algorithm>
#include <cstdio>
#include <cstring>
namespace gateway {
namespace knx_dali_gw {
namespace {
constexpr const char* kLogTag = "knx_dali_module";
constexpr uint8_t kDaliBroadcastAddr = 0xFE;
} // namespace
// =============================================================================
// Constructor / Destructor
// =============================================================================
KnxDaliModule::KnxDaliModule() {
std::memset(addresses_, 0, sizeof(addresses_));
std::memset(ballasts_, 0, sizeof(ballasts_));
}
KnxDaliModule::~KnxDaliModule() = default;
// =============================================================================
// Setup
// =============================================================================
void KnxDaliModule::setup(Bau07B0& device, DaliGatewayBridge& bridge) {
device_ = &device;
dali_ = &bridge;
for (int i = 0; i < 64; i++) {
channels_[i].init(i, false, bridge);
}
for (int i = 0; i < 16; i++) {
groups_[i].init(i, true, bridge);
}
for (int i = 0; i < 3; i++) {
curves_[i].setup(static_cast<uint8_t>(i));
}
}
// =============================================================================
// Main Loop
// =============================================================================
void KnxDaliModule::loop(bool configured) {
if (!configured || device_ == nullptr) return;
loopMessages();
loopAddressing();
loopAssigning();
loopBusState();
loopInitData();
loopGroupState();
for (auto& ch : channels_) ch.loop();
for (auto& grp : groups_) grp.loop();
for (auto& curve : curves_) curve.loop();
}
// =============================================================================
// Message Queue Execution
// =============================================================================
void KnxDaliModule::loopMessages() {
if (dali_ == nullptr) return;
Message msg;
while (queue_.pop(msg)) {
DaliTarget target;
switch (msg.addrtype) {
case 0: target = {DaliTargetKind::kShortAddress, static_cast<int>(msg.para1)}; break;
case 1: target = {DaliTargetKind::kGroup, static_cast<int>(msg.para1)}; break;
default: target = {DaliTargetKind::kBroadcast, 0}; break;
}
switch (msg.type) {
case MessageType::Arc:
dali_->setArc(target, msg.data);
break;
case MessageType::Cmd:
dali_->sendRaw(target, msg.data);
break;
case MessageType::SpecialCmd:
dali_->sendRaw(target, msg.data);
break;
case MessageType::Query:
if (auto resp = dali_->queryRaw(target, msg.data)) {
queue_.setResponse(msg.id, *resp);
} else {
queue_.setResponse(msg.id, -1);
}
break;
}
}
}
// =============================================================================
// Addressing State Machine
// =============================================================================
void KnxDaliModule::loopAddressing() {
if (adr_state_ == AddressingState::kOff || dali_ == nullptr) return;
switch (adr_state_) {
case AddressingState::kOff:
break;
case AddressingState::kInit:
adr_found_ = 0;
adr_iterations_ = 0;
ESP_LOGI(kLogTag, "Addressing: init (only_new=%d, randomize=%d, delete_all=%d)",
adr_only_new_, adr_randomize_, adr_delete_all_);
dali_->sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_INITIALIZE);
adr_state_ = AddressingState::kInit2;
break;
case AddressingState::kInit2:
dali_->sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_INITIALIZE);
if (adr_delete_all_) {
adr_state_ = AddressingState::kWriteDtr;
} else if (adr_randomize_) {
adr_state_ = AddressingState::kRandom;
} else {
adr_state_ = AddressingState::kStartSearch;
}
break;
case AddressingState::kWriteDtr:
dali_->setDtr(255);
adr_state_ = AddressingState::kRemoveShort;
break;
case AddressingState::kRemoveShort: {
dali_->sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_STORE_DTR_AS_SHORT_ADDRESS);
adr_state_ = AddressingState::kRemoveShort2;
break;
}
case AddressingState::kRemoveShort2:
dali_->sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_STORE_DTR_AS_SHORT_ADDRESS);
if (adr_randomize_) {
adr_state_ = AddressingState::kRandom;
} else {
adr_state_ = AddressingState::kStartSearch;
}
break;
case AddressingState::kRandom:
dali_->randomise();
adr_state_ = AddressingState::kRandom2;
break;
case AddressingState::kRandom2:
dali_->randomise();
adr_state_ = AddressingState::kRandomWait;
break;
case AddressingState::kRandomWait:
vTaskDelay(pdMS_TO_TICKS(100));
adr_state_ = AddressingState::kStartSearch;
break;
case AddressingState::kStartSearch:
adr_search_ = 0xFFFFFF;
adr_state_ = AddressingState::kSearchHigh;
break;
case AddressingState::kSearchHigh:
dali_->searchAddrH(static_cast<uint8_t>((adr_search_ >> 16) & 0xFF));
adr_state_ = AddressingState::kSearchMid;
break;
case AddressingState::kSearchMid:
dali_->searchAddrM(static_cast<uint8_t>((adr_search_ >> 8) & 0xFF));
adr_state_ = AddressingState::kSearchLow;
break;
case AddressingState::kSearchLow:
dali_->searchAddrL(static_cast<uint8_t>(adr_search_ & 0xFF));
adr_state_ = AddressingState::kCompare;
break;
case AddressingState::kCompare: {
dali_->compare();
adr_state_ = AddressingState::kCheckFound;
break;
}
case AddressingState::kCheckFound: {
// Binary search: query short address to check if a device responded.
auto resp = dali_->queryShort({DaliTargetKind::kBroadcast, 0});
bool found = resp.has_value() && *resp != 0xFF;
if (adr_iterations_ < 24) {
int64_t delta = static_cast<int64_t>(1) << (23 - adr_iterations_);
if (found) {
adr_search_ += delta;
} else {
adr_search_ -= delta;
}
adr_iterations_++;
adr_state_ = AddressingState::kSearchHigh;
} else {
if (found) {
adr_state_ = AddressingState::kGetShort;
} else {
// Check one address higher
adr_search_++;
if (adr_search_ > 0xFFFFFF) {
adr_state_ = AddressingState::kTerminate;
} else {
adr_iterations_ = 0;
adr_state_ = AddressingState::kSearchHigh;
}
}
}
break;
}
case AddressingState::kGetShort: {
auto short_addr = dali_->queryShort({DaliTargetKind::kBroadcast, 0});
if (short_addr.has_value()) {
ballasts_[adr_found_].address = *short_addr;
ballasts_[adr_found_].high = static_cast<uint8_t>((adr_search_ >> 16) & 0xFF);
ballasts_[adr_found_].middle = static_cast<uint8_t>((adr_search_ >> 8) & 0xFF);
ballasts_[adr_found_].low = static_cast<uint8_t>(adr_search_ & 0xFF);
if (*short_addr == 0xFF) {
// Unaddressed — assign a free short address
int free_addr = -1;
for (int i = 0; i < 64; i++) {
if (!addresses_[i]) { free_addr = i; break; }
}
if (free_addr >= 0) {
adr_new_ = static_cast<uint8_t>(free_addr);
adr_state_ = AddressingState::kProgramShort;
} else {
adr_state_ = AddressingState::kWithdraw;
}
} else {
addresses_[*short_addr] = true;
adr_found_++;
adr_state_ = AddressingState::kWithdraw;
}
}
break;
}
case AddressingState::kProgramShort:
dali_->programShort({DaliTargetKind::kBroadcast, 0}, adr_new_);
adr_state_ = AddressingState::kVerifyShort;
break;
case AddressingState::kVerifyShort:
dali_->verifyShort({DaliTargetKind::kBroadcast, 0});
adr_state_ = AddressingState::kVerifyShortResponse;
break;
case AddressingState::kVerifyShortResponse: {
auto verify = dali_->queryShort({DaliTargetKind::kBroadcast, 0});
if (verify.has_value() && *verify == adr_new_) {
addresses_[adr_new_] = true;
adr_found_++;
ESP_LOGI(kLogTag, "Addressed device %d", adr_new_);
}
adr_state_ = AddressingState::kWithdraw;
break;
}
case AddressingState::kWithdraw:
dali_->withdraw();
adr_state_ = AddressingState::kStartSearch;
break;
case AddressingState::kTerminate:
dali_->terminate();
ESP_LOGI(kLogTag, "Addressing complete: %d devices found", adr_found_);
adr_state_ = AddressingState::kOff;
break;
default:
break;
}
}
// =============================================================================
// Assigning State Machine
// =============================================================================
void KnxDaliModule::loopAssigning() {
if (ass_state_ == AssigningState::kOff || dali_ == nullptr) return;
switch (ass_state_) {
case AssigningState::kOff:
break;
case AssigningState::kInit:
dali_->sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_INITIALIZE);
ass_state_ = AssigningState::kInit2;
break;
case AssigningState::kInit2:
dali_->sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_INITIALIZE);
ass_state_ = AssigningState::kQuery;
break;
case AssigningState::kQuery: {
auto level = dali_->queryActualLevel(adr_new_);
ass_state_ = AssigningState::kCheckQuery;
break;
}
case AssigningState::kCheckQuery:
// If the target address already has a device, stop
ass_state_ = AssigningState::kStartSearch;
break;
case AssigningState::kStartSearch:
adr_iterations_ = 0;
ass_state_ = AssigningState::kSearchHigh;
break;
case AssigningState::kSearchHigh:
dali_->searchAddrH(static_cast<uint8_t>((adr_search_ >> 16) & 0xFF));
ass_state_ = AssigningState::kSearchMid;
break;
case AssigningState::kSearchMid:
dali_->searchAddrM(static_cast<uint8_t>((adr_search_ >> 8) & 0xFF));
ass_state_ = AssigningState::kSearchLow;
break;
case AssigningState::kSearchLow:
dali_->searchAddrL(static_cast<uint8_t>(adr_search_ & 0xFF));
ass_state_ = AssigningState::kCompare;
break;
case AssigningState::kCompare:
dali_->compare();
ass_state_ = AssigningState::kCheckFound;
break;
case AssigningState::kCheckFound:
if (!adr_assign_) {
adr_assign_ = true;
ass_state_ = AssigningState::kWithdraw;
} else {
auto resp = dali_->queryShort({DaliTargetKind::kBroadcast, 0});
if (resp.has_value() && *resp != 0xFF) {
ass_state_ = AssigningState::kProgramShort;
} else {
ass_state_ = AssigningState::kTerminate;
}
}
break;
case AssigningState::kWithdraw:
dali_->withdraw();
adr_assign_ = true;
ass_state_ = AssigningState::kSearchHigh;
break;
case AssigningState::kProgramShort:
dali_->programShort({DaliTargetKind::kBroadcast, 0}, adr_new_);
ass_state_ = AssigningState::kVerifyShort;
break;
case AssigningState::kVerifyShort:
dali_->verifyShort({DaliTargetKind::kBroadcast, 0});
ass_state_ = AssigningState::kVerifyShortResponse;
break;
case AssigningState::kVerifyShortResponse: {
auto verify = dali_->queryShort({DaliTargetKind::kBroadcast, 0});
if (verify.has_value() && *verify == adr_new_) {
addresses_[adr_new_] = true;
ESP_LOGI(kLogTag, "Assigned short address %d", adr_new_);
}
ass_state_ = AssigningState::kTerminate;
break;
}
case AssigningState::kTerminate:
dali_->terminate();
ass_state_ = AssigningState::kOff;
break;
}
}
// =============================================================================
// Bus State / Init Data
// =============================================================================
void KnxDaliModule::loopBusState() {
if (dali_ == nullptr) return;
dali_bus_state_ = true; // Simplified: assume bus is always OK
}
void KnxDaliModule::loopInitData() {
if (got_init_data_ || device_ == nullptr || !device_->configured()) return;
// Read initial device data from all channels
for (int i = 0; i < 64; i++) {
if (!addresses_[i]) continue;
channels_[i].setup();
}
got_init_data_ = true;
}
void KnxDaliModule::loopGroupState() {
if (last_changed_group_ == 255) return;
uint8_t group_idx = last_changed_group_ & 0x0F;
bool is_group = (last_changed_group_ & 0x80) != 0;
if (is_group && group_idx < 16) {
groups_[group_idx].setGroupState(group_idx, last_changed_value_);
} else if (!is_group && group_idx < 64) {
channels_[group_idx].setGroupState(group_idx, last_changed_value_);
}
last_changed_group_ = 255;
}
// =============================================================================
// processInputKo — KNX group write dispatch
// =============================================================================
void KnxDaliModule::processInputKo(GroupObject& ko) {
if (device_ == nullptr) return;
if (adr_state_ != AddressingState::kOff) return;
if (current_lock_state_) return;
uint16_t asap = ko.asap();
ESP_LOGD(kLogTag, "processInputKo asap=%d", asap);
// Channel KOs (64 channels x N group objects each)
int adr_relative = static_cast<int>(asap) - ADR_KoOffset;
if (adr_relative >= 0 && adr_relative < ADR_KoBlockSize * 64) {
int ch = adr_relative / ADR_KoBlockSize;
if (ch < 64) {
channels_[ch].processInputKo(ko);
return;
}
}
// Group KOs (16 groups x N group objects each)
int grp_relative = static_cast<int>(asap) - GRP_KoOffset;
if (grp_relative >= 0 && grp_relative < GRP_KoBlockSize * 16) {
int grp_idx = grp_relative / GRP_KoBlockSize;
int slot = grp_relative % GRP_KoBlockSize;
if (grp_idx < 16) {
groups_[grp_idx].processInputKo(ko);
// Track group state changes
if (slot == 0) { // switch state
last_changed_group_ = 0x80 | static_cast<uint8_t>(grp_idx);
}
return;
}
}
// HCL KOs
int hcl_relative = static_cast<int>(asap) - HCL_KoOffset;
if (hcl_relative >= 0 && hcl_relative < HCL_KoBlockSize * 3) {
int curve_idx = hcl_relative / HCL_KoBlockSize;
if (curve_idx < 3) {
// HCL: apply Kelvin to all channels and groups
KNXValue val = ko.value();
if (true) {
uint16_t kelvin = static_cast<uint16_t>(static_cast<float>(val));
for (int i = 0; i < 64; i++) {
channels_[i].setHcl(static_cast<uint8_t>(curve_idx), kelvin, 255);
}
for (int i = 0; i < 16; i++) {
groups_[i].setHcl(static_cast<uint8_t>(curve_idx), kelvin, 255);
}
}
return;
}
}
}
// =============================================================================
// Function Property Handlers (stubs — full port in subsequent iteration)
// =============================================================================
bool KnxDaliModule::processFunctionProperty(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length) {
// Only handle object 160, property 1 (REG1 DALI function properties)
if (object_index != 160 || property_id != 1 || length == 0 || data == nullptr) {
return false;
}
switch (data[0]) {
case 2: funcHandleType(data, result_data, result_length); return true;
case 3: funcHandleScan(data, result_data, result_length); return true;
case 4: funcHandleAssign(data, result_data, result_length); return true;
case 10: funcHandleEvgWrite(data, result_data, result_length); return true;
case 11: funcHandleEvgRead(data, result_data, result_length); return true;
case 12: funcHandleSetScene(data, result_data, result_length); return true;
case 13: funcHandleGetScene(data, result_data, result_length); return true;
case 14: funcHandleIdentify(data, result_data, result_length); return true;
default: return false;
}
}
bool KnxDaliModule::processFunctionPropertyState(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length) {
if (object_index != 160 || property_id != 1 || length == 0 || data == nullptr) {
return false;
}
switch (data[0]) {
case 3:
case 5: stateHandleScanAndAddress(data, result_data, result_length); return true;
case 4: stateHandleAssign(data, result_data, result_length); return true;
case 7: stateHandleFoundEVGs(data, result_data, result_length); return true;
default: return false;
}
}
// =============================================================================
// Function Property Implementations (simplified stubs)
// =============================================================================
void KnxDaliModule::funcHandleType(uint8_t*, uint8_t* result_data, uint8_t& result_length) {
// Query device type(s) for the selected short address
result_data[0] = 0; // working
result_length = 1;
}
void KnxDaliModule::funcHandleScan(uint8_t* data, uint8_t* result_data, uint8_t& result_length) {
if (data == nullptr) return;
adr_only_new_ = (data[1] & 0x01) != 0;
adr_randomize_ = (data[1] & 0x02) != 0;
adr_delete_all_ = (data[1] & 0x04) != 0;
adr_state_ = AddressingState::kInit;
result_data[0] = 0; // working
result_length = 1;
}
void KnxDaliModule::funcHandleAssign(uint8_t* data, uint8_t* result_data, uint8_t& result_length) {
if (data == nullptr) return;
adr_search_ = (static_cast<uint64_t>(data[1]) << 16) |
(static_cast<uint64_t>(data[2]) << 8) | data[3];
adr_new_ = data[4];
if (adr_new_ == 99) adr_new_ = 255; // "remove short address"
ass_state_ = AssigningState::kInit;
result_data[0] = 0;
result_length = 1;
}
void KnxDaliModule::funcHandleEvgWrite(uint8_t*, uint8_t*, uint8_t&) {}
void KnxDaliModule::funcHandleEvgRead(uint8_t*, uint8_t*, uint8_t&) {}
void KnxDaliModule::funcHandleSetScene(uint8_t*, uint8_t*, uint8_t&) {}
void KnxDaliModule::funcHandleGetScene(uint8_t*, uint8_t*, uint8_t&) {}
void KnxDaliModule::funcHandleIdentify(uint8_t*, uint8_t*, uint8_t&) {}
void KnxDaliModule::stateHandleType(uint8_t*, uint8_t*, uint8_t&) {}
void KnxDaliModule::stateHandleAssign(uint8_t*, uint8_t*, uint8_t&) {}
void KnxDaliModule::stateHandleScanAndAddress(uint8_t*, uint8_t*, uint8_t&) {}
void KnxDaliModule::stateHandleFoundEVGs(uint8_t*, uint8_t*, uint8_t&) {}
// =============================================================================
// Public state accessors
// =============================================================================
bool KnxDaliModule::isAddressingActive() const {
return adr_state_ != AddressingState::kOff || ass_state_ != AssigningState::kOff;
}
bool KnxDaliModule::isLocked() const { return current_lock_state_; }
void KnxDaliModule::setLocked(bool locked) { current_lock_state_ = locked; }
bool KnxDaliModule::isNight() const { return is_night_; }
void KnxDaliModule::setNight(bool night) { is_night_ = night; }
uint8_t KnxDaliModule::lastChangedGroup() const { return last_changed_group_; }
uint8_t KnxDaliModule::lastChangedValue() const { return last_changed_value_; }
KnxDaliChannel& KnxDaliModule::channel(int index) { return channels_[index]; }
KnxDaliChannel& KnxDaliModule::group(int index) { return groups_[index]; }
HclCurve& KnxDaliModule::curve(int index) { return curves_[index]; }
// =============================================================================
// DALI Send Helpers
// =============================================================================
uint8_t KnxDaliModule::sendMsg(MessageType type, uint8_t addr, uint8_t val,
uint8_t addr_type, bool wait) {
Message* msg = new Message();
msg->type = type;
msg->data = val;
msg->addrtype = addr_type;
msg->para1 = addr;
return queue_.push(msg);
}
uint8_t KnxDaliModule::sendCmd(uint8_t addr, uint8_t value, uint8_t addr_type, bool wait) {
return sendMsg(MessageType::Cmd, addr, value, addr_type, wait);
}
uint8_t KnxDaliModule::sendSpecialCmd(uint8_t command, uint8_t value, bool wait) {
return sendMsg(MessageType::SpecialCmd, command, value, 2, wait);
}
uint8_t KnxDaliModule::sendArc(uint8_t addr, uint8_t value, uint8_t addr_type) {
return sendMsg(MessageType::Arc, addr, value, addr_type, false);
}
int16_t KnxDaliModule::getInfo(uint8_t address, int command, uint8_t additional) {
(void)additional;
uint8_t msg_id = sendMsg(MessageType::Query, address, static_cast<uint8_t>(command), 0, true);
// Wait for response
for (int i = 0; i < 300; i++) {
vTaskDelay(pdMS_TO_TICKS(1));
int16_t resp = queue_.getResponse(msg_id);
if (resp != -200) return resp;
}
return -1;
}
// =============================================================================
// KO Handlers
// =============================================================================
void KnxDaliModule::koHandleSwitch(GroupObject& ko) {
KNXValue val = ko.value();
bool on = static_cast<bool>(val);
if (dali_ != nullptr) {
if (on) {
dali_->on({DaliTargetKind::kBroadcast, 0});
} else {
dali_->off({DaliTargetKind::kBroadcast, 0});
}
}
}
void KnxDaliModule::koHandleDimm(GroupObject& ko) {
KNXValue val = ko.value();
uint8_t percent = static_cast<uint8_t>(static_cast<float>(val) * 255.0f / 100.0f);
uint8_t arc = PercentToArc(static_cast<double>(percent) * 100.0 / 255.0);
if (dali_ != nullptr) {
dali_->setArc({DaliTargetKind::kBroadcast, 0}, arc);
}
}
void KnxDaliModule::koHandleDayNight(GroupObject& ko) {
is_night_ = static_cast<bool>(ko.value());
for (int i = 0; i < 64; i++) channels_[i].isNight = is_night_;
for (int i = 0; i < 16; i++) groups_[i].isNight = is_night_;
}
void KnxDaliModule::koHandleOnValue(GroupObject& ko) {
uint8_t on_value = static_cast<uint8_t>(static_cast<float>(ko.value()) * 255.0f / 100.0f);
for (int i = 0; i < 64; i++) channels_[i].setOnValue(on_value);
for (int i = 0; i < 16; i++) groups_[i].setOnValue(on_value);
}
void KnxDaliModule::koHandleScene(GroupObject& ko) {
uint8_t scene = static_cast<uint8_t>(ko.value());
if (dali_ != nullptr) {
dali_->goToScene({DaliTargetKind::kBroadcast, 0}, scene);
}
}
} // namespace knx_dali_gw
} // namespace gateway
@@ -0,0 +1,166 @@
#pragma once
// =============================================================================
// KnxDaliModule — Core DALI gateway module (ported from DaliModule)
// =============================================================================
// Handles:
// - DALI message queuing and execution
// - DALI commissioning (addressing + assigning state machines)
// - KNX group-object dispatch (processInputKo)
// - KNX function-property commands (ETS programming)
// - Broadcast switch/dim/scene handling
#include "dali_gateway_bridge.h"
#include "knx_dali_channel.h"
#include "hcl_curve.h"
#include "message_queue.h"
#include "ballast.hpp"
#include "knxprod.h"
#include "knx/group_object.h"
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
// Forward declarations
class Bau07B0;
namespace gateway {
namespace knx_dali_gw {
class KnxDaliModule {
public:
enum class AddressingState {
kOff, kInit, kInit2, kWriteDtr, kRemoveShort, kRemoveShort2,
kRandom, kRandom2, kRandomWait, kStartSearch, kSearchHigh,
kSearchMid, kSearchLow, kCompare, kGetShort, kCheckFound,
kProgramShort, kVerifyShort, kVerifyShortResponse, kWithdraw,
kTerminate, kSearchShort, kCheckSearchShort
};
enum class AssigningState {
kOff, kInit, kInit2, kQuery, kCheckQuery, kStartSearch,
kSearchHigh, kSearchMid, kSearchLow, kCompare, kCheckFound,
kWithdraw, kProgramShort, kVerifyShort, kVerifyShortResponse,
kTerminate
};
KnxDaliModule();
~KnxDaliModule();
// ---- Lifecycle ----
void setup(Bau07B0& device, DaliGatewayBridge& bridge);
void loop(bool configured);
// ---- KNX input ----
void processInputKo(GroupObject& ko);
// ---- Function properties (ETS programming) ----
bool processFunctionProperty(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length);
bool processFunctionPropertyState(uint8_t object_index, uint8_t property_id,
uint8_t length, uint8_t* data,
uint8_t* result_data, uint8_t& result_length);
// ---- Public state ----
bool isAddressingActive() const;
bool isLocked() const;
void setLocked(bool locked);
bool isNight() const;
void setNight(bool night);
uint8_t lastChangedGroup() const;
uint8_t lastChangedValue() const;
// ---- Channel / group access ----
KnxDaliChannel& channel(int index);
KnxDaliChannel& group(int index);
HclCurve& curve(int index);
private:
// ---- DALI helpers ----
uint8_t sendMsg(MessageType type, uint8_t addr, uint8_t value,
uint8_t addr_type = 0, bool wait = false);
uint8_t sendCmd(uint8_t addr, uint8_t value, uint8_t addr_type = 0,
bool wait = false);
uint8_t sendSpecialCmd(uint8_t command, uint8_t value = 0, bool wait = false);
uint8_t sendArc(uint8_t addr, uint8_t value, uint8_t addr_type);
int16_t getInfo(uint8_t address, int command, uint8_t additional = 0);
// ---- KNX KO handlers ----
void koHandleSwitch(GroupObject& ko);
void koHandleDimm(GroupObject& ko);
void koHandleDayNight(GroupObject& ko);
void koHandleOnValue(GroupObject& ko);
void koHandleScene(GroupObject& ko);
// ---- Function property handlers ----
void funcHandleType(uint8_t* data, uint8_t* result_data, uint8_t& result_length);
void funcHandleScan(uint8_t* data, uint8_t* result_data, uint8_t& result_length);
void funcHandleAssign(uint8_t* data, uint8_t* result_data, uint8_t& result_length);
void funcHandleEvgWrite(uint8_t* data, uint8_t* result_data, uint8_t& result_length);
void funcHandleEvgRead(uint8_t* data, uint8_t* result_data, uint8_t& result_length);
void funcHandleSetScene(uint8_t* data, uint8_t* result_data, uint8_t& result_length);
void funcHandleGetScene(uint8_t* data, uint8_t* result_data, uint8_t& result_length);
void funcHandleIdentify(uint8_t* data, uint8_t* result_data, uint8_t& result_length);
// ---- State handlers ----
void stateHandleType(uint8_t* data, uint8_t* result_data, uint8_t& result_length);
void stateHandleAssign(uint8_t* data, uint8_t* result_data, uint8_t& result_length);
void stateHandleScanAndAddress(uint8_t* data, uint8_t* result_data,
uint8_t& result_length);
void stateHandleFoundEVGs(uint8_t* data, uint8_t* result_data,
uint8_t& result_length);
// ---- Loops ----
void loopMessages();
void loopAddressing();
void loopAssigning();
void loopBusState();
void loopInitData();
void loopGroupState();
// ---- State ----
Bau07B0* device_{nullptr};
DaliGatewayBridge* dali_{nullptr};
// Addressing / commissioning
AddressingState adr_state_{AddressingState::kOff};
AssigningState ass_state_{AssigningState::kOff};
Ballast ballasts_[64];
bool addresses_[64]{};
int adr_found_{0};
uint8_t adr_new_{0};
uint8_t last_bus_state_{2};
uint8_t adr_iterations_{0};
uint64_t adr_search_{0};
bool adr_assign_{false};
bool adr_only_new_{false};
bool adr_randomize_{false};
bool adr_delete_all_{false};
// Group state
uint8_t last_changed_group_{255};
uint8_t last_changed_value_{0};
// Bus
bool got_init_data_{false};
bool dali_bus_state_{true};
bool dali_bus_state_to_set_{true};
uint64_t dali_state_last_{1};
// Lock / night
bool current_lock_state_{false};
bool is_night_{false};
// Channels / groups / curves
KnxDaliChannel channels_[64];
KnxDaliChannel groups_[16];
HclCurve curves_[3];
MessageQueue queue_;
};
} // namespace knx_dali_gw
} // namespace gateway
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
enum class MessageType {
Arc,
Cmd,
SpecialCmd,
Query
};
struct Message {
Message *next{nullptr};
uint8_t data{0};
MessageType type{MessageType::Arc};
uint8_t para1{0};
uint8_t addrtype{0};
uint8_t para2{0};
bool wait{false};
uint8_t id{0};
};
@@ -0,0 +1,74 @@
#include "message_queue.h"
#include "esp_timer.h"
uint8_t MessageQueue::push(Message *msg)
{
while(isLocked) ;
isLocked = true;
msg->next = nullptr;
if(tail == nullptr)
{
head = msg;
tail = msg;
isLocked = false;
return msg->id;
}
tail->next = msg;
tail = msg;
isLocked = false;
return msg->id;
}
bool MessageQueue::pop(Message &msg)
{
unsigned long started = esp_timer_get_time() / 1000ULL;
while(isLocked && ((esp_timer_get_time() / 1000ULL) - started < 3000)) ;
if(isLocked || head == nullptr) return false;
isLocked = true;
msg.addrtype = head->addrtype;
msg.data = head->data;
msg.id = head->id;
msg.para1 = head->para1;
msg.para2 = head->para2;
msg.type = head->type;
msg.wait = head->wait;
Message *temp = head;
if(head->next == nullptr)
{
head = nullptr;
tail = nullptr;
} else {
head = head->next;
}
delete temp;
isLocked = false;
return true;
}
uint8_t MessageQueue::getNextId()
{
currentId++;
if(currentId == 0) currentId++;
responses[currentId] = -200;
return currentId;
}
void MessageQueue::setResponse(uint8_t id, int16_t value)
{
responses[id] = value;
}
int16_t MessageQueue::getResponse(uint8_t id)
{
return responses[id];
}
@@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
#include "message.hpp"
class MessageQueue
{
public:
uint8_t push(Message *msg);
bool pop(Message &msg);
uint8_t getNextId();
void setResponse(uint8_t id, int16_t value);
int16_t getResponse(uint8_t id);
private:
Message *head;
Message *tail;
uint8_t currentId = 0;
int16_t responses[256];
bool isLocked = false;
};