How to add a custom attribute to your Z-Stack application

Juli 26, 2019

Sind Sie schon einmal mit ZigBee-Produkten in Berührung gekommen? Sie sind jetzt überall zu finden. Man findet sie als kleine Sensoren in Gebäuden oder als Wandschalter, wenn man keine Kabel in der Wand verlegen möchte. ZigBee ist ein recht vielseitiges drahtloses Protokoll. Es basiert auf IEEE 802.15.4.

Die ZigBee Aliance hat verschiedene Anwendungen beschrieben, in denen ZigBee eingesetzt werden kann. Diese Funktionalitäten sind in Clustern gruppiert. Jedem Cluster sind Befehle und Attribute zugeordnet.

Ein Beispiel: Eine Glühbirne oder ein Schalter würde sich auf den OnOff ZigBeeCluster verlassen. Dieser Cluster definiert die Befehle On, Off und Toggle. Das entsprechende Attribut wäre ein OnOff-Attribut, das den aktuellen Zustand des Schalters oder der Glühbirne darstellt.

Es gibt noch viele weitere vordefinierte Cluster. So gibt es beispielsweise Cluster für Smart-Metering-Anwendungen, Beleuchtung (RGB, Hue usw.), Hausautomatisierung und so weiter.

Nachdem ich mehrere verschiedene Sensoren und Glühbirnen verwendet habe, die alle ZigBee unterstützen, habe ich darüber nachgedacht, mein eigenes ZigBee-Endgerät zu bauen. Dafür habe ich den TI CC2530/31 Chipsatz als SOC gewählt. Dieser Chip ist relativ leicht erhältlich, auch auf einigen chinesischen Marktportalen 😉 und er kommt mit einer gut dokumentierten Firmware-Bibliothek namens Z-Stack.

Der Z-Stack macht es viel einfacher, ein eigenes Gerät mit ZigBee-Funktionalität zu implementieren. Früher dachte ich immer, dass ich all diese Dinge selbst programmieren sollte und muss. Sei es, weil ich nichts von der Existenz solcher Stacks wusste oder weil ich dachte, dass ich die tiefsten inneren Vorgänge eines solchen Netzwerks verstehen muss. Heute kann ich sagen, dass es gar nicht so schlecht war, diese Erfahrungen zu machen. Aber heutzutage sind meine Projekte viel komplexer als früher und ich mag es, wenn ich mit Hilfe von bereits funktionierenden Codebasen loslegen kann.

Heute zeige ich Ihnen, wie Sie Ihre eigenen, benutzerdefinierten Attribute zu einer Z-Stack-basierten Anwendung hinzufügen können und wie Sie aus einer zigbee2mqtt-Umgebung mit Hilfe von mqtt- und zigbee sheperd-Konvertern darauf schreiben können.

Was Sie brauchen werden:

  • IAR-Embedded Workbench für 8051. Es ist eine 30-Tage-Testversion verfügbar
  • CC-Debugger zum Herunterladen Ihres Codes auf das Target
  • Zielplatine. Es könnte auch ein CC2531 USB-Stick sein
  • zigbee2mqtt zum Laufen bringen (Siehe hier: https://www.zigbee2mqtt.io/getting_started/running_zigbee2mqtt.html)
  • mqtt-Client. MQTT.fx erledigt die Arbeit.
  • Text-Editor
  • Win SCP zum komfortablen Editieren der zigbee2mqtt Konfigurationsdateien
  • Z-Stack 1.2.2 Firmware (http://www.ti.com/tool/Z-STACK-ARCHIVE auch Z-STACK-HOME genannt

Öffnen Sie ein Beispielprojekt aus Ihrem Z-STACK Ordner. Zum Beispiel SampleLight.eww

Unter App/zcl_samplelight.c finden Sie diese Code-Zeile:

  // Register the application's attribute list
  zcl_registerAttrList( SAMPLELIGHT_ENDPOINT, zclSampleLight_NumAttributes, zclSampleLight_Attrs );

Hier registrieren wir alle unsere Attribute bei den zugrunde liegenden Betriebssystemfunktionen und -methoden.

Was passiert also, wenn unser Gerät eine Schreibanforderung für ein Attribut erhält? Dieses Ereignis ruft zclProcessInWriteCmd in der Datei zcl.c in Zeile 4376 aus

376

#ifdef ZCL_WRITE
/*********************************************************************
 * @fn      processInWriteCmd
 *
 * @brief   Process the "Profile" Write and Write No Response Commands
 *
 * @param   pInMsg - incoming message to process
 *
 * @return  TRUE if command processed. FALSE, otherwise.
 */
static uint8 zclProcessInWriteCmd( zclIncoming_t *pInMsg )
{
  zclWriteCmd_t *writeCmd;
  zclWriteRspCmd_t *writeRspCmd;
  uint8 sendRsp = FALSE;
  uint8 j = 0;
  uint8 i;

  writeCmd = (zclWriteCmd_t *)pInMsg->attrCmd;
  if ( pInMsg->hdr.commandID == ZCL_CMD_WRITE )
  {
    // We need to send a response back - allocate space for it
    writeRspCmd = (zclWriteRspCmd_t *)zcl_mem_alloc( sizeof( zclWriteRspCmd_t )
            + sizeof( zclWriteRspStatus_t ) * writeCmd->numAttr );
    if ( writeRspCmd == NULL )
    {
      return FALSE; // EMBEDDED RETURN
    }

    sendRsp = TRUE;
  }

  for ( i = 0; i < writeCmd->numAttr; i++ )
  {
    zclAttrRec_t attrRec;
    zclWriteRec_t *statusRec = &(writeCmd->attrList[i]);

    if ( zclFindAttrRec( pInMsg->msg->endPoint, pInMsg->msg->clusterId,
                         statusRec->attrID, &attrRec ) )
    {
      
      
      if ( statusRec->dataType == attrRec.attr.dataType )
      {
        uint8 status;

        // Write the new attribute value
        if ( attrRec.attr.dataPtr != NULL )

This function will search in our attributes list whether there is a attribute defined which match’s to the actually received write request. The actual request is stored inside of statusRec

But to be able to reach this function and therefore write into any attributes, you must enable ZCL_WRITE in your configuration. You can do this with preprocessor directives.

Da wir wissen, wie das System mit den Attributen umgeht, müssen wir unser eigenes Attribut zu dieser Liste hinzufügen. Die Attribute sind in der Datei zcl_samplelight_data.c definiert – beginnend in Zeile 193

{
    ZCL_CLUSTER_ID_GEN_BASIC,             // Cluster IDs - defined in the foundation (ie. zcl.h)
    {  // Attribute record
      ATTRID_BASIC_HW_VERSION,            // Attribute ID - Found in Cluster Library header (ie. zcl_general.h)
      ZCL_DATATYPE_UINT8,                 // Data Type - found in zcl.h
      ACCESS_CONTROL_READ,                // Variable access control - found in zcl.h
      (void *)&zclSampleLight_HWRevision  // Pointer to attribute variable
    }
  },

Der obige Code zeigt ein Beispielattribut. Jetzt werden wir unser eigenes definieren und am Ende der Liste anhängen.

// My custom attribute
  {
    ZCL_CLUSTER_ID_GEN_BASIC,
    { // Attribute record
      0x4010, //Custom Attribute ID
      ZCL_DATATYPE_UINT16,
      (ACCESS_CONTROL_READ | ACCESS_REPORTABLE | ACCESS_CONTROL_WRITE),
      (void *)&REG_MY_CUSTOM_REGISTER
    }
  }

Der obige Code zeigt unser eigenes Attribut. Ich habe den GEN_BASIC Cluster verwendet und einen UINT16 Datentyp definiert. Ich habe einige Inline-Kommentare gesehen, in denen jemand schrieb, dass es Probleme mit uint8 und Booleschen Werten gibt. Wenn Sie jemals Probleme mit ihnen haben, versuchen Sie stattdessen uint16.

Jetzt müssen wir über Änderungen an unserem Attribut informiert werden. Leider scheint es dafür keine Out-of-the-Box-Lösung zu geben. Es gibt eine für vordefinierte Attribut-IDs, aber wir haben unsere eigenen definiert. Also habe ich einen Workaround gefunden, der die Aufgabe recht gut zu erfüllen scheint.

Immer wenn ein Ereignis eintritt, springt das System in eine Ereignisschleife, die in der Datei zcl_samplelight.c in Zeile 403 deklariert ist. Es gibt mehrere Fälle, aber das Schreiben in ein Attribut erfüllt keinen dieser Fälle, also werden wir den Standardfall verwenden.

  default:
    //Check if a variable has been updated!
    if(REG_MY_CUSTOM_REGISTER_OLD != REG_MY_CUSTOM_REGISTER)
    {
      REG_MY_CUSTOM_REGISTER_OLD = REG_MY_CUSTOM_REGISTER;
    }        
    break;

Hier prüfe ich, ob sich das Register geändert hat. Wenn ja, kann ich meinen gewünschten Codeabschnitt ausführen. Diese Methode der Beobachtung auf Änderungen hat den Vorteil, dass sie nicht nach Änderungen fragt. Wir werden über sie informiert. Ok, nicht explizit über Änderungen, die unser Attribut betreffen, aber der Overhead, der dadurch entsteht, wenn man mehrere Attribute innerhalb des Standardfalls hat, wird trotzdem marginal sein.

Hinzufügen von Konvertern zu zigbee2mqtt

Nun möchten wir in der Lage sein, die Attributwerte von zigbee2mqtt zu setzen. Dazu müssen wir zunächst ein neues Gerät definieren. https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html

 

 

Der obige Link gibt Ihnen einige grundlegende Informationen über diesen Prozess. Wenn Sie Ihr Gerät hinzugefügt haben, dann müssen Sie einen “ToZigbee”-Konverter bereitstellen.

In Ihrer devices.js müssen Sie etwas linke dieses bereitstellen:


toZigbee: [tz.FooBar],

Die Implementierung von tz.FooBar in der Datei toZigbee.js wird wie folgt aussehen:

FooBar: {
        key: ['MyAttributeKey'],
        convert: (key, value, message, type, postfix, options) => {
            if (type === 'set') {
                if (value === 'default') {
                    value = 1;
                }
				const lookup = {
                'Right': '0',
                'Left': '1',
                'IDLE': '2',
				};
				
				value = lookup[value];
				if( ((value >= 0) && value < 3) == false ) value = 2;
				
                return [{
                    cid: 'genBasic',
                    cmd: 'write',
                    cmdType: 'foundation',
                    zclData: [{
                        attrId: 0x4010,
                        dataType: 0x21,
                        attrData: value,
                    }],
                    cfg: cfg.default,
                }];
            }
        },
    },

Lassen Sie mich den obigen Code erklären. Das erste, was wir sehen werden, ist der Parameter MyAttributeKey. Dies ist der Parameter, unter dem Sie Ihr Attribut mit mqtt einstellen können. Ich werde Ihnen das später zeigen. In Zeile 8 haben wir eine Lookup-Tabelle für die Umwandlung von Strings in Zahlen. Dies ist optional, bietet aber einige Vorteile. In Zeile 14 wandeln wir die Eingabe von einer Zeichenkette in eine Zahl um. In der nächsten Zeile prüfen wir, ob ein gültiger Wert vorliegt. Zeile 17 ist für den Aufbau unseres Zigbee-Pakets verantwortlich.

Wir müssen auch die Cluster-ID (cid) definieren, die identisch ist mit dem, was wir bisher in unserer Firmware definiert haben. Attribut-ID und Datentyp müssen ebenfalls übereinstimmen. Für den Datentyp gibt es im Internet eine Liste, in der die verschiedenen Typen aufgeführt sind. http://www.zigbee.org/wp-content/uploads/2014/10/07-5123-06-zigbee-cluster-library-specification.pdf
Beginnend auf Seite 85. Achten Sie darauf, dass Sie die richtige Klasse wählen!

Wenn Sie Ihre Konfigurationsdatei geschrieben haben, ist es an der Zeit, zigbee2mqtt neu zu starten.

Testen Sie Ihre Arbeit!
Setzen Sie hier einen Haltepunkt und beginnen Sie mit dem Debugging.

  default:
    //Check if a variable has been updated!
    if(REG_MY_CUSTOM_REGISTER_OLD != REG_MY_CUSTOM_REGISTER)
    {
      REG_MY_CUSTOM_REGISTER_OLD = REG_MY_CUSTOM_REGISTER;
    }        
    break;

Nachdem das Gerät in zigbee2mqtt angezeigt wird, sollten Sie in der Lage sein, neue Attributwerte mit mqtt an das Gerät zu senden.

Öffnen Sie mqtt.fx, konfigurieren Sie Ihren Server und verbinden Sie sich. Gehen Sie nun auf die Registerkarte “Veröffentlichen” und geben Sie die folgende Zeichenfolge ein:

zigbee2mqtt/YOUR UNIQUE IDENTIFIER/set/MyAttributeKey

Ersetzen Sie IHREN EINZIGARTIGEN IDENTIFIER durch Ihren Identifikator, der im Konsolenprotokoll von zigbee2mqtt angezeigt wird.

Nun können Sie einige Werte von mqtt.fx an Ihren Server senden und zigbee2mqtt sollte diese Nachrichten an Ihr Gerät liefern. Dies wird etwa so aussehen:

zigbee2mqtt:info XX/XX/XXXX, XX:XX:XX PM Zigbee publish to device '0x00124xxxxxxxxxx', genBasic - write - 
[{"attrId":16400,"dataType":33,"attrData":"1"}] - {"manufSpec":0,"disDefaultRsp":0} - 8

Sie sollten jetzt auch Ihren Haltepunkt treffen und den neuen Wert sehen, der Ihrem Attribut zugewiesen wurde!

Gut gemacht! Ich hoffe, dies war hilfreich für jemanden.

 

Have you ever got in touch with ZigBee products? Now they are everywhere. You find them as small sensors in buildings or as wall switches where you don’t want to install cables into the wall. ZigBee is a quite versatile wireless protocol. It is based upon IEEE 802.15.4.

The ZigBee Aliance has descibed several different applications, where you can use ZigBee. These functionalities are grouped into clusters. Each cluster have associated commands and attributes.

For example: a light bulb or a switch would rely on the OnOff ZigBeeCluster. This cluster defines a On, Off and a Toggle command. The corresponding attribute would be a OnOff attribute which represents the actual state of the switch or the light bulb.

There are many more pre-defined clusters. For example there are clusters for smart metering applications, lighting (RGB, Hue, etc.), home automation and so on.

After using several different sensors and light bulbs which are all supporting ZigBee, i thought about making my own ZigBee end device. For that i have chosen the TI CC2530/31 chipset as SOC. This chip is relatively easy available, also on some chinese market portal 😉 and it come with a well documented firmware library called Z-Stack.

The Z-Stack makes it much more easy to implement your own custom device with ZigBee functionality. In the earlier days i always thought that i should and must code all these things by my self. This because i didn’t know about the existence of such stacks or just because i thought that i must understand the deepest inner processes of such a network. Today i can say that it was not that bad to get these experience. But these days, my projects are much more complex than before and i like to get jump started with the help of already working code bases.

Today i’m gonna show you how you can add your own, custom, attribute to a Z-Stack based application and how you can write to it from a zigbee2mqtt environment using mqtt and zigbee sheperd converters.

What you will need:

Open up an example Project from your Z-STACK folder. For example SampleLight.eww

Under App/zcl_samplelight.c you can find this line of code:

  // Register the application's attribute list
  zcl_registerAttrList( SAMPLELIGHT_ENDPOINT, zclSampleLight_NumAttributes, zclSampleLight_Attrs );

This is where we register all of our attributes to the underlaying operating system functions and methods.

So what happens, when our device receives a write request for a attribute? This event will call zclProcessInWriteCmd in the file zcl.c on line 4376

#ifdef ZCL_WRITE
/*********************************************************************
 * @fn      processInWriteCmd
 *
 * @brief   Process the "Profile" Write and Write No Response Commands
 *
 * @param   pInMsg - incoming message to process
 *
 * @return  TRUE if command processed. FALSE, otherwise.
 */
static uint8 zclProcessInWriteCmd( zclIncoming_t *pInMsg )
{
  zclWriteCmd_t *writeCmd;
  zclWriteRspCmd_t *writeRspCmd;
  uint8 sendRsp = FALSE;
  uint8 j = 0;
  uint8 i;

  writeCmd = (zclWriteCmd_t *)pInMsg->attrCmd;
  if ( pInMsg->hdr.commandID == ZCL_CMD_WRITE )
  {
    // We need to send a response back - allocate space for it
    writeRspCmd = (zclWriteRspCmd_t *)zcl_mem_alloc( sizeof( zclWriteRspCmd_t )
            + sizeof( zclWriteRspStatus_t ) * writeCmd->numAttr );
    if ( writeRspCmd == NULL )
    {
      return FALSE; // EMBEDDED RETURN
    }

    sendRsp = TRUE;
  }

  for ( i = 0; i < writeCmd->numAttr; i++ )
  {
    zclAttrRec_t attrRec;
    zclWriteRec_t *statusRec = &(writeCmd->attrList[i]);

    if ( zclFindAttrRec( pInMsg->msg->endPoint, pInMsg->msg->clusterId,
                         statusRec->attrID, &attrRec ) )
    {
      
      
      if ( statusRec->dataType == attrRec.attr.dataType )
      {
        uint8 status;

        // Write the new attribute value
        if ( attrRec.attr.dataPtr != NULL )

This function will search in our attributes list whether there is a attribute defined which match’s to the actually received write request. The actual request is stored inside of statusRec

But to be able to reach this function and therefore write into any attributes, you must enable ZCL_WRITE in your configuration. You can do this with preprocessor directives.

Since we know, how the system handles the attributes, we must add our own attribute to this list. The attributes are defined in the file zcl_samplelight_data.c – beginning on line 193

{
    ZCL_CLUSTER_ID_GEN_BASIC,             // Cluster IDs - defined in the foundation (ie. zcl.h)
    {  // Attribute record
      ATTRID_BASIC_HW_VERSION,            // Attribute ID - Found in Cluster Library header (ie. zcl_general.h)
      ZCL_DATATYPE_UINT8,                 // Data Type - found in zcl.h
      ACCESS_CONTROL_READ,                // Variable access control - found in zcl.h
      (void *)&zclSampleLight_HWRevision  // Pointer to attribute variable
    }
  },

The above code shows one sample attribute. Now we will define our own, and append it at the end of the list.

// My custom attribute
  {
    ZCL_CLUSTER_ID_GEN_BASIC,
    { // Attribute record
      0x4010, //Custom Attribute ID
      ZCL_DATATYPE_UINT16,
      (ACCESS_CONTROL_READ | ACCESS_REPORTABLE | ACCESS_CONTROL_WRITE),
      (void *)&REG_MY_CUSTOM_REGISTER
    }
  }

The above code shows our own attribute. I have used the GEN_BASIC Cluster and defined a UINT16 datatype. I saw some inline commentaries where some one wrote that there are problems with uint8 and boolean’s. If you ever face problems with them, try uint16 instead.

Now we must get informed about changes in our attribute. Unfortunately there doesn’t seem to be a out of the box solution for that. There is one for pre defined attribute id’s but we have defined our own. So i have found a workaround which seems to do the job quite good.

Whenever an event occurs, the system will jump into an event loop declared in the file zcl_samplelight.c on line 403. There are several cases, but write to an attribute does not fullfill one of them, so we will hook into the default case.

  default:
    //Check if a variable has been updated!
    if(REG_MY_CUSTOM_REGISTER_OLD != REG_MY_CUSTOM_REGISTER)
    {
      REG_MY_CUSTOM_REGISTER_OLD = REG_MY_CUSTOM_REGISTER;
    }        
    break;

Here i check if the register has changed. If so, i can execute my desired code section. This method of observation for changes has the benefit, that it doesn’t poll for changes. We will get informed about them. Ok not explicit about changes which concerns our attribute, but the overhead that it will produce if you have multiple attributes inside of the default case will still be marginal.

Adding converters to zigbee2mqtt

Now we would like to be able to set the attributes value from zigbee2mqtt. For this we first must define a new device. https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html

The above link will give you some basic informations about that process. If you have added your device, then you must provide a “ToZigbee” converter.

In your devices.js you must provide something linke this:
toZigbee: [tz.FooBar],

The implementation of tz.FooBar in the file toZigbee.js will look like this:

FooBar: {
        key: ['MyAttributeKey'],
        convert: (key, value, message, type, postfix, options) => {
            if (type === 'set') {
                if (value === 'default') {
                    value = 1;
                }
				const lookup = {
                'Right': '0',
                'Left': '1',
                'IDLE': '2',
				};
				
				value = lookup[value];
				if( ((value >= 0) && value < 3) == false ) value = 2;
				
                return [{
                    cid: 'genBasic',
                    cmd: 'write',
                    cmdType: 'foundation',
                    zclData: [{
                        attrId: 0x4010,
                        dataType: 0x21,
                        attrData: value,
                    }],
                    cfg: cfg.default,
                }];
            }
        },
    },

Let me explain the above code. The first thing that we will see is the MyAttributeKey parameter. This will be the parameter under which you can set your attribute using mqtt. I will show you that later. On line 8 we have a lookup table for the conversion of string into numbers. This is optional but gives some advantages. On line 14 we will convert the input from a string into a number. On the next line we will check for a valid value. Line 17 is responsible for building our zigbee packet.

We must also define the cluster id (cid) identical to what we have defined in our firmware so far. Attribute ID, data type must also correspond accordingly. For the data type there is a list available on the internet which shows you the different types. http://www.zigbee.org/wp-content/uploads/2014/10/07-5123-06-zigbee-cluster-library-specification.pdf
Beginning on page 85. Be careful to check for the correct class!

If you have written your configuration file it’s time to restart zigbee2mqtt.

Test your work!
Now set a breakpoint here and start debugging.

  default:
    //Check if a variable has been updated!
    if(REG_MY_CUSTOM_REGISTER_OLD != REG_MY_CUSTOM_REGISTER)
    {
      REG_MY_CUSTOM_REGISTER_OLD = REG_MY_CUSTOM_REGISTER;
    }        
    break;

After the device shows up in zigbee2mqtt you should be able to send new attribute values to the device using mqtt.

Open up mqtt.fx, configure your server and connect. Now go to the publish tab and enter the following string:

zigbee2mqtt/YOUR UNIQUE IDENTIFIER/set/MyAttributeKey

Replace YOUR UNIQUE IDENTIFIER with your identifier which shows up in the console log of zigbee2mqtt.

Now you can send some values from mqtt.fx to your server and zigbee2mqtt should deliver these messages to your device. This will show something like this:

zigbee2mqtt:info XX/XX/XXXX, XX:XX:XX PM Zigbee publish to device '0x00124xxxxxxxxxx', genBasic - write - 
[{"attrId":16400,"dataType":33,"attrData":"1"}] - {"manufSpec":0,"disDefaultRsp":0} - 8

You should also hit your breakpoint now, and see the new value which was assigned to your attribute!

Well done! Hope this was helpful to someone.