| /** |
| * |
| * 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() {} |