Zpiffer Specification Language


A fixed value has a name that is taken from a header file, either from the official ZWave library or a local one representing bytes that are hard-coded in the specification.   They appear as defines whose value will be substituted for the string when building the state machine.  The value may include simple math expressions, like bit shifts and arithmetic operations.  The identifier string will be displayed as the readable name of the byte.  Because the ZWave naming convention is rather verbose and redundant, the DSL allows you to put a short name or abbreviation in single quotes after the identifier, and the parser will emit this if desired.



An identifier string is an alphabetical character followed by any alphanumeric character or underscore.  It may not start with an underscore.  To access the ZWave headers you must include them normally.

ex. #include <⁠ZW_⁠SerialAPI.h>⁠


A variable is placed between angle brackets.  The byte read at this position is the value of the variable.  The value will be displayed as an integer if single brackets are used, in hexadecimal with double brackets, or in ASCII with a bracket-quote combination.

ex. <⁠"code">⁠ for the user code (as a string) for a lock

ex. <⁠<⁠checksum>⁠>⁠ for the checksum byte at the end of a frame

ex. <⁠destnode>⁠ for the destination of a SendDataEx serial functions

Variables are displayed in the zpiffer with the name between brackets.


Not all values in the byte stream are single bytes.  To handle bit masks use the

&⁠ mask value

construction.  value can be either a fixed value identifier string or a variable.  We take the bitwise AND of the mask with the current byte.  If value is fixed then, if the bitwise AND matches the identifier's value, it is printed by the parser.  If it doesn't match, nothing is printed.  If the value is a variable, then it is printed after right-shifting to get rid of 0s in the mask.  The parser will normally move to the next byte in the stream after any match.  To hold the current byte for more matches, use +⁠&⁠.


   +⁠&⁠ 0x40 <⁠routing>⁠

   +⁠&⁠ 0x38 <⁠maxspeed>⁠

   +⁠&⁠ 0x07 <⁠protocolver>⁠

parses the first byte of the Node Protocol Information.  Listening support is a fixed, one bit flag that is only printed by the parser if the bit is set.  Routing is printed as a 0 or 1 value, maxspeed as a value from 0 t/m 7 from the next three bits, and the protocol version as a value from 0 t/m 7 in the lowest three bits.


Some fixed and variable values cover multiple bytes.  Use a #x construction (# is any digit).

ex. 2x<⁠year>⁠ is a 16 bit value for the year in the DCP Monitor command class

ex. 4x<⁠<⁠homeID>⁠>⁠ is a 4 byte value for the home identifier, displayed in hex

To define a two-byte fixed identifier, use a tuple with identifier strings as the two members.  This is used to build generic-specific type pairs and command class-command pairs, among others.  When building the state machine we will append the two hex values for the identifiers, dropping a '0x' in the middle.  If identifiers are defined in header files with multiple bytes, as manufacturer identifiers are, then you don't need a tuple, but you do need an initial 2x.

ex. 2xMFG_⁠ID_⁠NEXTENERGY resolves to 0x0075


The parser will combine the given number of bytes before matching a fixed value or assigning to a variable.  In a few instances you may need to hold the previous byte, or a part of one from a bitmask, and append the next.  The identifier or variable name must appear twice with +⁠_⁠ before the second occurrence..  For example, in the FIRMWARE_⁠UPDATE_⁠MD_⁠REPORT the report identifier is formed from 7 bits of one byte and the next full byte

ex. &⁠  0x80 <⁠last_⁠fragment>⁠

   +⁠&⁠ 0x7f <⁠reportID>⁠

   +⁠_⁠ <⁠reportID>⁠

+⁠_⁠ &⁠ combines the two values and applies a bitmask to the result.

ex. <⁠period>⁠

   +⁠_⁠ &⁠ 0x0f <⁠period>⁠

from the IR repeater configuration set command appends the lower four bits from the second byte to the first.


Identifiers may also be defined as some stream of bytes by putting a string and an equals sign.  The definition finishes with a period.  We indicate substitution by putting @⁠ before the string, and the parser will 'jump' to the definition and continue parsing.  When that definition is exhausted, it 'returns' to where it jumped.  This is used either to break up a series into shorted, named segments or to extract out common segments.  An example would be the transmit options byte, used in the many SendData serial function variations.

ex. TXOPTION 'TxOption' =

    &⁠  0x01 TRANSMIT_⁠OPTION_⁠ACK 'TxOptACK'

    +⁠&⁠ 0x02 TRANSMIT_⁠OPTION_⁠LOW_⁠POWER 'TxOptLowPower'

    +⁠&⁠ 0x04 TRANSMIT_⁠OPTION_⁠AUTO_⁠ROUTE 'TxOptAutoRoute'

    +⁠&⁠ 0x10 TRANSMIT_⁠OPTION_⁠NO_⁠ROUTE 'TxOptNoRoute'

    +⁠&⁠ 0x20 TRANSMIT_⁠OPTION_⁠EXPLORE 'TxOptExplore'


defines how to parse the five fixed values in the transmit options byte and


will switch to the definition, handle the byte, and then continue onward.


The state machine will normally follow along byte by byte, but we need some control flow operations.  The most common is a branching option, where the parser must match one value at the head of a branch.  The list is placed between square brackets, and a vertical line separates options.  It is an error if none of the branches match.

ex. FRAME = [ ACK | NAK | CAN | SOF ...] where there are more bytes in the SOF branch if taken

ex. 2xCHIPVER = [ 2xZW0102 | 2xZW0201 | 2xZW0301 | 2xZM0401 | 2xZW050x | 2xZW070x] is a two-byte chip version, where the ZW*⁠ identifiers are locally defined (not in the official header files)

If an identifier is defined as a branching list and referred to as @⁠identifer, then the parser will match one branch and follow it.  Without the @⁠ it matches only the first byte.

ex. CC will print the command class matching the current byte

ex. @⁠CC will parse the command class starting with the current byte

CC here is one of the two major branching lists in the spec, and contains all command classes.  SERCMD is the other major list and has all the serial functions.


The next control structure is a group of bytes of known length.  It begins with a variable with a colon before the angle bracket; this is the length.  The grouping is between colon-curly brackets and the contents are repeated the given number of times.

ex. :⁠<⁠size>⁠ :⁠{⁠ <⁠nodeID>⁠ }⁠:⁠ from FUNC_⁠ID_⁠SERIAL_⁠API_⁠GET_⁠INIT_⁠DATA is a group of size bytes of node IDs, displayed as separate integers


There are many variations.  If the contents of the group should exhaust the size without repeating, then use :⁠{⁠! }⁠:⁠ to have the parser generate a warning.

ex.  :⁠<⁠size>⁠ :⁠{⁠! @⁠CC }⁠:⁠ says that expanding the command should take size bytes, no more, no less

To treat the contents of the group as a loop controlled by the length value, use :⁠{⁠*⁠..}⁠:⁠.  The total number of bytes consumed will be the loop count times the size of the group, instead of using the size, which fixes the number of passes by dividing by the group size.

ex. :⁠<⁠count>⁠ :⁠{⁠*⁠ CC }⁠:⁠ from FUNC_⁠ID_⁠SERIAL_⁠API_⁠APPL_⁠NODE_⁠INFORMATION will look up count bytes and match each against the CC list, printing the identifier


Note that the length and group do not have to be adjacent.  The reference is to the last colon-identifier encountered.  From FUNC_⁠ID_⁠ZW_⁠REMOVE_⁠NODE_⁠ID_⁠FROM_⁠NETWORK

ex. :⁠<⁠count>⁠          # number of bytes

    BASTYPE           # branching list of basic types

    2xGENSPECTYPE     # branching list of all 2 byte combinations of generic and specific type

    :⁠{⁠*⁠ CC }⁠:⁠         # command classes

If you need to change the order, insert -⁠# between the colon and bracket (# is a digit).  There are a few command classes where all lengths are specified before the contents of the groups.  Note the backwards reference applies to any of the group types.  From CONFIGURATION_⁠BULK_⁠SET_⁠V2

ex. :⁠<⁠count>⁠

    &⁠  0x80 <⁠default>⁠

    +⁠&⁠ 0x40 <⁠handshake>⁠

    :⁠+⁠&⁠ 0x07 <⁠size>⁠             # length is taken from the lowest 3 bits of the byte after count

    :⁠-⁠2{⁠*⁠ :⁠-⁠1{⁠+⁠ <⁠value>⁠ }⁠:⁠ }⁠:⁠   # outer group uses count, inner size, ie. count values each of size bytes


We can append bytes within a group using :⁠{⁠+⁠ ⁠}⁠:⁠ to form a single value.  The earliest byte becomes the MSB.  In the SENSOR_⁠MULTILEVEL_⁠REPORT we have a byte with the precision, scale, and number of bytes in the value (1, 2, or 4), and then the value.

ex. &⁠  0xE0 <⁠precision>⁠

   +⁠&⁠ 0x18 <⁠scale>⁠

   :⁠+⁠&⁠ 0x07 <⁠size>⁠

   :⁠{⁠+⁠ <⁠value>⁠ }⁠:⁠      # depending on size this will be 1, 2, or 4 bytes in MSB to LSB order


The length byte must sometimes be adjusted.  Use :⁠adj, an arithmetic operation, and a value.

ex. FRAME = [ ACK | NAK | CAN |



   :⁠adj -⁠ 2


   :⁠{⁠! @⁠SERCMD }⁠:⁠      # command length is two less than the size



The top-level frame size is between the SOF to checksum but the serial command group includes neither the size byte nor the REQ/RES.  We must subtract these two.


We also need greedy groups, without a length specification, that will use all bytes available.  If there is a sequence after the group, those bytes are subtracted from what's available.  In other words, the parser will reserve enough bytes for the trailing series.  The same combinations hold, of {⁠.⁠.⁠}⁠, {⁠+⁠.⁠.⁠}⁠ for combining bytes into a single value, and {⁠*⁠.⁠.⁠}⁠ for repeating the contents.  The grouping symbol {} is the same, but we don't use the size marker :.

ex. {⁠ <⁠<⁠unknown>⁠>⁠ }⁠ means the payload of a command is unknown and printed as raw bytes

ex. {⁠+⁠ <⁠"name">⁠ }⁠ prints the remaining bytes in a CTRL_⁠REPLICATION_⁠TRANSFER_⁠SCENE command as text

ex. {⁠*⁠ @⁠METERRPT1 }⁠ parses the remaining bytes as one block, a meter report


{⁠$⁠..}⁠ flags a group that is handled specially in the parser.  There is one in the specification, under SECURITY_⁠MESSAGE_⁠ENCAPSULATE, and it marks the bytes to decrypt with S0.  We can prepend a variable with $⁠#, where # is a number between 0 and 7.  These are special numbered memory locations in the parser which are needed for decryption.

ex. $⁠1 8x<⁠<⁠initvec>⁠>⁠ prints the 8 byte initialization vector in hex, and stores it in memory 1

ex. {⁠$⁠ @⁠ENCAPMSG }⁠ parses the next bytes specially, decrypting them with memories 0 and 1 (0 is set beforehand when the nonce arrives)


Serial commands will parse differently depending whether they are outgoing, incoming with a RESPONSE byte in the frame, incoming with a REQUEST byte, or incoming in either case.  >⁠TX, <⁠RXRES, <⁠RXREQ, and <⁠RX distinguish these four cases.  They are branches where the parser knows to use the value previously encountered in the frame, and are treated as a special, hard-coded flag.  From FUNCTION_⁠ID_⁠SERIAL_⁠API_⁠SET_⁠TIMEOUTS

ex. >⁠TX






The device sends the two commands to set in the destination, and in the response gets back the values before they were changed.


A group between ?⁠{⁠ .. }⁠?⁠ is an optional series of bytes.  The group may be interrupted at any point if the bytes run out without raising an error.  Such a sequence cannot be followed by a greedy group; anything that follows must have a known size so that the parser can guarantee enough bytes remain.  You can treat the extra values from later versions of ZWave commands as optional.

ex. LEARN_⁠MODE_⁠SET_⁠STATUS 'SetStatus'


   [ LEARNMODE_⁠STATUS | learn_⁠mode_⁠security_⁠failed 'SecurityFailed']   # not in any header


   ?⁠{⁠ KEYTYPE    # V2 here.


      16x<⁠<⁠dsk>⁠>⁠ }⁠?⁠