feat: add KNX gateway snapshot and command transaction handling
Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
@@ -1148,6 +1148,46 @@ extern "C" void app_main(void) {
|
|||||||
bridge_config.gateway_device_name_provider = []() {
|
bridge_config.gateway_device_name_provider = []() {
|
||||||
return s_runtime == nullptr ? std::string() : s_runtime->deviceName();
|
return s_runtime == nullptr ? std::string() : s_runtime->deviceName();
|
||||||
};
|
};
|
||||||
|
bridge_config.knx_gateway_snapshot_provider = []() {
|
||||||
|
gateway::GatewayKnxGatewaySnapshot out;
|
||||||
|
if (s_controller == nullptr) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const auto snapshot = s_controller->snapshot();
|
||||||
|
out.channels.reserve(snapshot.channels.size());
|
||||||
|
for (const auto& channel : snapshot.channels) {
|
||||||
|
out.channels.push_back(
|
||||||
|
gateway::GatewayKnxGatewayChannelSnapshot{channel.gateway_id});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
bridge_config.knx_gateway_command_transactor =
|
||||||
|
[](const std::vector<uint8_t>& frame, uint32_t timeout_ms) {
|
||||||
|
gateway::GatewayKnxGatewayTransactionResult out;
|
||||||
|
if (s_controller == nullptr) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const auto result = s_controller->transactCommandFrame(frame, timeout_ms);
|
||||||
|
switch (result.status) {
|
||||||
|
case gateway::GatewayCommandTransactionStatus::kOk:
|
||||||
|
out.status = gateway::GatewayKnxGatewayTransactionStatus::kOk;
|
||||||
|
break;
|
||||||
|
case gateway::GatewayCommandTransactionStatus::kInvalidFrame:
|
||||||
|
out.status = gateway::GatewayKnxGatewayTransactionStatus::kInvalidFrame;
|
||||||
|
break;
|
||||||
|
case gateway::GatewayCommandTransactionStatus::kQueueRejected:
|
||||||
|
out.status = gateway::GatewayKnxGatewayTransactionStatus::kQueueRejected;
|
||||||
|
break;
|
||||||
|
case gateway::GatewayCommandTransactionStatus::kTimeout:
|
||||||
|
out.status = gateway::GatewayKnxGatewayTransactionStatus::kTimeout;
|
||||||
|
break;
|
||||||
|
case gateway::GatewayCommandTransactionStatus::kNoResponse:
|
||||||
|
out.status = gateway::GatewayKnxGatewayTransactionStatus::kNoResponse;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.frames = result.frames;
|
||||||
|
return out;
|
||||||
|
};
|
||||||
s_bridge = std::make_unique<gateway::GatewayBridgeService>(*s_dali_domain, *s_cache,
|
s_bridge = std::make_unique<gateway::GatewayBridgeService>(*s_dali_domain, *s_cache,
|
||||||
bridge_config);
|
bridge_config);
|
||||||
s_controller->setBridgeService(s_bridge.get());
|
s_controller->setBridgeService(s_bridge.get());
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ struct GatewayBridgeServiceConfig {
|
|||||||
UBaseType_t knx_task_priority{5};
|
UBaseType_t knx_task_priority{5};
|
||||||
std::optional<GatewayKnxConfig> default_knx_config;
|
std::optional<GatewayKnxConfig> default_knx_config;
|
||||||
std::function<std::string()> gateway_device_name_provider;
|
std::function<std::string()> gateway_device_name_provider;
|
||||||
|
GatewayKnxGatewaySnapshotProvider knx_gateway_snapshot_provider;
|
||||||
|
GatewayKnxGatewayCommandTransactor knx_gateway_command_transactor;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct GatewayBridgeHttpResponse {
|
struct GatewayBridgeHttpResponse {
|
||||||
|
|||||||
@@ -1715,6 +1715,8 @@ struct GatewayBridgeService::ChannelRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
knx = std::make_unique<GatewayKnxBridge>(*engine);
|
knx = std::make_unique<GatewayKnxBridge>(*engine);
|
||||||
|
knx->setGatewayControllerCallbacks(service_config.knx_gateway_snapshot_provider,
|
||||||
|
service_config.knx_gateway_command_transactor);
|
||||||
knx_router = std::make_unique<GatewayKnxTpIpRouter>(*knx, openKnxNamespace());
|
knx_router = std::make_unique<GatewayKnxTpIpRouter>(*knx, openKnxNamespace());
|
||||||
knx_router->setGroupWriteHandler(
|
knx_router->setGroupWriteHandler(
|
||||||
[this](uint16_t group_address, const uint8_t* data, size_t len) {
|
[this](uint16_t group_address, const uint8_t* data, size_t len) {
|
||||||
|
|||||||
@@ -65,6 +65,19 @@ struct GatewayControllerSnapshot {
|
|||||||
std::vector<GatewayChannelSnapshot> channels;
|
std::vector<GatewayChannelSnapshot> channels;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class GatewayCommandTransactionStatus : uint8_t {
|
||||||
|
kOk = 0,
|
||||||
|
kInvalidFrame = 1,
|
||||||
|
kQueueRejected = 2,
|
||||||
|
kTimeout = 3,
|
||||||
|
kNoResponse = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GatewayCommandTransactionResult {
|
||||||
|
GatewayCommandTransactionStatus status{GatewayCommandTransactionStatus::kNoResponse};
|
||||||
|
std::vector<uint8_t> frames;
|
||||||
|
};
|
||||||
|
|
||||||
class GatewayController {
|
class GatewayController {
|
||||||
public:
|
public:
|
||||||
using NotificationSink = std::function<void(const std::vector<uint8_t>& frame)>;
|
using NotificationSink = std::function<void(const std::vector<uint8_t>& frame)>;
|
||||||
@@ -79,6 +92,11 @@ class GatewayController {
|
|||||||
|
|
||||||
esp_err_t start();
|
esp_err_t start();
|
||||||
bool enqueueCommandFrame(const std::vector<uint8_t>& frame);
|
bool enqueueCommandFrame(const std::vector<uint8_t>& frame);
|
||||||
|
GatewayCommandTransactionResult transactCommandFrame(
|
||||||
|
const std::vector<uint8_t>& frame,
|
||||||
|
uint32_t timeout_ms = 700,
|
||||||
|
uint32_t idle_ms = 30,
|
||||||
|
size_t max_response_bytes = 2048);
|
||||||
void addNotificationSink(NotificationSink sink);
|
void addNotificationSink(NotificationSink sink);
|
||||||
void addBleStateSink(BleStateSink sink);
|
void addBleStateSink(BleStateSink sink);
|
||||||
void addWifiStateSink(WifiStateSink sink);
|
void addWifiStateSink(WifiStateSink sink);
|
||||||
@@ -122,6 +140,15 @@ class GatewayController {
|
|||||||
uint8_t short_address{0};
|
uint8_t short_address{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct TransactionWaiter {
|
||||||
|
uint8_t gateway_id{0};
|
||||||
|
uint8_t opcode{0};
|
||||||
|
SemaphoreHandle_t signal{nullptr};
|
||||||
|
std::vector<uint8_t> frames;
|
||||||
|
size_t max_response_bytes{0};
|
||||||
|
bool overflow{false};
|
||||||
|
};
|
||||||
|
|
||||||
static void TaskEntry(void* arg);
|
static void TaskEntry(void* arg);
|
||||||
void taskLoop();
|
void taskLoop();
|
||||||
void dispatchCommand(const std::vector<uint8_t>& command);
|
void dispatchCommand(const std::vector<uint8_t>& command);
|
||||||
@@ -144,6 +171,9 @@ class GatewayController {
|
|||||||
void refreshRuntimeGatewayNames();
|
void refreshRuntimeGatewayNames();
|
||||||
void publishPayload(uint8_t gateway_id, const std::vector<uint8_t>& payload);
|
void publishPayload(uint8_t gateway_id, const std::vector<uint8_t>& payload);
|
||||||
void publishFrame(const std::vector<uint8_t>& frame);
|
void publishFrame(const std::vector<uint8_t>& frame);
|
||||||
|
bool transactionFrameMatches(const TransactionWaiter& waiter,
|
||||||
|
const std::vector<uint8_t>& frame) const;
|
||||||
|
void captureTransactionFrame(const std::vector<uint8_t>& frame);
|
||||||
void handleDaliRawFrame(const DaliRawFrame& frame);
|
void handleDaliRawFrame(const DaliRawFrame& frame);
|
||||||
bool handleApplicationControllerFrame(const DaliRawFrame& frame);
|
bool handleApplicationControllerFrame(const DaliRawFrame& frame);
|
||||||
std::optional<uint8_t> applicationControllerResponse(uint8_t gateway_id, uint8_t first,
|
std::optional<uint8_t> applicationControllerResponse(uint8_t gateway_id, uint8_t first,
|
||||||
@@ -203,11 +233,13 @@ class GatewayController {
|
|||||||
GatewayControllerConfig config_;
|
GatewayControllerConfig config_;
|
||||||
TaskHandle_t task_handle_{nullptr};
|
TaskHandle_t task_handle_{nullptr};
|
||||||
SemaphoreHandle_t maintenance_lock_{nullptr};
|
SemaphoreHandle_t maintenance_lock_{nullptr};
|
||||||
|
SemaphoreHandle_t transaction_lock_{nullptr};
|
||||||
std::vector<NotificationSink> notification_sinks_;
|
std::vector<NotificationSink> notification_sinks_;
|
||||||
std::vector<BleStateSink> ble_state_sinks_;
|
std::vector<BleStateSink> ble_state_sinks_;
|
||||||
std::vector<WifiStateSink> wifi_state_sinks_;
|
std::vector<WifiStateSink> wifi_state_sinks_;
|
||||||
std::vector<GatewayNameSink> gateway_name_sinks_;
|
std::vector<GatewayNameSink> gateway_name_sinks_;
|
||||||
std::map<uint16_t, BridgeTransportRequestState> bridge_transport_requests_;
|
std::map<uint16_t, BridgeTransportRequestState> bridge_transport_requests_;
|
||||||
|
std::vector<TransactionWaiter*> transaction_waiters_;
|
||||||
std::map<uint8_t, ReconciliationJob> reconciliation_jobs_;
|
std::map<uint8_t, ReconciliationJob> reconciliation_jobs_;
|
||||||
std::map<uint8_t, CacheRefreshJob> cache_refresh_jobs_;
|
std::map<uint8_t, CacheRefreshJob> cache_refresh_jobs_;
|
||||||
std::atomic<int> maintenance_activity_gateway_{-1};
|
std::atomic<int> maintenance_activity_gateway_{-1};
|
||||||
|
|||||||
@@ -282,9 +282,14 @@ GatewayController::GatewayController(GatewayRuntime& runtime, DaliDomainService&
|
|||||||
dali_domain_(dali_domain),
|
dali_domain_(dali_domain),
|
||||||
cache_(cache),
|
cache_(cache),
|
||||||
config_(config),
|
config_(config),
|
||||||
maintenance_lock_(xSemaphoreCreateMutex()) {}
|
maintenance_lock_(xSemaphoreCreateMutex()),
|
||||||
|
transaction_lock_(xSemaphoreCreateMutex()) {}
|
||||||
|
|
||||||
GatewayController::~GatewayController() {
|
GatewayController::~GatewayController() {
|
||||||
|
if (transaction_lock_ != nullptr) {
|
||||||
|
vSemaphoreDelete(transaction_lock_);
|
||||||
|
transaction_lock_ = nullptr;
|
||||||
|
}
|
||||||
if (maintenance_lock_ != nullptr) {
|
if (maintenance_lock_ != nullptr) {
|
||||||
vSemaphoreDelete(maintenance_lock_);
|
vSemaphoreDelete(maintenance_lock_);
|
||||||
maintenance_lock_ = nullptr;
|
maintenance_lock_ = nullptr;
|
||||||
@@ -341,6 +346,74 @@ bool GatewayController::enqueueCommandFrame(const std::vector<uint8_t>& frame) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GatewayCommandTransactionResult GatewayController::transactCommandFrame(
|
||||||
|
const std::vector<uint8_t>& frame,
|
||||||
|
uint32_t timeout_ms,
|
||||||
|
uint32_t idle_ms,
|
||||||
|
size_t max_response_bytes) {
|
||||||
|
GatewayCommandTransactionResult result;
|
||||||
|
if (!GatewayRuntime::isGatewayCommandFrame(frame) || !GatewayRuntime::hasValidChecksum(frame) ||
|
||||||
|
frame.size() < 5) {
|
||||||
|
result.status = GatewayCommandTransactionStatus::kInvalidFrame;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionWaiter waiter;
|
||||||
|
waiter.gateway_id = frame[2];
|
||||||
|
waiter.opcode = frame[3];
|
||||||
|
waiter.max_response_bytes = max_response_bytes;
|
||||||
|
waiter.signal = xSemaphoreCreateBinary();
|
||||||
|
if (waiter.signal == nullptr) {
|
||||||
|
result.status = GatewayCommandTransactionStatus::kQueueRejected;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
LockGuard guard(transaction_lock_);
|
||||||
|
transaction_waiters_.push_back(&waiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool enqueued = enqueueCommandFrame(frame);
|
||||||
|
if (!enqueued) {
|
||||||
|
{
|
||||||
|
LockGuard guard(transaction_lock_);
|
||||||
|
transaction_waiters_.erase(std::remove(transaction_waiters_.begin(),
|
||||||
|
transaction_waiters_.end(), &waiter),
|
||||||
|
transaction_waiters_.end());
|
||||||
|
}
|
||||||
|
vSemaphoreDelete(waiter.signal);
|
||||||
|
result.status = GatewayCommandTransactionStatus::kQueueRejected;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms);
|
||||||
|
const BaseType_t first = xSemaphoreTake(waiter.signal, timeout_ticks);
|
||||||
|
if (first == pdTRUE && idle_ms > 0) {
|
||||||
|
while (xSemaphoreTake(waiter.signal, pdMS_TO_TICKS(idle_ms)) == pdTRUE) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
LockGuard guard(transaction_lock_);
|
||||||
|
transaction_waiters_.erase(std::remove(transaction_waiters_.begin(),
|
||||||
|
transaction_waiters_.end(), &waiter),
|
||||||
|
transaction_waiters_.end());
|
||||||
|
result.frames = waiter.frames;
|
||||||
|
}
|
||||||
|
vSemaphoreDelete(waiter.signal);
|
||||||
|
|
||||||
|
if (waiter.overflow) {
|
||||||
|
result.status = GatewayCommandTransactionStatus::kQueueRejected;
|
||||||
|
} else if (!result.frames.empty()) {
|
||||||
|
result.status = GatewayCommandTransactionStatus::kOk;
|
||||||
|
} else if (first == pdTRUE) {
|
||||||
|
result.status = GatewayCommandTransactionStatus::kNoResponse;
|
||||||
|
} else {
|
||||||
|
result.status = GatewayCommandTransactionStatus::kTimeout;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void GatewayController::addNotificationSink(NotificationSink sink) {
|
void GatewayController::addNotificationSink(NotificationSink sink) {
|
||||||
if (sink) {
|
if (sink) {
|
||||||
notification_sinks_.push_back(std::move(sink));
|
notification_sinks_.push_back(std::move(sink));
|
||||||
@@ -1109,11 +1182,65 @@ void GatewayController::publishBridgeTransportResponse(uint8_t gateway_id, uint8
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GatewayController::publishFrame(const std::vector<uint8_t>& frame) {
|
void GatewayController::publishFrame(const std::vector<uint8_t>& frame) {
|
||||||
|
captureTransactionFrame(frame);
|
||||||
for (const auto& sink : notification_sinks_) {
|
for (const auto& sink : notification_sinks_) {
|
||||||
sink(frame);
|
sink(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GatewayController::transactionFrameMatches(const TransactionWaiter& waiter,
|
||||||
|
const std::vector<uint8_t>& frame) const {
|
||||||
|
if (frame.size() < 3 || frame[0] != 0x22) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const uint8_t response_opcode = frame[1];
|
||||||
|
const uint8_t response_gateway = frame.size() > 2 ? frame[2] : 0;
|
||||||
|
const bool gateway_matches = response_gateway == waiter.gateway_id ||
|
||||||
|
waiter.gateway_id == 0 ||
|
||||||
|
response_gateway == 0xff;
|
||||||
|
switch (waiter.opcode) {
|
||||||
|
case 0x06:
|
||||||
|
return response_opcode == 0x03 && gateway_matches;
|
||||||
|
case 0x09:
|
||||||
|
return (response_opcode == 0x09 && waiter.gateway_id == 0) ||
|
||||||
|
((response_opcode == 0x03 || response_opcode == 0x04) && gateway_matches);
|
||||||
|
case 0x14:
|
||||||
|
return (response_opcode == 0x03 || response_opcode == 0x04) && gateway_matches;
|
||||||
|
case kDali103QueryOpcode:
|
||||||
|
return (response_opcode == kDali103QueryResponseOpcode ||
|
||||||
|
response_opcode == kDali103NoResponseOpcode) &&
|
||||||
|
gateway_matches;
|
||||||
|
case 0x05:
|
||||||
|
case 0x0A:
|
||||||
|
case 0x30:
|
||||||
|
case kGatewayCacheOpcode:
|
||||||
|
case 0xA0:
|
||||||
|
case 0xA2:
|
||||||
|
return response_opcode == waiter.opcode && gateway_matches;
|
||||||
|
case kBridgeTransportRequestOpcode:
|
||||||
|
return response_opcode == kBridgeTransportResponseOpcode && gateway_matches;
|
||||||
|
default:
|
||||||
|
return response_opcode == waiter.opcode && gateway_matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GatewayController::captureTransactionFrame(const std::vector<uint8_t>& frame) {
|
||||||
|
LockGuard guard(transaction_lock_);
|
||||||
|
for (auto* waiter : transaction_waiters_) {
|
||||||
|
if (waiter == nullptr || waiter->signal == nullptr ||
|
||||||
|
!transactionFrameMatches(*waiter, frame)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (waiter->frames.size() + frame.size() > waiter->max_response_bytes) {
|
||||||
|
waiter->overflow = true;
|
||||||
|
xSemaphoreGive(waiter->signal);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
waiter->frames.insert(waiter->frames.end(), frame.begin(), frame.end());
|
||||||
|
xSemaphoreGive(waiter->signal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<uint8_t> GatewayController::applicationControllerResponse(
|
std::optional<uint8_t> GatewayController::applicationControllerResponse(
|
||||||
uint8_t gateway_id, uint8_t first, uint8_t instance, uint8_t opcode) const {
|
uint8_t gateway_id, uint8_t first, uint8_t instance, uint8_t opcode) const {
|
||||||
const uint8_t gateway_short =
|
const uint8_t gateway_short =
|
||||||
|
|||||||
@@ -169,6 +169,32 @@ struct GatewayKnxReg1ScanOptions {
|
|||||||
bool assign{false};
|
bool assign{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct GatewayKnxGatewayChannelSnapshot {
|
||||||
|
uint8_t gateway_id{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GatewayKnxGatewaySnapshot {
|
||||||
|
std::vector<GatewayKnxGatewayChannelSnapshot> channels;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class GatewayKnxGatewayTransactionStatus : uint8_t {
|
||||||
|
kOk = 0,
|
||||||
|
kInvalidFrame = 1,
|
||||||
|
kQueueRejected = 2,
|
||||||
|
kTimeout = 3,
|
||||||
|
kNoResponse = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GatewayKnxGatewayTransactionResult {
|
||||||
|
GatewayKnxGatewayTransactionStatus status{GatewayKnxGatewayTransactionStatus::kNoResponse};
|
||||||
|
std::vector<uint8_t> frames;
|
||||||
|
};
|
||||||
|
|
||||||
|
using GatewayKnxGatewaySnapshotProvider = std::function<GatewayKnxGatewaySnapshot()>;
|
||||||
|
using GatewayKnxGatewayCommandTransactor =
|
||||||
|
std::function<GatewayKnxGatewayTransactionResult(const std::vector<uint8_t>& frame,
|
||||||
|
uint32_t timeout_ms)>;
|
||||||
|
|
||||||
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value);
|
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value);
|
||||||
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config);
|
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config);
|
||||||
std::optional<GatewayKnxOamRouterConfig> GatewayKnxOamRouterConfigFromValue(
|
std::optional<GatewayKnxOamRouterConfig> GatewayKnxOamRouterConfigFromValue(
|
||||||
@@ -194,6 +220,8 @@ class GatewayKnxBridge {
|
|||||||
|
|
||||||
void setConfig(const GatewayKnxConfig& config);
|
void setConfig(const GatewayKnxConfig& config);
|
||||||
void setRuntimeContext(const openknx::EtsDeviceRuntime* runtime);
|
void setRuntimeContext(const openknx::EtsDeviceRuntime* runtime);
|
||||||
|
void setGatewayControllerCallbacks(GatewayKnxGatewaySnapshotProvider snapshot_provider,
|
||||||
|
GatewayKnxGatewayCommandTransactor command_transactor);
|
||||||
const GatewayKnxConfig& config() const;
|
const GatewayKnxConfig& config() const;
|
||||||
size_t etsBindingCount() const;
|
size_t etsBindingCount() const;
|
||||||
|
|
||||||
@@ -211,6 +239,13 @@ class GatewayKnxBridge {
|
|||||||
std::vector<uint8_t>* response);
|
std::vector<uint8_t>* response);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct GatewayTransferState {
|
||||||
|
uint16_t total_length{0};
|
||||||
|
uint8_t total_chunks{0};
|
||||||
|
std::map<uint8_t, std::vector<uint8_t>> request_chunks;
|
||||||
|
std::vector<uint8_t> response_bytes;
|
||||||
|
};
|
||||||
|
|
||||||
DaliBridgeResult executeForDecodedWrite(uint16_t group_address,
|
DaliBridgeResult executeForDecodedWrite(uint16_t group_address,
|
||||||
GatewayKnxDaliDataType data_type,
|
GatewayKnxDaliDataType data_type,
|
||||||
GatewayKnxDaliTarget target,
|
GatewayKnxDaliTarget target,
|
||||||
@@ -244,13 +279,29 @@ class GatewayKnxBridge {
|
|||||||
std::vector<uint8_t>* response);
|
std::vector<uint8_t>* response);
|
||||||
bool handleReg1FoundEvgsState(const uint8_t* data, size_t len,
|
bool handleReg1FoundEvgsState(const uint8_t* data, size_t len,
|
||||||
std::vector<uint8_t>* response);
|
std::vector<uint8_t>* response);
|
||||||
|
bool handleDalimasterDiscoveryState(const uint8_t* data, size_t len,
|
||||||
|
std::vector<uint8_t>* response);
|
||||||
|
bool handleDalimasterTransferCommand(const uint8_t* data, size_t len,
|
||||||
|
std::vector<uint8_t>* response);
|
||||||
|
bool handleDalimasterTransferState(const uint8_t* data, size_t len,
|
||||||
|
std::vector<uint8_t>* response);
|
||||||
|
bool executeDalimasterTransfer(uint16_t transaction_id, uint32_t timeout_ms,
|
||||||
|
std::vector<uint8_t>* response);
|
||||||
|
bool appendDalimasterTransferResponse(uint8_t transfer_status,
|
||||||
|
uint16_t transaction_id,
|
||||||
|
const std::vector<uint8_t>& response_bytes,
|
||||||
|
uint8_t chunk_index,
|
||||||
|
std::vector<uint8_t>* response) const;
|
||||||
static void CommissioningScanTaskEntry(void* arg);
|
static void CommissioningScanTaskEntry(void* arg);
|
||||||
void runCommissioningScanTask();
|
void runCommissioningScanTask();
|
||||||
|
|
||||||
DaliBridgeEngine& engine_;
|
DaliBridgeEngine& engine_;
|
||||||
|
GatewayKnxGatewaySnapshotProvider gateway_snapshot_provider_;
|
||||||
|
GatewayKnxGatewayCommandTransactor gateway_command_transactor_;
|
||||||
GatewayKnxConfig config_;
|
GatewayKnxConfig config_;
|
||||||
const openknx::EtsDeviceRuntime* runtime_{nullptr};
|
const openknx::EtsDeviceRuntime* runtime_{nullptr};
|
||||||
std::map<uint16_t, std::vector<GatewayKnxDaliBinding>> ets_bindings_by_group_address_;
|
std::map<uint16_t, std::vector<GatewayKnxDaliBinding>> ets_bindings_by_group_address_;
|
||||||
|
std::map<uint16_t, GatewayTransferState> gateway_transfer_states_;
|
||||||
SemaphoreHandle_t commissioning_lock_{nullptr};
|
SemaphoreHandle_t commissioning_lock_{nullptr};
|
||||||
TaskHandle_t commissioning_scan_task_{nullptr};
|
TaskHandle_t commissioning_scan_task_{nullptr};
|
||||||
std::atomic_bool commissioning_scan_cancel_requested_{false};
|
std::atomic_bool commissioning_scan_cancel_requested_{false};
|
||||||
|
|||||||
@@ -418,6 +418,13 @@ void GatewayKnxBridge::setRuntimeContext(const openknx::EtsDeviceRuntime* runtim
|
|||||||
runtime_ = runtime;
|
runtime_ = runtime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GatewayKnxBridge::setGatewayControllerCallbacks(
|
||||||
|
GatewayKnxGatewaySnapshotProvider snapshot_provider,
|
||||||
|
GatewayKnxGatewayCommandTransactor command_transactor) {
|
||||||
|
gateway_snapshot_provider_ = std::move(snapshot_provider);
|
||||||
|
gateway_command_transactor_ = std::move(command_transactor);
|
||||||
|
}
|
||||||
|
|
||||||
const GatewayKnxConfig& GatewayKnxBridge::config() const { return config_; }
|
const GatewayKnxConfig& GatewayKnxBridge::config() const { return config_; }
|
||||||
|
|
||||||
size_t GatewayKnxBridge::etsBindingCount() const {
|
size_t GatewayKnxBridge::etsBindingCount() const {
|
||||||
@@ -615,8 +622,14 @@ DaliBridgeResult GatewayKnxBridge::handleGroupObjectWrite(uint16_t group_object_
|
|||||||
bool GatewayKnxBridge::handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
|
bool GatewayKnxBridge::handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
|
||||||
const uint8_t* data, size_t len,
|
const uint8_t* data, size_t len,
|
||||||
std::vector<uint8_t>* response) {
|
std::vector<uint8_t>* response) {
|
||||||
if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId ||
|
if (object_index != kReg1DaliFunctionObjectIndex || data == nullptr || len == 0 ||
|
||||||
data == nullptr || len == 0 || response == nullptr) {
|
response == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (property_id == kDalimasterTransferFunctionPropertyId) {
|
||||||
|
return handleDalimasterTransferCommand(data, len, response);
|
||||||
|
}
|
||||||
|
if (property_id != kReg1DaliFunctionPropertyId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
switch (data[0]) {
|
switch (data[0]) {
|
||||||
@@ -644,8 +657,16 @@ bool GatewayKnxBridge::handleFunctionPropertyCommand(uint8_t object_index, uint8
|
|||||||
bool GatewayKnxBridge::handleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
|
bool GatewayKnxBridge::handleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
|
||||||
const uint8_t* data, size_t len,
|
const uint8_t* data, size_t len,
|
||||||
std::vector<uint8_t>* response) {
|
std::vector<uint8_t>* response) {
|
||||||
if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId ||
|
if (object_index != kReg1DaliFunctionObjectIndex || response == nullptr) {
|
||||||
data == nullptr || len == 0 || response == nullptr) {
|
return false;
|
||||||
|
}
|
||||||
|
if (property_id == kDalimasterDiscoveryFunctionPropertyId) {
|
||||||
|
return handleDalimasterDiscoveryState(data, len, response);
|
||||||
|
}
|
||||||
|
if (property_id == kDalimasterTransferFunctionPropertyId) {
|
||||||
|
return handleDalimasterTransferState(data, len, response);
|
||||||
|
}
|
||||||
|
if (property_id != kReg1DaliFunctionPropertyId || data == nullptr || len == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
switch (data[0]) {
|
switch (data[0]) {
|
||||||
@@ -661,6 +682,238 @@ bool GatewayKnxBridge::handleFunctionPropertyState(uint8_t object_index, uint8_t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GatewayKnxBridge::handleDalimasterDiscoveryState(const uint8_t*, size_t,
|
||||||
|
std::vector<uint8_t>* response) {
|
||||||
|
if (response == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
response->clear();
|
||||||
|
response->reserve(32);
|
||||||
|
response->push_back(0x00);
|
||||||
|
response->push_back('D');
|
||||||
|
response->push_back('M');
|
||||||
|
response->push_back('K');
|
||||||
|
response->push_back('G');
|
||||||
|
response->push_back(0x01);
|
||||||
|
response->push_back(kDalimasterTransferChunkBytes);
|
||||||
|
uint16_t feature_flags = 0x0100 | 0x0080;
|
||||||
|
if (config_.tunnel_enabled) {
|
||||||
|
feature_flags |= 0x0001;
|
||||||
|
}
|
||||||
|
if (config_.multicast_enabled) {
|
||||||
|
feature_flags |= 0x0002;
|
||||||
|
}
|
||||||
|
if (config_.oam_router.secure_tunnel_enabled) {
|
||||||
|
feature_flags |= 0x0004;
|
||||||
|
}
|
||||||
|
if (config_.oam_router.secure_routing_enabled) {
|
||||||
|
feature_flags |= 0x0008;
|
||||||
|
}
|
||||||
|
response->push_back(static_cast<uint8_t>(feature_flags & 0xff));
|
||||||
|
response->push_back(static_cast<uint8_t>((feature_flags >> 8) & 0xff));
|
||||||
|
response->push_back(kReg1DaliFunctionObjectIndex);
|
||||||
|
response->push_back(kDalimasterDiscoveryFunctionPropertyId);
|
||||||
|
response->push_back(kDalimasterTransferFunctionPropertyId);
|
||||||
|
const uint16_t individual_address =
|
||||||
|
runtime_ != nullptr ? runtime_->individualAddress() : config_.individual_address;
|
||||||
|
const uint16_t tunnel_address =
|
||||||
|
runtime_ != nullptr ? runtime_->tunnelClientAddress() : config_.ip_interface_individual_address;
|
||||||
|
response->push_back(static_cast<uint8_t>((individual_address >> 8) & 0xff));
|
||||||
|
response->push_back(static_cast<uint8_t>(individual_address & 0xff));
|
||||||
|
response->push_back(static_cast<uint8_t>((tunnel_address >> 8) & 0xff));
|
||||||
|
response->push_back(static_cast<uint8_t>(tunnel_address & 0xff));
|
||||||
|
|
||||||
|
const auto snapshot = gateway_snapshot_provider_ ? gateway_snapshot_provider_()
|
||||||
|
: GatewayKnxGatewaySnapshot{};
|
||||||
|
const auto channel_count = std::min<size_t>(snapshot.channels.size(), 32);
|
||||||
|
response->push_back(static_cast<uint8_t>(channel_count));
|
||||||
|
for (size_t index = 0; index < channel_count; ++index) {
|
||||||
|
response->push_back(snapshot.channels[index].gateway_id);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GatewayKnxBridge::handleDalimasterTransferCommand(const uint8_t* data, size_t len,
|
||||||
|
std::vector<uint8_t>* response) {
|
||||||
|
if (data == nullptr || len == 0 || response == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto read_le16 = [](const uint8_t* value) -> uint16_t {
|
||||||
|
return static_cast<uint16_t>(value[0] | (static_cast<uint16_t>(value[1]) << 8));
|
||||||
|
};
|
||||||
|
const uint8_t op = data[0];
|
||||||
|
if (op == kDalimasterTransferOpSingle) {
|
||||||
|
if (len < 6) {
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusInvalidFrame, 0, {}, 0,
|
||||||
|
response);
|
||||||
|
}
|
||||||
|
const uint16_t transaction_id = read_le16(data + 1);
|
||||||
|
const uint32_t timeout_ms = read_le16(data + 3);
|
||||||
|
gateway_transfer_states_[transaction_id] = GatewayTransferState{};
|
||||||
|
gateway_transfer_states_[transaction_id].request_chunks[0] =
|
||||||
|
std::vector<uint8_t>(data + 5, data + len);
|
||||||
|
gateway_transfer_states_[transaction_id].total_length =
|
||||||
|
static_cast<uint16_t>(len - 5);
|
||||||
|
gateway_transfer_states_[transaction_id].total_chunks = 1;
|
||||||
|
return executeDalimasterTransfer(transaction_id, timeout_ms, response);
|
||||||
|
}
|
||||||
|
if (op == kDalimasterTransferOpBegin) {
|
||||||
|
if (len < 6) {
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusInvalidFrame, 0, {}, 0,
|
||||||
|
response);
|
||||||
|
}
|
||||||
|
const uint16_t transaction_id = read_le16(data + 1);
|
||||||
|
GatewayTransferState state;
|
||||||
|
state.total_length = read_le16(data + 3);
|
||||||
|
state.total_chunks = data[5];
|
||||||
|
gateway_transfer_states_[transaction_id] = std::move(state);
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusOk, transaction_id, {}, 0,
|
||||||
|
response);
|
||||||
|
}
|
||||||
|
if (op == kDalimasterTransferOpChunk) {
|
||||||
|
if (len < 6) {
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusInvalidFrame, 0, {}, 0,
|
||||||
|
response);
|
||||||
|
}
|
||||||
|
const uint16_t transaction_id = read_le16(data + 1);
|
||||||
|
const uint8_t chunk_index = data[3];
|
||||||
|
const uint8_t chunk_len = data[4];
|
||||||
|
if (len < static_cast<size_t>(5 + chunk_len) ||
|
||||||
|
chunk_len > kDalimasterTransferChunkBytes ||
|
||||||
|
gateway_transfer_states_.find(transaction_id) == gateway_transfer_states_.end()) {
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusInvalidFrame,
|
||||||
|
transaction_id, {}, chunk_index, response);
|
||||||
|
}
|
||||||
|
gateway_transfer_states_[transaction_id].request_chunks[chunk_index] =
|
||||||
|
std::vector<uint8_t>(data + 5, data + 5 + chunk_len);
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusOk, transaction_id, {},
|
||||||
|
chunk_index, response);
|
||||||
|
}
|
||||||
|
if (op == kDalimasterTransferOpExecute) {
|
||||||
|
if (len < 5) {
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusInvalidFrame, 0, {}, 0,
|
||||||
|
response);
|
||||||
|
}
|
||||||
|
return executeDalimasterTransfer(read_le16(data + 1), read_le16(data + 3), response);
|
||||||
|
}
|
||||||
|
if (op == kDalimasterTransferOpReadResponse) {
|
||||||
|
return handleDalimasterTransferState(data, len, response);
|
||||||
|
}
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusInvalidFrame, 0, {}, 0,
|
||||||
|
response);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GatewayKnxBridge::handleDalimasterTransferState(const uint8_t* data, size_t len,
|
||||||
|
std::vector<uint8_t>* response) {
|
||||||
|
if (data == nullptr || len < 4 || response == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const uint16_t transaction_id =
|
||||||
|
static_cast<uint16_t>(data[1] | (static_cast<uint16_t>(data[2]) << 8));
|
||||||
|
const uint8_t chunk_index = data[3];
|
||||||
|
const auto it = gateway_transfer_states_.find(transaction_id);
|
||||||
|
if (it == gateway_transfer_states_.end()) {
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusInvalidFrame,
|
||||||
|
transaction_id, {}, chunk_index, response);
|
||||||
|
}
|
||||||
|
const auto response_bytes = it->second.response_bytes;
|
||||||
|
const bool ok = appendDalimasterTransferResponse(kDalimasterTransferStatusOk, transaction_id,
|
||||||
|
response_bytes, chunk_index, response);
|
||||||
|
const size_t total_chunks =
|
||||||
|
std::max<size_t>(1, (response_bytes.size() + kDalimasterTransferChunkBytes - 1) /
|
||||||
|
kDalimasterTransferChunkBytes);
|
||||||
|
if (chunk_index + 1 >= total_chunks) {
|
||||||
|
gateway_transfer_states_.erase(it);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GatewayKnxBridge::executeDalimasterTransfer(uint16_t transaction_id, uint32_t timeout_ms,
|
||||||
|
std::vector<uint8_t>* response) {
|
||||||
|
const auto it = gateway_transfer_states_.find(transaction_id);
|
||||||
|
if (it == gateway_transfer_states_.end() || response == nullptr) {
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusInvalidFrame,
|
||||||
|
transaction_id, {}, 0, response);
|
||||||
|
}
|
||||||
|
std::vector<uint8_t> frame;
|
||||||
|
frame.reserve(it->second.total_length);
|
||||||
|
for (uint8_t index = 0; index < it->second.total_chunks; ++index) {
|
||||||
|
const auto chunk = it->second.request_chunks.find(index);
|
||||||
|
if (chunk == it->second.request_chunks.end()) {
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusInvalidFrame,
|
||||||
|
transaction_id, {}, 0, response);
|
||||||
|
}
|
||||||
|
frame.insert(frame.end(), chunk->second.begin(), chunk->second.end());
|
||||||
|
}
|
||||||
|
if (it->second.total_length != frame.size() || frame.size() < 4 ||
|
||||||
|
frame[0] != 0x28 || frame[1] != 0x01) {
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusInvalidFrame,
|
||||||
|
transaction_id, {}, 0, response);
|
||||||
|
}
|
||||||
|
if (!gateway_command_transactor_) {
|
||||||
|
return appendDalimasterTransferResponse(kDalimasterTransferStatusNoController,
|
||||||
|
transaction_id, {}, 0, response);
|
||||||
|
}
|
||||||
|
const auto transaction =
|
||||||
|
gateway_command_transactor_(frame, timeout_ms == 0 ? 700 : timeout_ms);
|
||||||
|
uint8_t status = kDalimasterTransferStatusOk;
|
||||||
|
switch (transaction.status) {
|
||||||
|
case GatewayKnxGatewayTransactionStatus::kOk:
|
||||||
|
status = kDalimasterTransferStatusOk;
|
||||||
|
break;
|
||||||
|
case GatewayKnxGatewayTransactionStatus::kInvalidFrame:
|
||||||
|
status = kDalimasterTransferStatusInvalidFrame;
|
||||||
|
break;
|
||||||
|
case GatewayKnxGatewayTransactionStatus::kQueueRejected:
|
||||||
|
status = kDalimasterTransferStatusQueueRejected;
|
||||||
|
break;
|
||||||
|
case GatewayKnxGatewayTransactionStatus::kTimeout:
|
||||||
|
status = kDalimasterTransferStatusTimeout;
|
||||||
|
break;
|
||||||
|
case GatewayKnxGatewayTransactionStatus::kNoResponse:
|
||||||
|
status = kDalimasterTransferStatusNoResponse;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
it->second.response_bytes = transaction.frames;
|
||||||
|
return appendDalimasterTransferResponse(status, transaction_id, it->second.response_bytes, 0,
|
||||||
|
response);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GatewayKnxBridge::appendDalimasterTransferResponse(
|
||||||
|
uint8_t transfer_status, uint16_t transaction_id, const std::vector<uint8_t>& response_bytes,
|
||||||
|
uint8_t chunk_index, std::vector<uint8_t>* response) const {
|
||||||
|
if (response == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const size_t total_chunks =
|
||||||
|
std::max<size_t>(1, (response_bytes.size() + kDalimasterTransferChunkBytes - 1) /
|
||||||
|
kDalimasterTransferChunkBytes);
|
||||||
|
if (chunk_index >= total_chunks) {
|
||||||
|
response->assign({0x00, kDalimasterTransferStatusInvalidFrame,
|
||||||
|
static_cast<uint8_t>(transaction_id & 0xff),
|
||||||
|
static_cast<uint8_t>((transaction_id >> 8) & 0xff), 0x00, 0x00,
|
||||||
|
static_cast<uint8_t>(total_chunks & 0xff), chunk_index, 0x00});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const size_t start = static_cast<size_t>(chunk_index) * kDalimasterTransferChunkBytes;
|
||||||
|
const size_t chunk_len =
|
||||||
|
std::min<size_t>(kDalimasterTransferChunkBytes, response_bytes.size() - start);
|
||||||
|
response->clear();
|
||||||
|
response->reserve(9 + chunk_len);
|
||||||
|
response->push_back(0x00);
|
||||||
|
response->push_back(transfer_status);
|
||||||
|
response->push_back(static_cast<uint8_t>(transaction_id & 0xff));
|
||||||
|
response->push_back(static_cast<uint8_t>((transaction_id >> 8) & 0xff));
|
||||||
|
response->push_back(static_cast<uint8_t>(response_bytes.size() & 0xff));
|
||||||
|
response->push_back(static_cast<uint8_t>((response_bytes.size() >> 8) & 0xff));
|
||||||
|
response->push_back(static_cast<uint8_t>(total_chunks & 0xff));
|
||||||
|
response->push_back(chunk_index);
|
||||||
|
response->push_back(static_cast<uint8_t>(chunk_len & 0xff));
|
||||||
|
response->insert(response->end(), response_bytes.begin() + start,
|
||||||
|
response_bytes.begin() + start + chunk_len);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool GatewayKnxBridge::handleReg1TypeCommand(const uint8_t* data, size_t len,
|
bool GatewayKnxBridge::handleReg1TypeCommand(const uint8_t* data, size_t len,
|
||||||
std::vector<uint8_t>* response) {
|
std::vector<uint8_t>* response) {
|
||||||
if (len < 2 || response == nullptr) {
|
if (len < 2 || response == nullptr) {
|
||||||
|
|||||||
@@ -138,6 +138,20 @@ constexpr uint8_t kDaliCmdOnStepUp = 0x08;
|
|||||||
constexpr uint8_t kDaliCmdStopFade = 0xff;
|
constexpr uint8_t kDaliCmdStopFade = 0xff;
|
||||||
constexpr uint8_t kReg1DaliFunctionObjectIndex = 160;
|
constexpr uint8_t kReg1DaliFunctionObjectIndex = 160;
|
||||||
constexpr uint8_t kReg1DaliFunctionPropertyId = 1;
|
constexpr uint8_t kReg1DaliFunctionPropertyId = 1;
|
||||||
|
constexpr uint8_t kDalimasterDiscoveryFunctionPropertyId = 210;
|
||||||
|
constexpr uint8_t kDalimasterTransferFunctionPropertyId = 211;
|
||||||
|
constexpr uint8_t kDalimasterTransferChunkBytes = 192;
|
||||||
|
constexpr uint8_t kDalimasterTransferOpSingle = 0x01;
|
||||||
|
constexpr uint8_t kDalimasterTransferOpBegin = 0x02;
|
||||||
|
constexpr uint8_t kDalimasterTransferOpChunk = 0x03;
|
||||||
|
constexpr uint8_t kDalimasterTransferOpExecute = 0x04;
|
||||||
|
constexpr uint8_t kDalimasterTransferOpReadResponse = 0x05;
|
||||||
|
constexpr uint8_t kDalimasterTransferStatusOk = 0x00;
|
||||||
|
constexpr uint8_t kDalimasterTransferStatusInvalidFrame = 0x01;
|
||||||
|
constexpr uint8_t kDalimasterTransferStatusNoController = 0x02;
|
||||||
|
constexpr uint8_t kDalimasterTransferStatusQueueRejected = 0x03;
|
||||||
|
constexpr uint8_t kDalimasterTransferStatusTimeout = 0x04;
|
||||||
|
constexpr uint8_t kDalimasterTransferStatusNoResponse = 0x05;
|
||||||
constexpr uint8_t kReg1FunctionType = 2;
|
constexpr uint8_t kReg1FunctionType = 2;
|
||||||
constexpr uint8_t kReg1FunctionScan = 3;
|
constexpr uint8_t kReg1FunctionScan = 3;
|
||||||
constexpr uint8_t kReg1FunctionAssign = 4;
|
constexpr uint8_t kReg1FunctionAssign = 4;
|
||||||
|
|||||||
Reference in New Issue
Block a user