blob: 1d4b4245f77c4e7d232840cf770575cfeb9d84f0 [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 *
* Client Operation:
* 1. Look for ZDO device announce notification.
* 2. Perform ZDO match descriptor on device.
* 3. If supports IAS Zone Server, Add that server to our known list.
* Write CIE Address.
* 4. Read CIE address, verify it is ours. This is done mostly because
* the test case requires it.
* 5. Read the IAS Zone Server attributes.
* Record in table.
* 6. When we get an enroll request, give them our (only) zone ID.
* 7. When we get a notification, read their attributes.
*
* Improvements that could be made:
* Add support for multiple endpoints on server. Most often this is a
* legacy security system retrofitted with a single ZigBee radio. Therefore
* each sensor is on a different endpoint. Right now our client only
* handles a single endpoint per node.
*
* Integration with Poll Control. When the device boots we should configure
* its polling to make it possible to read/write its attributes.
*
* Update the emberAfIasZoneClientKnownServers list when we know a server
* un-enrolls. Right now, we don't have any way to tell when we don't need
* to keep track of a server anymore, i.e., when it un-enrolls. Therefore,
* we could potentially keep adding servers to our known list, and run out
* of room to add more. Fortunately, we have two things working for us:
* 1. Servers will most likely stay around in a network. It is unlikely
* that an IAS Zone Client in production will have to handle 254
* different servers.
* 2. If a server un-enrolls and then enrolls again, it will get the same
* Zone ID and have a spot in the list, since we store servers by
* long address.
*******************************************************************************
******************************************************************************/
#include "af.h"
#include "ias-zone-client.h"
//-----------------------------------------------------------------------------
// Globals
IasZoneDevice emberAfIasZoneClientKnownServers[EMBER_AF_PLUGIN_IAS_ZONE_CLIENT_MAX_DEVICES];
typedef enum {
IAS_ZONE_CLIENT_STATE_NONE,
IAS_ZONE_CLIENT_STATE_DISCOVER_ENDPOINT,
IAS_ZONE_CLIENT_STATE_SET_CIE_ADDRESS,
IAS_ZONE_CLIENT_STATE_READ_CIE_ADDRESS,
IAS_ZONE_CLIENT_STATE_READ_ATTRIBUTES,
} IasZoneClientState;
static IasZoneClientState iasZoneClientState = IAS_ZONE_CLIENT_STATE_NONE;
static uint8_t currentIndex = NO_INDEX;
static uint8_t myEndpoint = 0;
EmberEventControl emberAfPluginIasZoneClientStateMachineEventControl;
//-----------------------------------------------------------------------------
// Forward Declarations
void readIasZoneServerAttributes(EmberNodeId nodeId);
static void iasClientSaveCommand(void);
static void iasClientLoadCommand(void);
//-----------------------------------------------------------------------------
// Functions
void emberAfIasZoneClusterClientInitCallback(uint8_t endpoint)
{
emAfClearServers();
myEndpoint = endpoint;
iasClientLoadCommand();
}
void emAfClearServers(void)
{
MEMSET(emberAfIasZoneClientKnownServers, 0xFF,
sizeof(IasZoneDevice)
* EMBER_AF_PLUGIN_IAS_ZONE_CLIENT_MAX_DEVICES);
}
static void clearState(void)
{
currentIndex = 0;
iasZoneClientState = IAS_ZONE_CLIENT_STATE_NONE;
}
static void setServerZoneStatus(uint8_t serverIndex, uint16_t zoneStatus)
{
emberAfIasZoneClientKnownServers[serverIndex].zoneStatus = zoneStatus;
iasClientSaveCommand();
}
static void setServerIeee(uint8_t serverIndex, uint8_t* ieeeAddress)
{
MEMCOPY(emberAfIasZoneClientKnownServers[serverIndex].ieeeAddress, ieeeAddress, EUI64_SIZE);
iasClientSaveCommand();
}
static void clearServerIeee(uint8_t serverIndex)
{
MEMSET(emberAfIasZoneClientKnownServers[serverIndex].ieeeAddress,
0xFF,
sizeof(IasZoneDevice));
iasClientSaveCommand();
}
static void setServerNodeId(uint8_t serverIndex, EmberNodeId nodeId)
{
emberAfIasZoneClientKnownServers[serverIndex].nodeId = nodeId;
}
static void clearServerNodeId(uint8_t serverIndex)
{
emberAfIasZoneClientKnownServers[serverIndex].nodeId = EMBER_NULL_NODE_ID;
}
static void setServerZoneState(uint8_t serverIndex, uint8_t zoneState)
{
emberAfIasZoneClientKnownServers[serverIndex].zoneState = zoneState;
iasClientSaveCommand();
}
static void setServerEndpoint(uint8_t serverIndex, uint8_t endpoint)
{
emberAfIasZoneClientKnownServers[serverIndex].endpoint = endpoint;
iasClientSaveCommand();
}
static void setServerZoneType(uint8_t serverIndex, uint16_t zoneType)
{
emberAfIasZoneClientKnownServers[serverIndex].zoneType = zoneType;
iasClientSaveCommand();
}
static void setServerZoneId(uint8_t serverIndex, uint16_t zoneId)
{
emberAfIasZoneClientKnownServers[serverIndex].zoneId = zoneId;
iasClientSaveCommand();
}
static void setCurrentIndex(uint8_t serverIndex)
{
currentIndex = serverIndex;
iasClientSaveCommand();
}
static void setIasZoneClientState(uint8_t clientState)
{
iasZoneClientState = clientState;
iasClientSaveCommand();
}
static void iasClientSaveCommand(void)
{
#if defined(EZSP_HOST) && !defined(EMBER_TEST) && defined(UNIX_HOST)
FILE *fp;
uint16_t i, j;
// save zone server list
fp = fopen("iaszone.txt", "w");
for (i = 0; i < EMBER_AF_PLUGIN_IAS_ZONE_CLIENT_MAX_DEVICES; i++) {
if (emberAfIasZoneClientKnownServers[i].zoneId != 0xFF) {
fprintf(fp, "%x %x %x %x %x ", emberAfIasZoneClientKnownServers[i].zoneId,
emberAfIasZoneClientKnownServers[i].zoneStatus,
emberAfIasZoneClientKnownServers[i].zoneState,
emberAfIasZoneClientKnownServers[i].endpoint,
emberAfIasZoneClientKnownServers[i].zoneType);
for (j = 0; j < 8; j++) {
fprintf(fp, "%x ", emberAfIasZoneClientKnownServers[i].ieeeAddress[j]);
}
}
}
// Write something to mark the end of the file.
fprintf(fp, "ff");
assert(fclose(fp) == 0);
#endif //#if defined(EZSP_HOST) && !defined(EMBER_TEST) && defined(UNIX_HOST)
}
static void iasClientLoadCommand(void)
{
#if defined(EZSP_HOST) && !defined(EMBER_TEST) && defined(UNIX_HOST)
FILE *fp;
uint16_t i, j;
unsigned int data1, data2, data3, data4, data5;
fp = fopen("iaszone.txt", "r");
if (!fp) {
return;
}
for (i = 0; i < EMBER_AF_PLUGIN_IAS_ZONE_CLIENT_MAX_DEVICES; i++) {
if (feof(fp)) {
break;
}
fscanf(fp, "%x ", &data1);
if (data1 == 0xff) {
break;
}
fscanf(fp,
"%x %x %x %x ",
&data2,
&data3,
&data4,
&data5);
emberAfIasZoneClientKnownServers[i].zoneId = (uint8_t) data1;
emberAfIasZoneClientKnownServers[i].zoneStatus = (uint16_t) data2;
emberAfIasZoneClientKnownServers[i].zoneState = (uint8_t) data3;
emberAfIasZoneClientKnownServers[i].endpoint = (uint8_t) data4;
emberAfIasZoneClientKnownServers[i].zoneType = (uint16_t) data5;
for (j = 0; j < 8; j++) {
fscanf(fp, "%x ", &data1);
emberAfIasZoneClientKnownServers[i].ieeeAddress[j] = (uint8_t) data1;
}
}
assert(fclose(fp) == 0);
#endif // #if defined(EZSP_HOST) && !defined(EMBER_TEST) && defined(UNIX_HOST)
}
static uint8_t findIasZoneServerByIeee(uint8_t* ieeeAddress)
{
uint8_t i;
for (i = 0; i < EMBER_AF_PLUGIN_IAS_ZONE_CLIENT_MAX_DEVICES; i++) {
if (0 == MEMCOMPARE(ieeeAddress,
emberAfIasZoneClientKnownServers[i].ieeeAddress,
EUI64_SIZE)) {
return i;
}
}
return NO_INDEX;
}
static uint8_t findIasZoneServerByNodeId(EmberNodeId nodeId)
{
uint8_t i;
for (i = 0; i < EMBER_AF_PLUGIN_IAS_ZONE_CLIENT_MAX_DEVICES; i++) {
if (nodeId == emberAfIasZoneClientKnownServers[i].nodeId) {
return i;
}
}
// If we didn't find the node ID in the table, see if the stack knows about
// it.
EmberEUI64 eui64;
if (emberLookupEui64ByNodeId(nodeId, eui64) == EMBER_SUCCESS) {
i = findIasZoneServerByIeee(eui64);
if (i != NO_INDEX) {
setServerNodeId(i, nodeId);
}
}
return i;
}
bool emberAfIasZoneClusterZoneStatusChangeNotificationCallback(uint16_t zoneStatus,
uint8_t extendedStatus,
uint8_t zoneId,
uint16_t delay)
{
uint8_t serverIndex = findIasZoneServerByNodeId(emberAfCurrentCommand()->source);
uint8_t status = EMBER_ZCL_STATUS_NOT_FOUND;
if (serverIndex != NO_INDEX) {
status = EMBER_ZCL_STATUS_SUCCESS;
setServerZoneStatus(serverIndex, zoneStatus);
emberAfIasZoneClusterPrintln("Zone %d status change, 0x%2X from 0x%2X",
zoneId,
zoneStatus,
emberAfCurrentCommand()->source);
// The Test case calls for readding attributes after status change.
// that is silly for the production device.
// readIasZoneServerAttributes(emberAfCurrentCommand()->source);
}
emberAfSendDefaultResponse(emberAfCurrentCommand(), status);
return true;
}
bool emberAfIasZoneClusterZoneEnrollRequestCallback(uint16_t zoneType,
uint16_t manufacturerCode)
{
EmberAfIasEnrollResponseCode responseCode = EMBER_ZCL_IAS_ENROLL_RESPONSE_CODE_NO_ENROLL_PERMIT;
uint8_t zoneId = UNKNOWN_ZONE_ID;
uint8_t serverIndex = findIasZoneServerByNodeId(emberAfCurrentCommand()->source);
EmberStatus status;
if (serverIndex != NO_INDEX) {
responseCode = EMBER_ZCL_IAS_ENROLL_RESPONSE_CODE_SUCCESS;
zoneId = serverIndex;
setServerZoneId(serverIndex, zoneId);
}
emberAfFillCommandIasZoneClusterZoneEnrollResponse(responseCode,
zoneId);
// Need to send this command with our source EUI because the server will
// check our EUI64 against his CIE Address to see if we're his CIE.
emberAfGetCommandApsFrame()->options |= EMBER_APS_OPTION_SOURCE_EUI64;
status = emberAfSendResponse();
emberAfCorePrintln("Sent enroll response with responseCode: 0x%X, zoneId: 0x%X, status: 0x%X",
responseCode,
zoneId,
status);
return true;
}
void emberAfPluginIasZoneClientStateMachineEventHandler(void)
{
emberAfIasZoneClusterPrintln("IAS Zone Client Timeout waiting for message response.");
emberEventControlSetInactive(emberAfPluginIasZoneClientStateMachineEventControl);
clearState();
}
static uint8_t addServer(EmberNodeId nodeId, uint8_t* ieeeAddress)
{
uint8_t i = findIasZoneServerByIeee(ieeeAddress);
if (i != NO_INDEX) {
return i;
}
for (i = 0; i < EMBER_AF_PLUGIN_IAS_ZONE_CLIENT_MAX_DEVICES; i++) {
const uint8_t unsetEui64[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
if (0 == MEMCOMPARE(emberAfIasZoneClientKnownServers[i].ieeeAddress,
unsetEui64,
EUI64_SIZE)) {
setServerIeee(i, ieeeAddress);
setServerNodeId(i, nodeId);
setServerEndpoint(i, UNKNOWN_ENDPOINT);
return i;
}
}
return NO_INDEX;
}
static void removeServer(uint8_t* ieeeAddress)
{
uint8_t index = findIasZoneServerByIeee(ieeeAddress);
clearServerIeee(index);
clearServerNodeId(index);
}
static EmberStatus sendCommand(EmberNodeId destAddress)
{
emberAfSetCommandEndpoints(myEndpoint, emberAfIasZoneClientKnownServers[currentIndex].endpoint);
EmberStatus status = emberAfSendCommandUnicast(EMBER_OUTGOING_DIRECT, destAddress);
emberAfIasZoneClusterPrintln("Sent IAS Zone Client Command to 0x%2X (%d -> %d) status: 0x%X",
destAddress,
myEndpoint,
emberAfIasZoneClientKnownServers[currentIndex].endpoint,
status);
if (status != EMBER_SUCCESS) {
clearState();
}
return status;
}
static void setCieAddress(EmberNodeId destAddress)
{
uint8_t writeAttributes[] = {
LOW_BYTE(ZCL_IAS_CIE_ADDRESS_ATTRIBUTE_ID),
HIGH_BYTE(ZCL_IAS_CIE_ADDRESS_ATTRIBUTE_ID),
ZCL_IEEE_ADDRESS_ATTRIBUTE_TYPE,
0, 0, 0, 0, 0, 0, 0, 0, // ieee (filled in later)
};
emberAfGetEui64(&writeAttributes[3]);
emberAfFillCommandGlobalClientToServerWriteAttributes(ZCL_IAS_ZONE_CLUSTER_ID,
writeAttributes,
sizeof(writeAttributes));
emberAfIasZoneClusterPrintln("Writing CIE Address to IAS Zone Server");
if (EMBER_SUCCESS == sendCommand(destAddress)) {
setIasZoneClientState(IAS_ZONE_CLIENT_STATE_SET_CIE_ADDRESS);
}
}
static void iasZoneClientServiceDiscoveryCallback(const EmberAfServiceDiscoveryResult* result)
{
if (result->status == EMBER_AF_UNICAST_SERVICE_DISCOVERY_COMPLETE_WITH_RESPONSE
&& result->zdoRequestClusterId == MATCH_DESCRIPTORS_REQUEST) {
const EmberAfEndpointList* endpointList = (const EmberAfEndpointList *)result->responseData;
if (endpointList->count > 0) {
setServerEndpoint(currentIndex, endpointList->list[0]);
emberAfIasZoneClusterPrintln("Device 0x%2X supports IAS Zone Server",
result->matchAddress);
setCieAddress(result->matchAddress);
return;
}
}
clearState();
}
static void checkForIasZoneServer(EmberNodeId emberNodeId, uint8_t* ieeeAddress)
{
uint8_t endpointIndex = emberAfIndexFromEndpoint(myEndpoint);
uint16_t profileId = emberAfProfileIdFromIndex(endpointIndex);
uint8_t serverIndex = addServer(emberNodeId, ieeeAddress);
if (serverIndex == NO_INDEX) {
emberAfIasZoneClusterPrintln("Error: Could not add IAS Zone server.");
return;
}
setCurrentIndex(serverIndex);
if (emberAfIasZoneClientKnownServers[serverIndex].endpoint != UNKNOWN_ENDPOINT) {
// If a remote endpoint that you have already seen announces itself,
// write your IEEE in them just in case they left and are rejoining. --agkeesle
// Bug: EMAPPFWKV2-1078
setCieAddress(emberNodeId);
emberAfIasZoneClusterPrintln("Node 0x%2X already known to IAS client", emberNodeId);
return;
}
EmberStatus status = emberAfFindDevicesByProfileAndCluster(emberNodeId,
profileId,
ZCL_IAS_ZONE_CLUSTER_ID,
true, // server cluster?
iasZoneClientServiceDiscoveryCallback);
if (status != EMBER_SUCCESS) {
emberAfIasZoneClusterPrintln("Error: Failed to initiate service discovery for IAS Zone Server 0x%2X", emberNodeId);
clearState();
}
}
void emberAfPluginIasZoneClientZdoMessageReceivedCallback(EmberNodeId emberNodeId,
EmberApsFrame* apsFrame,
uint8_t* message,
uint16_t length)
{
emberAfIasZoneClusterPrintln("Incoming ZDO, Cluster: 0x%2X", apsFrame->clusterId);
if (apsFrame->clusterId == END_DEVICE_ANNOUNCE) {
checkForIasZoneServer(emberNodeId, &(message[3]));
}
}
void readIasZoneServerAttributes(EmberNodeId nodeId)
{
uint8_t iasZoneAttributeIds[] = {
LOW_BYTE(ZCL_ZONE_STATE_ATTRIBUTE_ID),
HIGH_BYTE(ZCL_ZONE_STATE_ATTRIBUTE_ID),
LOW_BYTE(ZCL_ZONE_TYPE_ATTRIBUTE_ID),
HIGH_BYTE(ZCL_ZONE_TYPE_ATTRIBUTE_ID),
LOW_BYTE(ZCL_ZONE_STATUS_ATTRIBUTE_ID),
HIGH_BYTE(ZCL_ZONE_STATUS_ATTRIBUTE_ID),
};
emberAfFillCommandGlobalClientToServerReadAttributes(ZCL_IAS_ZONE_CLUSTER_ID,
iasZoneAttributeIds,
sizeof(iasZoneAttributeIds));
if (EMBER_SUCCESS == sendCommand(nodeId)) {
setIasZoneClientState(IAS_ZONE_CLIENT_STATE_READ_ATTRIBUTES);
}
}
void readIasZoneServerCieAddress(EmberNodeId nodeId)
{
uint8_t iasZoneAttributeIds[] = {
LOW_BYTE(ZCL_IAS_CIE_ADDRESS_ATTRIBUTE_ID),
HIGH_BYTE(ZCL_IAS_CIE_ADDRESS_ATTRIBUTE_ID),
};
emberAfFillCommandGlobalClientToServerReadAttributes(ZCL_IAS_ZONE_CLUSTER_ID,
iasZoneAttributeIds,
sizeof(iasZoneAttributeIds));
if (EMBER_SUCCESS == sendCommand(nodeId)) {
setIasZoneClientState(IAS_ZONE_CLIENT_STATE_READ_CIE_ADDRESS);
}
}
void emberAfPluginIasZoneClientWriteAttributesResponseCallback(EmberAfClusterId clusterId,
uint8_t * buffer,
uint16_t bufLen)
{
if (clusterId == ZCL_IAS_ZONE_CLUSTER_ID
&& iasZoneClientState == IAS_ZONE_CLIENT_STATE_SET_CIE_ADDRESS
&& buffer[0] == EMBER_ZCL_STATUS_SUCCESS) {
readIasZoneServerCieAddress(emberAfCurrentCommand()->source);
return;
}
return;
}
void emberAfPluginIasZoneClientReadAttributesResponseCallback(EmberAfClusterId clusterId,
uint8_t * buffer,
uint16_t bufLen)
{
uint8_t zoneStatus, zoneType, zoneState;
if (clusterId == ZCL_IAS_ZONE_CLUSTER_ID
&& (iasZoneClientState == IAS_ZONE_CLIENT_STATE_READ_ATTRIBUTES
|| iasZoneClientState == IAS_ZONE_CLIENT_STATE_READ_CIE_ADDRESS)) {
uint16_t i = 0;
while ((i + 3) <= bufLen) { // 3 to insure we can read at least the attribute ID
// and the status
uint16_t attributeId = buffer[i] + (buffer[i + 1] << 8);
uint8_t status = buffer[i + 2];
i += 3;
//emberAfIasZoneClusterPrintln("Parsing Attribute 0x%2X, Status: 0x%X", attributeId, status);
if (status == EMBER_ZCL_STATUS_SUCCESS) {
if ((i + 1) > bufLen) {
// Too short, dump the message.
return;
}
i++; // skip the type of the attribute. We already know what it should be.
switch (attributeId) {
case ZCL_ZONE_STATUS_ATTRIBUTE_ID:
if ((i + 2) > bufLen) {
// Too short, dump the message.
return;
}
zoneStatus = (buffer[i] + (buffer[i + 1] << 8));
setServerZoneStatus(currentIndex, zoneStatus);
i += 2;
break;
case ZCL_ZONE_TYPE_ATTRIBUTE_ID:
if ((i + 2) > bufLen) {
// Too short, dump the message.
return;
}
zoneType = (buffer[i] + (buffer[i + 1] << 8));
setServerZoneType(currentIndex, zoneType);
i += 2;
break;
case ZCL_ZONE_STATE_ATTRIBUTE_ID:
if ((i + 1) > bufLen) {
// Too short, dump the message
return;
}
zoneState = buffer[i];
setServerZoneState(currentIndex, zoneState);
i++;
break;
case ZCL_IAS_CIE_ADDRESS_ATTRIBUTE_ID: {
uint8_t myIeee[EUI64_SIZE];
emberAfGetEui64(myIeee);
if ((i + 8) > bufLen) {
// Too short, dump the message
} else if (0 != MEMCOMPARE(&(buffer[i]), myIeee, EUI64_SIZE)) {
emberAfIasZoneClusterPrintln("CIE Address not set to mine, removing IAS zone server.");
removeServer(&(buffer[i]));
clearState();
} else {
readIasZoneServerAttributes(emberAfCurrentCommand()->source);
}
return;
}
}
}
}
emberAfIasZoneClusterPrintln("Retrieved IAS Zone Server attributes from 0x%2X",
emberAfCurrentCommand()->source);
clearState();
}
}