blob: 00ffa3a57193996333ca0f613f6b911f84b894b3 [file] [log] [blame]
/**
*
* Copyright (c) 2020 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
*
* Copyright (c) 2020 Silicon Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/****************************************************************************
* @file
* @brief This is the source for the plugin used to
*add an IAS Zone cluster server to a project. This
*source handles zone enrollment and storing of
* attributes from a CIE device, and provides an API
*for different plugins to post updated zone status
*values.
*******************************************************************************
******************************************************************************/
// *****************************************************************************
// * ias-zone-server.c
// *
// *
// *
// * Copyright 2015 Silicon Laboratories, Inc. *80*
// *****************************************************************************
#include "ias-zone-server.h"
#include <app-common/zap-generated/att-storage.h>
#include <app-common/zap-generated/attribute-id.h>
#include <app-common/zap-generated/attribute-type.h>
#include <app-common/zap-generated/callback.h>
#include <app-common/zap-generated/cluster-id.h>
#include <app-common/zap-generated/cluster-objects.h>
#include <app-common/zap-generated/command-id.h>
#include <app/CommandHandler.h>
#include <app/ConcreteCommandPath.h>
#include <app/util/af-event.h>
#include <app/util/af.h>
#include <app/util/binding-table.h>
#include <app/util/util.h>
#include <system/SystemLayer.h>
using namespace chip;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::IasZone;
#define UNDEFINED_ZONE_ID 0xFF
#define DELAY_TIMER_MS (1 * MILLISECOND_TICKS_PER_SECOND)
#define IAS_ZONE_SERVER_PAYLOAD_COMMAND_IDX 0x02
#define ZCL_FRAME_CONTROL_IDX 0x00
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
#if defined(EMBER_AF_PLUGIN_WWAH_APP_EVENT_RETRY_MANAGER)
#define NUM_QUEUE_ENTRIES EMBER_AF_PLUGIN_WWAH_APP_EVENT_RETRY_MANAGER_QUEUE_SIZE
#else
#define NUM_QUEUE_ENTRIES EMBER_AF_PLUGIN_IAS_ZONE_SERVER_QUEUE_DEPTH
#endif
#else
#define NUM_QUEUE_ENTRIES 0
#endif
#define DEFAULT_ENROLLMENT_METHOD EMBER_ZCL_IAS_ZONE_ENROLLMENT_MODE_REQUEST
// TODO: Need to figure out what needs to happen wrt HAL tokens here, but for
// now define ESZP_HOST to disable it. See
// https://github.com/project-chip/connectedhomeip/issues/3275
#define EZSP_HOST
typedef struct
{
EndpointId endpoint;
uint16_t status;
System::Clock::Timestamp eventTime;
} IasZoneStatusQueueEntry;
typedef struct
{
uint8_t entriesInQueue;
uint8_t startIdx;
uint8_t lastIdx;
IasZoneStatusQueueEntry buffer[NUM_QUEUE_ENTRIES];
} IasZoneStatusQueue;
//-----------------------------------------------------------------------------
// Globals
EmberEventControl emberAfPluginIasZoneServerManageQueueEventControl;
static EmberAfIasZoneEnrollmentMode enrollmentMethod;
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
IasZoneStatusQueue messageQueue;
// Status queue retry parameters
typedef struct
{
IasZoneStatusQueueRetryConfig config;
uint32_t currentBackoffTimeSec;
uint8_t currentRetryCount;
} IasZoneStatusQueueRetryParameters;
// Set up status queue retry parameters.
IasZoneStatusQueueRetryParameters queueRetryParams = {
.config = { .firstBackoffTimeSec = EMBER_AF_PLUGIN_IAS_ZONE_SERVER_FIRST_BACKOFF_TIME_SEC,
.backoffSeqCommonRatio = EMBER_AF_PLUGIN_IAS_ZONE_SERVER_BACKOFF_SEQUENCE_COMMON_RATIO,
.maxBackoffTimeSec = EMBER_AF_PLUGIN_IAS_ZONE_SERVER_MAX_BACKOFF_TIME_SEC,
#ifdef EMBER_AF_PLUGIN_IAS_ZONE_SERVER_UNLIMITED_RETRIES
.unlimitedRetries = true,
#else
.unlimitedRetries = false,
#endif
.maxRetryAttempts = EMBER_AF_PLUGIN_IAS_ZONE_SERVER_MAX_RETRY_ATTEMPTS },
.currentBackoffTimeSec = EMBER_AF_PLUGIN_IAS_ZONE_SERVER_FIRST_BACKOFF_TIME_SEC,
.currentRetryCount = 0,
};
static void resetCurrentQueueRetryParams(void)
{
queueRetryParams.currentRetryCount = 0;
queueRetryParams.currentBackoffTimeSec = queueRetryParams.config.firstBackoffTimeSec;
}
#endif // EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE
//-----------------------------------------------------------------------------
// Forward declarations
static void setZoneId(EndpointId endpoint, uint8_t zoneId);
static bool areZoneServerAttributesNonVolatile(EndpointId endpoint);
static bool isValidEnrollmentMode(EmberAfIasZoneEnrollmentMode method);
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
static uint16_t computeElapsedTimeQs(IasZoneStatusQueueEntry * entry);
static void bufferInit(IasZoneStatusQueue * ring);
static int16_t copyToBuffer(IasZoneStatusQueue * ring, const IasZoneStatusQueueEntry * entry);
static int16_t popFromBuffer(IasZoneStatusQueue * ring, IasZoneStatusQueueEntry * entry);
#endif
// TODO: https://github.com/project-chip/connectedhomeip/issues/3276 needs to be
// fixed to implement this for real.
EmberNetworkStatus emberAfNetworkState(void)
{
return EMBER_JOINED_NETWORK;
}
//-----------------------------------------------------------------------------
// Functions
static EmberStatus sendToClient(EndpointId endpoint)
{
EmberStatus status;
// If the device is not a network, there is no one to send to, so do nothing
if (emberAfNetworkState() != EMBER_JOINED_NETWORK)
{
return EMBER_NETWORK_DOWN;
}
// Remote endpoint need not be set, since it will be provided by the call to
// emberAfSendCommandUnicastToBindings()
emberAfSetCommandEndpoints(endpoint, 0);
// TODO: Figure out how this sending should actually work in Matter.
#if 0
// A binding table entry is created on Zone Enrollment for each endpoint, so
// a simple call to SendCommandUnicastToBinding will handle determining the
// destination endpoint, address, etc for us.
status = emberAfSendCommandUnicastToBindings();
#else
status = EMBER_ERR_FATAL;
#endif
if (EMBER_SUCCESS != status)
{
return status;
}
return status;
}
static void enrollWithClient(EndpointId endpoint)
{
EmberStatus status;
emberAfFillExternalBuffer((ZCL_CLUSTER_SPECIFIC_COMMAND | ZCL_FRAME_CONTROL_SERVER_TO_CLIENT), ZCL_IAS_ZONE_CLUSTER_ID,
ZCL_ZONE_ENROLL_REQUEST_COMMAND_ID, "vv", EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ZONE_TYPE,
EMBER_AF_MANUFACTURER_CODE);
status = sendToClient(endpoint);
if (status == EMBER_SUCCESS)
{
emberAfIasZoneClusterPrintln("Sent enroll request to IAS Zone client.");
}
else
{
emberAfIasZoneClusterPrintln("Error sending enroll request: 0x%x\n", status);
}
}
Protocols::InteractionModel::Status
MatterIasZoneClusterServerPreAttributeChangedCallback(const app::ConcreteAttributePath & attributePath,
EmberAfAttributeType attributeType, uint16_t size, uint8_t * value)
{
uint8_t i;
bool zeroAddress;
EmberBindingTableEntry bindingEntry;
NodeId destNodeId;
EndpointId endpoint = attributePath.mEndpointId;
uint8_t ieeeAddress[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
// If this is not a CIE Address write, the CIE address has already been
// written, or the IAS Zone server is already enrolled, do nothing.
if (attributePath.mAttributeId != ZCL_IAS_CIE_ADDRESS_ATTRIBUTE_ID || emberAfCurrentCommand() == NULL)
{
return Protocols::InteractionModel::Status::Success;
}
memcpy(&destNodeId, value, sizeof(NodeId));
// Create the binding table entry
// This code assumes that the endpoint and device that is setting the CIE
// address is the CIE device itself, and as such the remote endpoint to bind
// to is the endpoint that generated the attribute change. This
// assumption is made based on analysis of the behavior of CIE devices
// currently existing in the field.
bindingEntry.type = EMBER_UNICAST_BINDING;
bindingEntry.local = endpoint;
bindingEntry.clusterId = MakeOptional(ZCL_IAS_ZONE_CLUSTER_ID);
bindingEntry.remote = emberAfCurrentCommand()->apsFrame->sourceEndpoint;
bindingEntry.nodeId = destNodeId;
bool foundSameEntry = false;
// Cycle through the binding table until we find a valid entry that is not
// being used, then use the created entry to make the bind.
for (const auto & currentBind : BindingTable::GetInstance())
{
// If the binding table entry created based on the response already exists
// do nothing.
if ((currentBind.local == bindingEntry.local) && (currentBind.clusterId == bindingEntry.clusterId) &&
(currentBind.remote == bindingEntry.remote) && (currentBind.type == bindingEntry.type))
{
foundSameEntry = true;
break;
}
}
if (!foundSameEntry)
{
BindingTable::GetInstance().Add(bindingEntry);
}
zeroAddress = true;
emberAfReadServerAttribute(endpoint, ZCL_IAS_ZONE_CLUSTER_ID, ZCL_IAS_CIE_ADDRESS_ATTRIBUTE_ID, (uint8_t *) ieeeAddress, 8);
for (i = 0; i < 8; i++)
{
if (ieeeAddress[i] != 0)
{
zeroAddress = false;
}
}
emberAfAppPrint("\nzero address: %d\n", zeroAddress);
if ((zeroAddress == true) && (enrollmentMethod == EMBER_ZCL_IAS_ZONE_ENROLLMENT_MODE_REQUEST))
{
// Only send the enrollment request if the mode is AUTO-ENROLL-REQUEST.
// We need to delay to get around a bug where we can't send a command
// at this point because then the Write Attributes response will not
// be sent. But we also delay to give the client time to configure us.
emberAfIasZoneClusterPrintln("Sending enrollment after %d ms", DELAY_TIMER_MS);
emberAfScheduleServerTickExtended(endpoint, ZCL_IAS_ZONE_CLUSTER_ID, DELAY_TIMER_MS, EMBER_AF_SHORT_POLL,
EMBER_AF_STAY_AWAKE);
}
return Protocols::InteractionModel::Status::Success;
}
EmberAfStatus emberAfPluginIasZoneClusterSetEnrollmentMethod(EndpointId endpoint, EmberAfIasZoneEnrollmentMode method)
{
EmberAfStatus status;
if (emberAfIasZoneClusterAmIEnrolled(endpoint))
{
emberAfIasZoneClusterPrintln("Error: Already enrolled");
status = EMBER_ZCL_STATUS_NOT_AUTHORIZED;
}
else if (!isValidEnrollmentMode(method))
{
emberAfIasZoneClusterPrintln("Invalid IAS Zone Server Enrollment Mode: %d", method);
status = EMBER_ZCL_STATUS_INVALID_VALUE;
}
else
{
enrollmentMethod = method;
#ifndef EZSP_HOST
halCommonSetToken(TOKEN_PLUGIN_IAS_ZONE_SERVER_ENROLLMENT_METHOD, &enrollmentMethod);
#endif
emberAfIasZoneClusterPrintln("IAS Zone Server Enrollment Mode: %d", method);
status = EMBER_ZCL_STATUS_SUCCESS;
}
return status;
}
static bool isValidEnrollmentMode(EmberAfIasZoneEnrollmentMode method)
{
return ((method == EMBER_ZCL_IAS_ZONE_ENROLLMENT_MODE_TRIP_TO_PAIR) ||
(method == EMBER_ZCL_IAS_ZONE_ENROLLMENT_MODE_AUTO_ENROLLMENT_RESPONSE) ||
(method == EMBER_ZCL_IAS_ZONE_ENROLLMENT_MODE_REQUEST));
}
bool emberAfIasZoneClusterAmIEnrolled(EndpointId endpoint)
{
EmberAfIasZoneState zoneState = EMBER_ZCL_IAS_ZONE_STATE_NOT_ENROLLED; // Clear this out completely.
EmberAfStatus status;
status =
emberAfReadServerAttribute(endpoint, ZCL_IAS_ZONE_CLUSTER_ID, ZCL_ZONE_STATE_ATTRIBUTE_ID, (unsigned char *) &zoneState,
1); // uint8_t size
return (status == EMBER_ZCL_STATUS_SUCCESS && zoneState == EMBER_ZCL_IAS_ZONE_STATE_ENROLLED);
}
static void updateEnrollState(EndpointId endpoint, bool enrolled)
{
EmberAfIasZoneState zoneState = (enrolled ? EMBER_ZCL_IAS_ZONE_STATE_ENROLLED : EMBER_ZCL_IAS_ZONE_STATE_NOT_ENROLLED);
emberAfWriteServerAttribute(endpoint, ZCL_IAS_ZONE_CLUSTER_ID, ZCL_ZONE_STATE_ATTRIBUTE_ID, (uint8_t *) &zoneState,
ZCL_INT8U_ATTRIBUTE_TYPE);
emberAfIasZoneClusterPrintln("IAS Zone Server State: %pEnrolled", (enrolled ? "" : "NOT "));
}
bool emberAfIasZoneClusterZoneEnrollResponseCallback(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath,
const Commands::ZoneEnrollResponse::DecodableType & commandData)
{
auto & enrollResponseCode = commandData.enrollResponseCode;
auto & zoneId = commandData.zoneId;
EndpointId endpoint;
uint8_t epZoneId;
EmberAfStatus status;
endpoint = emberAfCurrentEndpoint();
status = emberAfReadServerAttribute(endpoint, ZCL_IAS_ZONE_CLUSTER_ID, ZCL_ZONE_ID_ATTRIBUTE_ID, &epZoneId, sizeof(uint8_t));
if (status == EMBER_ZCL_STATUS_SUCCESS)
{
if (enrollResponseCode == EMBER_ZCL_IAS_ENROLL_RESPONSE_CODE_SUCCESS)
{
updateEnrollState(endpoint, true);
setZoneId(endpoint, zoneId);
}
else
{
updateEnrollState(endpoint, false);
setZoneId(endpoint, UNDEFINED_ZONE_ID);
}
return true;
}
emberAfAppPrintln("ERROR: IAS Zone Server unable to read zone ID attribute");
return true;
}
static EmberStatus sendZoneUpdate(uint16_t zoneStatus, uint16_t timeSinceStatusOccurredQs, EndpointId endpoint)
{
EmberStatus status;
if (!emberAfIasZoneClusterAmIEnrolled(endpoint))
{
return EMBER_INVALID_CALL;
}
emberAfFillExternalBuffer((ZCL_CLUSTER_SPECIFIC_COMMAND | ZCL_FRAME_CONTROL_SERVER_TO_CLIENT), ZCL_IAS_ZONE_CLUSTER_ID,
ZCL_ZONE_STATUS_CHANGE_NOTIFICATION_COMMAND_ID, "vuuv", zoneStatus,
0 /*extended status, must be zero per spec*/, emberAfPluginIasZoneServerGetZoneId(endpoint),
timeSinceStatusOccurredQs /* called "delay" in the spec */);
status = sendToClient(endpoint);
return status;
}
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
static void addNewEntryToQueue(const IasZoneStatusQueueEntry * newEntry)
{
emberAfIasZoneClusterPrintln("Adding new entry to queue");
copyToBuffer(&messageQueue, newEntry);
}
#endif
EmberStatus emberAfPluginIasZoneServerUpdateZoneStatus(EndpointId endpoint, uint16_t newStatus, uint16_t timeSinceStatusOccurredQs)
{
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
IasZoneStatusQueueEntry newBufferEntry;
newBufferEntry.endpoint = endpoint;
newBufferEntry.status = newStatus;
newBufferEntry.eventTime = System::SystemClock().GetMonotonicTimestamp();
#endif
EmberStatus sendStatus = EMBER_SUCCESS;
emberAfWriteServerAttribute(endpoint, ZCL_IAS_ZONE_CLUSTER_ID, ZCL_ZONE_STATUS_ATTRIBUTE_ID, (uint8_t *) &newStatus,
ZCL_INT16U_ATTRIBUTE_TYPE);
if (enrollmentMethod == EMBER_ZCL_IAS_ZONE_ENROLLMENT_MODE_TRIP_TO_PAIR)
{
// If unenrolled, send Zone Enroll Request command.
if (!emberAfIasZoneClusterAmIEnrolled(endpoint))
{
emberAfScheduleServerTick(endpoint, ZCL_IAS_ZONE_CLUSTER_ID, DELAY_TIMER_MS);
// Don't send the zone status update since not enrolled.
return EMBER_SUCCESS;
}
}
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
// If there are items in the queue waiting to send, this event should not
// be transmitted, as that could cause the client to receive the events out
// of order. Instead, just add the device to the queue
if (messageQueue.entriesInQueue == 0)
{
sendStatus = sendZoneUpdate(newStatus, timeSinceStatusOccurredQs, endpoint);
}
else
{
// Add a new element to the status queue and depending on the network state
// either try to resend the first element in the queue immediately or try to
// restart the parent research pattern.
addNewEntryToQueue(&newBufferEntry);
EmberNetworkStatus networkState = emberAfNetworkState();
if (networkState == EMBER_JOINED_NETWORK_NO_PARENT)
{
emberAfStartMoveCallback();
}
else if (networkState == EMBER_JOINED_NETWORK)
{
resetCurrentQueueRetryParams();
emberEventControlSetActive(&emberAfPluginIasZoneServerManageQueueEventControl);
}
return EMBER_SUCCESS;
}
#else
sendStatus = sendZoneUpdate(newStatus, timeSinceStatusOccurredQs, endpoint);
#endif
if (sendStatus == EMBER_SUCCESS)
{
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
// Add a new entry to the zoneUpdate buffer
addNewEntryToQueue(&newBufferEntry);
#endif
}
else
{
// If we're not on a network and never were, we don't need to do anything.
// If we used to be on a network and can't talk to our parent, we should
// try to rejoin the network and add the message to the queue
if (emberAfNetworkState() == EMBER_JOINED_NETWORK_NO_PARENT)
{
emberAfStartMoveCallback();
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
// Add a new entry to the zoneUpdate buffer
addNewEntryToQueue(&newBufferEntry);
#endif
}
emberAfIasZoneClusterPrintln("Failed to send IAS Zone update. Err 0x%x", sendStatus);
}
return sendStatus;
}
void emberAfPluginIasZoneServerManageQueueEventHandler(void)
{
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
IasZoneStatusQueueEntry * bufferStart;
uint16_t status;
uint16_t elapsedTimeQs;
uint16_t airTimeRemainingMs;
// If the queue was emptied without our interaction, do nothing
if (messageQueue.entriesInQueue == 0)
{
emberEventControlSetInactive(&emberAfPluginIasZoneServerManageQueueEventControl);
return;
}
// Otherwise, pull out the first item and attempt to retransmit it. The
// message complete callback will handle removing items from the queue
// To prevent an activity storm from flooding with retry requests, only
// re-send a message if it's been at least
// EMBER_AF_PLUGIN_IAS_ZONE_SERVER_MIN_OTA_TIME_MS since it was sent.
bufferStart = &(messageQueue.buffer[messageQueue.startIdx]);
elapsedTimeQs = computeElapsedTimeQs(bufferStart);
if (elapsedTimeQs < (EMBER_AF_PLUGIN_IAS_ZONE_SERVER_MIN_OTA_TIME_MS / (MILLISECOND_TICKS_PER_SECOND / 4)))
{
airTimeRemainingMs = EMBER_AF_PLUGIN_IAS_ZONE_SERVER_MIN_OTA_TIME_MS - (elapsedTimeQs * MILLISECOND_TICKS_PER_SECOND / 4);
emberAfIasZoneClusterPrintln("Not enough time passed for a retry, sleeping %d more mS", airTimeRemainingMs);
emberEventControlSetDelayMS(emberAfPluginIasZoneServerManageQueueEventControl, airTimeRemainingMs);
}
else
{
status = bufferStart->status;
emberAfIasZoneClusterPrintln("Attempting to resend a queued zone status update (status: 0x%02X, "
"event time (s): %d) with time of %d. Retry count: %d",
bufferStart->status, bufferStart->eventTime / MILLISECOND_TICKS_PER_SECOND, elapsedTimeQs,
queueRetryParams.currentRetryCount);
sendZoneUpdate(status, elapsedTimeQs, bufferStart->endpoint);
emberEventControlSetInactive(&emberAfPluginIasZoneServerManageQueueEventControl);
}
#else
emberEventControlSetInactive(&emberAfPluginIasZoneServerManageQueueEventControl);
#endif
}
void emberAfIasZoneClusterServerInitCallback(EndpointId endpoint)
{
EmberAfIasZoneType zoneType;
if (!areZoneServerAttributesNonVolatile(endpoint))
{
emberAfAppPrint("WARNING: ATTRIBUTES ARE NOT BEING STORED IN FLASH! ");
emberAfAppPrintln("DEVICE WILL NOT FUNCTION PROPERLY AFTER REBOOTING!!");
}
#ifndef EZSP_HOST
halCommonGetToken(&enrollmentMethod, TOKEN_PLUGIN_IAS_ZONE_SERVER_ENROLLMENT_METHOD);
#else
enrollmentMethod = DEFAULT_ENROLLMENT_METHOD;
#endif
if (!isValidEnrollmentMode(enrollmentMethod))
{
// Default Enrollment Method to AUTO-ENROLL-REQUEST.
enrollmentMethod = DEFAULT_ENROLLMENT_METHOD;
}
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
bufferInit(&messageQueue);
#endif
zoneType = (EmberAfIasZoneType) EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ZONE_TYPE;
emberAfWriteAttribute(endpoint, ZCL_IAS_ZONE_CLUSTER_ID, ZCL_ZONE_TYPE_ATTRIBUTE_ID, CLUSTER_MASK_SERVER, (uint8_t *) &zoneType,
ZCL_INT16U_ATTRIBUTE_TYPE);
emberAfPluginIasZoneServerUpdateZoneStatus(endpoint,
0, // status: All alarms cleared
0); // time since status occurred
}
void emberAfIasZoneClusterServerTickCallback(EndpointId endpoint)
{
enrollWithClient(endpoint);
}
uint8_t emberAfPluginIasZoneServerGetZoneId(EndpointId endpoint)
{
uint8_t zoneId = UNDEFINED_ZONE_ID;
emberAfReadServerAttribute(endpoint, ZCL_IAS_ZONE_CLUSTER_ID, ZCL_ZONE_ID_ATTRIBUTE_ID, &zoneId,
emberAfGetDataSize(ZCL_INT8U_ATTRIBUTE_TYPE));
return zoneId;
}
//------------------------------------------------------------------------------
//
// This function will verify that all attributes necessary for the IAS zone
// server to properly retain functionality through a power failure are
// non-volatile.
//
//------------------------------------------------------------------------------
static bool areZoneServerAttributesNonVolatile(EndpointId endpoint)
{
if (!emberAfIsNonVolatileAttribute(endpoint, IasZone::Id, Attributes::IasCieAddress::Id, true) ||
!emberAfIsNonVolatileAttribute(endpoint, IasZone::Id, Attributes::ZoneState::Id, true) ||
!emberAfIsNonVolatileAttribute(endpoint, IasZone::Id, Attributes::ZoneType::Id, true) ||
!emberAfIsNonVolatileAttribute(endpoint, IasZone::Id, Attributes::ZoneId::Id, true))
{
return false;
}
return true;
}
static void setZoneId(EndpointId endpoint, uint8_t zoneId)
{
emberAfIasZoneClusterPrintln("IAS Zone Server Zone ID: 0x%X", zoneId);
emberAfWriteServerAttribute(endpoint, ZCL_IAS_ZONE_CLUSTER_ID, ZCL_ZONE_ID_ATTRIBUTE_ID, &zoneId, ZCL_INT8U_ATTRIBUTE_TYPE);
}
static void unenrollSecurityDevice(EndpointId endpoint)
{
uint8_t ieeeAddress[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
uint16_t zoneType = EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ZONE_TYPE;
emberAfWriteServerAttribute(endpoint, ZCL_IAS_ZONE_CLUSTER_ID, ZCL_IAS_CIE_ADDRESS_ATTRIBUTE_ID, (uint8_t *) ieeeAddress,
ZCL_NODE_ID_ATTRIBUTE_TYPE);
emberAfWriteServerAttribute(endpoint, ZCL_IAS_ZONE_CLUSTER_ID, ZCL_ZONE_TYPE_ATTRIBUTE_ID, (uint8_t *) &zoneType,
ZCL_INT16U_ATTRIBUTE_TYPE);
setZoneId(endpoint, UNDEFINED_ZONE_ID);
// Restore the enrollment method back to its default value.
emberAfPluginIasZoneClusterSetEnrollmentMethod(endpoint, DEFAULT_ENROLLMENT_METHOD);
updateEnrollState(endpoint, false); // enrolled?
}
// If you leave the network, unenroll yourself.
void emberAfPluginIasZoneServerStackStatusCallback(EmberStatus status)
{
EndpointId endpoint;
uint8_t i;
// If the device has left the network, unenroll all endpoints on the device
// that are servers of the IAS Zone Cluster
if (status == EMBER_NETWORK_DOWN && emberAfNetworkState() == EMBER_NO_NETWORK)
{
for (i = 0; i < emberAfEndpointCount(); i++)
{
endpoint = emberAfEndpointFromIndex(i);
if (emberAfContainsServer(endpoint, ZCL_IAS_ZONE_CLUSTER_ID))
{
unenrollSecurityDevice(endpoint);
}
}
}
else if (status == EMBER_NETWORK_UP)
{
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
// If we're reconnecting, send any items still in the queue
emberAfIasZoneClusterPrintln("Rejoined network, retransmiting any queued event");
emberEventControlSetActive(&emberAfPluginIasZoneServerManageQueueEventControl);
#endif
}
}
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
EmberStatus emberAfIasZoneServerConfigStatusQueueRetryParams(IasZoneStatusQueueRetryConfig * retryConfig)
{
if (!(retryConfig->firstBackoffTimeSec) || (!retryConfig->backoffSeqCommonRatio) ||
(retryConfig->maxBackoffTimeSec < retryConfig->firstBackoffTimeSec) ||
(retryConfig->maxBackoffTimeSec > IAS_ZONE_STATUS_QUEUE_RETRY_ABS_MAX_BACKOFF_TIME_SEC) || (!retryConfig->maxRetryAttempts))
{
return EMBER_BAD_ARGUMENT;
}
queueRetryParams.config.firstBackoffTimeSec = retryConfig->firstBackoffTimeSec;
queueRetryParams.config.backoffSeqCommonRatio = retryConfig->backoffSeqCommonRatio;
queueRetryParams.config.maxBackoffTimeSec = retryConfig->maxBackoffTimeSec;
queueRetryParams.config.unlimitedRetries = retryConfig->unlimitedRetries;
queueRetryParams.config.maxRetryAttempts = retryConfig->maxRetryAttempts;
queueRetryParams.currentBackoffTimeSec = retryConfig->firstBackoffTimeSec;
queueRetryParams.currentRetryCount = 0;
return EMBER_SUCCESS;
}
void emberAfIasZoneServerSetStatusQueueRetryParamsToDefault(void)
{
queueRetryParams.config.firstBackoffTimeSec = EMBER_AF_PLUGIN_IAS_ZONE_SERVER_FIRST_BACKOFF_TIME_SEC;
queueRetryParams.config.backoffSeqCommonRatio = EMBER_AF_PLUGIN_IAS_ZONE_SERVER_BACKOFF_SEQUENCE_COMMON_RATIO;
queueRetryParams.config.maxBackoffTimeSec = EMBER_AF_PLUGIN_IAS_ZONE_SERVER_MAX_BACKOFF_TIME_SEC;
#ifdef EMBER_AF_PLUGIN_IAS_ZONE_SERVER_UNLIMITED_RETRIES
queueRetryParams.config.unlimitedRetries = true;
#else
queueRetryParams.config.unlimitedRetries = false;
#endif
queueRetryParams.config.maxRetryAttempts = EMBER_AF_PLUGIN_IAS_ZONE_SERVER_MAX_RETRY_ATTEMPTS;
queueRetryParams.currentBackoffTimeSec = EMBER_AF_PLUGIN_IAS_ZONE_SERVER_FIRST_BACKOFF_TIME_SEC;
queueRetryParams.currentRetryCount = 0;
}
void emberAfIasZoneServerDiscardPendingEventsInStatusQueue(void)
{
emberEventControlSetInactive(&emberAfPluginIasZoneServerManageQueueEventControl);
bufferInit(&messageQueue);
resetCurrentQueueRetryParams();
}
#if defined(EMBER_AF_PLUGIN_WWAH_APP_EVENT_RETRY_MANAGER)
EmberStatus emberAfWwahAppEventRetryManagerConfigBackoffParamsCallback(uint8_t firstBackoffTimeSeconds,
uint8_t backoffSeqCommonRatio,
uint32_t maxBackoffTimeSeconds,
uint8_t maxRedeliveryAttempts)
{
IasZoneStatusQueueRetryConfig retryConfig = { firstBackoffTimeSeconds, backoffSeqCommonRatio, maxBackoffTimeSeconds,
(maxRedeliveryAttempts == 0xFF), maxRedeliveryAttempts };
// Setting up retry parameters
return emberAfIasZoneServerConfigStatusQueueRetryParams(&retryConfig);
}
void emberAfWwahAppEventRetryManagerSetBackoffParamsToDefault(void)
{
emberAfIasZoneServerSetStatusQueueRetryParamsToDefault();
}
#endif // defined(EMBER_AF_PLUGIN_WWAH_APP_EVENT_RETRY_MANAGER)
void emberAfPluginIasZoneServerPrintQueue(void)
{
emberAfIasZoneClusterPrintln("%d/%d entries", messageQueue.entriesInQueue, NUM_QUEUE_ENTRIES);
for (int i = 0; i < messageQueue.entriesInQueue; i++)
{
emberAfIasZoneClusterPrintln("Entry %d: Endpoint: %d Status: %d EventTimeMs: %d", i, messageQueue.buffer[i].endpoint,
messageQueue.buffer[i].status, messageQueue.buffer[i].eventTime);
}
}
void emberAfPluginIasZoneServerPrintQueueConfig(void)
{
emberAfCorePrintln("First backoff time (sec): %d", queueRetryParams.config.firstBackoffTimeSec);
emberAfCorePrintln("Backoff sequence common ratio: %d", queueRetryParams.config.backoffSeqCommonRatio);
emberAfCorePrintln("Max backoff time (sec): %d", queueRetryParams.config.maxBackoffTimeSec);
emberAfCorePrintln("Max redelivery attempts: %d", queueRetryParams.config.maxRetryAttempts);
}
#endif // defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
// This callback will be generated any time the node receives an ACK or a NAK
// for a message transmitted for the IAS Zone Cluster Server. Note that this
// will not be called in the case that the message was not delivered to the
// destination when the destination is the only router the node is joined to.
// In that case, the command will never have been sent, as the device will have
// had no router by which to send the command.
void emberAfIasZoneClusterServerMessageSentCallback(const MessageSendDestination & destination, EmberApsFrame * apsFrame,
uint16_t msgLen, uint8_t * message, EmberStatus status)
{
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
uint8_t frameControl;
CommandId commandId;
IasZoneStatusQueueEntry dummyEntry;
// Verify that this response is for a ZoneStatusChangeNotification command
// by checking the message length, the command and direction bits of the
// Frame Control byte, and the command ID
if (msgLen < IAS_ZONE_SERVER_PAYLOAD_COMMAND_IDX)
{
return;
}
frameControl = message[ZCL_FRAME_CONTROL_IDX];
if (!(frameControl & ZCL_CLUSTER_SPECIFIC_COMMAND) || !(frameControl & ZCL_FRAME_CONTROL_SERVER_TO_CLIENT))
{
return;
}
commandId = message[IAS_ZONE_SERVER_PAYLOAD_COMMAND_IDX];
if (commandId != ZCL_ZONE_STATUS_CHANGE_NOTIFICATION_COMMAND_ID)
{
return;
}
// If a change status change notification command is not received by the
// client, delay the option specified amount of time and try to resend it.
// The event handler will perform the retransmit per the preset queue retry
// parameters, and the original send request will handle populating the buffer.
// Do not try to retransmit again if the maximum number of retries attempts
// is reached, this is however discarded if configured for unlimited retries.
if ((status == EMBER_DELIVERY_FAILED) &&
(queueRetryParams.config.unlimitedRetries ||
(queueRetryParams.currentRetryCount < queueRetryParams.config.maxRetryAttempts)))
{
queueRetryParams.currentRetryCount++;
emberAfIasZoneClusterPrintln("Status command update failed to send... Retrying in %d seconds...",
queueRetryParams.currentBackoffTimeSec);
// Delay according to the current retransmit backoff time.
emberEventControlSetDelayMS(emberAfPluginIasZoneServerManageQueueEventControl,
queueRetryParams.currentBackoffTimeSec * MILLISECOND_TICKS_PER_SECOND);
// The backoff time needs to be increased if the maximum backoff time is not reached yet.
if ((queueRetryParams.currentBackoffTimeSec * queueRetryParams.config.backoffSeqCommonRatio) <=
queueRetryParams.config.maxBackoffTimeSec)
{
queueRetryParams.currentBackoffTimeSec *= queueRetryParams.config.backoffSeqCommonRatio;
}
}
else
{
// If a command message was sent or max redelivery attempts were reached,
// remove it from the queue and move on to the next queued message until the queue is empty.
if (status == EMBER_SUCCESS)
{
emberAfIasZoneClusterPrintln("\nZone update successful, remove entry from queue");
}
else
{
emberAfIasZoneClusterPrintln("\nZone update unsuccessful, max retry attempts reached, remove entry from queue");
}
popFromBuffer(&messageQueue, &dummyEntry);
// Reset queue retry parameters.
resetCurrentQueueRetryParams();
if (messageQueue.entriesInQueue)
{
emberEventControlSetActive(&emberAfPluginIasZoneServerManageQueueEventControl);
}
}
#endif
}
#if defined(EMBER_AF_PLUGIN_IAS_ZONE_SERVER_ENABLE_QUEUE)
static void bufferInit(IasZoneStatusQueue * ring)
{
ring->entriesInQueue = 0;
ring->startIdx = 0;
ring->lastIdx = NUM_QUEUE_ENTRIES - 1;
}
// Add the entry to the buffer by copying, returning the index at which it was
// added. If the buffer is full, return -1, but still copy the entry over the
// last item of the buffer, to ensure that the last item in the buffer is
// always representative of the last known device state.
static int16_t copyToBuffer(IasZoneStatusQueue * ring, const IasZoneStatusQueueEntry * entry)
{
if (ring->entriesInQueue == NUM_QUEUE_ENTRIES)
{
ring->buffer[ring->lastIdx] = *entry;
return -1;
}
// Increment the last pointer. If it rolls over the size, circle it back to
// zero.
ring->lastIdx++;
if (ring->lastIdx >= NUM_QUEUE_ENTRIES)
{
ring->lastIdx = 0;
}
ring->buffer[ring->lastIdx].endpoint = entry->endpoint;
ring->buffer[ring->lastIdx].status = entry->status;
ring->buffer[ring->lastIdx].eventTime = entry->eventTime;
ring->entriesInQueue++;
return ring->lastIdx;
}
// Return the idx of the popped entry, or -1 if the buffer was empty.
static int16_t popFromBuffer(IasZoneStatusQueue * ring, IasZoneStatusQueueEntry * entry)
{
int16_t retVal;
if (ring->entriesInQueue == 0)
{
return -1;
}
// Copy out the first entry, then increment the start pointer. If it rolls
// over, circle it back to zero.
*entry = ring->buffer[ring->startIdx];
retVal = ring->startIdx;
ring->startIdx++;
if (ring->startIdx >= NUM_QUEUE_ENTRIES)
{
ring->startIdx = 0;
}
ring->entriesInQueue--;
return retVal;
}
uint16_t computeElapsedTimeQs(IasZoneStatusQueueEntry * entry)
{
System::Clock::Milliseconds64 currentTimeMs = System::SystemClock().GetMonotonicMilliseconds64();
int64_t deltaTimeMs = currentTimeMs.count() - entry->eventTime.count();
if (deltaTimeMs < 0)
{
deltaTimeMs = -deltaTimeMs + (0xFFFFFFFF - currentTimeMs);
}
return deltaTimeMs / MILLISECOND_TICKS_PER_QUARTERSECOND;
}
#endif
void MatterIasZonePluginServerInitCallback() {}