/* packet-cemi.c
 * Routines for cEMI (Common External Message Interface) dissection
 * By Jan Kessler <kessler@ise.de>
 * Copyright 2004, Jan Kessler <kessler@ise.de>
 *
 * Wireshark - Network traffic analyzer
 * By Gerald Combs <gerald@wireshark.org>
 * Copyright 1998 Gerald Combs
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "config.h"

#include <epan/packet.h>
#include "packet-knxip.h"

void proto_register_cemi(void);
void proto_reg_handoff_cemi(void);

/* cEMI Message Codes
*/
#define CEMI_L_BUSMON_IND     0x2B
#define CEMI_L_RAW_IND        0x2D
#define CEMI_L_RAW_REQ        0x10
#define CEMI_L_RAW_CON        0x2F
#define CEMI_L_DATA_REQ       0x11
#define CEMI_L_DATA_CON       0x2E
#define CEMI_L_DATA_IND       0x29
#define CEMI_L_POLL_DATA_REQ  0x13
#define CEMI_L_POLL_DATA_CON  0x25
#define CEMI_T_DATA_INDIVIDUAL_REQ 0x4A
#define CEMI_T_DATA_INDIVIDUAL_IND 0x94
#define CEMI_T_DATA_CONNECTED_REQ 0x41
#define CEMI_T_DATA_CONNECTED_IND 0x89
#define CEMI_M_PROPREAD_REQ   0xFC
#define CEMI_M_PROPREAD_CON   0xFB
#define CEMI_M_PROPWRITE_REQ  0xF6
#define CEMI_M_PROPWRITE_CON  0xF5
#define CEMI_M_PROPINFO_IND   0xF7
#define CEMI_M_FUNCPROPCMD_REQ  0xF8
#define CEMI_M_FUNCPROPREAD_REQ  0xF9
#define CEMI_M_FUNCPROP_CON   0xFA
#define CEMI_M_RESET_REQ      0xF1
#define CEMI_M_RESET_IND      0xF0

/* Additional Information Types
*/
                                    /* 0x00  Reserved. */
#define CEMI_PL_MEDIUM_INFORMATION     0x01  /*!< (2 octets) Domain Address used by PL medium; Client <-> Server */
#define CEMI_RF_MEDIUM_INFORMATION     0x02  /*!< (7 octet)  RF-Control byte and serial number/DoA;
                                                             Client <-> Server Busmonitor */
#define CEMI_STATUS_INFO               0x03  /*!< (1 octet)  Busmonitor Error Flags; see clause 2.5.5.5; Client <- Server */
#define CEMI_TIMESTAMP_RELATIVE        0x04  /*!< (2 octets) Relative timestamp; e.g. for L_Raw.ind; Client <- Server */
#define CEMI_TIME_DELAY_UNTIL_SENDING  0x05  /*!< (4 octets) Time delay (L_Raw.req, see clause 2.5.5.3); Client <- Server */
                                    /* 0x06-0xFE  Not used. */
                                    /* 0xFF       For future system extension (ESC Code). */

/* Error Codes
*/
#define CEMI_UNSPECIFIED_ERROR       0x00  /*!< Unknown error (R/W). */
#define CEMI_OUT_OF_RANGE            0x01  /*!< Write value not allowed (general, if not error 2 or 3) (W). */
#define CEMI_OUT_OF_MAXRANGE         0x02  /*!< Write value to high (W). */
#define CEMI_OUT_OF_MINRANGE         0x03  /*!< Write value to low (W). */
#define CEMI_MEMORY_ERROR            0x04  /*!< Memory can not be written or only with fault(s) (W). */
#define CEMI_READ_ONLY               0x05  /*!< Write access to a 'read only' or a write protected property (W). */
#define CEMI_ILLEGAL_COMMAND         0x06  /*!< COMMAND not valid or not supported (W). */
#define CEMI_VOID_DP                 0x07  /*!< Read or write access to an non existing property (R/W). */
#define CEMI_TYPE_CONFLICT           0x08  /*!< Write access with a wrong data type (datapoint length) (W). */
#define CEMI_PROP_INDEX_RANGE_ERROR  0x09  /* Read or write access to a non existing property array index (R/W). */

/* Common EMI specific device server properties
*/
#define CEMI_PID_DOMAIN_ADDRESS  0x70  /*!< Domain Address of a PL medium (cEMI server) device.
                                            PDT_UNSIGNED_INT O - r/w */
#define CEMI_PID_IO_LIST         0x71  /*!< List of Interface Objects in the (cEMI server) device.
                                            PDT_UNSIGNED_INT O - r/w */

#define CEMI_PID_MEDIUM_TYPE          0x51  /*!< Media Type(s) supported by cEMI server.
                                                 DPT_Media M - read only */
#define CEMI_PID_COMM_MODE            0x52  /*!< Link Layer / Raw (Busmonitor) / Transport L.
                                                 DPT_CommMode O - r/w */
#define CEMI_PID_MEDIUM_AVAILABILITY  0x53  /*!< Bus available (1) or not (0) ?
                                                 DPT_Media O - read only */
#define CEMI_PID_ADD_INFO_TYPES       0x54  /*!< cEMI supported Additional Information Types.
                                                 DPT_AddInfoTypes O - read only */
#define CEMI_PID_TRANSP_ENABLE        0x56  /*!< LL Transparency Mode of cEMI server.
                                                 DPT_Enable O - r/w */
                                   /* 0x57  Reserved for cEMI client's subnetwork address.
                                            PDT_UNSIGNED_CHAR O - r/w */
                                   /* 0x58  Reserved for cEMI client's device address.
                                            PDT_UNSIGNED_CHAR O - r/w */
                                   /* 0x59  t.b.d.*/
                                   /* 0x60  t.b.d.*/
                                   /* 0x61  DoA Filter. t.b.d. O - read only */

#define CEMI_PID_MEDIUM_TYPE_TP0    0x0001  /*!< TP 0 */
#define CEMI_PID_MEDIUM_TYPE_TP1    0x0002  /*!< TP 1 */
#define CEMI_PID_MEDIUM_TYPE_PL110  0x0004  /*!< PL 110 */
#define CEMI_PID_MEDIUM_TYPE_PL132  0x0008  /*!< PL 132 */
#define CEMI_PID_MEDIUM_TYPE_RF     0x0010  /*!< RF */

#define CEMI_PID_COMM_MODE_LL   0x00  /*!< Link Layer = default comm. mode. */
#define CEMI_PID_COMM_MODE_LLB  0x01  /*!< Link Layer Busmonitor. */
#define CEMI_PID_COMM_MODE_LLR  0x02  /*!< Link Layer Raw Frames. */
                             /* 0x03  Reserved for Network Layer. */
                             /* 0x04  Reserved for TL group oriented. */
                             /* 0x05  Reserved for TL connection oriented. */
                             /* 0x05-0xFF  Reserved for other 'destination layers'. */

/* - - - - - - -  T R E E   V I E W   I D E N T I F I E R  - - - - - - - -
*/

/* Initialize the protocol identifier that is needed for the
 protocol hook and to register the fields in the protocol tree
*/
static gint proto_cemi = -1;

/* Initialize the registered fields identifiers. These fields
 will be registered with the protocol during initialization.
 Protocol fields are like type definitions. The protocol dissector
 later on adds items of these types to the protocol tree.
*/
static gint hf_bytes = -1;
static gint hf_folder = -1;
static gint hf_cemi_mc = -1;
static gint hf_cemi_error = -1;
static gint hf_cemi_ai_length = -1;
static gint hf_cemi_aie_type = -1;
static gint hf_cemi_aie_length = -1;
static gint hf_cemi_ot = -1;
static gint hf_cemi_oi = -1;
static gint hf_cemi_ox = -1;
static gint hf_cemi_px = -1;
static gint hf_cemi_pid = -1;
static gint hf_cemi_ne = -1;
static gint hf_cemi_sx = -1;
static gint hf_cemi_ft = -1;
static gint hf_cemi_rep = -1;
static gint hf_cemi_bt = -1;
static gint hf_cemi_prio = -1;
static gint hf_cemi_ack = -1;
static gint hf_cemi_ce = -1;
static gint hf_cemi_at = -1;
static gint hf_cemi_hc = -1;
static gint hf_cemi_eff = -1;
static gint hf_cemi_sa = -1;
static gint hf_cemi_da = -1;
static gint hf_cemi_len = -1;
static gint hf_cemi_tpt = -1;
static gint hf_cemi_tst = -1;
static gint hf_cemi_num = -1;
static gint hf_cemi_tc = -1;
static gint hf_cemi_ac = -1;
static gint hf_cemi_ad = -1;
static gint hf_cemi_ad_memory_length = -1;
static gint hf_cemi_ad_channel = -1;
static gint hf_cemi_ad_type = -1;
static gint hf_cemi_ax = -1;
static gint hf_cemi_pw = -1;
static gint hf_cemi_pdt = -1;
static gint hf_cemi_me = -1;
static gint hf_cemi_ra = -1;
static gint hf_cemi_wa = -1;
static gint hf_cemi_ext_oi = -1;
static gint hf_cemi_ext_pid = -1;
static gint hf_cemi_ext_ne = -1;
static gint hf_cemi_ext_sx = -1;
static gint hf_cemi_ext_dt = -1;
static gint hf_cemi_ext_px = -1;
static gint hf_cemi_ext_memory_length = -1;
static gint hf_cemi_ext_memory_address = -1;
static gint hf_cemi_memory_length = -1;
static gint hf_cemi_memory_address = -1;
static gint hf_cemi_memory_address_ext = -1;
static gint hf_cemi_level = -1;
static gint hf_cemi_snp_pid = -1;
static gint hf_cemi_snp_reserved = -1;
static gint hf_cemi_dpt_major = -1;
static gint hf_cemi_dpt_minor = -1;
static gint hf_cemi_scf = -1;
static gint hf_cemi_scf_t = -1;
static gint hf_cemi_scf_sai = -1;
static gint hf_cemi_scf_sbc = -1;
static gint hf_cemi_scf_svc = -1;
static gint hf_cemi_adc_count = -1;

/* Initialize the subtree pointers. These pointers are needed to
 display the protocol in a structured tree. Subtrees hook on
 already defined fields or (the topmost) on the protocol itself
*/
static gint ett_cemi = -1;
static gint ett_cemi_ai = -1;
static gint ett_cemi_aie = -1;
static gint ett_cemi_ctrl1 = -1;
static gint ett_cemi_ctrl2 = -1;
static gint ett_cemi_tpci = -1;
static gint ett_cemi_apci = -1;
static gint ett_cemi_range = -1;
static gint ett_cemi_pd = -1;
static gint ett_cemi_dpt = -1;
static gint ett_cemi_scf = -1;
static gint ett_cemi_decrypted = -1;

/* - - - - - - - - - - -  V A L U E   T A B L E S  - - - - - - - - - - - -
*/

/* See following docs:

  "AN033 v03 cEMI.pdf",
  "AN057 v01 System B RfV.pdf",
  "KSG259 2004.02.03 Identifiers.pdf",
  "03_07_03 Standardized Identifier Tables.pdf"
*/

/* Message Code
*/
static const value_string mc_vals[] = {
  { CEMI_L_BUSMON_IND, "L_Busmon.ind" },
  { CEMI_L_RAW_IND, "L_Raw.ind" },
  { CEMI_L_RAW_REQ, "L_Raw.req" },
  { CEMI_L_RAW_CON, "L_Raw.con" },
  { CEMI_L_DATA_REQ, "L_Data.req" },
  { CEMI_L_DATA_CON, "L_Data.con" },
  { CEMI_L_DATA_IND, "L_Data.ind" },
  { CEMI_L_POLL_DATA_REQ, "L_PollData.req" },
  { CEMI_L_POLL_DATA_CON, "L_PollData.con" },
  { CEMI_T_DATA_INDIVIDUAL_REQ, "T_Data_Individual.req" },
  { CEMI_T_DATA_INDIVIDUAL_IND, "T_Data_Individual.ind" },
  { CEMI_T_DATA_CONNECTED_REQ, "T_Data_Connected.req" },
  { CEMI_T_DATA_CONNECTED_IND, "T_Data_Connected.ind" },
  { CEMI_M_PROPREAD_REQ, "M_PropRead.req" },
  { CEMI_M_PROPREAD_CON, "M_PropRead.con" },
  { CEMI_M_PROPWRITE_REQ, "M_PropWrite.req" },
  { CEMI_M_PROPWRITE_CON, "M_PropWrite.con" },
  { CEMI_M_PROPINFO_IND, "M_PropInfo.ind" },
  { CEMI_M_FUNCPROPCMD_REQ, "M_FuncPropCmd.req" },
  { CEMI_M_FUNCPROPREAD_REQ, "M_FuncPropRead.req" },
  { CEMI_M_FUNCPROP_CON, "M_FuncProp.con" },
  { CEMI_M_RESET_REQ, "M_Reset.req" },
  { CEMI_M_RESET_IND, "M_Reset.ind" },
  { 0, NULL }
};

/* Property access flags
*/
#define PA_RESPONSE     0x01
#define PA_DATA         0x02

/* Additional Info Element Type
*/
static const value_string aiet_vals[] = {
  { 1, "PL Medium Info" },
  { 2, "RF Medium Info" },
  { 3, "BusMonitor Status Info" },
  { 4, "Timestamp Relative" },
  { 5, "Time Delay Until Sending" },
  { 6, "Extended Relative Timestamp" },
  { 7, "BiBat Info" },
  { 0, NULL }
};

/* Frame Type
*/
static const value_string ft_vals[] = {
  { 0, "Extended" },
  { 1, "Standard" },
  { 0, NULL }
};

/* Broadcast Type
*/
static const value_string bt_vals[] = {
  { 0, "System" },
  { 1, "Domain" },
  { 0, NULL }
};

/* Priority
*/
static const value_string prio_vals[] = {
  { 0, "System" },
  { 2, "Urgent" },
  { 1, "Normal" },
  { 3, "Low" },
  { 0, NULL }
};

/* Address Type
*/
static const value_string at_vals[] = {
  { 0, "Individual" },
  { 1, "Group" },
  { 0, NULL }
};

/* Packet Type
*/
static const value_string pt_vals[] = {
  { 0, "Data" },
  { 1, "Control" },
  { 0, NULL }
};

/* Sequence Type
*/
static const value_string st_vals[] = {
  { 0, "Unnumbered" },
  { 1, "Numbered" },
  { 0, NULL }
};

/* Transport Layer Code
*/
static const value_string tc_vals[] = {
  { 0, "Connect" },
  { 1, "Disconnect" },
  { 2, "ACK" },
  { 3, "NAK" },
  { 0, NULL }
};

/* Application Layer Code
*/
#define AC_GroupValueRead     0
#define AC_GroupValueResp     1
#define AC_GroupValueWrite    2
#define AC_IndAddrWrite       3
#define AC_IndAddrRead        4
#define AC_IndAddrResp        5
#define AC_AdcRead            6
#define AC_AdcResp            7
#define AC_MemRead            8
#define AC_MemResp            9
#define AC_MemWrite           10
#define AC_UserMsg            11
#define AC_DevDescrRead       12
#define AC_DevDescrResp       13
#define AC_Restart            14
#define AC_Escape             15

static const value_string ac_vals[] =
{
  { AC_GroupValueRead, "GroupValueRead" },
  { AC_GroupValueResp, "GroupValueResp" },
  { AC_GroupValueWrite, "GroupValueWrite" },
  { AC_IndAddrWrite, "IndAddrWrite" },
  { AC_IndAddrRead, "IndAddrRead" },
  { AC_IndAddrResp, "IndAddrResp" },
  { AC_AdcRead, "AdcRead" },
  { AC_AdcResp, "AdcResp" },
  { AC_MemRead, "MemRead" },
  { AC_MemResp, "MemResp" },
  { AC_MemWrite, "MemWrite" },
  { AC_UserMsg, "UserMsg" },
  { AC_DevDescrRead, "DevDescrRead" },
  { AC_DevDescrResp, "DevDescrResp" },
  { AC_Restart, "Restart" },
  { AC_Escape, "Escape" },
  { 0, NULL }
};

/* Extended AL codes
*/
#define AX_SysNwkParamRead  0x1C8
#define AX_SysNwkParamResp  0x1C9
#define AX_SysNwkParamWrite  0x1CA
#define AX_PropExtValueRead  0x1CC
#define AX_PropExtValueResp  0x1CD
#define AX_PropExtValueWriteCon  0x1CE
#define AX_PropExtValueWriteConRes  0x1CF
#define AX_PropExtValueWriteUnCon  0x1D0
#define AX_PropExtValueInfoReport  0x1D1
#define AX_PropExtDescrRead  0x1D2
#define AX_PropExtDescrResp  0x1D3
#define AX_FuncPropExtCmd  0x1D4
#define AX_FuncPropExtRead  0x1D5
#define AX_FuncPropExtResp  0x1D6
#define AX_MemExtWrite  0x1FB
#define AX_MemExtWriteResp  0x1FC
#define AX_MemExtRead  0x1FD
#define AX_MemExtReadResp  0x1FE
#define AX_UserMemRead  0x2C0
#define AX_UserMemResp  0x2C1
#define AX_UserMemWrite  0x2C2
#define AX_UserMemBitWrite  0x2C4
#define AX_UserMfrInfoRead  0x2C5
#define AX_UserMfrInfoResp  0x2C6
#define AX_FuncPropCmd  0x2C7
#define AX_FuncPropRead  0x2C8
#define AX_FuncPropResp  0x2C9
#define AX_Restart  0x380
#define AX_RestartReq  0x381
#define AX_RestartResp  0x3A1
#define AX_RoutingTableOpen  0x3C0
#define AX_RoutingTableRead  0x3C1
#define AX_RoutingTableResp  0x3C2
#define AX_RoutingTableWrite  0x3C3
#define AX_RouterMemRead  0x3C8
#define AX_RouterMemResp  0x3C9
#define AX_RouterMemWrite  0x3CA
#define AX_RouterStatusRead  0x3CD
#define AX_RouterStatusResp  0x3CE
#define AX_RouterStatusWrite  0x3CF
#define AX_MemBitWrite  0x3D0
#define AX_AuthReq  0x3D1
#define AX_AuthResp  0x3D2
#define AX_KeyWrite  0x3D3
#define AX_KeyResp  0x3D4
#define AX_PropValueRead  0x3D5
#define AX_PropValueResp  0x3D6
#define AX_PropValueWrite  0x3D7
#define AX_PropDescrRead  0x3D8
#define AX_PropDescrResp  0x3D9
#define AX_NwkParamRead  0x3DA
#define AX_NwkParamResp  0x3DB
#define AX_IndAddrSerNumRead  0x3DC
#define AX_IndAddrSerNumResp  0x3DD
#define AX_IndAddrSerNumWrite  0x3DE
#define AX_DomAddrWrite  0x3E0
#define AX_DomAddrRead  0x3E1
#define AX_DomAddrResp  0x3E2
#define AX_DomAddrSelRead  0x3E3
#define AX_NwkParamWrite  0x3E4
#define AX_LinkRead  0x3E5
#define AX_LinkResp  0x3E6
#define AX_LinkWrite  0x3E7
#define AX_GroupPropValueRead  0x3E8
#define AX_GroupPropValueResp  0x3E9
#define AX_GroupPropValueWrite  0x3EA
#define AX_GroupPropValueInfo  0x3EB
#define AX_DomAddrSerNumRead  0x3EC
#define AX_DomAddrSerNumResp  0x3ED
#define AX_DomAddrSerNumWrite  0x3EE
#define AX_FileStreamInfo  0x3F0
#define AX_DataSec  0x3F1

static const value_string ax_vals[] =
{
  { AX_SysNwkParamRead, "SysNwkParamRead" },
  { AX_SysNwkParamResp, "SysNwkParamResp" },
  { AX_SysNwkParamWrite, "SysNwkParamWrite" },
  { AX_PropExtValueRead, "PropExtValueRead" },
  { AX_PropExtValueResp, "PropExtValueResp" },
  { AX_PropExtValueWriteCon, "PropExtValueWriteCon" },
  { AX_PropExtValueWriteConRes, "PropExtValueWriteConRes" },
  { AX_PropExtValueWriteUnCon, "PropExtValueWriteUnCon" },
  { AX_PropExtDescrRead, "PropExtDescrRead" },
  { AX_PropExtDescrResp, "PropExtDescrResp" },
  { AX_FuncPropExtCmd, "FuncPropExtCmd" },
  { AX_FuncPropExtRead, "FuncPropExtRead" },
  { AX_FuncPropExtResp, "FuncPropExtResp" },
  { AX_MemExtWrite, "MemExtWrite" },
  { AX_MemExtWriteResp, "MemExtWriteResp" },
  { AX_MemExtRead, "MemExtRead" },
  { AX_MemExtReadResp, "MemExtReadResp" },
  { AX_UserMemRead, "UserMemRead" },
  { AX_UserMemResp, "UserMemResp" },
  { AX_UserMemWrite, "UserMemWrite" },
  { AX_UserMemBitWrite, "UserMemBitWrite" },
  { AX_UserMfrInfoRead, "UserMfrInfoRead" },
  { AX_UserMfrInfoResp, "UserMfrInfoResp" },
  { AX_FuncPropCmd, "FuncPropCmd" },
  { AX_FuncPropRead, "FuncPropRead" },
  { AX_FuncPropResp, "FuncPropResp" },
  { AX_Restart, "Restart" },
  { AX_RestartReq, "RestartReq" },
  { AX_RestartResp, "RestartResp" },
  { AX_RoutingTableOpen, "RoutingTableOpen" },
  { AX_RoutingTableRead, "RoutingTableRead" },
  { AX_RoutingTableResp, "RoutingTableResp" },
  { AX_RoutingTableWrite, "RoutingTableWrite" },
  { AX_RouterMemRead, "RouterMemRead" },
  { AX_RouterMemResp, "RouterMemResp" },
  { AX_RouterMemWrite, "RouterMemWrite" },
  { AX_RouterStatusRead, "RouterStatusRead" },
  { AX_RouterStatusResp, "RouterStatusResp" },
  { AX_RouterStatusWrite, "RouterStatusWrite" },
  { AX_MemBitWrite, "MemBitWrite" },
  { AX_AuthReq, "AuthReq" },
  { AX_AuthResp, "AuthResp" },
  { AX_KeyWrite, "KeyWrite" },
  { AX_KeyResp, "KeyResp" },
  { AX_PropValueRead, "PropValueRead" },
  { AX_PropValueResp, "PropValueResp" },
  { AX_PropValueWrite, "PropValueWrite" },
  { AX_PropDescrRead, "PropDescrRead" },
  { AX_PropDescrResp, "PropDescrResp" },
  { AX_NwkParamRead, "NwkParamRead" },
  { AX_NwkParamResp, "NwkParamResp" },
  { AX_IndAddrSerNumRead, "IndAddrSerNumRead" },
  { AX_IndAddrSerNumResp, "IndAddrSerNumResp" },
  { AX_IndAddrSerNumWrite, "IndAddrSerNumWrite" },
  { AX_DomAddrWrite, "DomAddrWrite" },
  { AX_DomAddrRead, "DomAddrRead" },
  { AX_DomAddrResp, "DomAddrResp" },
  { AX_DomAddrSelRead, "DomAddrSelRead" },
  { AX_NwkParamWrite, "NwkParamWrite" },
  { AX_LinkRead, "LinkRead" },
  { AX_LinkResp, "LinkResp" },
  { AX_LinkWrite, "LinkWrite" },
  { AX_GroupPropValueRead, "GroupPropValueRead" },
  { AX_GroupPropValueResp, "GroupPropValueResp" },
  { AX_GroupPropValueWrite, "GroupPropValueWrite" },
  { AX_GroupPropValueInfo, "GroupPropValueInfo" },
  { AX_DomAddrSerNumRead, "DomAddrSerNumRead" },
  { AX_DomAddrSerNumResp, "DomAddrSerNumResp" },
  { AX_DomAddrSerNumWrite, "DomAddrSerNumWrite" },
  { AX_FileStreamInfo, "FileStreamInfo" },
  { AX_DataSec, "DataSec" },
  { 0, NULL }
};

/* SCF (Security Control Field)
*/
static const value_string scf_vals[] =
{
  { 0x00, "CCM S-A_Data with Authentication-only" },
  { 0x10, "CCM S-A_Data with Authentication+Confidentiality" },
  { 0x12, "CCM S-A_Sync_Req with Authentication+Confidentiality" },
  { 0x13, "CCM S-A_Sync_Res with Authentication+Confidentiality" },
  { 0x08, "CCM S-A_Data with Authentication-only, System Broadcast" },
  { 0x18, "CCM S-A_Data with Authentication+Confidentiality, System Broadcast" },
  { 0x1a, "CCM S-A_Sync_Req with Authentication+Confidentiality, System Broadcast" },
  { 0x1b, "CCM S-A_Sync_Res with Authentication+Confidentiality, System Broadcast" },
  { 0x80, "CCM S-A_Data with Authentication-only, Tool Access" },
  { 0x90, "CCM S-A_Data with Authentication+Confidentiality, Tool Access" },
  { 0x92, "CCM S-A_Sync_Req with Authentication+Confidentiality, Tool Access" },
  { 0x93, "CCM S-A_Sync_Res with Authentication+Confidentiality, Tool Access" },
  { 0x88, "CCM S-A_Data with Authentication-only, System Broadcast, Tool Access" },
  { 0x98, "CCM S-A_Data with Authentication+Confidentiality, Tool Access, System Broadcast" },
  { 0x9a, "CCM S-A_Sync_Req with Authentication+Confidentiality, Tool Access, System Broadcast" },
  { 0x9b, "CCM S-A_Sync_Res with Authentication+Confidentiality, Tool Access, System Broadcast" },
  { 0, NULL }
};

/* SCF (Security Control Field).
*/
static const value_string scf_short_vals[] =
{
  { 0x00, "Data+A" },
  { 0x10, "Data+A+C" },
  { 0x12, "SyncReq" },
  { 0x13, "SyncRes" },
  { 0x08, "Data+A+SBC" },
  { 0x18, "Data+A+C+SBC" },
  { 0x1a, "SyncReq+SBC" },
  { 0x1b, "SyncRes+SBC" },
  { 0x80, "Data+A+T" },
  { 0x90, "Data+A+C+T" },
  { 0x92, "SyncReq+T" },
  { 0x93, "SyncRes+T" },
  { 0x88, "Data+A+T+SBC" },
  { 0x98, "Data+A+C+T+SBC" },
  { 0x9a, "SyncReq+T+SBC" },
  { 0x9b, "SyncRes+T+SBC" },
  { 0, NULL }
};

/* SCF.SAI (Security Algorithm Identifier)
*/
static const value_string scf_sai_vals[] =
{
  { 0, "CCM A" },
  { 1, "CCM A+S" },
  { 0, NULL }
};

/* SCF.Service
*/
static const value_string scf_svc_vals[] =
{
  { 0, "Data" },
  { 2, "Sync_Req" },
  { 3, "Sync_Res" },
  { 0, NULL }
};

/* See KNX documents:
* "03_07_03 Standardized Identifier Tables v01.03.01 AS"
* "03_05_01 Resources v01.09.03 AS"
*/

/* Property Data Types
* See "4 Property Datatypes Identifiers" in "03_07_03 Standardized Identifier Tables v01.03.01 AS"
*/
static const value_string pdt_vals[] = {
  { 0x00, "PDT_CONTROL" },
  { 0x01, "PDT_CHAR" },
  { 0x02, "PDT_UNSIGNED_CHAR" },
  { 0x03, "PDT_INT" },
  { 0x04, "PDT_UNSIGNED_INT" },
  { 0x05, "PDT_KNX_FLOAT" },
  { 0x06, "PDT_DATE" },
  { 0x07, "PDT_TIME" },
  { 0x08, "PDT_LONG" },
  { 0x09, "PDT_UNSIGNED_LONG" },
  { 0x0A, "PDT_FLOAT" },
  { 0x0B, "PDT_DOUBLE" },
  { 0x0C, "PDT_CHAR_BLOCK" },
  { 0x0D, "PDT_POLL_GROUP_SETTINGS" },
  { 0x0E, "PDT_SHORT_CHAR_BLOCK" },
  { 0x0F, "PDT_DATE_TIME" },
  { 0x10, "PDT_VARIABLE_LENGTH" },
  { 0x11, "PDT_GENERIC_01" },
  { 0x12, "PDT_GENERIC_02" },
  { 0x13, "PDT_GENERIC_03" },
  { 0x14, "PDT_GENERIC_04" },
  { 0x15, "PDT_GENERIC_05" },
  { 0x16, "PDT_GENERIC_06" },
  { 0x17, "PDT_GENERIC_07" },
  { 0x18, "PDT_GENERIC_08" },
  { 0x19, "PDT_GENERIC_09" },
  { 0x1A, "PDT_GENERIC_10" },
  { 0x1B, "PDT_GENERIC_11" },
  { 0x1C, "PDT_GENERIC_12" },
  { 0x1D, "PDT_GENERIC_13" },
  { 0x1E, "PDT_GENERIC_14" },
  { 0x1F, "PDT_GENERIC_15" },
  { 0x20, "PDT_GENERIC_16" },
  { 0x21, "PDT_GENERIC_17" },
  { 0x22, "PDT_GENERIC_18" },
  { 0x23, "PDT_GENERIC_19" },
  { 0x24, "PDT_GENERIC_20" },
  { 0x2F, "PDT_UTF-8" },
  { 0x30, "PDT_VERSION" },
  { 0x31, "PDT_ALARM_INFO" },
  { 0x32, "PDT_BINARY_INFORMATION" },
  { 0x33, "PDT_BITSET8" },
  { 0x34, "PDT_BITSET16" },
  { 0x35, "PDT_ENUM8" },
  { 0x36, "PDT_SCALING" },
  { 0x3C, "PDT_NE_VL" },
  { 0x3D, "PDT_NE_FL" },
  { 0x3E, "PDT_FUNCTION" },
  { 0x3F, "PDT_ESCAPE" },
  { 0, NULL }
};

/* Interface Object Types
* See "2 Interface Object Types" in "03_07_03 Standardized Identifier Tables v01.03.01 AS"
*/
static const value_string ot_vals[] = {
  { 0, "Device" },
  { 1, "Address Table" },
  { 2, "Association Table" },
  { 3, "Application Program" },
  { 4, "Interface Program" },
  { 5, "KNX-Object Association Table" },
  { 6, "Router" },
  { 7, "LTE Address Routing Table" },
  { 8, "cEMI Server" },
  { 9, "Group Object Table" },
  { 10, "Polling Master" },
  { 11, "KNXnet/IP Parameter" },
  { 13, "File Server" },
  { 17, "Data Security" },
  { 0, NULL }
};

/* IOT independent PIDs
* See "3.2 Interface Object Type independent standard Properties" in "03_07_03 Standardized Identifier Tables v01.03.01 AS"
* See "4.2 Interface Object Type independent Properties" in "03_05_01 Resources v01.09.03 AS"
*/
static const value_string pid_vals[] = {
  { 1, "PID_OBJECT_TYPE" },
  { 2, "PID_OBJECT_NAME" },
  { 3, "PID_SEMAPHOR" },
  { 4, "PID_GROUP_OBJECT_REFERENCE" },
  { 5, "PID_LOAD_STATE_CONTROL" },
  { 6, "PID_RUN_STATE_CONTROL" },
  { 7, "PID_TABLE_REFERENCE" },
  { 8, "PID_SERVICE_CONTROL" },
  { 9, "PID_FIRMWARE_REVISION" },
  { 10, "PID_SERVICES_SUPPORTED" },
  { 11, "PID_SERIAL_NUMBER" },
  { 12, "PID_MANUFACTURER_ID" },
  { 13, "PID_PROGRAM_VERSION" },
  { 14, "PID_DEVICE_CONTROL" },
  { 15, "PID_ORDER_INFO" },
  { 16, "PID_PEI_TYPE" },
  { 17, "PID_PORT_CONFIGURATION" },
  { 18, "PID_POLL_GROUP_SETTINGS" },
  { 19, "PID_MANUFACTURER_DATA" },
  { 21, "PID_DESCRIPTION" },
  { 23, "PID_TABLE" },
  { 24, "PID_ENROL" },
  { 25, "PID_VERSION" },
  { 26, "PID_GROUP_OBJECT_LINK" },
  { 27, "PID_MCB_TABLE" },
  { 28, "PID_ERROR_CODE" },
  { 29, "PID_OBJECT_INDEX" },
  { 30, "PID_DOWNLOAD_COUNTER" },
  { 0, NULL }
};

/* PIDs for IOT = 0 (Device)
* See "3.3.1 Device Object Interface Object (Object Type = 0)" in "03_07_03 Standardized Identifier Tables v01.03.01 AS"
* See "4.3 Device Object (Object Type 0)" in "03_05_01 Resources v01.09.03 AS"
*/
static const value_string pid0_vals[] = {
  { 51, "PID_ROUTING_COUNT" },
  { 52, "PID_MAX_RETRY_COUNT" },
  { 53, "PID_ERROR_FLAGS" },
  { 54, "PID_PROGMODE" },
  { 55, "PID_PRODUCT_ID" },
  { 56, "PID_MAX_APDULENGTH" },
  { 57, "PID_SUBNET_ADDR" },
  { 58, "PID_DEVICE_ADDR" },
  { 59, "PID_PB_CONFIG" },
  { 60, "PID_ADDR_REPORT" },
  { 61, "PID_ADDR_CHECK" },
  { 62, "PID_OBJECT_VALUE" },
  { 63, "PID_OBJECTLINK" },
  { 64, "PID_APPLICATION" },
  { 65, "PID_PARAMETER" },
  { 66, "PID_OBJECTADDRESS" },
  { 67, "PID_PSU_TYPE" },
  { 68, "PID_PSU_STATUS" },
  { 69, "PID_PSU_ENABLE" },
  { 70, "PID_DOMAIN_ADDRESS" },
  { 71, "PID_IO_LIST" },
  { 72, "PID_MGT_DESCRIPTOR_01" },
  { 73, "PID_PL110_PARAM" },
  { 74, "PID_RF_REPEAT_COUNTER" },
  { 75, "PID_RECEIVE_BLOCK_TABLE" },
  { 76, "PID_RANDOM_PAUSE_TABLE" },
  { 77, "PID_RECEIVE_BLOCK_NR" },
  { 78, "PID_HARDWARE_TYPE" },
  { 79, "PID_RETRANSMITTER_NUMBER" },
  { 80, "PID_SERIAL_NR_TABLE" },
  { 81, "PID_BIBATMASTER_ADDRESS" },
  { 82, "PID_RF_DOMAIN_ADDRESS" },
  { 83, "PID_DEVICE_DESCRIPTOR" },
  { 84, "PID_METERING_FILTER_TABLE" },
  { 85, "PID_GROUP_TELEGR_RATE_LIMIT_TIME_BASE" },
  { 86, "PID_GROUP_TELEGR_RATE_LIMIT_NO_OF_TELEGR" },
  { 0, NULL }
};

/* PIDs for IOT = 1 (Address Table)
* See "4.10.6 Group Address Table - Realisation Type 6" in "03_05_01 Resources v01.09.03 AS"
* See "4.10.7 Group Address Table - Realisation Type 7" in "03_05_01 Resources v01.09.03 AS"
*/
static const value_string pid1_vals[] = {
  { 51, "PID_EXT_FRAMEFORMAT" },
  { 52, "PID_ADDRTAB1" },
  { 53, "PID_GROUP_RESPONSER_TABLE" },
  { 0, NULL }
};

/* PIDs for IOT = 6 (Router)
* See "4.4 Router Object (Object Type 6)" in "03_05_01 Resources v01.09.03 AS"
* See "2.4.4 Router Object" in "AN161 v05 Coupler Model 2.0 AS"
*/
static const value_string pid6_vals[] = {
  { 51, "PID_MEDIUM_STATUS" },  /* alias "PID_LINE_STATUS" */
  { 52, "PID_MAIN_LCCONFIG" },
  { 53, "PID_SUB_LCCONFIG" },
  { 54, "PID_MAIN_LCGRPCONFIG" },
  { 55, "PID_SUB_LCGRPCONFIG" },
  { 56, "PID_ROUTETABLE_CONTROL" },
  { 57, "PID_COUPL_SERV_CONTROL" },
  { 58, "PID_MAX_APDU_LENGTH" },
  { 59, "PID_L2_COUPLER_TYPE" },
  { 61, "PID_HOP_COUNT" },
  { 63, "PID_MEDIUM" },
  { 67, "PID_FILTER_TABLE_USE" },
  { 104, "PID_PL110_SBC_CONTROL" },
  { 105, "PID_PL110_DOA" },
  { 112, "PID_RF_SBC_CONTROL" },
  { 0, NULL }
};

/* PIDs for IOT = 7 (LTE Address Routing Table)
* See "4.5 LTE Address Routing Table Object (Object Type 7)" in "03_05_01 Resources v01.09.03 AS"
*/
static const value_string pid7_vals[] = {
  { 51, "PID_LTE_ROUTESELECT" },
  { 52, "PID_LTE_ROUTETABLE" },
  { 0, NULL }
};

/* PIDs for IOT = 8 (cEMI Server)
* See "4.6 cEMI Server Object (Object Type 8)" in "03_05_01 Resources v01.09.03 AS"
*/
static const value_string pid8_vals[] = {
  { 51, "PID_MEDIUM_TYPE" },
  { 52, "PID_COMM_MODE" },
  { 53, "PID_MEDIUM_AVAILABILITY" },
  { 54, "PID_ADD_INFO_TYPES" },
  { 55, "PID_TIME_BASE" },
  { 56, "PID_TRANSP_ENABLE" },
  { 59, "PID_BIBAT_NEXTBLOCK" },
  { 60, "PID_RF_MODE_SELECT" },
  { 61, "PID_RF_MODE_SUPPORT" },
  { 62, "PID_RF_FILTERING_MODE_SELECT" },
  { 63, "PID_RF_FILTERING_MODE_SUPPORT" },
  { 0, NULL }
};

/* PIDs for IOT = 9 (Group Object Table)
* See "4.12.4 Group Object Table - Realisation Type 6" in "03_05_01 Resources v01.09.03 AS"
*/
static const value_string pid9_vals[] = {
  { 51, "PID_GRPOBJTABLE" },
  { 52, "PID_EXT_GRPOBJREFERENCE" },
  { 0, NULL }
};

/* PIDs for IOT = 11 (KNXnet/IP Parameter),
* See "2.5 KNXnet/IP Parameter Object" in "03_08_03 Management v01.06.02 AS"
* See "2.3.1 KNXnet/IP Parameter Object" in "AN159 v06 KNXnet-IP Secure AS"
*/
static const value_string pid11_vals[] = {
  { 51, "PID_PROJECT_INSTALLATION_ID" },
  { 52, "PID_KNX_INDIVIDUAL_ADDRESS" },
  { 53, "PID_ADDITIONAL_INDIVIDUAL_ADDRESSES" },
  { 54, "PID_CURRENT_IP_ASSIGNMENT_METHOD" },
  { 55, "PID_IP_ASSIGNMENT_METHOD" },
  { 56, "PID_IP_CAPABILITIES" },
  { 57, "PID_CURRENT_IP_ADDRESS" },
  { 58, "PID_CURRENT_SUBNET_MASK" },
  { 59, "PID_CURRENT_DEFAULT_GATEWAY" },
  { 60, "PID_IP_ADDRESS" },
  { 61, "PID_SUBNET_MASK" },
  { 62, "PID_DEFAULT_GATEWAY" },
  { 63, "PID_DHCP_BOOTP_SERVER" },
  { 64, "PID_MAC_ADDRESS" },
  { 65, "PID_SYSTEM_SETUP_MULTICAST_ADDRESS" },
  { 66, "PID_ROUTING_MULTICAST_ADDRESS" },
  { 67, "PID_TTL" },
  { 68, "PID_KNXNETIP_DEVICE_CAPABILITIES" },
  { 69, "PID_KNXNETIP_DEVICE_STATE" },
  { 70, "PID_KNXNETIP_ROUTING_CAPABILITIES" },
  { 71, "PID_PRIORITY_FIFO_ENABLED" },
  { 72, "PID_QUEUE_OVERFLOW_TO_IP" },
  { 73, "PID_QUEUE_OVERFLOW_TO_KNX" },
  { 74, "PID_MSG_TRANSMIT_TO_IP" },
  { 75, "PID_MSG_TRANSMIT_TO_KNX" },
  { 76, "PID_FRIENDLY_NAME" },
  { 78, "PID_ROUTING_BUSY_WAIT_TIME" },
  { 91, "PID_BACKBONE_KEY" },
  { 92, "PID_DEVICE_AUTHENTICATION_CODE" },
  { 93, "PID_PASSWORD_HASHES" },
  { 94, "PID_SECURED_SERVICE_FAMILIES" },
  { 95, "PID_MULTICAST_LATENCY_TOLERANCE" },
  { 96, "PID_SYNC_LATENCY_FRACTION" },
  { 97, "PID_TUNNELLING_USERS" },
  { 0, NULL }
};

/* PIDs for IOT = 17 (Security)
* See "2.3.5 Security Interface Object" in "KSG638-26.03 KNX Data Security"
*/
static const value_string pid17_vals[] = {
  { 51, "PID_SECURITY_MODE" },
  { 52, "PID_P2P_KEY_TABLE" },
  { 53, "PID_GRP_KEY_TABLE" },
  { 54, "PID_SECURITY_INDIVIDUAL_ADDRESS_TABLE" },
  { 55, "PID_SECURITY_FAILURES_LOG" },
  { 56, "PID_TOOL_KEY" },
  { 57, "PID_SECURITY_REPORT" },
  { 58, "PID_SECURITY_REPORT_CONTROL" },
  { 59, "PID_SEQUENCE_NUMBER_SENDING" },
  { 60, "PID_ZONE_KEY_TABLE" },
  { 61, "PID_GO_SECURITY_FLAGS" },
  { 62, "PID_ROLE_TABLE" },
  { 0, NULL }
};

/* - - - - - - - - -  H E L P E R   F U N C T I O N S  - - - - - - - - - -
*/

/* Add raw data to list view, tree view, and parent folder
*/
static proto_item* proto_tree_add_data( proto_tree* tree, tvbuff_t* tvb, gint offset, gint length, column_info* cinfo, proto_item* item,
  const gchar* name, const gchar* text1, const gchar* text2 )
{
  proto_item* new_item = proto_tree_add_bytes_format( tree, hf_bytes, tvb, offset, length, NULL, "%s: $", name );
  if( text1 ) col_append_str( cinfo, COL_INFO, text1 );
  if( text2 ) proto_item_append_text( item, "%s", text2 );

  while( length > 0 )
  {
    guint8 value = tvb_get_guint8( tvb, offset );
    if( text1 ) col_append_fstr( cinfo, COL_INFO, "%02X", value );
    if( text2 ) proto_item_append_text( item, "%02X", value );
    proto_item_append_text( new_item, " %02X", value );
    offset++;
    length--;
  }

  return new_item;
}

static const gchar* get_pid_name( gint ot, gint pid )
{
  if( pid <= 50 )
  {
    return try_val_to_str( pid, pid_vals );
  }
  {
    const value_string* vals = NULL;
    switch( ot )
    {
    case 0:
      vals = pid0_vals;
      break;
    case 1:
      vals = pid1_vals;
      break;
    case 6:
      vals = pid6_vals;
      break;
    case 7:
      vals = pid7_vals;
      break;
    case 8:
      vals = pid8_vals;
      break;
    case 9:
      vals = pid9_vals;
      break;
    case 11:
      vals = pid11_vals;
      break;
    case 17:
      vals = pid17_vals;
      break;
    }
    if( vals )
    {
      return try_val_to_str( pid, vals );
    }
  }
  return NULL;
}

/* Decrypt data security APDU with a specific key.
*/
static const guint8* decrypt_data_security_data_with_key( wmem_allocator_t *pool, const guint8* key, const guint8* encrypted, gint encrypted_size, const guint8* cemi, gint cemi_size )
{
  guint8 ctr_0[ KNX_KEY_LENGTH ];
  guint8 b_0[ KNX_KEY_LENGTH ];
  guint8 mac[ KNX_KEY_LENGTH ];
  guint8* a_bytes = 0;
  const guint8* p_bytes = NULL;
  gint a_length = 0;
  gint p_length = 0;

  guint8* decrypted = NULL;

  if( encrypted_size > 4 )  // contains 4 bytes MAC
  {
    if( cemi_size >= 2 )
    {
      gint additionalInfoLength = cemi[ 1 ];
      gint offsetToData = additionalInfoLength + 11;
      if( offsetToData + 6 <= cemi_size )
      {
        /* 1 byte Security Control Field */
        guint8 scf = cemi[ offsetToData ];

        // Get A and P.
        if( (scf & 0x30) == 0x10 ) // A+C
        {
          p_bytes = encrypted;
          p_length = encrypted_size - 4;
        }

        // Build b_0.
        b_0[ 0 ] = cemi[ offsetToData + 1 ]; // SeqNr
        b_0[ 1 ] = cemi[ offsetToData + 2 ];
        b_0[ 2 ] = cemi[ offsetToData + 3 ];
        b_0[ 3 ] = cemi[ offsetToData + 4 ];
        b_0[ 4 ] = cemi[ offsetToData + 5 ];
        b_0[ 5 ] = cemi[ offsetToData + 6 ];
        b_0[ 6 ] = cemi[ additionalInfoLength + 4 ]; // SA
        b_0[ 7 ] = cemi[ additionalInfoLength + 5 ];
        b_0[ 8 ] = cemi[ additionalInfoLength + 6 ]; // DA
        b_0[ 9 ] = cemi[ additionalInfoLength + 7 ];
        b_0[ 10 ] = 0; // cemi[additionalInfoLength + 2] & 0x80; // FT
        b_0[ 11 ] = cemi[ additionalInfoLength + 3 ] & 0x8F; // AT (AT+EFF)
        b_0[ 12 ] = cemi[ additionalInfoLength + 9 ]; // TPCI + ApciSec
        b_0[ 13 ] = cemi[ additionalInfoLength + 10 ];
        b_0[ 14 ] = 0;
        b_0[ 15 ] = (guint8) p_length;

        // Build ctr_0.
        ctr_0[ 0 ] = cemi[ offsetToData + 1 ]; // SeqNr
        ctr_0[ 1 ] = cemi[ offsetToData + 2 ];
        ctr_0[ 2 ] = cemi[ offsetToData + 3 ];
        ctr_0[ 3 ] = cemi[ offsetToData + 4 ];
        ctr_0[ 4 ] = cemi[ offsetToData + 5 ];
        ctr_0[ 5 ] = cemi[ offsetToData + 6 ];
        ctr_0[ 6 ] = cemi[ additionalInfoLength + 4 ]; // SA
        ctr_0[ 7 ] = cemi[ additionalInfoLength + 5 ];
        ctr_0[ 8 ] = cemi[ additionalInfoLength + 6 ]; // DA
        ctr_0[ 9 ] = cemi[ additionalInfoLength + 7 ];
        ctr_0[ 10 ] = 0;
        ctr_0[ 11 ] = 0;
        ctr_0[ 12 ] = 0;
        ctr_0[ 13 ] = 0;
        ctr_0[ 14 ] = 0x01;
        ctr_0[ 15 ] = 0;

        decrypted = knx_ccm_encrypt( 0, key, p_bytes, p_length, encrypted + encrypted_size - 4, 4, ctr_0, 4 );

        a_bytes = (guint8*) wmem_alloc( pool, encrypted_size );
        if( (scf & 0x30) == 0x10 ) // A+C
        {
          a_bytes[ 0 ] = scf;
          a_length = 1;
          p_bytes = decrypted;
          p_length = encrypted_size - 4;
        }
        else if( (scf & 0x30) == 0x00 ) // A
        {
          a_bytes[ 0 ] = scf;
          memcpy( a_bytes + 1, decrypted, encrypted_size - 4 );
          a_length = encrypted_size - 3;
        }

        knx_ccm_calc_cbc_mac( mac, key, a_bytes, a_length, p_bytes, p_length, b_0 );
        wmem_free( pool, a_bytes );

        if( memcmp( mac, decrypted + p_length, 4 ) != 0 )
        {
          // Wrong mac. Return 0.
          wmem_free( pool, decrypted );
          decrypted = NULL;
        }
      }
    }
  }

  return decrypted;
}

/* Context info for decrypt_data_security_data
*/
struct data_security_info
{
  guint16 source;       // KNX source address
  guint16 dest;         // KNX source address
  guint8 multicast;     // KNX multicast (group addressed)?
  guint64 seq_nr;       // 6-byte data security sequence number
  gchar output_text[ 128 ];  // buffer for diagnostic output text
};

/* Decrypt data security APDU.
*/
static const guint8* decrypt_data_security_data( wmem_allocator_t *pool, const guint8* encrypted, gint encrypted_size, const guint8* cemi, gint cemi_size, struct data_security_info* info )
{
  const guint8* key = NULL;
  const guint8* decrypted = NULL;
  guint8 keys_found = 0;

  // Get context info
  guint16 source = info->source;
  guint16 dest = info->dest;
  guint8 multicast = info->multicast;

  gchar* output = info->output_text;
  gint output_max = sizeof info->output_text;
  snprintf( output, output_max, "with " );
  while( *output ) { ++output; --output_max; }

  // Try keys from keyring.XML
  if( multicast )
  {
    // Try keys associated with GA
    struct knx_keyring_ga_keys* ga_key;

    for( ga_key = knx_keyring_ga_keys; ga_key; ga_key = ga_key->next )
    {
      if( ga_key->ga == dest )
      {
        keys_found = 1;
        key = ga_key->key;
        decrypted = decrypt_data_security_data_with_key( pool, key, encrypted, encrypted_size, cemi, cemi_size );

        if( decrypted )
        {
          snprintf( output, output_max, "GA " );
          while( *output ) { ++output; --output_max; }
          break;
        }
      }
    }
  }
  else
  {
    // Try keys associated with dest IA
    struct knx_keyring_ia_keys* ia_key;

    for( ia_key = knx_keyring_ia_keys; ia_key; ia_key = ia_key->next )
    {
      if( ia_key->ia == dest )
      {
        keys_found = 1;
        key = ia_key->key;
        decrypted = decrypt_data_security_data_with_key( pool, key, encrypted, encrypted_size, cemi, cemi_size );

        if( decrypted )
        {
          snprintf( output, output_max, "dest IA " );
          while( *output ) { ++output; --output_max; }
          break;
        }
      }
    }
  }

  if( !decrypted )
  {
    // Try keys associated with source IA
    struct knx_keyring_ia_keys* ia_key;

    for( ia_key = knx_keyring_ia_keys; ia_key; ia_key = ia_key->next )
    {
      if( ia_key->ia == source )
      {
        keys_found = 1;
        key = ia_key->key;
        decrypted = decrypt_data_security_data_with_key( pool, key, encrypted, encrypted_size, cemi, cemi_size );

        if( decrypted )
        {
          snprintf( output, output_max, "source IA " );
          while( *output ) { ++output; --output_max; }
          break;
        }
      }
    }
  }

  if( !decrypted && knx_decryption_key_count )
  {
    // Try all explicitly specified keys
    guint8 key_index;

    for( key_index = 0; key_index < knx_decryption_key_count; ++key_index )
    {
      keys_found = 1;
      key = knx_decryption_keys[ key_index ];
      decrypted = decrypt_data_security_data_with_key( pool, key, encrypted, encrypted_size, cemi, cemi_size );

      if( decrypted )
      {
        break;
      }
    }
  }

  if( decrypted )
  {
    guint8 count;

    snprintf( output, output_max, "key" );

    for( count = 16; count; --count )
    {
      while( *output ) { ++output; --output_max; }
      snprintf( output, output_max, " %02X", *key++ );
    }
  }
  else
  {
    snprintf( info->output_text, sizeof info->output_text, keys_found ? "failed" : "no keys found" );
  }

  return decrypted;
}

/* Dissect Object Index (1 byte)
*/
static guint8 dissect_ox( tvbuff_t *tvb, packet_info *pinfo, proto_item *node, proto_tree *list, gint *p_offset, gint end_pos, guint8 *p_error )
{
  gint offset = *p_offset;

  if( offset < end_pos )
  {
    guint8 ox = tvb_get_guint8( tvb, offset );
    column_info *cinfo = pinfo->cinfo;

    col_append_fstr( cinfo, COL_INFO, " OX=%u", ox );
    proto_item_append_text( node, ", OX=%u", ox );
    proto_tree_add_item( list, hf_cemi_ox, tvb, offset, 1, ENC_BIG_ENDIAN );

    *p_offset = ++offset;
    return ox;
  }

  proto_tree_add_expert_format( list, pinfo, KIP_ERROR, tvb, offset, 0, "? Object Index: expected 1 byte" );

  if( p_error )
  {
    *p_error = 1;
  }

  return 0;
}

/* Dissect Object Type (2 bytes)
*/
static guint16 dissect_ot( tvbuff_t *tvb, packet_info *pinfo, proto_item *node, proto_tree *list, gint *p_offset, gint end_pos, guint8 *p_error )
{
  gint offset = *p_offset;

  if( offset + 1 < end_pos )
  {
    guint16 ot = tvb_get_ntohs( tvb, offset );
    column_info *cinfo = pinfo->cinfo;

    col_append_fstr( cinfo, COL_INFO, " OT=%u", ot );
    proto_item_append_text( node, ", OT=%u", ot );

    proto_tree_add_item( list, hf_cemi_ot, tvb, offset, 2, ENC_BIG_ENDIAN );
    offset += 2;

    *p_offset = offset;
    return ot;
  }

  node = proto_tree_add_bytes_format( list, hf_bytes, tvb, offset, end_pos - offset, NULL, "? Object Type" );
  expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 2 bytes" );

  if( p_error )
  {
    *p_error = 1;
  }

  *p_offset = end_pos;
  return 0;
}

/* Dissect Property Identifier (1 byte)
*/
static guint8 dissect_pid( tvbuff_t *tvb, packet_info *pinfo, proto_item *node, proto_tree *list, gint *p_offset, gint end_pos, gint ot, guint8 show, guint8 *p_error )
{
  gint offset = *p_offset;

  if( offset < end_pos )
  {
    guint8 pid = tvb_get_guint8( tvb, offset );
    column_info *cinfo = pinfo->cinfo;
    const gchar* name;

    if( pid || show )
    {
      col_append_fstr( cinfo, COL_INFO, " P=%u", pid );
      proto_item_append_text( node, ", PID=%u", pid );
    }

    if( list )
    {
      node = proto_tree_add_item( list, hf_cemi_pid, tvb, offset, 1, ENC_BIG_ENDIAN );
      name = get_pid_name( ot, pid );
      if( name )
      {
        proto_item_append_text( node, " = %s", name );
      }
    }

    *p_offset = ++offset;
    return pid;
  }

  proto_tree_add_expert_format( list, pinfo, KIP_ERROR, tvb, offset, 0, "? Property ID: expected 1 byte" );

  if( p_error )
  {
    *p_error = 1;
  }

  return 0;
}

/* Dissect Property Index (1 byte)
*/
static guint8 dissect_px( tvbuff_t *tvb, packet_info *pinfo, proto_item *node, proto_tree *list, gint *p_offset, gint end_pos, guint8 show, guint8 *p_error )
{
  gint offset = *p_offset;

  if( offset < end_pos )
  {
    guint8 px = tvb_get_guint8( tvb, offset );

    if( show )
    {
      column_info *cinfo = pinfo->cinfo;
      col_append_fstr( cinfo, COL_INFO, " PX=%u", px );
      proto_item_append_text( node, ", PX=%u", px );
    }

    proto_tree_add_item( list, hf_cemi_px, tvb, offset, 1, ENC_BIG_ENDIAN );

    *p_offset = ++offset;
    return px;
  }

  proto_tree_add_expert_format( list, pinfo, KIP_ERROR, tvb, offset, 0, "? Property Index: expected 1 byte" );

  if( p_error )
  {
    *p_error = 1;
  }

  return 0;
}

/* Dissect Property Range (2 bytes: Number Of Elements (4 bits), Start Index (12 bits))
  and subsequent Data
*/
static void dissect_range( tvbuff_t *tvb, packet_info *pinfo, proto_item *node, proto_tree *list, gint *p_offset, gint end_pos, guint8 pa_flags, guint8 *p_error )
{
  gint offset = *p_offset;
  guint8 error = 0;

  if( offset + 1 >= end_pos )
  {
    node = proto_tree_add_bytes_format( list, hf_bytes, tvb, offset, end_pos - offset, NULL, "? Range" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 2 bytes" );

    *p_offset = end_pos;
    error = 1;
  }
  else
  {
    column_info *cinfo = pinfo->cinfo;
    proto_item *cemi_node = node;
    guint16 sx = tvb_get_ntohs( tvb, offset );
    guint8 ne = sx >> 12;
    sx &= 0x0FFF;

    /* 4 bits Number Of Elements */
    if( ne != 1 )
    {
      col_append_fstr( cinfo, COL_INFO, " N=%u", ne );
      proto_item_append_text( node, ", N=%u", ne );

      if( ne == 0 && !(pa_flags & PA_RESPONSE) )
      {
        error = 1;
      }
      else if( sx == 0 )
      {
        error = 2;
      }
    }

    /* 12 bits Start Index */
    if( sx != 1 )
    {
      col_append_fstr( cinfo, COL_INFO, " X=%u", sx );
      proto_item_append_text( node, ", X=%u", sx );
    }

    if( list )
    {
      proto_item *range_node = proto_tree_add_none_format( list, hf_folder, tvb, offset, 2, "Range: %u element%s at position %u", ne, (ne == 1) ? "" : "s", sx );
      proto_tree *range_list = proto_item_add_subtree( range_node, ett_cemi_range );

      /* 4 bits Number Of Elements */
      node = proto_tree_add_item( range_list, hf_cemi_ne, tvb, offset, 1, ENC_BIG_ENDIAN );

      if( error )
      {
        proto_item_prepend_text( range_node, "? " );
        proto_item_prepend_text( node, "? " );
        expert_add_info_format( pinfo, node, KIP_ERROR, (error == 1) ? "Expected: >= 1 element(s)" : "Expected: 1 element" );
      }

      /* 12 bits Start Index */
      proto_tree_add_item( range_list, hf_cemi_sx, tvb, offset, 2, ENC_BIG_ENDIAN );
    }

    offset += 2;

    /* Data */
    {
      gint length = end_pos - offset;
      if( length > 0 )
      {
        node = proto_tree_add_data( list, tvb, offset, length, cinfo, cemi_node, "Data", " $", ", $" );
        if( !pa_flags )
        {
          proto_item_prepend_text( node, "? " );
          expert_add_info_format( pinfo, node, KIP_ERROR, "Unexpected" );
          error = 1;
        }
        else if( (pa_flags & PA_RESPONSE) && (!(pa_flags & PA_DATA) || ne == 0) && length != 1 )
        {
          proto_item_prepend_text( node, "? " );
          expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: max 1 byte" );
          error = 1;
        }
        else if( pa_flags & PA_DATA )
        {
          if( ne == 1 && sx == 0 && length != 2 )
          {
            proto_item_prepend_text( node, "? " );
            expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 2 bytes" );
            error = 1;
          }
          else if( ne >= 2 && (length % ne) != 0 )
          {
            proto_item_prepend_text( node, "? " );
            expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: multiple of %u bytes", ne );
            error = 1;
          }
        }
      }
    }

    *p_offset = end_pos;
  }

  if( error && p_error )
  {
    *p_error = 1;
  }
}

/* Dissect Property Description: PDT (1 byte), Max Count (2 bytes), Access Levels (1 byte)
*/
static void dissect_prop_descr( tvbuff_t* tvb, packet_info* pinfo, proto_item* cemi_node, proto_tree* cemi_list, gint* p_offset, gint size, guint8* p_error )
{
  gint offset = *p_offset;
  column_info* cinfo = pinfo->cinfo;
  guint8 error = 0;

  /* 4 bytes Property Description */
  if( offset + 4 > size )
  {
    proto_item* node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Property Description" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 4 bytes" );

    error = 1;
    offset = size;
  }
  else
  {
    /* 1 bit Writability, 1 bit reserved, 6 bits Property Data Type */
    guint8 pdt = tvb_get_guint8( tvb, offset );
    guint8 writable = (pdt & 0x80) != 0;
    pdt &= 0x3F;
    col_append_fstr( cinfo, COL_INFO, " T=%u", pdt );
    proto_item_append_text( cemi_node, ", T=%u", pdt );

    /* 4 bits reserved, 12 bits Max Elements */
    guint16 me = tvb_get_ntohs( tvb, offset + 1) & 0x0FFF;
    if( me != 1 )
    {
      col_append_fstr( cinfo, COL_INFO, " N=%u", me );
      proto_item_append_text( cemi_node, ", N=%u", me );
    }

    /* 4 bits Read Access, 4 bits Write Access */
    guint8 wa = tvb_get_guint8( tvb, offset + 3 );
    guint8 ra = (wa & 0xF0) >> 4;
    wa &= 0x0F;
    col_append_fstr( cinfo, COL_INFO, " R=%u", ra );
    if( writable )
      col_append_fstr( cinfo, COL_INFO, " W=%u", wa );
    proto_item_append_text( cemi_node, ", R=%u", ra );
    if( writable )
      proto_item_append_text( cemi_node, ", W=%u", wa );

    if( cemi_list )
    {
      proto_item *pd_node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 4, "Property Description: " );
      proto_tree *pd_list = proto_item_add_subtree( pd_node, ett_cemi_pd );

      const gchar* pdt_name = try_val_to_str( pdt, pdt_vals );
      if( pdt_name )
        proto_item_append_text( pd_node, "%s", pdt_name );
      else
        proto_item_append_text( pd_node, "PDT = 0x%02X", pdt );

      if( me != 1 )
        proto_item_append_text( pd_node, ", Max Elements = %u", me );

      proto_item_append_text( pd_node, ", Read = %u", ra );
      if( writable )
        proto_item_append_text( pd_node, ", Write = %u", wa );

      proto_tree_add_item( pd_list, hf_cemi_pw, tvb, offset, 1, ENC_BIG_ENDIAN );
      proto_tree_add_item( pd_list, hf_cemi_pdt, tvb, offset, 1, ENC_BIG_ENDIAN );
      proto_tree_add_item( pd_list, hf_cemi_me, tvb, offset + 1, 2, ENC_BIG_ENDIAN );
      proto_tree_add_item( pd_list, hf_cemi_ra, tvb, offset + 3, 1, ENC_BIG_ENDIAN );
      proto_tree_add_item( pd_list, hf_cemi_wa, tvb, offset + 3, 1, ENC_BIG_ENDIAN );
    }

    offset += 4;
  }

  if( error && p_error )
  {
    *p_error = 1;
  }

  *p_offset = offset;
}

/* Dissect OT (2 bytes), OI (12 bits), and PID (12 bits) for PropertyExt services
*/
static void dissect_pid_ext( tvbuff_t *tvb, packet_info *pinfo, proto_item *cemi_node, proto_tree *cemi_list, gint *p_offset, gint size, guint8 *p_error )
{
  gint offset = *p_offset;
  column_info* cinfo = pinfo->cinfo;
  guint8 error = 0;

  /* 2 bytes Object Type */
  guint16 ot = dissect_ot( tvb, pinfo, cemi_node, cemi_list, &offset, size, &error );

  if( offset + 3 > size )
  {
    proto_item* node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Object Instance, PID" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 3 bytes" );

    error = 1;
    offset = size;
  }
  else
  {
    /* 12 bits Object Instance */
    guint16 cc = tvb_get_ntohs( tvb, offset ) >> 4;
    col_append_fstr( cinfo, COL_INFO, " OI=%u", cc );
    proto_item_append_text( cemi_node, ", OI=%u", cc );
    proto_tree_add_item( cemi_list, hf_cemi_ext_oi, tvb, offset, 2, ENC_BIG_ENDIAN );
    ++offset;

    /* 12 bits Property ID */
    cc = tvb_get_ntohs( tvb, offset ) & 0x0FFF;
    col_append_fstr( cinfo, COL_INFO, " P=%u", cc );
    proto_item_append_text( cemi_node, ", PID=%u", cc );

    if( cemi_list )
    {
      proto_item* node = proto_tree_add_item( cemi_list, hf_cemi_ext_pid, tvb, offset, 2, ENC_BIG_ENDIAN );
      const gchar* name = get_pid_name( ot, cc );
      if( name )
      {
        proto_item_append_text( node, " = %s", name );
      }
    }

    offset += 2;
  }

  if( error && p_error )
  {
    *p_error = 1;
  }

  *p_offset = offset;
}

/* Dissect cEMI Management packet
  (M_PropRead.req, M_PropRead.con, M_PropWrite.req, M_PropWrite.con, M_PropInfo.ind, M_Reset.req, M_Reset.ind)
*/
static void dissect_cemi_mgmt_packet( tvbuff_t* tvb, packet_info* pinfo, proto_item *cemi_node, proto_tree *cemi_list, guint8 mc, gint *p_offset, gint size, guint8* p_pa_flags, guint8 *p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  guint8 min_size = 1;

  switch( mc )
  {
  case CEMI_M_PROPREAD_REQ:
    pa_flags = 0;
    goto case_CEMI_M_PROP;

  case CEMI_M_PROPWRITE_CON:
    pa_flags = PA_RESPONSE;
    goto case_CEMI_M_PROP;

  case CEMI_M_PROPREAD_CON:
    pa_flags = PA_RESPONSE | PA_DATA;
    goto case_CEMI_M_PROP;

  case CEMI_M_PROPWRITE_REQ:
  case CEMI_M_PROPINFO_IND:
    //pa_flags = PA_DATA;

  case_CEMI_M_PROP:
    min_size = 7;
    break;

  case CEMI_M_FUNCPROPCMD_REQ:
  case CEMI_M_FUNCPROPREAD_REQ:
  case CEMI_M_FUNCPROP_CON:
    //pa_flags = PA_DATA;
    min_size = 5;
    break;

  case CEMI_M_RESET_REQ:
  case CEMI_M_RESET_IND:
    pa_flags = 0;
    break;
  }

  if( min_size >= 5 )
  {
    /* 2 bytes Object Type */
    guint16 ot = dissect_ot( tvb, pinfo, cemi_node, cemi_list, &offset, size, &error );

    /* 1 byte Object Instance */
    if( size < 4 )
    {
      proto_tree_add_expert_format( cemi_list, pinfo, KIP_ERROR, tvb, offset, 0, "? Object Instance: expected 1 byte" );
      error = 1;
    }
    else
    {
      guint8 oi = tvb_get_guint8( tvb, 3 );
      if( oi != 1 )
      {
        col_append_fstr( cinfo, COL_INFO, " OI=%u", oi );
        proto_item_append_text( cemi_node, ", OI=%u", oi );
      }
      proto_tree_add_item( cemi_list, hf_cemi_oi, tvb, 3, 1, ENC_BIG_ENDIAN );
      offset = 4;
    }

    /* 1 byte Property ID
    */
    dissect_pid( tvb, pinfo, cemi_node, cemi_list, &offset, size, ot, 1, &error );

    if( min_size >= 7 )
    {
      /* Range (Start Index, Number Of Elements) and Data
      */
      dissect_range( tvb, pinfo, cemi_node, cemi_list, &offset, size, pa_flags, &error );
      pa_flags = 0;
    }
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect A_MemoryExt service */
static void dissect_memory_ext_service( tvbuff_t* tvb, packet_info* pinfo, proto_item* cemi_node, proto_tree* cemi_list,
  guint16 ax, gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  /* 4 bytes Range (1 byte Memory Length, 3 bytes Memory Address) */
  if( offset + 4 > size )
  {
    proto_item* node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Range" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 4 bytes" );
    error = 1;
    offset = size;
  }
  else
  {
    /* 1 byte Memory Length or Error Code */
    guint8 is_response = (ax == AX_MemExtReadResp || ax == AX_MemExtWriteResp);
    guint8 n = tvb_get_guint8( tvb, offset );
    if( is_response )
    {
      if( n != 0 )
      {
        col_append_fstr( cinfo, COL_INFO, " E=$%02X", n );
        proto_item_append_text( cemi_node, ", E=$%02X", n );
      }
    }
    else
    {
      if( n != 1 )
      {
        col_append_fstr( cinfo, COL_INFO, " N=%u", n );
        proto_item_append_text( cemi_node, ", N=%u", n );
      }
    }

    /* 3 bytes Memory Address */
    guint32 x = tvb_get_guint24( tvb, offset + 1, ENC_BIG_ENDIAN );
    col_append_fstr( cinfo, COL_INFO, " X=$%06" PRIX32, x );
    proto_item_append_text( cemi_node, ", X=$%06" PRIX32, x );

    if( is_response )
    {
      proto_tree_add_item( cemi_list, hf_cemi_error, tvb, offset, 1, ENC_BIG_ENDIAN );
    }
    else
    {
      proto_tree_add_item( cemi_list, hf_cemi_ext_memory_length, tvb, offset, 1, ENC_BIG_ENDIAN );
    }

    proto_tree_add_item( cemi_list, hf_cemi_ext_memory_address, tvb, offset + 1, 3, ENC_BIG_ENDIAN );

    offset += 4;
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect A_UserMemory service */
static void dissect_user_memory_service( tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree, proto_item* cemi_node, proto_tree* cemi_list,
  gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  proto_item* node;
  proto_tree* list;

  /* 3 bytes Range (Memory Length, Memory Address) */
  if( offset + 3 > size )
  {
    node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Range" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 3 bytes" );
    error = 1;
    offset = size;
  }
  else
  {
    guint8 c2 = tvb_get_guint8( tvb, offset );
    guint8 c1 = c2 >> 4;
    guint32 c3 = tvb_get_ntohs( tvb, offset + 1 );
    c2 &= 0x0F;
    c3 |= c1 << 16UL;
    if( c2 != 1 )
      col_append_fstr( cinfo, COL_INFO, " N=%u", c2 );
    col_append_fstr( cinfo, COL_INFO, " X=$%05X", c3 );
    if( tree )
    {
      if( c2 != 1 )
        proto_item_append_text( cemi_node, ", N=%u", c2 );
      proto_item_append_text( cemi_node, ", X=$%05X", c3 );
      node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 1,
        "Range: %u byte%s at address $%05X", c2, (c2 == 1) ? "" : "s", c3 );
      list = proto_item_add_subtree( node, ett_cemi_range );
      proto_tree_add_item( list, hf_cemi_memory_address_ext, tvb, offset, 1, ENC_BIG_ENDIAN );
      proto_tree_add_item( list, hf_cemi_memory_length, tvb, offset, 1, ENC_BIG_ENDIAN );
      proto_tree_add_item( list, hf_cemi_memory_address, tvb, offset + 1, 2, ENC_BIG_ENDIAN );
    }
    offset += 3;
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect A_FunctionProperty service */
static void dissect_function_property_service( tvbuff_t* tvb, packet_info* pinfo, proto_item* cemi_node, proto_tree* cemi_list,
  gint* p_offset, gint size, guint8* p_error )
{
  /* 1 byte Object Index */
  dissect_ox( tvb, pinfo, cemi_node, cemi_list, p_offset, size, p_error );

  /* 1 byte Property ID */
  dissect_pid( tvb, pinfo, cemi_node, cemi_list, p_offset, size, -1, 1, p_error );
}

/* Dissect (obsolete) A_Router service */
static void dissect_router_service( tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree, proto_item* cemi_node, proto_tree* cemi_list,
  gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  proto_item* node;
  proto_tree* list;

  /* 3 bytes Range (1 byte Memory Length, 2 bytes Memory Address) */
  if( offset + 3 > size )
  {
    node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Range" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 3 bytes" );
    error = 1;
    offset = size;
  }
  else
  {
    guint8 c = tvb_get_guint8( tvb, offset );
    guint16 cc = tvb_get_ntohs( tvb, offset + 1 );
    if( c != 1 )
      col_append_fstr( cinfo, COL_INFO, " N=%u", c );
    col_append_fstr( cinfo, COL_INFO, " X=$%04X", cc );
    if( tree )
    {
      proto_item_append_text( cemi_node, ", N=%u, X=$%04X", c, cc );
      node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 3,
        "Range: %u byte%s at address $%04X", c, (c == 1) ? "" : "s", cc );
      list = proto_item_add_subtree( node, ett_cemi_range );
      proto_tree_add_item( list, hf_cemi_ext_memory_length, tvb, offset, 1, ENC_BIG_ENDIAN );
      proto_tree_add_item( list, hf_cemi_ext_memory_address, tvb, offset + 1, 2, ENC_BIG_ENDIAN );
    }
    offset += 3;
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect A_Authenticate or A_Key service */
static void dissect_authenticate_service( tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree, proto_item* cemi_node, proto_tree* cemi_list,
  guint16 ax, gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  /* 1 byte Level */
  if( offset >= size )
  {
    proto_tree_add_expert_format( cemi_list, pinfo, KIP_ERROR, tvb, offset, 0, "? Level: expected 1 byte" );
    error = 1;
  }
  else
  {
    guint8 c = tvb_get_guint8( tvb, offset );
    if( ax != AX_AuthReq || c != 0 )
    {
      col_append_fstr( cinfo, COL_INFO, " L=%u", c );
      if( tree )
      {
        proto_item_append_text( cemi_node, ", L=%u", c );
      }
    }
    if( tree )
    {
      proto_tree_add_item( cemi_list, hf_cemi_level, tvb, offset, 1, ENC_BIG_ENDIAN );
    }
    offset++;
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect A_PropertyValue service */
static void dissect_property_value_service( tvbuff_t* tvb, packet_info* pinfo, proto_item* cemi_node, proto_tree* cemi_list,
  gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  /* 1 byte Object Index */
  dissect_ox( tvb, pinfo, cemi_node, cemi_list, p_offset, size, p_error );

  /* 1 byte Property ID */
  dissect_pid( tvb, pinfo, cemi_node, cemi_list, p_offset, size, -1, 1, p_error );

  /* 2 bytes Range */
  dissect_range( tvb, pinfo, cemi_node, cemi_list, p_offset, size, *p_pa_flags, p_error );
}

/* Dissect A_PropertyDescription service */
static void dissect_property_description_service( tvbuff_t* tvb, packet_info* pinfo, proto_item* cemi_node, proto_tree* cemi_list,
  gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  /* 1 byte Object Index */
  dissect_ox( tvb, pinfo, cemi_node, cemi_list, p_offset, size, p_error );

  /* 1 byte Property ID */
  {
    guint8 pa_flags = *p_pa_flags;
    guint8 pid = dissect_pid( tvb, pinfo, cemi_node, cemi_list, p_offset, size, -1, pa_flags, p_error );

    /* 1 byte Property Index */
    dissect_px( tvb, pinfo, cemi_node, cemi_list, p_offset, size, pa_flags || !pid, p_error );

    if( pa_flags )  /* A_PropertyDescription_Response */
    {
      /* 1 byte PDT, 2 bytes Max Elements, 1 byte Access Levels */
      dissect_prop_descr( tvb, pinfo, cemi_node, cemi_list, p_offset, size, p_error );

      /* No further trailing data */
      *p_pa_flags = 0;
    }
  }
}

/* Dissect A_NetworkParameter or A_GroupPropertyValue service */
static void dissect_network_parameter_service( tvbuff_t* tvb, packet_info* pinfo, proto_item* cemi_node, proto_tree* cemi_list,
  gint* p_offset, gint size, guint8* p_error )
{
  /* 2 bytes Object Type */
  guint16 ot = dissect_ot( tvb, pinfo, cemi_node, cemi_list, p_offset, size, p_error );

  /* 1 byte Property ID */
  dissect_pid( tvb, pinfo, cemi_node, cemi_list, p_offset, size, ot, 1, p_error );
}

/* Dissect A_IndividualAddressSerialNumber or A_DomainAddressSerialNumber service */
static void dissect_ia_serial_number_service( tvbuff_t* tvb, packet_info* pinfo, proto_item* cemi_node, proto_tree* cemi_list,
  gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  proto_item* node;

  /* 6 bytes Serial Nr */
  if( offset + 6 > size )
  {
    node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Serial Number" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 6 bytes" );
    error = 1;
    offset = size;
  }
  else
  {
    proto_tree_add_data( cemi_list, tvb, offset, 6, cinfo, cemi_node, "Serial Number", " SN=$", ", SerNr=$" );
    offset += 6;
  }

  if( pa_flags )
  {
    if( offset >= size )
    {
      proto_tree_add_expert_format( cemi_list, pinfo, KIP_ERROR, tvb, offset, 0, "? Data: missing" );
      error = 1;
    }
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect A_SystemNetworkParameter service */
static void dissect_system_network_parameter_service( tvbuff_t* tvb, packet_info* pinfo, proto_item* cemi_node, proto_tree* cemi_list,
  gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  proto_item* node;
  const gchar* name;
  guint16 ot;
  guint16 cc;
  guint8 c;

  /* 2 bytes Object Type */
  if( offset + 1 >= size )
  {
    ot = 0;
    node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Object Type" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 2 bytes" );
    error = 1;
    offset = size;
  }
  else
  {
    ot = cc = tvb_get_ntohs( tvb, offset );

    if( cc )
    {
      col_append_fstr( cinfo, COL_INFO, " OT=%u", cc );
      proto_item_append_text( cemi_node, ", OT=%u", cc );
    }

    if( cemi_list )
    {
      node = proto_tree_add_item( cemi_list, hf_cemi_ot, tvb, offset, 2, ENC_BIG_ENDIAN );
      name = try_val_to_str( cc, ot_vals );
      if( name )
      {
        proto_item_append_text( node, " = %s", name );
      }
    }

    offset += 2;
  }

  /* 2 bytes Property ID (12 bits) and Reserved (4 bits) */
  if( offset + 1 >= size )
  {
    node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Property ID" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 2 bytes" );
    error = 1;
    offset = size;
  }
  else
  {
    /* 12 bits Property ID */
    cc = tvb_get_ntohs( tvb, offset );
    c = cc & 0x000F;
    cc >>= 4;

    col_append_fstr( cinfo, COL_INFO, " P=%u", cc );
    proto_item_append_text( cemi_node, ", PID=%u", cc );

    if( cemi_list )
    {
      node = proto_tree_add_item( cemi_list, hf_cemi_snp_pid, tvb, offset, 2, ENC_BIG_ENDIAN );
      name = get_pid_name( ot, cc );
      if( name )
      {
        proto_item_append_text( node, " = %s", name );
      }
    }

    ++offset;

    /* 4 bits Reserved */
    if( c )
    {
      col_append_fstr( cinfo, COL_INFO, " $%X", c );
      proto_item_append_text( cemi_node, ", $%X", c );
      node = proto_tree_add_item( cemi_list, hf_cemi_snp_reserved, tvb, offset, 1, ENC_BIG_ENDIAN );
      expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: zero" );
      error = 1;
    }

    ++offset;
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect A_PropertyExtValue service */
static void dissect_property_ext_value_service( tvbuff_t* tvb, packet_info* pinfo, proto_item* cemi_node, proto_tree* cemi_list,
  gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  proto_item* node;

  /* 2 bytes OT, 12 bits OI, 12 bits PID */
  dissect_pid_ext( tvb, pinfo, cemi_node, cemi_list, &offset, size, &error );

  /* 3 bytes Range (1 byte Count, 2 bytes Index) */
  if( offset + 3 > size )
  {
    node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Range" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 3 bytes" );
    error = 1;
    offset = size;
  }
  else
  {
    /* 1 byte Count */
    guint8 ne = tvb_get_guint8( tvb, offset );
    if( ne != 1 )
    {
      col_append_fstr( cinfo, COL_INFO, " N=%u", ne );
      proto_item_append_text( cemi_node, ", N=%u", ne );
    }

    /* 2 bytes Index */
    guint16 sx = tvb_get_ntohs( tvb, offset + 1 );
    if( sx != 1 )
    {
      col_append_fstr( cinfo, COL_INFO, " X=%u", sx );
      proto_item_append_text( cemi_node, ", X=%u", sx );
    }

    if( cemi_list )
    {
      proto_item *range_node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 3, "Range: %u element%s at position %u", ne, (ne == 1) ? "" : "s", sx );
      proto_tree *range_list = proto_item_add_subtree( range_node, ett_cemi_range );
      proto_tree_add_item( range_list, hf_cemi_ext_ne, tvb, offset, 1, ENC_BIG_ENDIAN );
      proto_tree_add_item( range_list, hf_cemi_ext_sx, tvb, offset + 1, 2, ENC_BIG_ENDIAN );
    }

    offset += 3;
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect A_PropertyExtDescription service */
static void dissect_property_ext_description_service( tvbuff_t* tvb, packet_info* pinfo, proto_item* cemi_node, proto_tree* cemi_list,
  gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  proto_item* node;
  guint16 cc;
  guint8 c;

  /* 2 bytes OT, 12 bits OI, 12 bits PID */
  dissect_pid_ext( tvb, pinfo, cemi_node, cemi_list, &offset, size, &error );

  /* 4 bits Description Type */
  if( offset >= size )
  {
    node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Description Type" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 4 bits" );
    error = 1;
  }
  else
  {
    c = tvb_get_guint8( tvb, offset ) >> 4;
    col_append_fstr( cinfo, COL_INFO, " D=%u", c );
    proto_item_append_text( cemi_node, ", D=%u", c );
    proto_tree_add_item( cemi_list, hf_cemi_ext_dt, tvb, offset, 1, ENC_BIG_ENDIAN );
  }

  /* 12 bits Property Index */
  if( offset + 2 > size )
  {
    node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Property Index" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 12 bits" );
    error = 1;
    offset = size;
  }
  else
  {
    cc = tvb_get_ntohs( tvb, offset ) & 0x0FFF;
    col_append_fstr( cinfo, COL_INFO, " PX=%u", cc );
    proto_item_append_text( cemi_node, ", PX=%u", cc );
    proto_tree_add_item( cemi_list, hf_cemi_ext_px, tvb, offset, 2, ENC_BIG_ENDIAN );
    offset += 2;
  }

  if( pa_flags ) /* AX_PropExtDescrResp */
  {
    /* 4 bytes DPT (2 bytes DPT Major, 2 bytes DPT Minor) */
    if( offset + 4 > size )
    {
      node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Data Point Type" );
      expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 4 bytes" );
      error = 1;
      offset = size;
    }
    else
    {
      guint16 dpt_major = tvb_get_ntohs( tvb, offset );
      guint16 dpt_minor = tvb_get_ntohs( tvb, offset + 2 );

      if( cemi_list )
      {
        proto_item *dpt_node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 2, "Data Point Type: %u.%u", dpt_major, dpt_minor );
        proto_tree *dpt_list = proto_item_add_subtree( dpt_node, ett_cemi_dpt );
        proto_tree_add_item( dpt_list, hf_cemi_dpt_major, tvb, offset, 2, ENC_BIG_ENDIAN );
        proto_tree_add_item( dpt_list, hf_cemi_dpt_minor, tvb, offset + 2, 2, ENC_BIG_ENDIAN );
      }

      offset += 4;

      if( dpt_major || dpt_minor )
      {
        col_append_fstr( cinfo, COL_INFO, " DPT=%u.%u", dpt_major, dpt_minor );
        proto_item_append_text( cemi_node, ", DPT=%u.%u", dpt_major, dpt_minor );
      }
    }

    /* 1 byte PDT, 2 bytes Max Elements, 1 byte Access Levels */
    dissect_prop_descr( tvb, pinfo, cemi_node, cemi_list, &offset, size, &error );

    /* No further trailing data */
    pa_flags = 0;
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect A_DataSecurity service */
static void dissect_data_security_service( tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree, proto_item* cemi_node, proto_tree* cemi_list,
  guint16 source_addr, proto_item* source_node, guint16 dest_addr, proto_item* dest_node, guint8 unicast,
  const gchar* name, gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  proto_tree* root_tree = tree;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  proto_item* node;
  proto_tree* list;

  // 1 byte SCF, 6 bytes SeqNr, ...
  // and either another SeqNr for sync or Apci+Mac (2+4 bytes) for data.
  if( offset + 13 > size )
  {
    node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? SCF, SeqNr, ..." );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: min 13 bytes" );
    error = 1;
    offset = size;
  }
  else
  {
    /* 1 byte SCF */
    guint8 scf = tvb_get_guint8( tvb, offset );
    guint8 is_sync = (scf & 6) == 0x02;
    guint8 is_sync_req = is_sync && (scf & 1) == 0;
    guint8 is_sync_res = is_sync && !is_sync_req;
    guint64 seq_nr;

    name = try_val_to_str( scf, scf_short_vals );
    if( !name ) name = "?";
    col_append_fstr( cinfo, COL_INFO, " %s", name );
    proto_item_append_text( cemi_node, ", %s", name );

    node = proto_tree_add_item( cemi_list, hf_cemi_scf, tvb, offset, 1, ENC_BIG_ENDIAN );
    list = proto_item_add_subtree( node, ett_cemi_scf );
    proto_tree_add_item( list, hf_cemi_scf_t, tvb, offset, 1, ENC_BIG_ENDIAN );
    proto_tree_add_item( list, hf_cemi_scf_sai, tvb, offset, 1, ENC_BIG_ENDIAN );
    proto_tree_add_item( list, hf_cemi_scf_sbc, tvb, offset, 1, ENC_BIG_ENDIAN );
    proto_tree_add_item( list, hf_cemi_scf_svc, tvb, offset, 1, ENC_BIG_ENDIAN );

    ++offset;

    /*  6 bytes SeqNr */
    name = is_sync_req ? "SeqNrLocal" : is_sync_res ? "Challenge" : "SeqNr";
    seq_nr = tvb_get_ntoh48( tvb, offset );
    proto_tree_add_data( cemi_list, tvb, offset, 6, cinfo, cemi_node, name, NULL, is_sync_res ? NULL : ", SeqNrLocal=$" );
    offset += 6;

    if( is_sync )
    {
      /* 6 bytes SyncReq SerNr or SyncRes SeqNrRemote */
      name = is_sync_req ? "SerNr" : "SeqNrRemote";
      proto_tree_add_data( cemi_list, tvb, offset, 6, cinfo, cemi_node, name, NULL, is_sync_res ? ", SeqNrRemote=$" : NULL );
      offset += 6;

      /* 6 bytes SyncReq Challenge or SyncRes SeqNrLocal */
      name = is_sync_req ? "Challenge" : "SeqNrLocal";
      if( offset + 6 > size )
      {
        node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "%s", name );
        proto_item_prepend_text( node, "? " );
        expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 6 bytes" );
        error = 1;
        offset = size;
      }
      else
      {
        proto_tree_add_data( cemi_list, tvb, offset, 6, NULL, NULL, name, NULL, NULL );
        offset += 6;

        if( offset < size )
        {
          /* 4 bytes MAC */
          node = proto_tree_add_data( cemi_list, tvb, offset, size - offset, NULL, NULL, "Message Authentication Code", NULL, NULL );
          if( offset + 4 != size )
          {
            proto_item_prepend_text( node, "? " );
            expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 4 bytes" );
            error = 1;
          }
          offset = size;
        }
      }
    }
    else // Data
    {
      struct data_security_info info;
      struct knx_keyring_ia_seqs* ia_seq;
      const guint8* cemi;
      const guint8* encrypted;
      gint encrypted_size;
      const guint8* decrypted;
      proto_item* item;

      info.source = source_addr;
      info.dest = dest_addr;
      info.multicast = !unicast;
      info.seq_nr = seq_nr;
      *info.output_text = '\0';

      if( !unicast )  // multicast or broadcast
      {
        // Check sending IA
        guint8 ga_found = 0;
        guint8 ia_ok = 0;
        struct knx_keyring_ga_senders* ga_sender = knx_keyring_ga_senders;
        for( ; ga_sender; ga_sender = ga_sender->next )
        {
          if( ga_sender->ga == dest_addr )
          {
            ga_found = 1;

            if( ga_sender->ia == source_addr )
            {
              ia_ok = 1;
              break;
            }
          }
        }

        if( !ia_ok )
        {
          if( ga_found )
          {
            expert_add_info_format( pinfo, source_node, KIP_ERROR, "Unknown sender" );
            error = 1;
          }
          else
          {
            expert_add_info_format( pinfo, dest_node, KIP_WARNING, "Unknown group address" );
          }
        }
      }

      // Check SeqNr
      for( ia_seq = knx_keyring_ia_seqs; ia_seq; ia_seq = ia_seq->next )
      {
        if( ia_seq->ia == source_addr )
        {
          if( ia_seq->seq > seq_nr )
          {
            expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: min $%012" PRIX64, ia_seq->seq );
            break;
          }
        }
      }

      // Get encrypted data.
      cemi = tvb_get_ptr( tvb, 0, size );
      encrypted = cemi + offset;
      encrypted_size = size - offset;

      // Decrypt.
      decrypted = decrypt_data_security_data( pinfo->pool, encrypted, encrypted_size, cemi, size, &info );

      if( decrypted )
      {
        tvbuff_t* tvb2 = tvb_new_child_real_data( tvb, decrypted, encrypted_size, encrypted_size );
        gint size2 = encrypted_size - 4;  // > 0, guaranteed by decrypt_data_security_data
        proto_item_append_text( cemi_node, ", MAC OK" );
        //tvb_set_free_cb(tvb2, wmem_free);
        add_new_data_source( pinfo, tvb2, "Decrypted" );

        item = proto_tree_add_none_format( cemi_list, hf_folder, tvb2, 0, encrypted_size, "Decrypted" );
        tree = proto_item_add_subtree( item, ett_cemi_decrypted );

        if( *info.output_text )
        {
          proto_item_append_text( item, " (%s)", info.output_text );
        }

        proto_tree_add_data( tree, tvb2, 0, size2, NULL, NULL, "Embedded APDU", NULL, NULL );
        proto_tree_add_data( tree, tvb2, size2, 4, NULL, NULL, "Message Authentication Code", NULL, NULL );

        /* Dissect embedded APDU */
        {
          // Hack: To save us from splitting another sub dissector which only
          // dissects the Apci+Apdu
          // we synthesize a telegram from the outer ApciSec telegram fields and the inner
          // decrypted apci+apdu and then we dissect this as a new cEMI frame.
          gint innerTelegramSize = size - 13;     // > 0, already checked above
          gint additionalInfoLength = cemi[ 1 ];  // cemi size > 13, already checked above
          gint offsetToApci = additionalInfoLength + 9;
          if( offsetToApci < size )
          {
            if( offsetToApci + size2 <= innerTelegramSize )
            {
              guint8* innerTelegram = (guint8*) wmem_alloc( pinfo->pool, innerTelegramSize );

              memcpy( innerTelegram, cemi, offsetToApci );
              memcpy( innerTelegram + offsetToApci, decrypted, size2 );
              innerTelegram[ additionalInfoLength + 8 ] = (guint8) (size2 - 1);

              tvbuff_t* tvb3 = tvb_new_child_real_data( tvb, innerTelegram, innerTelegramSize, innerTelegramSize );
              //tvb_set_free_cb(tvb3, wmem_free);
              add_new_data_source( pinfo, tvb3, "Inner Decrypted Telegram" );

              dissector_handle_t cemi_handle = find_dissector( "cemi" );
              if( cemi_handle )
              {
                call_dissector( cemi_handle, tvb3, pinfo, root_tree );
              }
            }
          }
        }
      }
      else
      {
        // Could not be decrypted.
        proto_item_append_text( cemi_node, ", Could not be decrypted" );

        if( *info.output_text )
        {
          proto_item_append_text( cemi_node, " (%s)", info.output_text );
        }
      }

      offset = size;
    }
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect extended AL service (10 bit AL service code)
*/
static void dissect_extended_app_service( tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree, proto_item* cemi_node, proto_tree* cemi_list,
  guint16 source_addr, proto_item* source_node, guint16 dest_addr, proto_item* dest_node, guint8 unicast,
  guint16 ax, const gchar* name,
  gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  proto_item* node = NULL;
  proto_tree* list = NULL;

  col_append_fstr( cinfo, COL_INFO, " %s", name );

  if( tree )
  {
    proto_item_append_text( cemi_node, ", %s", name );
    node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 2, "APCI: %s", name );
    list = proto_item_add_subtree( node, ett_cemi_apci );
    proto_tree_add_item( list, hf_cemi_ax, tvb, offset, 2, ENC_BIG_ENDIAN );
  }

  offset += 2;

  pa_flags = PA_RESPONSE | PA_DATA;

  switch( ax )
  {
  case AX_UserMemRead:
  case AX_MemExtRead:
  case AX_RoutingTableRead:
  case AX_RouterMemRead:
  case AX_PropValueRead:
  case AX_PropDescrRead:
  case AX_IndAddrSerNumRead:
  case AX_DomAddrSerNumRead:
  case AX_PropExtValueRead:
  case AX_PropExtDescrRead:
    pa_flags = 0;
    break;
  }

  switch( ax )
  {
  case AX_MemExtRead:
  case AX_MemExtReadResp:
  case AX_MemExtWrite:
  case AX_MemExtWriteResp:
    dissect_memory_ext_service( tvb, pinfo, cemi_node, cemi_list, ax, &offset, size, &pa_flags, &error );
    break;

  case AX_UserMemRead:
  case AX_UserMemResp:
  case AX_UserMemWrite:
  case AX_UserMemBitWrite:
    dissect_user_memory_service( tvb, pinfo, tree, cemi_node, cemi_list, &offset, size, &pa_flags, &error );
    break;

  case AX_FuncPropCmd:
  case AX_FuncPropRead:
  case AX_FuncPropResp:
    dissect_function_property_service( tvb, pinfo, cemi_node, cemi_list, &offset, size, &error );
    break;

  case AX_RoutingTableRead:
  case AX_RouterMemRead:
  case AX_RoutingTableResp:
  case AX_RoutingTableWrite:
  case AX_RouterMemResp:
  case AX_RouterMemWrite:
  case AX_MemBitWrite:
    dissect_router_service( tvb, pinfo, tree, cemi_node, cemi_list, &offset, size, &pa_flags, &error );
    break;

  case AX_AuthReq:
  case AX_AuthResp:
  case AX_KeyWrite:
  case AX_KeyResp:
    dissect_authenticate_service( tvb, pinfo, tree, cemi_node, cemi_list, ax, &offset, size, &pa_flags, &error );
    break;

  case AX_PropValueRead:
  case AX_PropValueResp:
  case AX_PropValueWrite:
    dissect_property_value_service( tvb, pinfo, cemi_node, cemi_list, &offset, size, &pa_flags, &error );
    break;

  case AX_PropDescrRead:
  case AX_PropDescrResp:
    dissect_property_description_service( tvb, pinfo, cemi_node, cemi_list, &offset, size, &pa_flags, &error );
    break;

  case AX_NwkParamRead:
  case AX_NwkParamResp:
  case AX_NwkParamWrite:
  case AX_GroupPropValueRead:
  case AX_GroupPropValueResp:
  case AX_GroupPropValueWrite:
  case AX_GroupPropValueInfo:
    dissect_network_parameter_service( tvb, pinfo, cemi_node, cemi_list, &offset, size, &error );
    break;

  case AX_IndAddrSerNumRead:
  case AX_DomAddrSerNumRead:
  case AX_IndAddrSerNumResp:
  case AX_IndAddrSerNumWrite:
  case AX_DomAddrSerNumResp:
  case AX_DomAddrSerNumWrite:
    dissect_ia_serial_number_service( tvb, pinfo, cemi_node, cemi_list, &offset, size, &pa_flags, &error );
    break;

  case AX_SysNwkParamRead:
  case AX_SysNwkParamResp:
  case AX_SysNwkParamWrite:
    dissect_system_network_parameter_service( tvb, pinfo, cemi_node, cemi_list, &offset, size, &pa_flags, &error );
    break;

  case AX_PropExtValueRead:
  case AX_PropExtValueResp:
  case AX_PropExtValueWriteCon:
  case AX_PropExtValueWriteConRes:
  case AX_PropExtValueWriteUnCon:
    dissect_property_ext_value_service( tvb, pinfo, cemi_node, cemi_list, &offset, size, &pa_flags, &error );
    break;

  case AX_PropExtDescrRead:
  case AX_PropExtDescrResp:
    dissect_property_ext_description_service( tvb, pinfo, cemi_node, cemi_list, &offset, size, &pa_flags, &error );
    break;

  case AX_FuncPropExtCmd:
  case AX_FuncPropExtRead:
  case AX_FuncPropExtResp:

    /* 2 bytes OT, 12 bits OI, 12 bits PID */
    dissect_pid_ext( tvb, pinfo, cemi_node, cemi_list, &offset, size, &error );
    break;

  case AX_DataSec:
    dissect_data_security_service( tvb, pinfo, tree, cemi_node, cemi_list, source_addr, source_node, dest_addr, dest_node, unicast,
      name, &offset, size, &pa_flags, &error );
    break;
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect simple AL service (4 bit AL service code)
*/
static void dissect_simple_app_service( tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree, proto_item* cemi_node, proto_tree* cemi_list,
  guint8 ac, guint8 ad, gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  proto_item* node = NULL;
  proto_tree* list = NULL;

  guint8 c;
  guint16 cc;

  const gchar* name = val_to_str( ac, ac_vals, "AC=%u" );
  col_append_fstr( cinfo, COL_INFO, " %s", name );
  if( tree )
  {
    proto_item_append_text( cemi_node, ", %s", name );
    node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 2, "APCI: %s", name );
    list = proto_item_add_subtree( node, ett_cemi_apci );
    proto_tree_add_item( list, hf_cemi_ac, tvb, offset, 2, ENC_BIG_ENDIAN );
  }

  offset++;

  switch( ac )
  {
  case AC_GroupValueRead:
  case AC_MemRead:
  case AC_AdcRead:
  case AC_DevDescrRead:
    pa_flags = 0;
    break;
  }

  switch( ac )
  {
  case AC_GroupValueRead:
  case AC_GroupValueResp:
  case AC_GroupValueWrite:
  case AC_Restart:
    {
      guint8 expected = ((pa_flags && offset + 1 >= size) || ac == AC_Restart);

      if( expected || ad != 0 )
      {
        /* Show APCI 6-bit data
        */
        if( !expected )
        {
          error = 1;
        }
        else if( ad != 0 || ac != AC_Restart || offset + 1 < size )
        {
          col_append_fstr( cinfo, COL_INFO, " $%02X", ad );
          proto_item_append_text( cemi_node, " $%02X", ad );
        }

        if( tree )
        {
          node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 1, "Data: %02X", ad );
          list = proto_item_add_subtree( node, ett_cemi_apci );
          proto_tree_add_item( list, hf_cemi_ad, tvb, offset, 1, ENC_BIG_ENDIAN );

          if( !expected )
          {
            proto_item_prepend_text( node, "? " );
            expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 0x00" );
          }
        }
      }
    }
    break;

  case AC_MemRead:
  case AC_MemResp:
  case AC_MemWrite:

    /* 6 bits Memory Length, 2 bytes Memory Address */
    if( offset + 3 > size )
    {
      node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset + 1, size - offset - 1, NULL, "? Memory Address" );
      expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 2 bytes" );
      error = 1;
      offset = size - 1;
    }
    else
    {
      cc = tvb_get_ntohs( tvb, offset + 1 );
      if( ad != 1 )
        col_append_fstr( cinfo, COL_INFO, " N=%u", ad );
      col_append_fstr( cinfo, COL_INFO, " X=$%04X", cc );
      if( tree )
      {
        if( ad != 1 )
          proto_item_append_text( cemi_node, ", N=%u", ad );
        proto_item_append_text( cemi_node, ", X=$%04X", cc );
        node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 3, "Range: %u byte%s at address $%04X", ad, (ad == 1) ? "" : "s", cc );
        list = proto_item_add_subtree( node, ett_cemi_range );
        proto_tree_add_item( list, hf_cemi_ad_memory_length, tvb, offset, 1, ENC_BIG_ENDIAN );
        proto_tree_add_item( list, hf_cemi_memory_address, tvb, offset + 1, 2, ENC_BIG_ENDIAN );
      }
      offset += 2;
    }
    break;

  case AC_AdcRead:
  case AC_AdcResp:

    /* 6 bits Channel */
    col_append_fstr( cinfo, COL_INFO, " #%u", ad );
    if( tree )
    {
      proto_item_append_text( cemi_node, " #%u", ad );
      node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 1, "Channel: %u", ad );
      list = proto_item_add_subtree( node, ett_cemi_apci );
      proto_tree_add_item( list, hf_cemi_ad_channel, tvb, offset, 1, ENC_BIG_ENDIAN );
    }
    ++offset;

    /* 1 byte Count */
    if( offset >= size )
    {
      proto_tree_add_expert_format( cemi_list, pinfo, KIP_ERROR, tvb, offset, 0, "? Count: expected 1 byte" );
      error = 1;
      --offset;
    }
    else
    {
      c = tvb_get_guint8( tvb, offset );
      if( c != 1 )
      {
        col_append_fstr( cinfo, COL_INFO, " N=%u", c );
        proto_item_append_text( cemi_node, ", N=%u", c );
      }
      proto_tree_add_item( cemi_list, hf_cemi_adc_count, tvb, offset, 1, ENC_BIG_ENDIAN );
    }
    break;

  case AC_DevDescrRead:
  case AC_DevDescrResp:

    /* 6 bits Descriptor Type */
    if( ad != 0 )
      col_append_fstr( cinfo, COL_INFO, " #%u", ad );
    if( tree )
    {
      if( ad != 0 )
        proto_item_append_text( cemi_node, " #%u", ad );
      node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 1, "Descriptor Type: %u", ad );
      list = proto_item_add_subtree( node, ett_cemi_apci );
      proto_tree_add_item( list, hf_cemi_ad_type, tvb, offset, 1, ENC_BIG_ENDIAN );
    }
    break;

  case AC_UserMsg:
  case AC_Escape:

    /* 6 bits Data */
    col_append_fstr( cinfo, COL_INFO, " #%u", ad );
    if( tree )
    {
      proto_item_append_text( cemi_node, " #%u", ad );
      proto_item_append_text( node, " $%02X", ad );
      proto_tree_add_item( list, hf_cemi_ad, tvb, offset, 1, ENC_BIG_ENDIAN );
    }
    break;
  }

  offset++;

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect cEMI Application Layer
*/
static void dissect_cemi_app_layer( tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree, proto_item* cemi_node, proto_tree* cemi_list,
  guint16 source_addr, proto_item* source_node, guint16 dest_addr, proto_item* dest_node, guint8 unicast,
  gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  /* 10 bits APCI
  */
  if( offset + 1 >= size )
  {
    proto_item* node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? APCI" );
    expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 2 bytes" );
    error = 1;
    offset = size;
  }
  else
  {
    /* Extract and split AL service code */
    guint8 tb = tvb_get_guint8( tvb, offset );
    guint8 ab = tvb_get_guint8( tvb, offset + 1 );

    /* 4 bits simple AL service code */
    guint8 ac = ((tb & 0x03) << 2) | ((ab & 0xC0) >> 6);

    /* 6 bits data */
    guint8 ad = ab & 0x3F;

    /* 10 = 4 + 6 bits extended AL service code */
    guint16 ax = (ac << 6) | ad;

    const gchar* name = try_val_to_str( ax, ax_vals );

    if( name )  /* Extended AL code (10 bits) */
    {
      dissect_extended_app_service( tvb, pinfo, tree, cemi_node, cemi_list, source_addr, source_node, dest_addr, dest_node, unicast,
        ax, name, &offset, size, &pa_flags, &error );
    }
    else  /* Simple AL code (4 bits) followed by data (6 bits) */
    {
      dissect_simple_app_service( tvb, pinfo, tree, cemi_node, cemi_list, ac, ad, &offset, size, &pa_flags, &error );
    }
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect cEMI Transport Layer
*/
static void dissect_cemi_transport_layer( tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree, proto_item* cemi_node, proto_tree* cemi_list,
  guint8 is_tdata, guint16 source_addr, proto_item* source_node, guint16 dest_addr, proto_item* dest_node, guint8 unicast,
  gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  proto_item* node;
  const gchar* name;
  gchar text[ 128 ];
  guint8 c;

  /* 6 bits TPCI */
  if( offset >= size )
  {
    proto_tree_add_expert_format( cemi_list, pinfo, KIP_ERROR, tvb, offset, 0, "? TPCI: expected 1 byte" );
    error = 1;
  }
  else
  {
    guint8 tb = tvb_get_guint8( tvb, offset );
    proto_item *tpci_node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 1, "TPCI" );
    proto_tree *tpci_list = proto_item_add_subtree( tpci_node, ett_cemi_tpci );
    guint8 tpci_error = 0;

    node = proto_tree_add_item( tpci_list, hf_cemi_tpt, tvb, offset, 1, ENC_BIG_ENDIAN );
    if( is_tdata && (tb & 0x80) )
    {
      proto_item_prepend_text( node, "? " );
      expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: zero" );
      tpci_error = 1;
    }

    node = proto_tree_add_item( tpci_list, hf_cemi_tst, tvb, offset, 1, ENC_BIG_ENDIAN );
    if( is_tdata && (tb & 0x40) )
    {
      proto_item_prepend_text( node, "? " );
      expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: zero" );
      tpci_error = 1;
    }

    c = (tb & 0x3C) >> 2;

    if( c || tb & 0x40 )  /* Numbered Packet? */
    {
      node = proto_tree_add_item( tpci_list, hf_cemi_num, tvb, offset, 1, ENC_BIG_ENDIAN );
      proto_item_append_text( tpci_node, ", SeqNum = %u", c );
      if( !(tb & 0x40) )
      {
        expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: zero" );
        tpci_error = 1;
      }
    }

    if( tb & 0x80 )  /* Control Packet */
    {
      /* 2 bits TPCI Code */
      guint8 tc = tb & 0x03;
      name = try_val_to_str( tc, tc_vals );
      if( !name )
      {
        snprintf( text, sizeof text, "TC=%u", tc );
        name = text;
      }
      col_append_fstr( cinfo, COL_INFO, " %s", name );
      if( tree )
      {
        proto_item_append_text( cemi_node, ", %s", name );
        proto_item_append_text( tpci_node, ": %s", name );
        proto_tree_add_item( tpci_list, hf_cemi_tc, tvb, offset, 1, ENC_BIG_ENDIAN );
      }
    }

    if( tpci_error )
    {
      proto_item_prepend_text( tpci_node, "? " );
      error = 1;
    }

    if( tb & 0x80 )  /* Control Packet */
    {
      pa_flags = 0;
      offset++;
    }
    else  /* Data Packet */
    {
      /* APCI etc */
      dissect_cemi_app_layer( tvb, pinfo, tree, cemi_node, cemi_list, source_addr, source_node, dest_addr, dest_node, unicast, &offset, size, &pa_flags, &error );
    }
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

/* Dissect cEMI Link Layer
  (typically L_Data or T_Data)
*/
static void dissect_cemi_link_layer( tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree, proto_item* cemi_node, proto_tree* cemi_list, guint8 mc, gint* p_offset, gint size, guint8* p_pa_flags, guint8* p_error )
{
  column_info* cinfo = pinfo->cinfo;
  gint offset = *p_offset;
  guint8 pa_flags = *p_pa_flags;
  guint8 error = *p_error;

  proto_item* node = NULL;
  proto_tree* list = NULL;

  const gchar* name;
  gchar text[ 128 ];
  guint8 c;

  guint8 is_tdata = 0;
  guint8 is_ldata = 0;
  guint16 source_addr = 0;
  guint16 dest_addr = 0;
  guint8 unicast = 0;

  proto_item* source_node = NULL;
  proto_item* dest_node = NULL;

  proto_item* ai_node;
  proto_tree* ai_list;

  if( size < 2 )
  {
    ai_list = proto_tree_add_subtree( cemi_list, tvb, offset, 0, ett_cemi_ai, &ai_node, "? Additional Info" );
    proto_tree_add_expert_format( ai_list, pinfo, KIP_ERROR, tvb, offset, 0, "? Length: expected 1 byte" );
    offset = size;
    error = 1;
  }
  else
  {
    /* Additional Information */
    guint8 ai_len = tvb_get_guint8( tvb, 1 );
    gint ai_end = 2 + ai_len;
    gint ai_size = ai_len;

    if( ai_end > size )
    {
      error = 2;
      ai_size = size - 2;
      ai_end = size;
    }

    ai_node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, 1, ai_size + 1, "Additional Info (%u bytes)", ai_len );
    ai_list = proto_item_add_subtree( ai_node, ett_cemi_ai );
    node = proto_tree_add_item( ai_list, hf_cemi_ai_length, tvb, 1, 1, ENC_BIG_ENDIAN );

    if( error == 2 )
    {
      proto_item_prepend_text( node, "? " );
      expert_add_info_format( pinfo, node, KIP_ERROR, "Available: %d bytes", ai_size );
    }

    offset = 2;
    while( offset < ai_end )
    {
      /* Additional Information Element */
      guint8 aie_type = tvb_get_guint8( tvb, offset );
      guint8 aie_len;
      gint aie_size;
      proto_item *aie_node;
      proto_tree *aie_list;

      name = try_val_to_str( aie_type, aiet_vals );

      if( offset + 1 >= ai_end )
      {
        error = 3;
        aie_len = 0;
        aie_size = 1;
      }
      else
      {
        aie_len = tvb_get_guint8( tvb, offset + 1 );
        aie_size = ai_end - offset - 2;
        if( aie_size < aie_len )
        {
          error = 4;
        }
        else
        {
          aie_size = aie_len;
        }
        aie_size += 2;
      }

      aie_node = proto_tree_add_none_format( ai_list, hf_folder, tvb, offset, aie_size, "Additional Info: %s", name ? name : "?" );
      aie_list = proto_item_add_subtree( aie_node, ett_cemi_aie );
      node = proto_tree_add_item( aie_list, hf_cemi_aie_type, tvb, offset, 1, ENC_BIG_ENDIAN );
      if( name ) proto_item_append_text( node, " = %s", name );
      offset++;

      if( error == 3 )
      {
        proto_item_prepend_text( aie_node, "? " );
        proto_tree_add_expert_format( aie_list, pinfo, KIP_ERROR, tvb, offset, 0, "? Length: expected 1 byte" );
        break;
      }

      proto_item_append_text( aie_node, " (%u bytes)", aie_len );
      node = proto_tree_add_item( aie_list, hf_cemi_aie_length, tvb, offset, 1, ENC_BIG_ENDIAN );
      offset++;

      if( error == 4 )
      {
        proto_item_prepend_text( aie_node, "? " );
        proto_item_prepend_text( node, "? " );
        expert_add_info_format( pinfo, node, KIP_ERROR, "Available: %d bytes", aie_size - 2 );
        break;
      }

      if( aie_len > 0 )
      {
        proto_tree_add_data( aie_list, tvb, offset, aie_len, NULL, NULL, "Data", NULL, NULL );
        offset += aie_len;
      }
      else
      {
        proto_item_prepend_text( aie_node, "? " );
        proto_item_append_text( node, " (?)" );
        expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: >= 1 byte(s)" );
        error = 5;
      }
    }

    if( error >= 2 )
    {
      proto_item_prepend_text( ai_node, "? " );
    }

    offset = ai_end;
  }

  switch( mc )
  {
  case CEMI_L_BUSMON_IND:
  case CEMI_L_RAW_IND:
  case CEMI_L_RAW_REQ:
  case CEMI_L_RAW_CON:
    break;

  default:

    switch( mc )
    {
    case CEMI_L_DATA_REQ:
    case CEMI_L_DATA_CON:
    case CEMI_L_DATA_IND:
      is_ldata = 1;
      break;

    case CEMI_T_DATA_INDIVIDUAL_REQ:
    case CEMI_T_DATA_INDIVIDUAL_IND:
    case CEMI_T_DATA_CONNECTED_REQ:
    case CEMI_T_DATA_CONNECTED_IND:
      is_tdata = 1;
      break;
    }

    if( is_tdata )
    {
      gint length = (size >= offset + 6) ? 6 : size - offset;
      node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, length, NULL, "Reserved" );
      if( length < 6 )
      {
        proto_item_prepend_text( node, "? " );
        expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 6 bytes" );
        error = 1;
      }
      else
      {
        gint pos = 0;
        for( ; pos < 6; pos++ )
        {
          if( tvb_get_guint8( tvb, offset + pos ) != 0 )
          {
            proto_item_prepend_text( node, "? " );
            expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: zero" );
            error = 1;
            break;
          }
        }
      }

      is_tdata = 1;
      offset += length;
    }
    else
    {
      /* 1 byte Control Field 1 */
      if( offset >= size )
      {
        proto_tree_add_expert_format( cemi_list, pinfo, KIP_ERROR, tvb, offset, 0, "? Ctrl1: expected 1 byte" );
        error = 1;
      }
      else
      {
        if( tree )
        {
          c = tvb_get_guint8( tvb, offset );
          proto_item_append_text( cemi_node, ", " );
          node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 1, "Ctrl1: " );
          if( !(c & 0x80) )
          {
            proto_item_append_text( cemi_node, "X " );
            proto_item_append_text( node, "Extended, " );
          }
          if( !(c & 0x20) )
          {
            proto_item_append_text( cemi_node, "R " );
            proto_item_append_text( node, "Repeat On Error, " );
          }
          if( !(c & 0x10) )
          {
            proto_item_append_text( cemi_node, "B " );
            proto_item_append_text( node, "System Broadcast, " );
          }
          if( c & 0x02 )
          {
            proto_item_append_text( cemi_node, "A " );
            proto_item_append_text( node, "Ack Wanted, " );
          }
          if( c & 0x01 )
          {
            proto_item_append_text( cemi_node, "C " );
            proto_item_append_text( node, "Unconfirmed, " );
          }

          name = try_val_to_str( (c & 0x0C) >> 2, prio_vals );
          if( !name )
            name = "?";
          proto_item_append_text( cemi_node, "P=%s", name );
          proto_item_append_text( node, "Prio = %s", name );
          list = proto_item_add_subtree( node, ett_cemi_ctrl1 );
          proto_tree_add_item( list, hf_cemi_ft, tvb, offset, 1, ENC_BIG_ENDIAN );
          proto_tree_add_item( list, hf_cemi_rep, tvb, offset, 1, ENC_BIG_ENDIAN );
          proto_tree_add_item( list, hf_cemi_bt, tvb, offset, 1, ENC_BIG_ENDIAN );
          proto_tree_add_item( list, hf_cemi_prio, tvb, offset, 1, ENC_BIG_ENDIAN );
          proto_tree_add_item( list, hf_cemi_ack, tvb, offset, 1, ENC_BIG_ENDIAN );
          proto_tree_add_item( list, hf_cemi_ce, tvb, offset, 1, ENC_BIG_ENDIAN );
        }

        offset++;
      }

      /* 1 byte Control Field 2 */
      if( offset >= size )
      {
        proto_tree_add_expert_format( cemi_list, pinfo, KIP_ERROR, tvb, offset, 0, "? Ctrl2: expected 1 byte" );
        error = 1;
      }
      else
      {
        c = tvb_get_guint8( tvb, offset );

        unicast = !(c & 0x80);  /* Address Type (IA or GA) */

        if( tree )
        {
          guint8 hc = (c & 0x70) >> 4;  /* Hop Count */
          guint8 eff = c & 0x0F;  /* Extended Frame Format (0 = standard) */

          snprintf( text, sizeof text, "%u", (c & 0x70) >> 4 );   /* hop count */
          proto_item_append_text( cemi_node, ", H=%u", hc );
          node = proto_tree_add_none_format( cemi_list, hf_folder, tvb, offset, 1, "Ctrl2: Hops = %u", hc );
          if( eff )
          {
            proto_item_append_text( cemi_node, " F=%u", eff );
            proto_item_append_text( cemi_node, " Frame = %u", eff );
          }
          list = proto_item_add_subtree( node, ett_cemi_ctrl2 );
          proto_tree_add_item( list, hf_cemi_at, tvb, offset, 1, ENC_BIG_ENDIAN );
          proto_tree_add_item( list, hf_cemi_hc, tvb, offset, 1, ENC_BIG_ENDIAN );
          proto_tree_add_item( list, hf_cemi_eff, tvb, offset, 1, ENC_BIG_ENDIAN );
        }

        offset++;
      }

      /* 2 bytes Source Address */
      if( offset + 1 >= size )
      {
        node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Source" );
        expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 2 bytes" );
        error = 1;
        offset = size;
      }
      else
      {
        source_addr = tvb_get_ntohs( tvb, offset );
        snprintf( text, sizeof text, "%u.%u.%u", (source_addr >> 12) & 0xF, (source_addr >> 8) & 0xF, source_addr & 0xFF );
        col_append_fstr( cinfo, COL_INFO, " %s", text );
        if( tree )
        {
          proto_item_append_text( cemi_node, ", Src=%s", text );
          source_node = proto_tree_add_item( cemi_list, hf_cemi_sa, tvb, offset, 2, ENC_BIG_ENDIAN );
          proto_item_append_text( source_node, " = %s", text );
        }

        offset += 2;
      }

      /* 2 bytes Destination Address */
      if( offset + 1 >= size )
      {
        node = proto_tree_add_bytes_format( cemi_list, hf_bytes, tvb, offset, size - offset, NULL, "? Destination" );
        expert_add_info_format( pinfo, node, KIP_ERROR, "Expected: 2 bytes" );
        error = 1;
        offset = size;
      }
      else
      {
        dest_addr = tvb_get_ntohs( tvb, offset );

        if( unicast )
        {
          /* Individual Address */
          snprintf( text, sizeof text, "%u.%u.%u", (dest_addr >> 12) & 0xF, (dest_addr >> 8) & 0xF, dest_addr & 0xFF );
        }
        else
        {
          /* Group Address */
          snprintf( text, sizeof text, "%u/%u/%u", (dest_addr >> 11) & 0x1F, (dest_addr >> 8) & 0x7, dest_addr & 0xFF );
        }

        col_append_fstr( cinfo, COL_INFO, "->%s", text );

        if( tree )
        {
          proto_item_append_text( cemi_node, ", Dst=%s", text );
          dest_node = proto_tree_add_item( cemi_list, hf_cemi_da, tvb, offset, 2, ENC_BIG_ENDIAN );
          proto_item_append_text( dest_node, " = %s", text );
        }

        offset += 2;
      }
    }

    if( is_ldata || is_tdata )
    {
      /* 1 byte NPDU Length */
      if( offset >= size )
      {
        proto_tree_add_expert_format( cemi_list, pinfo, KIP_ERROR, tvb, offset, 0, "? Length: expected 1 byte" );
        error = 1;
      }
      else
      {
        guint8 data_len = tvb_get_guint8( tvb, offset );
        node = proto_tree_add_item( cemi_list, hf_cemi_len, tvb, offset, 1, ENC_BIG_ENDIAN );

        if( offset + 2 + data_len != size )
        {
          proto_item_prepend_text( node, "? " );
          expert_add_info_format( pinfo, node, KIP_ERROR, "Available: %d bytes", size - offset - 2 );
          error = 1;
        }

        offset++;
      }

      /* TPCI etc */
      dissect_cemi_transport_layer( tvb, pinfo, tree, cemi_node, cemi_list, is_tdata, source_addr, source_node, dest_addr, dest_node, unicast, &offset, size, &pa_flags, &error );
    }

    break;
  }

  *p_offset = offset;
  *p_pa_flags = pa_flags;
  *p_error = error;
}

static gint dissect_cemi( tvbuff_t* tvb, packet_info* pinfo, proto_tree* tree, void* data _U_ )
{
  gint offset = 0;
  gint size = tvb_captured_length_remaining( tvb, 0 );
  guint8 error = 0;
  column_info* cinfo = pinfo->cinfo;

  /* cEMI node in tree view */
  proto_item* cemi_node = proto_tree_add_item( tree, proto_cemi, tvb, 0, size, ENC_BIG_ENDIAN );

  /* Subnodes of cEMI node */
  proto_tree* cemi_list = proto_item_add_subtree( cemi_node, ett_cemi );

  guint8 pa_flags = PA_DATA;

  /* Only add cEMI information to the info column (not replacing it).
    This means that we do not have to clear that column here, but
    are adding a seperator here.
  */
  col_append_str( cinfo, COL_INFO, " " );

  /* Replace long name "Common External Message Interface" by short name "cEMI" */
  proto_item_set_text( cemi_node, "cEMI" );

  if( size <= 0 )
  {
    expert_add_info_format( pinfo, cemi_node, KIP_ERROR, "Expected: min 1 byte" );
    error = 1;
  }
  else
  {
    /* 1 byte cEMI Message Code */
    guint8 mc = tvb_get_guint8( tvb, 0 );
    const gchar* name = try_val_to_str( mc, mc_vals );

    if( !name )
    {
      /* Unknown Message Code */
      col_append_str( cinfo, COL_INFO, "cEMI" );
      pa_flags = 0;
    }
    else
    {
      /* Add cEMI message code to info column */
      col_append_str( cinfo, COL_INFO, name );

      /* Show MC in cEMI node, and more detailed in a subnode */
      proto_item_append_text( cemi_node, " %s", name );
      proto_tree_add_item( cemi_list, hf_cemi_mc, tvb, 0, 1, ENC_BIG_ENDIAN );

      offset = 1;

      if( mc >= 0xF0 )
      {
        /* cEMI Management packet */
        dissect_cemi_mgmt_packet( tvb, pinfo, cemi_node, cemi_list, mc, &offset, size, &pa_flags, &error );
      }
      else
      {
        /* cEMI Link Layer packet */
        dissect_cemi_link_layer( tvb, pinfo, tree, cemi_node, cemi_list, mc, &offset, size, &pa_flags, &error );
      }
    }
  }

  if( offset < size )
  {
    /* Trailing data */
    proto_item* node = proto_tree_add_data( cemi_list, tvb, offset, size - offset, cinfo, cemi_node, "Data", " $", ", $" );

    if( !pa_flags )
    {
      proto_item_prepend_text( node, "? " );
      expert_add_info_format( pinfo, node, KIP_ERROR, "Unexpected" );
      error = 1;
    }

    offset = size;
  }

  if( error )
  {
    /* If not already done */
    if( !knxip_error )
    {
      knxip_error = 1;
      col_prepend_fstr( cinfo, COL_INFO, "? " );
    }

    proto_item_prepend_text( cemi_node, "? " );
  }

  return size;
}

void proto_register_cemi( void )
{
  /* Header fields */
  static hf_register_info hf[] = {
    { &hf_bytes, { "Data", "cemi.data", FT_BYTES, BASE_NONE, NULL, 0, NULL, HFILL } },
    { &hf_folder, { "Folder", "cemi.folder", FT_NONE, BASE_NONE, NULL, 0, NULL, HFILL } },
    { &hf_cemi_mc, { "Message Code", "cemi.mc", FT_UINT8, BASE_HEX, VALS( mc_vals ), 0, NULL, HFILL } },
    { &hf_cemi_error, { "Error", "cemi.e", FT_UINT8, BASE_HEX, NULL, 0, NULL, HFILL } },
    { &hf_cemi_ai_length, { "Additional Information Length", "cemi.ai.n", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_aie_type, { "Additional Information Element Type", "cemi.ait.n", FT_UINT8, BASE_HEX, NULL, 0, NULL, HFILL } },
    { &hf_cemi_aie_length, { "Additional Information Element Length", "cemi.aie.n", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_ot, { "Object Type", "cemi.ot", FT_UINT16, BASE_DEC, VALS( ot_vals ), 0, NULL, HFILL } },
    { &hf_cemi_oi, { "Object Instance", "cemi.oi", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_ox, { "Object Index", "cemi.ox", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_px, { "Property Index", "cemi.px",FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_pid, { "Property ID", "cemi.pid", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_ne, { "Count", "cemi.n", FT_UINT8, BASE_DEC, NULL, 0xF0, NULL, HFILL } },
    { &hf_cemi_sx, { "Index", "cemi.x", FT_UINT16, BASE_DEC, NULL, 0x0FFF, NULL, HFILL } },
    { &hf_cemi_ft, { "Frame Type", "cemi.ft", FT_UINT8, BASE_DEC, VALS( ft_vals ), 0x80, NULL, HFILL } },
    { &hf_cemi_rep, { "Repeat On Error", "cemi.rep", FT_BOOLEAN, 8, TFS(&tfs_no_yes), 0x20, NULL, HFILL } },
    { &hf_cemi_bt, { "Broadcast Type", "cemi.bt", FT_UINT8, BASE_DEC, VALS( bt_vals ), 0x10, NULL, HFILL } },
    { &hf_cemi_prio, { "Priority", "cemi.prio", FT_UINT8, BASE_DEC, VALS( prio_vals ), 0x0C, NULL, HFILL } },
    { &hf_cemi_ack, { "Ack Wanted", "cemi.ack", FT_BOOLEAN, 8, TFS(&tfs_no_yes), 0x02, NULL, HFILL } },
    { &hf_cemi_ce, { "Confirmation Error", "cemi.ce", FT_BOOLEAN, 8, TFS(&tfs_no_yes), 0x01, NULL, HFILL } },
    { &hf_cemi_at, { "Address Type", "cemi.at", FT_UINT8, BASE_DEC, VALS( at_vals ), 0x80, NULL, HFILL } },
    { &hf_cemi_hc, { "Hop Count", "cemi.hc", FT_UINT8, BASE_DEC, NULL, 0x70, NULL, HFILL } },
    { &hf_cemi_eff, { "Extended Frame Format", "cemi.eff", FT_UINT8, BASE_HEX, NULL, 0x0F, NULL, HFILL } },
    { &hf_cemi_sa, { "Source", "cemi.sa", FT_UINT16, BASE_HEX, NULL, 0, NULL, HFILL } },
    { &hf_cemi_da, { "Destination", "cemi.da", FT_UINT16, BASE_HEX, NULL, 0, NULL, HFILL } },
    { &hf_cemi_len, { "Length", "cemi.len", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_tpt, { "Packet Type", "cemi.tpt", FT_UINT8, BASE_DEC, VALS( pt_vals ), 0x80, NULL, HFILL } },
    { &hf_cemi_tst, { "Sequence Type", "cemi.st", FT_UINT8, BASE_DEC, VALS( st_vals ), 0x40, NULL, HFILL } },
    { &hf_cemi_num, { "Sequence Number", "cemi.num", FT_UINT8, BASE_DEC, NULL, 0x3C, NULL, HFILL } },
    { &hf_cemi_tc, { "Service", "cemi.tc", FT_UINT8, BASE_HEX, VALS( tc_vals ), 0x03, NULL, HFILL } },
    { &hf_cemi_ac, { "Service", "cemi.ac", FT_UINT16, BASE_HEX, VALS( ac_vals ), 0x03C0, NULL, HFILL } },
    { &hf_cemi_ad, { "Data", "cemi.ad", FT_UINT8, BASE_HEX, NULL, 0x3F, NULL, HFILL } },
    { &hf_cemi_ad_memory_length, { "Memory Length", "cemi.ad.ml", FT_UINT8, BASE_HEX, NULL, 0x3F, NULL, HFILL } },
    { &hf_cemi_ad_channel, { "Channel", "cemi.ad.ch", FT_UINT8, BASE_HEX, NULL, 0x3F, NULL, HFILL } },
    { &hf_cemi_ad_type, { "Data", "cemi.ad.type", FT_UINT8, BASE_HEX, NULL, 0x3F, NULL, HFILL } },
    { &hf_cemi_ax, { "Service", "cemi.ax", FT_UINT16, BASE_HEX, VALS( ax_vals ), 0x03FF, NULL, HFILL } },
    { &hf_cemi_pw, { "Writable", "cemi.pw", FT_UINT8, BASE_DEC, NULL, 0x80, NULL, HFILL } },
    { &hf_cemi_pdt, { "Property Data Type", "cemi.pdt", FT_UINT8, BASE_HEX, VALS( pdt_vals ), 0x3F, NULL, HFILL } },
    { &hf_cemi_me, { "Max Elements", "cemi.me", FT_UINT16, BASE_DEC, NULL, 0x0FFF, NULL, HFILL } },
    { &hf_cemi_ra, { "Read Access", "cemi.ra", FT_UINT8, BASE_DEC, NULL, 0xF0, NULL, HFILL } },
    { &hf_cemi_wa, { "Write Access", "cemi.wa", FT_UINT8, BASE_DEC, NULL, 0x0F, NULL, HFILL } },
    { &hf_cemi_ext_oi, { "Object Instance", "cemi.oi", FT_UINT16, BASE_DEC, NULL, 0xFFF0, NULL, HFILL } },
    { &hf_cemi_ext_pid, { "Property ID", "cemi.pid", FT_UINT16, BASE_DEC, NULL, 0x0FFF, NULL, HFILL } },
    { &hf_cemi_ext_ne, { "Count", "cemi.n", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_ext_sx, { "Index", "cemi.x", FT_UINT16, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_ext_dt, { "Description Type", "cemi.dt", FT_UINT8, BASE_DEC, NULL, 0xF0, NULL, HFILL } },
    { &hf_cemi_ext_px, { "Property Index", "cemi.px", FT_UINT16, BASE_DEC, NULL, 0x0FFF, NULL, HFILL } },
    { &hf_cemi_ext_memory_length, { "Memory Length", "cemi.n", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_ext_memory_address, { "Memory Address", "cemi.x", FT_UINT16, BASE_HEX, NULL, 0, NULL, HFILL } },
    { &hf_cemi_memory_length, { "Memory Length", "cemi.n", FT_UINT8, BASE_DEC, NULL, 0x0F, NULL, HFILL } },
    { &hf_cemi_memory_address, { "Memory Address", "cemi.x", FT_UINT16, BASE_HEX, NULL, 0, NULL, HFILL } },
    { &hf_cemi_memory_address_ext, { "Memory Address Extension", "cemi.xx", FT_UINT8, BASE_HEX, NULL, 0xF0, NULL, HFILL } },
    { &hf_cemi_level, { "Level", "cemi.level", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_snp_pid, { "Property ID", "cemi.pid", FT_UINT16, BASE_DEC, NULL, 0xFFF0, NULL, HFILL } },
    { &hf_cemi_snp_reserved, { "Reserved", "cemi.reserved", FT_UINT16, BASE_DEC, NULL, 0x0F, NULL, HFILL } },
    { &hf_cemi_dpt_major, { "Data Point Type Major", "cemi.pdt.major", FT_UINT16, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_dpt_minor, { "Data Point Type Minor", "cemi.pdt.minor", FT_UINT16, BASE_DEC, NULL, 0, NULL, HFILL } },
    { &hf_cemi_scf, { "Security Control Field", "cemi.scf", FT_UINT8, BASE_HEX, VALS( scf_vals ), 0, NULL, HFILL } },
    { &hf_cemi_scf_t, { "Tool Access", "cemi.scf.t", FT_UINT8, BASE_DEC, NULL, 0x80, NULL, HFILL } },
    { &hf_cemi_scf_sai, { "Security Algorithm Identifier", "cemi.scf.sai", FT_UINT8, BASE_HEX, VALS( scf_sai_vals ), 0x70, NULL, HFILL } },
    { &hf_cemi_scf_sbc, { "System Broadcast", "cemi.scf.sbc", FT_UINT8, BASE_DEC, NULL, 0x08, NULL, HFILL } },
    { &hf_cemi_scf_svc, { "Service", "cemi.scf.sai", FT_UINT8, BASE_HEX, VALS( scf_svc_vals ), 0x07, NULL, HFILL } },
    { &hf_cemi_adc_count, { "Count", "cemi.adc.n", FT_UINT8, BASE_DEC, NULL, 0, NULL, HFILL } },
  };

  /* Subtrees */
  static gint *ett[] = {
    &ett_cemi,
    &ett_cemi_ai,
    &ett_cemi_aie,
    &ett_cemi_ctrl1,
    &ett_cemi_ctrl2,
    &ett_cemi_tpci,
    &ett_cemi_apci,
    &ett_cemi_range,
    &ett_cemi_pd,
    &ett_cemi_dpt,
    &ett_cemi_scf,
    &ett_cemi_decrypted
  };

  proto_cemi = proto_register_protocol( "Common External Message Interface", "cEMI", "cemi" );

  proto_register_field_array( proto_cemi, hf, array_length( hf ) );
  proto_register_subtree_array( ett, array_length( ett ) );

  register_dissector( "cemi", dissect_cemi, proto_cemi );
}

void proto_reg_handoff_cemi( void )
{
}

/*
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 2
 * tab-width: 8
 * indent-tabs-mode: nil
 * End:
 *
 * vi: set shiftwidth=2 tabstop=8 expandtab:
 * :indentSize=2:tabSize=8:noTabs=true:
 */
