diff --git a/configure.ac b/configure.ac
index d4e312c..12c2eb9 100644
--- a/configure.ac
+++ b/configure.ac
@@ -26,6 +26,7 @@ AC_CONFIG_FILES([Makefile
 	utils/keytable/Makefile
 	utils/media-ctl/Makefile
 	utils/rds/Makefile
+	utils/cec-compliance/Makefile
 	utils/v4l2-compliance/Makefile
 	utils/v4l2-ctl/Makefile
 	utils/v4l2-dbg/Makefile
diff --git a/utils/Makefile.am b/utils/Makefile.am
index 31b2979..c78e97b 100644
--- a/utils/Makefile.am
+++ b/utils/Makefile.am
@@ -5,6 +5,7 @@ SUBDIRS = \
 	decode_tm6000 \
 	keytable \
 	media-ctl \
+	cec-compliance \
 	v4l2-compliance \
 	v4l2-ctl \
 	v4l2-dbg \
diff --git a/utils/cec-compliance/Makefile.am b/utils/cec-compliance/Makefile.am
new file mode 100644
index 0000000..da4c0ef
--- /dev/null
+++ b/utils/cec-compliance/Makefile.am
@@ -0,0 +1,3 @@
+bin_PROGRAMS = cec-compliance
+
+cec_compliance_SOURCES = cec-compliance.cpp
diff --git a/utils/cec-compliance/cec-compliance.cpp b/utils/cec-compliance/cec-compliance.cpp
new file mode 100644
index 0000000..59db8ab
--- /dev/null
+++ b/utils/cec-compliance/cec-compliance.cpp
@@ -0,0 +1,926 @@
+/*
+    Copyright 2015 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
+    Author: Hans Verkuil <hans.verkuil@cisco.com>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+ */
+
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <inttypes.h>
+#include <getopt.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <ctype.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <config.h>
+
+#include "cec-compliance.h"
+
+/* Short option list
+
+   Please keep in alphabetical order.
+   That makes it easier to see which short options are still free.
+
+   In general the lower case is used to set something and the upper
+   case is used to retrieve a setting. */
+enum Option {
+	OptPhysAddr = 'a',
+	OptSetDevice = 'd',
+	OptHelp = 'h',
+	OptNoWarnings = 'n',
+	OptTrace = 'T',
+	OptVerbose = 'v',
+	OptVendorID = 'V',
+
+	OptTV,
+	OptRecord,
+	OptTuner,
+	OptPlayback,
+	OptAudio,
+	OptProcessor,
+	OptSwitch,
+	OptCDCOnly,
+	OptUnregistered,
+	OptLast = 256
+};
+
+static char options[OptLast];
+
+static int app_result;
+static int tests_total, tests_ok;
+
+bool show_info;
+bool show_warnings = true;
+unsigned warnings;
+
+static struct option long_options[] = {
+	{"device", required_argument, 0, OptSetDevice},
+	{"help", no_argument, 0, OptHelp},
+	{"no-warnings", no_argument, 0, OptNoWarnings},
+	{"trace", no_argument, 0, OptTrace},
+	{"verbose", no_argument, 0, OptVerbose},
+	{"phys-addr", required_argument, 0, OptPhysAddr},
+	{"vendor-id", required_argument, 0, OptVendorID},
+
+	{"tv", no_argument, 0, OptTV},
+	{"record", no_argument, 0, OptRecord},
+	{"tuner", no_argument, 0, OptTuner},
+	{"playback", no_argument, 0, OptPlayback},
+	{"audio", no_argument, 0, OptAudio},
+	{"processor", no_argument, 0, OptProcessor},
+	{"switch", no_argument, 0, OptSwitch},
+	{"cdc-only", no_argument, 0, OptCDCOnly},
+	{"unregistered", no_argument, 0, OptUnregistered},
+	{0, 0, 0, 0}
+};
+
+static void usage(void)
+{
+	printf("Usage:\n"
+	       "  -d, --device=<dev> Use device <dev> instead of /dev/cec0\n"
+	       "                     If <dev> starts with a digit, then /dev/cec<dev> is used.\n"
+	       "  -h, --help         Display this help message\n"
+	       "  -n, --no-warnings  Turn off warning messages.\n"
+	       "  -T, --trace        Trace all called ioctls.\n"
+	       "  -v, --verbose      Turn on verbose reporting.\n"
+	       "  -a, --phys-addr=<addr>\n"
+	       "		     Use this physical address.\n"
+	       "  -V, --vendor-id=<id>\n"
+	       "		     Use this vendor ID.\n"
+	       "  --tv               This is a TV\n"
+	       "  --record           This is a recording device\n"
+	       "  --tuner            This is a tuner device\n"
+	       "  --playback         This is a playback device\n"
+	       "  --audio            This is an audio system device\n"
+	       "  --processor        This is a processor device\n"
+	       "  --switch           This is a pure CEC switch\n"
+	       "  --cdc-only         This is a CDC-only device\n"
+	       "  --unregistered     This is an unregistered device\n"
+	       );
+}
+
+static std::string caps2s(unsigned caps)
+{
+	std::string s;
+
+	if (caps & CEC_CAP_STATE)
+		s += "\t\tState\n";
+	if (caps & CEC_CAP_PHYS_ADDR)
+		s += "\t\tPhysical Address\n";
+	if (caps & CEC_CAP_LOG_ADDRS)
+		s += "\t\tLogical Addresses\n";
+	if (caps & CEC_CAP_IO)
+		s += "\t\tI/O\n";
+	if (caps & CEC_CAP_VENDOR_ID)
+		s += "\t\tVendor ID\n";
+	if (caps & CEC_CAP_PASSTHROUGH)
+		s += "\t\tPassthrough\n";
+	if (caps & CEC_CAP_RC)
+		s += "\t\tRemote Control Support\n";
+	if (caps & CEC_CAP_ARC)
+		s += "\t\tAudio Return Channel\n";
+	if (caps & CEC_CAP_CDC_HPD)
+		s += "\t\tCapability Discovery and Control HPD\n";
+	if (caps & CEC_CAP_IS_SOURCE)
+		s += "\t\tIs Source\n";
+	return s;
+}
+
+static const char *version2s(unsigned version)
+{
+	switch (version) {
+	case CEC_OP_CEC_VERSION_1_3A:
+		return "1.3a";
+	case CEC_OP_CEC_VERSION_1_4:
+		return "1.4";
+	case CEC_OP_CEC_VERSION_2_0:
+		return "2.0";
+	default:
+		return "Unknown";
+	}
+}
+
+static const char *power_status2s(unsigned status)
+{
+	switch (status) {
+	case CEC_OP_POWER_STATUS_ON:
+		return "On";
+	case CEC_OP_POWER_STATUS_STANDBY:
+		return "Standby";
+	case CEC_OP_POWER_STATUS_TO_ON:
+		return "In Transition Standby to On";
+	case CEC_OP_POWER_STATUS_TO_STANDBY:
+		return "In Transition On to Standby";
+	default:
+		return "Unknown";
+	}
+}
+
+static const char *prim_type2s(unsigned type)
+{
+	switch (type) {
+	case CEC_OP_PRIM_DEVTYPE_TV:
+		return "TV";
+	case CEC_OP_PRIM_DEVTYPE_RECORD:
+		return "Record";
+	case CEC_OP_PRIM_DEVTYPE_TUNER:
+		return "Tuner";
+	case CEC_OP_PRIM_DEVTYPE_PLAYBACK:
+		return "Playback";
+	case CEC_OP_PRIM_DEVTYPE_AUDIOSYSTEM:
+		return "Audio System";
+	case CEC_OP_PRIM_DEVTYPE_SWITCH:
+		return "Switch";
+	case CEC_OP_PRIM_DEVTYPE_PROCESSOR:
+		return "Processor";
+	default:
+		return "Unknown";
+	}
+}
+
+static const char *la_type2s(unsigned type)
+{
+	switch (type) {
+	case CEC_LOG_ADDR_TYPE_TV:
+		return "TV";
+	case CEC_LOG_ADDR_TYPE_RECORD:
+		return "Record";
+	case CEC_LOG_ADDR_TYPE_TUNER:
+		return "Tuner";
+	case CEC_LOG_ADDR_TYPE_PLAYBACK:
+		return "Playback";
+	case CEC_LOG_ADDR_TYPE_AUDIOSYSTEM:
+		return "Audio System";
+	case CEC_LOG_ADDR_TYPE_SPECIFIC:
+		return "Specific";
+	case CEC_LOG_ADDR_TYPE_UNREGISTERED:
+		return "Unregistered";
+	default:
+		return "Unknown";
+	}
+}
+
+static const char *la2s(unsigned la)
+{
+	switch (la & 0xf) {
+	case 0:
+		return "TV";
+	case 1:
+		return "Recording Device 1";
+	case 2:
+		return "Recording Device 2";
+	case 3:
+		return "Tuner 1";
+	case 4:
+		return "Playback Device 1";
+	case 5:
+		return "Audio System";
+	case 6:
+		return "Tuner 2";
+	case 7:
+		return "Tuner 3";
+	case 8:
+		return "Playback Device 2";
+	case 9:
+		return "Playback Device 3";
+	case 10:
+		return "Tuner 4";
+	case 11:
+		return "Playback Device 3";
+	case 12:
+		return "Reserved 1";
+	case 13:
+		return "Reserved 2";
+	case 14:
+		return "Specific";
+	case 15:
+	default:
+		return "Unregistered";
+	}
+}
+
+static std::string status2s(unsigned stat)
+{
+	std::string s;
+
+	if (stat & CEC_TX_STATUS_ARB_LOST)
+		s += "ArbitrationLost ";
+	if (stat & CEC_TX_STATUS_REPLY_TIMEOUT)
+		s += "ReplyTimeout ";
+	if (stat & CEC_TX_STATUS_RETRY_TIMEOUT)
+		s += "RetryTimeout ";
+	if (stat & CEC_TX_STATUS_FEATURE_ABORT)
+		s += "FeatureAbort ";
+	return s;
+}
+
+static std::string all_dev_types2s(unsigned types)
+{
+	std::string s;
+
+	if (types & CEC_OP_ALL_DEVTYPE_TV)
+		s += "TV, ";
+	if (types & CEC_OP_ALL_DEVTYPE_RECORD)
+		s += "Record, ";
+	if (types & CEC_OP_ALL_DEVTYPE_TUNER)
+		s += "Tuner, ";
+	if (types & CEC_OP_ALL_DEVTYPE_PLAYBACK)
+		s += "Playback, ";
+	if (types & CEC_OP_ALL_DEVTYPE_AUDIOSYSTEM)
+		s += "Audio System, ";
+	if (types & CEC_OP_ALL_DEVTYPE_SWITCH)
+		s += "Switch, ";
+	return s.erase(s.length() - 2, 2);
+}
+
+static std::string rc_src_prof2s(unsigned prof)
+{
+	std::string s;
+
+	prof &= 0x1f;
+	if (prof == 0)
+		return "\t\tNone\n";
+	if (prof & CEC_OP_FEAT_RC_SRC_HAS_DEV_ROOT_MENU)
+		s += "\t\tSource Has Device Root Menu\n";
+	if (prof & CEC_OP_FEAT_RC_SRC_HAS_DEV_SETUP_MENU)
+		s += "\t\tSource Has Device Setup Menu\n";
+	if (prof & CEC_OP_FEAT_RC_SRC_HAS_MEDIA_CONTEXT_MENU)
+		s += "\t\tSource Has Contents Menu\n";
+	if (prof & CEC_OP_FEAT_RC_SRC_HAS_MEDIA_TOP_MENU)
+		s += "\t\tSource Has Media Top Menu\n";
+	if (prof & CEC_OP_FEAT_RC_SRC_HAS_MEDIA_CONTEXT_MENU)
+		s += "\t\tSource Has Media Context-Sensitive Menu\n";
+	return s;
+}
+
+static std::string dev_feat2s(unsigned feat)
+{
+	std::string s;
+
+	feat &= 0x3e;
+	if (feat == 0)
+		return "\t\tNone\n";
+	if (feat & CEC_OP_FEAT_DEV_HAS_RECORD_TV_SCREEN)
+		s += "\t\tTV Supports <Record TV Screen>\n";
+	if (feat & CEC_OP_FEAT_DEV_HAS_SET_OSD_STRING)
+		s += "\t\tTV Supports <Set OSD String>\n";
+	if (feat & CEC_OP_FEAT_DEV_HAS_DECK_CONTROL)
+		s += "\t\tSupports Deck Control\n";
+	if (feat & CEC_OP_FEAT_DEV_HAS_SET_AUDIO_RATE)
+		s += "\t\tSource Supports <Set Audio Rate>\n";
+	if (feat & CEC_OP_FEAT_DEV_SINK_HAS_ARC_TX)
+		s += "\t\tSink Supports ARC Tx\n";
+	if (feat & CEC_OP_FEAT_DEV_SOURCE_HAS_ARC_RX)
+		s += "\t\tSource Supports ARC Rx\n";
+	return s;
+}
+
+int cec_named_ioctl(int fd, const char *name,
+		    unsigned long int request, void *parm)
+{
+	int retval = ioctl(fd, request, parm);
+	int e;
+
+	e = retval == 0 ? 0 : errno;
+	if (options[OptTrace])
+		printf("\t\t%s returned %d (%s)\n",
+			name, retval, strerror(e));
+
+	if (retval < 0)
+		app_result = -1;
+
+	return retval == -1 ? e : (retval ? -1 : 0);
+}
+
+const char *ok(int res)
+{
+	static char buf[100];
+
+	if (res == ENOTTY) {
+		strcpy(buf, "OK (Not Supported)");
+		res = 0;
+	} else {
+		strcpy(buf, "OK");
+	}
+	tests_total++;
+	if (res) {
+		app_result = res;
+		sprintf(buf, "FAIL");
+	} else {
+		tests_ok++;
+	}
+	return buf;
+}
+
+int check_0(const void *p, int len)
+{
+	const __u8 *q = (const __u8 *)p;
+
+	while (len--)
+		if (*q++)
+			return 1;
+	return 0;
+}
+
+static int testCap(struct node *node)
+{
+	struct cec_caps caps;
+
+	memset(&caps, 0xff, sizeof(caps));
+	// Must always be there
+	fail_on_test(doioctl(node, CEC_ADAP_G_CAPS, &caps));
+	fail_on_test(check_0(caps.reserved, sizeof(caps.reserved)));
+	fail_on_test(caps.available_log_addrs == 0 ||
+		     caps.available_log_addrs > CEC_MAX_LOG_ADDRS);
+	fail_on_test((caps.capabilities & CEC_CAP_PASSTHROUGH) &&
+		     !(caps.capabilities & CEC_CAP_IO));
+	return 0;
+}
+
+static int testAdapPhysAddr(struct node *node, __u16 set_phys_addr)
+{
+	__u16 pa = 0xefff;
+
+	fail_on_test(doioctl(node, CEC_ADAP_G_PHYS_ADDR, &pa));
+	fail_on_test(pa == 0xefff);
+	if (node->caps & CEC_CAP_PHYS_ADDR) {
+		fail_on_test(doioctl(node, CEC_ADAP_S_PHYS_ADDR, &set_phys_addr));
+		fail_on_test(doioctl(node, CEC_ADAP_G_PHYS_ADDR, &pa));
+		fail_on_test(pa != set_phys_addr);
+	} else {
+		fail_on_test(doioctl(node, CEC_ADAP_S_PHYS_ADDR, &pa) != ENOTTY);
+	}
+	return 0;
+}
+
+static int testVendorID(struct node *node, __u32 set_vendor_id)
+{
+	__u32 vendor_id = 0xeeeeeeee;
+
+	fail_on_test(doioctl(node, CEC_ADAP_G_VENDOR_ID, &vendor_id));
+	fail_on_test(vendor_id == 0xeeeeeeee);
+	fail_on_test(vendor_id != CEC_VENDOR_ID_NONE &&
+		     (vendor_id & 0xff000000));
+	if (node->caps & CEC_CAP_VENDOR_ID) {
+		vendor_id = 0xeeeeeeee;
+		fail_on_test(doioctl(node, CEC_ADAP_S_VENDOR_ID, &vendor_id) != EINVAL);
+		fail_on_test(doioctl(node, CEC_ADAP_S_VENDOR_ID, &set_vendor_id));
+		fail_on_test(doioctl(node, CEC_ADAP_G_VENDOR_ID, &vendor_id));
+		fail_on_test(vendor_id != set_vendor_id);
+	} else {
+		fail_on_test(doioctl(node, CEC_ADAP_S_VENDOR_ID, &vendor_id) != ENOTTY);
+	}
+	return 0;
+}
+
+static int testAdapState(struct node *node)
+{
+	__u32 state = 0xffffffff;
+
+	fail_on_test(doioctl(node, CEC_ADAP_G_STATE, &state));
+	fail_on_test(state > CEC_ADAP_ENABLED);
+	if (node->caps & CEC_CAP_STATE) {
+		state = CEC_ADAP_DISABLED;
+		fail_on_test(doioctl(node, CEC_ADAP_S_STATE, &state));
+		fail_on_test(doioctl(node, CEC_ADAP_G_STATE, &state));
+		fail_on_test(state != CEC_ADAP_DISABLED);
+		state = CEC_ADAP_ENABLED;
+		fail_on_test(doioctl(node, CEC_ADAP_S_STATE, &state));
+		fail_on_test(doioctl(node, CEC_ADAP_G_STATE, &state));
+		fail_on_test(state != CEC_ADAP_ENABLED);
+
+		/*
+		 * Do this again, thus guaranteeing that there is always
+		 * a disabled -> enabled and an enabled -> disabled state
+		 * transition tested.
+		 */
+		state = CEC_ADAP_DISABLED;
+		fail_on_test(doioctl(node, CEC_ADAP_S_STATE, &state));
+		fail_on_test(doioctl(node, CEC_ADAP_G_STATE, &state));
+		fail_on_test(state != CEC_ADAP_DISABLED);
+		state = CEC_ADAP_ENABLED;
+		fail_on_test(doioctl(node, CEC_ADAP_S_STATE, &state));
+		fail_on_test(doioctl(node, CEC_ADAP_G_STATE, &state));
+		fail_on_test(state != CEC_ADAP_ENABLED);
+	} else {
+		fail_on_test(doioctl(node, CEC_ADAP_S_STATE, &state) != ENOTTY);
+	}
+	return 0;
+}
+
+static int testAdapLogAddrs(struct node *node, unsigned flags,
+			    const char *osd_name)
+{
+	struct cec_log_addrs laddrs;
+
+	memset(&laddrs, 0xff, sizeof(laddrs));
+	fail_on_test(doioctl(node, CEC_ADAP_G_LOG_ADDRS, &laddrs));
+	fail_on_test(check_0(laddrs.reserved, sizeof(laddrs.reserved)));
+	fail_on_test(laddrs.cec_version != CEC_OP_CEC_VERSION_1_4 &&
+		     laddrs.cec_version != CEC_OP_CEC_VERSION_2_0);
+	fail_on_test(laddrs.num_log_addrs > CEC_MAX_LOG_ADDRS);
+	if (node->caps & CEC_CAP_LOG_ADDRS) {
+		memset(&laddrs, 0, sizeof(laddrs));
+		fail_on_test(doioctl(node, CEC_ADAP_S_LOG_ADDRS, &laddrs));
+		fail_on_test(laddrs.num_log_addrs != 0);
+		fail_on_test(doioctl(node, CEC_ADAP_G_LOG_ADDRS, &laddrs));
+		fail_on_test(laddrs.num_log_addrs != 0);
+
+		memset(&laddrs, 0, sizeof(laddrs));
+		laddrs.cec_version = CEC_OP_CEC_VERSION_2_0;
+		strcpy(laddrs.osd_name, osd_name);
+		for (unsigned i = 0; i < 8; i++) {
+			unsigned la_type;
+			unsigned all_dev_type;
+
+			if (!(flags & (1 << i)))
+				continue;
+			fail_on_test(laddrs.num_log_addrs == node->available_log_addrs);
+			switch (i) {
+			case CEC_OP_PRIM_DEVTYPE_TV:
+				la_type = CEC_LOG_ADDR_TYPE_TV;
+				all_dev_type = CEC_OP_ALL_DEVTYPE_TV;
+				break;
+			case CEC_OP_PRIM_DEVTYPE_RECORD:
+				la_type = CEC_LOG_ADDR_TYPE_RECORD;
+				all_dev_type = CEC_OP_ALL_DEVTYPE_RECORD;
+				break;
+			case CEC_OP_PRIM_DEVTYPE_TUNER:
+				la_type = CEC_LOG_ADDR_TYPE_TUNER;
+				all_dev_type = CEC_OP_ALL_DEVTYPE_TUNER;
+				break;
+			case CEC_OP_PRIM_DEVTYPE_PLAYBACK:
+				la_type = CEC_LOG_ADDR_TYPE_PLAYBACK;
+				all_dev_type = CEC_OP_ALL_DEVTYPE_PLAYBACK;
+				break;
+			case CEC_OP_PRIM_DEVTYPE_AUDIOSYSTEM:
+				la_type = CEC_LOG_ADDR_TYPE_AUDIOSYSTEM;
+				all_dev_type = CEC_OP_ALL_DEVTYPE_AUDIOSYSTEM;
+				break;
+			case CEC_OP_PRIM_DEVTYPE_PROCESSOR:
+				la_type = CEC_LOG_ADDR_TYPE_SPECIFIC;
+				all_dev_type = CEC_OP_ALL_DEVTYPE_SWITCH;
+				break;
+			case CEC_OP_PRIM_DEVTYPE_SWITCH:
+			default:
+				la_type = CEC_LOG_ADDR_TYPE_UNREGISTERED;
+				all_dev_type = CEC_OP_ALL_DEVTYPE_SWITCH;
+				break;
+			}
+			laddrs.log_addr_type[laddrs.num_log_addrs] = la_type;
+			laddrs.all_device_types[laddrs.num_log_addrs] = all_dev_type;
+			laddrs.primary_device_type[laddrs.num_log_addrs++] = i;
+		}
+
+		fail_on_test(doioctl(node, CEC_ADAP_S_LOG_ADDRS, &laddrs));
+		fail_on_test(laddrs.num_log_addrs == 0 ||
+			     laddrs.num_log_addrs > CEC_MAX_LOG_ADDRS);
+		node->num_log_addrs = laddrs.num_log_addrs;
+		memcpy(node->log_addr, laddrs.log_addr, laddrs.num_log_addrs);
+		fail_on_test(doioctl(node, CEC_ADAP_S_LOG_ADDRS, &laddrs) != EBUSY);
+	} else {
+		node->num_log_addrs = laddrs.num_log_addrs;
+		memcpy(node->log_addr, laddrs.log_addr, laddrs.num_log_addrs);
+		fail_on_test(doioctl(node, CEC_ADAP_S_LOG_ADDRS, &laddrs) != ENOTTY);
+	}
+	return 0;
+}
+
+static int testTopologyDevice(struct node *node, unsigned i, unsigned la)
+{
+	struct cec_msg msg = { };
+	char osd_name[15];
+
+	printf("\tSystem Information for device %d (%s) from device %d (%s):\n",
+	       i, la2s(i), la, la2s(la));
+
+	cec_msg_init(&msg, la, i);
+	cec_msg_get_cec_version(&msg, true);
+	fail_on_test(doioctl(node, CEC_TRANSMIT, &msg));
+	printf("\t\tCEC Version                : ");
+	if (msg.status)
+		warn("%s\n", status2s(msg.status).c_str());
+	else
+		printf("%s\n", version2s(msg.msg[2]));
+
+	cec_msg_init(&msg, la, i);
+	cec_msg_give_physical_addr(&msg, true);
+	doioctl(node, CEC_TRANSMIT, &msg);
+	printf("\t\tPhysical Address           : ");
+	if (msg.status) {
+		warn("\n%s\n", status2s(msg.status).c_str());
+	} else {
+		__u16 phys_addr = (msg.msg[2] << 8) | msg.msg[3];
+
+		printf("%x.%x.%x.%x\n",
+		       phys_addr >> 12, (phys_addr >> 8) & 0xf,
+		       (phys_addr >> 4) & 0xf, phys_addr & 0xf);
+		printf("\t\tPrimary Device Type        : %s\n",
+		       prim_type2s(msg.msg[4]));
+	}
+
+	cec_msg_init(&msg, la, i);
+	cec_msg_give_device_vendor_id(&msg, true);
+	doioctl(node, CEC_TRANSMIT, &msg);
+	printf("\t\tVendor ID                  : ");
+	if (msg.status)
+		warn("\n%s\n", status2s(msg.status).c_str());
+	else
+		printf("0x%02x%02x%02x\n",
+		       msg.msg[2], msg.msg[3], msg.msg[4]);
+
+	cec_msg_init(&msg, la, i);
+	cec_msg_give_device_power_status(&msg, true);
+	doioctl(node, CEC_TRANSMIT, &msg);
+	printf("\t\tPower Status               : ");
+	if (msg.status)
+		warn("\n%s\n", status2s(msg.status).c_str());
+	else
+		printf("%s\n", power_status2s(msg.msg[2]));
+
+	cec_msg_init(&msg, la, i);
+	cec_msg_give_osd_name(&msg, true);
+	doioctl(node, CEC_TRANSMIT, &msg);
+	cec_ops_set_osd_name(&msg, osd_name);
+	printf("\t\tOSD Name                   : ");
+	if (msg.status)
+		warn("\n%s\n", status2s(msg.status).c_str());
+	else
+		printf("%s\n", osd_name);
+	return 0;
+}
+
+static int testTopology(struct node *node)
+{
+	struct cec_msg msg = { };
+	struct cec_log_addrs laddrs = { };
+
+	fail_on_test(doioctl(node, CEC_ADAP_G_LOG_ADDRS, &laddrs));
+
+	if (!(node->caps & CEC_CAP_IO)) {
+		cec_msg_init(&msg, 0xf, 0);
+		fail_on_test(doioctl(node, CEC_TRANSMIT, &msg) != ENOTTY);
+		return -ENOTTY;
+	}
+
+	for (unsigned i = 0; i < 15; i++) {
+		int ret;
+
+		cec_msg_init(&msg, 0xf, i);
+		ret = doioctl(node, CEC_TRANSMIT, &msg);
+
+		switch (msg.status) {
+		case CEC_TX_STATUS_OK:
+			fail_on_test(testTopologyDevice(node, i, laddrs.log_addr[0]));
+			break;
+		case CEC_TX_STATUS_ARB_LOST:
+			warn("tx arbitration lost for addr %d\n", i);
+			break;
+		case CEC_TX_STATUS_RETRY_TIMEOUT:
+			break;
+		default:
+			return fail("ret ? %d\n", ret);
+		}
+	}
+	return 0;
+}
+
+static int testARC(struct node *node)
+{
+	struct cec_msg msg = { };
+	struct cec_log_addrs laddrs = { };
+	unsigned la;
+	unsigned i;
+
+	fail_on_test(doioctl(node, CEC_ADAP_G_LOG_ADDRS, &laddrs));
+	la = laddrs.log_addr[0];
+
+	for (i = 0; i < 15; i++) {
+		if (i == la)
+			continue;
+		cec_msg_init(&msg, la, i);
+		cec_msg_initiate_arc(&msg, true);
+		fail_on_test(doioctl(node, CEC_TRANSMIT, &msg));
+		if (msg.status == 0) {
+			cec_msg_init(&msg, la, i);
+			cec_msg_terminate_arc(&msg, true);
+			msg.reply = CEC_MSG_REPORT_ARC_TERMINATED;
+			fail_on_test(doioctl(node, CEC_TRANSMIT, &msg));
+			fail_on_test(msg.status);
+			printf("logical address %d supports ARC\n", i);
+		} else {
+			printf("logical address %d doesn't support ARC\n", i);
+		}
+	}
+	return 0;
+}
+
+int main(int argc, char **argv)
+{
+	const char *device = "/dev/cec0";	/* -d device */
+	char short_options[26 * 2 * 2 + 1];
+	__u32 vendor_id;
+	__u32 set_vendor_id = 0x000c03; /* HDMI LLC vendor ID */
+	__u16 phys_addr;
+	__u16 set_phys_addr = 0;
+	int idx = 0;
+	int fd = -1;
+	int ch;
+	int i;
+
+	for (i = 0; long_options[i].name; i++) {
+		if (!isalpha(long_options[i].val))
+			continue;
+		short_options[idx++] = long_options[i].val;
+		if (long_options[i].has_arg == required_argument)
+			short_options[idx++] = ':';
+	}
+	while (1) {
+		int option_index = 0;
+
+		short_options[idx] = 0;
+		ch = getopt_long(argc, argv, short_options,
+				 long_options, &option_index);
+		if (ch == -1)
+			break;
+
+		options[(int)ch] = 1;
+		switch (ch) {
+		case OptHelp:
+			usage();
+			return 0;
+		case OptSetDevice:
+			device = optarg;
+			if (device[0] >= '0' && device[0] <= '9' && strlen(device) <= 3) {
+				static char newdev[20];
+
+				sprintf(newdev, "/dev/cec%s", device);
+				device = newdev;
+			}
+			break;
+		case OptNoWarnings:
+			show_warnings = false;
+			break;
+		case OptVerbose:
+			show_info = true;
+			break;
+		case OptPhysAddr:
+			set_phys_addr = strtoul(optarg, NULL, 0);
+			break;
+		case OptVendorID:
+			set_vendor_id = strtoul(optarg, NULL, 0) & 0x00ffffff;
+			break;
+		case OptSwitch:
+			if (options[OptCDCOnly] || options[OptUnregistered]) {
+				fprintf(stderr, "--switch cannot be combined with --cdc-only or --unregistered.\n");
+				usage();
+				return 1;
+			}
+			break;
+		case OptCDCOnly:
+			if (options[OptSwitch] || options[OptUnregistered]) {
+				fprintf(stderr, "--cdc-only cannot be combined with --switch or --unregistered.\n");
+				usage();
+				return 1;
+			}
+			break;
+		case OptUnregistered:
+			if (options[OptCDCOnly] || options[OptSwitch]) {
+				fprintf(stderr, "--unregistered cannot be combined with --cdc-only or --switch.\n");
+				usage();
+				return 1;
+			}
+			break;
+		case ':':
+			fprintf(stderr, "Option '%s' requires a value\n",
+				argv[optind]);
+			usage();
+			return 1;
+		case '?':
+			if (argv[optind])
+				fprintf(stderr, "Unknown argument '%s'\n", argv[optind]);
+			usage();
+			return 1;
+		}
+	}
+	if (optind < argc) {
+		printf("unknown arguments: ");
+		while (optind < argc)
+			printf("%s ", argv[optind++]);
+		printf("\n");
+		usage();
+		return 1;
+	}
+
+	if ((fd = open(device, O_RDWR)) < 0) {
+		fprintf(stderr, "Failed to open %s: %s\n", device,
+			strerror(errno));
+		exit(1);
+	}
+
+	struct node node;
+	struct cec_caps caps = { };
+
+	node.fd = fd;
+	node.device = device;
+	doioctl(&node, CEC_ADAP_G_CAPS, &caps);
+	node.caps = caps.capabilities;
+	node.available_log_addrs = caps.available_log_addrs;
+
+	unsigned flags = 0;
+	const char *osd_name;
+
+	if (options[OptTV])
+		osd_name = "TV";
+	else if (options[OptRecord])
+		osd_name = "Record";
+	else if (options[OptPlayback])
+		osd_name = "Playback";
+	else if (options[OptTuner])
+		osd_name = "Tuner";
+	else if (options[OptAudio])
+		osd_name = "Audio System";
+	else if (options[OptProcessor])
+		osd_name = "Processor";
+	else if (options[OptSwitch] || options[OptCDCOnly] || options[OptUnregistered])
+		osd_name = "";
+	else
+		osd_name = "TV";
+
+	if (options[OptTV])
+		flags |= 1 << CEC_OP_PRIM_DEVTYPE_TV;
+	if (options[OptRecord])
+		flags |= 1 << CEC_OP_PRIM_DEVTYPE_RECORD;
+	if (options[OptTuner])
+		flags |= 1 << CEC_OP_PRIM_DEVTYPE_TUNER;
+	if (options[OptPlayback])
+		flags |= 1 << CEC_OP_PRIM_DEVTYPE_PLAYBACK;
+	if (options[OptAudio])
+		flags |= 1 << CEC_OP_PRIM_DEVTYPE_AUDIOSYSTEM;
+	if (options[OptProcessor])
+		flags |= 1 << CEC_OP_PRIM_DEVTYPE_PROCESSOR;
+	if (options[OptSwitch] || options[OptCDCOnly] || options[OptUnregistered])
+		flags |= 1 << CEC_OP_PRIM_DEVTYPE_SWITCH;
+	if (flags == 0)
+		flags |= 1 << CEC_OP_PRIM_DEVTYPE_TV;
+
+	printf("Driver Info:\n");
+	printf("\tCapabilities               : 0x%08x\n", caps.capabilities);
+	printf("%s", caps2s(caps.capabilities).c_str());
+	printf("\tInputs                     : %u\n", caps.ninputs);
+	printf("\tAvailable Logical Addresses: %u\n",
+	       caps.available_log_addrs);
+
+	doioctl(&node, CEC_ADAP_G_PHYS_ADDR, &phys_addr);
+	printf("\tPhysical Address           : %x.%x.%x.%x\n",
+	       phys_addr >> 12, (phys_addr >> 8) & 0xf,
+	       (phys_addr >> 4) & 0xf, phys_addr & 0xf);
+	if (!options[OptPhysAddr] && phys_addr == CEC_PHYS_ADDR_INVALID &&
+	    (node.caps & CEC_CAP_PHYS_ADDR))
+		warn("Perhaps you should use option --phys-addr?\n");
+
+	doioctl(&node, CEC_ADAP_G_VENDOR_ID, &vendor_id);
+	if (vendor_id != CEC_VENDOR_ID_NONE)
+		printf("\tVendor ID                  : 0x%06x\n", vendor_id);
+
+	__u32 adap_state;
+	doioctl(&node, CEC_ADAP_G_STATE, &adap_state);
+	printf("\tAdapter State              : %s\n", adap_state ? "Enabled" : "Disabled");
+
+	struct cec_log_addrs laddrs = { };
+	doioctl(&node, CEC_ADAP_G_LOG_ADDRS, &laddrs);
+	printf("\tCEC Version                : %s\n", version2s(laddrs.cec_version));
+	printf("\tLogical Addresses          : %u\n", laddrs.num_log_addrs);
+	for (unsigned i = 0; i < laddrs.num_log_addrs; i++) {
+		printf("\t  Logical Address          : %d\n",
+		       laddrs.log_addr[i]);
+		printf("\t    Primary Device Type    : %s\n",
+		       prim_type2s(laddrs.primary_device_type[i]));
+		printf("\t    Logical Address Type   : %s\n",
+		       la_type2s(laddrs.log_addr_type[i]));
+		if (laddrs.cec_version < CEC_OP_CEC_VERSION_2_0)
+			continue;
+		printf("\t    All Device Types       : %s\n",
+		       all_dev_types2s(laddrs.all_device_types[i]).c_str());
+
+		bool is_dev_feat = false;
+		for (unsigned idx = 0; idx < sizeof(laddrs.features[0]); idx++) {
+			__u8 byte = laddrs.features[i][idx];
+
+			if (!is_dev_feat) {
+				if (byte & 0x40) {
+					printf("\t    RC Source Profile      :\n%s\n",
+					       rc_src_prof2s(byte).c_str());
+				} else {
+					const char *s = "Reserved";
+
+					switch (byte & 0xf) {
+					case 0:
+						s = "None";
+						break;
+					case 2:
+						s = "RC Profile 1";
+						break;
+					case 6:
+						s = "RC Profile 2";
+						break;
+					case 10:
+						s = "RC Profile 3";
+						break;
+					case 14:
+						s = "RC Profile 4";
+						break;
+					}
+					printf("\t    RC TV Profile          : %s\n", s);
+				}
+			} else {
+				printf("\t    Device Features        :\n%s\n",
+				       dev_feat2s(byte).c_str());
+			}
+			if (byte & CEC_OP_FEAT_EXT)
+				continue;
+			if (!is_dev_feat)
+				is_dev_feat = true;
+			else
+				break;
+		}
+	}
+
+	printf("\nCompliance test for device %s:\n\n", device);
+
+	/* Required ioctls */
+
+	printf("Required ioctls:\n");
+	printf("\ttest CEC_ADAP_G_CAPS: %s\n", ok(testCap(&node)));
+	if (options[OptPhysAddr] || phys_addr == CEC_PHYS_ADDR_INVALID)
+		phys_addr = set_phys_addr;
+	printf("\ttest CEC_ADAP_G/S_PHYS_ADDR: %s\n", ok(testAdapPhysAddr(&node, phys_addr)));
+	if (options[OptVendorID] || vendor_id == CEC_VENDOR_ID_NONE)
+		vendor_id = set_vendor_id;
+	printf("\ttest CEC_ADAP_G/S_VENDOR_ID: %s\n", ok(testVendorID(&node, vendor_id)));
+	printf("\ttest CEC_ADAP_G/S_STATE: %s\n", ok(testAdapState(&node)));
+	printf("\ttest CEC_ADAP_G/S_LOG_ADDRS: %s\n", ok(testAdapLogAddrs(&node, flags, osd_name)));
+	printf("\ttest CEC topology discovery: %s\n", ok(testTopology(&node)));
+	printf("\ttest CEC ARC: %s\n", ok(testARC(&node)));
+	printf("\n");
+
+	/* Final test report */
+
+	close(fd);
+	printf("Total: %d, Succeeded: %d, Failed: %d, Warnings: %d\n",
+			tests_total, tests_ok, tests_total - tests_ok, warnings);
+	exit(app_result);
+}
diff --git a/utils/cec-compliance/cec-compliance.h b/utils/cec-compliance/cec-compliance.h
new file mode 100644
index 0000000..cd02c24
--- /dev/null
+++ b/utils/cec-compliance/cec-compliance.h
@@ -0,0 +1,87 @@
+/*
+    CEC API compliance test tool.
+
+    Copyright 2015 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
+    Author: Hans Verkuil <hans.verkuil@cisco.com>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+ */
+
+#ifndef _CEC_COMPLIANCE_H_
+#define _CEC_COMPLIANCE_H_
+
+#include <stdarg.h>
+#include <cerrno>
+#include <string>
+#include <linux/cec-funcs.h>
+
+extern bool show_info;
+extern bool show_warnings;
+extern unsigned warnings;
+
+struct node {
+	int fd;
+	const char *device;
+	unsigned caps;
+	unsigned available_log_addrs;
+	unsigned num_log_addrs;
+	__u8 log_addr[CEC_MAX_LOG_ADDRS];
+};
+
+#define info(fmt, args...) 					\
+	do {							\
+		if (show_info)					\
+ 			printf("\t\tinfo: " fmt, ##args);	\
+	} while (0)
+
+#define warn(fmt, args...) 					\
+	do {							\
+		warnings++;					\
+		if (show_warnings)				\
+ 			printf("\t\twarn: %s(%d): " fmt, __FILE__, __LINE__, ##args);	\
+	} while (0)
+
+#define warn_once(fmt, args...)						\
+	do {								\
+		static bool show;					\
+									\
+		if (!show) {						\
+			show = true;					\
+			warnings++;					\
+			if (show_warnings)				\
+				printf("\t\twarn: %s(%d): " fmt,	\
+					__FILE__, __LINE__, ##args); 	\
+		}							\
+	} while (0)
+
+#define fail(fmt, args...) 						\
+({ 									\
+ 	printf("\t\tfail: %s(%d): " fmt, __FILE__, __LINE__, ##args);	\
+	1;								\
+})
+
+#define fail_on_test(test) 				\
+	do {						\
+	 	if (test)				\
+			return fail("%s\n", #test);	\
+	} while (0)
+
+int cec_named_ioctl(int fd, const char *name,
+		    unsigned long int request, void *parm);
+
+#define doioctl(n, r, p) cec_named_ioctl((n)->fd, #r, r, p)
+
+const char *ok(int res);
+
+// CEC core tests
+int testCore(struct node *node);
+
+#endif
