diff options
-rw-r--r-- | utils/cec-compliance/.gitignore | 2 | ||||
-rw-r--r-- | utils/cec-compliance/Makefile.am | 11 | ||||
-rw-r--r-- | utils/cec-compliance/cec-compliance.cpp | 1342 | ||||
-rw-r--r-- | utils/cec-compliance/cec-compliance.h | 459 | ||||
-rw-r--r-- | utils/cec-compliance/cec-test-adapter.cpp | 916 | ||||
-rw-r--r-- | utils/cec-compliance/cec-test-audio.cpp | 831 | ||||
-rw-r--r-- | utils/cec-compliance/cec-test-power.cpp | 441 | ||||
-rw-r--r-- | utils/cec-compliance/cec-test.cpp | 1493 |
8 files changed, 5495 insertions, 0 deletions
diff --git a/utils/cec-compliance/.gitignore b/utils/cec-compliance/.gitignore new file mode 100644 index 00000000..35073175 --- /dev/null +++ b/utils/cec-compliance/.gitignore @@ -0,0 +1,2 @@ +cec-compliance +cec-table.h diff --git a/utils/cec-compliance/Makefile.am b/utils/cec-compliance/Makefile.am new file mode 100644 index 00000000..331b03e3 --- /dev/null +++ b/utils/cec-compliance/Makefile.am @@ -0,0 +1,11 @@ +bin_PROGRAMS = cec-compliance + +cec_compliance_SOURCES = cec-compliance.cpp cec-test.cpp cec-test-adapter.cpp cec-test-audio.cpp cec-test-power.cpp + +cec-compliance.cpp: cec-table.h + +cec-table.h: ../cec-ctl/msg2ctl.pl ../../include/linux/cec.h ../../include/linux/cec-funcs.h + ../cec-ctl/msg2ctl.pl 2 ../../include/linux/cec.h ../../include/linux/cec-funcs.h >$@ + +clean-local: + -rm -vf cec-table.h diff --git a/utils/cec-compliance/cec-compliance.cpp b/utils/cec-compliance/cec-compliance.cpp new file mode 100644 index 00000000..d94e06ec --- /dev/null +++ b/utils/cec-compliance/cec-compliance.cpp @@ -0,0 +1,1342 @@ +/* + * Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved. + * + * This program is free software; you may redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#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 <sys/wait.h> +#include <sstream> + +#include "cec-compliance.h" + +#include "cec-table.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 { + OptTestAdapter = 'A', + OptSetDevice = 'd', + OptHelp = 'h', + OptNoWarnings = 'n', + OptRemote = 'r', + OptTrace = 'T', + OptVerbose = 'v', + OptInteractive = 'i', + + OptTestCore = 128, + OptTestAudioRateControl, + OptTestARCControl, + OptTestCapDiscoveryControl, + OptTestDeckControl, + OptTestDeviceMenuControl, + OptTestDeviceOSDTransfer, + OptTestDynamicAutoLipsync, + OptTestOSDDisplay, + OptTestOneTouchPlay, + OptTestOneTouchRecord, + OptTestPowerStatus, + OptTestRemoteControlPassthrough, + OptTestRoutingControl, + OptTestSystemAudioControl, + OptTestSystemInformation, + OptTestTimerProgramming, + OptTestTunerControl, + OptTestVendorSpecificCommands, + OptTestStandbyResume, + 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}, + {"remote", optional_argument, 0, OptRemote}, + {"trace", no_argument, 0, OptTrace}, + {"verbose", no_argument, 0, OptVerbose}, + {"interactive", no_argument, 0, OptInteractive}, + + {"test-adapter", no_argument, 0, OptTestAdapter}, + {"test-core", no_argument, 0, OptTestCore}, + {"test-audio-rate-control", no_argument, 0, OptTestAudioRateControl}, + {"test-audio-return-channel-control", no_argument, 0, OptTestARCControl}, + {"test-capability-discovery-and-control", no_argument, 0, OptTestCapDiscoveryControl}, + {"test-deck-control", no_argument, 0, OptTestDeckControl}, + {"test-device-menu-control", no_argument, 0, OptTestDeviceMenuControl}, + {"test-device-osd-transfer", no_argument, 0, OptTestDeviceOSDTransfer}, + {"test-dynamic-auto-lipsync", no_argument, 0, OptTestDynamicAutoLipsync}, + {"test-osd-display", no_argument, 0, OptTestOSDDisplay}, + {"test-one-touch-play", no_argument, 0, OptTestOneTouchPlay}, + {"test-one-touch-record", no_argument, 0, OptTestOneTouchRecord}, + {"test-power-status", no_argument, 0, OptTestPowerStatus}, + {"test-remote-control-passthrough", no_argument, 0, OptTestRemoteControlPassthrough}, + {"test-routing-control", no_argument, 0, OptTestRoutingControl}, + {"test-system-audio-control", no_argument, 0, OptTestSystemAudioControl}, + {"test-system-information", no_argument, 0, OptTestSystemInformation}, + {"test-timer-programming", no_argument, 0, OptTestTimerProgramming}, + {"test-tuner-control", no_argument, 0, OptTestTunerControl}, + {"test-vendor-specific-commands", no_argument, 0, OptTestVendorSpecificCommands}, + {"test-standby-resume", no_argument, 0, OptTestStandbyResume}, + {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" + " -r, --remote[=<la>] As initiator test the remote logical address or all LAs if no LA was given\n" + " -i, --interactive Interactive mode when doing remote tests\n" + "\n" + " -A, --test-adapter Test the CEC adapter API\n" + " --test-core Test the core functionality\n" + " --test-audio-rate-control Test the Audio Rate Control feature\n" + " --test-audio-return-channel-control Test the Audio Return Channel Control feature\n" + " --test-capability-discovery-and-control Test the Capability Discovery and Control feature\n" + " --test-deck-control Test the Deck Control feature\n" + " --test-device-menu-control Test the Device Menu Control feature\n" + " --test-device-osd-transfer Test the Device OSD Transfer feature\n" + " --test-dynamic-audio-lipsync Test the Dynamic Audio Lipsync feature\n" + " --test-osd-display Test the OSD Display feature\n" + " --test-one-touch-play Test the One Touch Play feature\n" + " --test-one-touch-record Test the One Touch Record feature\n" + " --test-power-status Test the Power Status feature\n" + " --test-remote-control-passthrough Test the Remote Control Passthrough feature\n" + " --test-routing-control Test the Routing Control feature\n" + " --test-system-audio-control Test the System Audio Control feature\n" + " --test-system-information Test the System Information feature\n" + " --test-timer-programming Test the Timer Programming feature\n" + " --test-tuner-control Test the Tuner Control feature\n" + " --test-vendor-specific-commands Test the Vendor Specific Commands feature\n" + " --test-standby-resume Test standby and resume functionality. This will activate\n" + " testing of Standby, Give Device Power Status and One Touch Play.\n" + "\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" + ); +} + +static std::string caps2s(unsigned caps) +{ + std::string s; + + 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_TRANSMIT) + s += "\t\tTransmit\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_MONITOR_ALL) + s += "\t\tMonitor All\n"; + return s; +} + +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"; + } +} + +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"; + } +} + +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"; + } +} + +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 tx_status2s(const struct cec_msg &msg) +{ + std::string s; + char num[4]; + unsigned stat = msg.tx_status; + + if (stat) + s += "Tx"; + if (stat & CEC_TX_STATUS_OK) + s += ", OK"; + if (stat & CEC_TX_STATUS_ARB_LOST) { + sprintf(num, "%u", msg.tx_arb_lost_cnt); + s += ", Arbitration Lost"; + if (msg.tx_arb_lost_cnt) + s += " (" + std::string(num) + ")"; + } + if (stat & CEC_TX_STATUS_NACK) { + sprintf(num, "%u", msg.tx_nack_cnt); + s += ", Not Acknowledged"; + if (msg.tx_nack_cnt) + s += " (" + std::string(num) + ")"; + } + if (stat & CEC_TX_STATUS_LOW_DRIVE) { + sprintf(num, "%u", msg.tx_low_drive_cnt); + s += ", Low Drive"; + if (msg.tx_low_drive_cnt) + s += " (" + std::string(num) + ")"; + } + if (stat & CEC_TX_STATUS_ERROR) { + sprintf(num, "%u", msg.tx_error_cnt); + s += ", Error"; + if (msg.tx_error_cnt) + s += " (" + std::string(num) + ")"; + } + if (stat & CEC_TX_STATUS_MAX_RETRIES) + s += ", Max Retries"; + return s; +} + +static std::string rx_status2s(unsigned stat) +{ + std::string s; + + if (stat) + s += "Rx"; + if (stat & CEC_RX_STATUS_OK) + s += ", OK"; + if (stat & CEC_RX_STATUS_TIMEOUT) + s += ", Timeout"; + if (stat & CEC_RX_STATUS_FEATURE_ABORT) + s += ", Feature Abort"; + return s; +} + +std::string status2s(const struct cec_msg &msg) +{ + std::string s; + + if (msg.tx_status) + s = tx_status2s(msg); + if (msg.rx_status) { + if (!s.empty()) + s += ", "; + s += rx_status2s(msg.rx_status); + } + return s; +} + +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); +} + +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; +} + +std::string dev_feat2s(unsigned feat) +{ + std::string s; + + feat &= 0x7e; + 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; +} + +const char *power_status2s(__u8 power_status) +{ + switch (power_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 std::string audio_format_code2s(__u8 format_code) +{ + switch (format_code) { + case 0: + return "Reserved"; + case SAD_FMT_CODE_LPCM: + return "L-PCM"; + case SAD_FMT_CODE_AC3: + return "AC-3"; + case SAD_FMT_CODE_MPEG1: + return "MPEG-1"; + case SAD_FMT_CODE_MP3: + return "MP3"; + case SAD_FMT_CODE_MPEG2: + return "MPEG2"; + case SAD_FMT_CODE_AAC_LC: + return "AAC LC"; + case SAD_FMT_CODE_DTS: + return "DTS"; + case SAD_FMT_CODE_ATRAC: + return "ATRAC"; + case SAD_FMT_CODE_ONE_BIT_AUDIO: + return "One Bit Audio"; + case SAD_FMT_CODE_ENHANCED_AC3: + return "Enhanced AC-3"; + case SAD_FMT_CODE_DTS_HD: + return "DTS-HD"; + case SAD_FMT_CODE_MAT: + return "MAT"; + case SAD_FMT_CODE_DST: + return "DST"; + case SAD_FMT_CODE_WMA_PRO: + return "WMA Pro"; + case SAD_FMT_CODE_EXTENDED: + return "Extended"; + default: + return "Illegal"; + }; +} + +std::string extension_type_code2s(__u8 type_code) +{ + switch (type_code) { + case 0: + case 1: + case 2: + case 3: + return "Not in use"; + case SAD_EXT_TYPE_MPEG4_HE_AAC: + return "MPEG-4 HE AAC"; + case SAD_EXT_TYPE_MPEG4_HE_AACv2: + return "MPEG-4 HE AAC v2"; + case SAD_EXT_TYPE_MPEG4_AAC_LC: + return "MPEG-4 AAC LC"; + case SAD_EXT_TYPE_DRA: + return "DRA"; + case SAD_EXT_TYPE_MPEG4_HE_AAC_SURROUND: + return "MPEG-4 HE AAC + MPEG Surround"; + case SAD_EXT_TYPE_MPEG4_AAC_LC_SURROUND: + return "MPEG-4 AAC LC + MPEG Surround"; + case SAD_EXT_TYPE_MPEG_H_3D_AUDIO: + return "MPEG-H 3D Audio"; + case SAD_EXT_TYPE_AC_4: + return "AC-4"; + case SAD_EXT_TYPE_LPCM_3D_AUDIO: + return "L-PCM 3D Audio"; + default: + return "Reserved"; + } +} + +std::string short_audio_desc2s(const struct short_audio_desc &sad) +{ + std::stringstream oss; + + if (sad.format_code != SAD_FMT_CODE_EXTENDED) + oss << audio_format_code2s(sad.format_code); + else + oss << extension_type_code2s(sad.extension_type_code); + oss << ", " << (int)sad.num_channels << " channels"; + + oss << ", sampling rates (kHz): "; + if (sad.sample_freq_mask & SAD_SAMPLE_FREQ_MASK_32) + oss << "32,"; + if (sad.sample_freq_mask & SAD_SAMPLE_FREQ_MASK_44_1) + oss << "44.1,"; + if (sad.sample_freq_mask & SAD_SAMPLE_FREQ_MASK_48) + oss << "48,"; + if (sad.sample_freq_mask & SAD_SAMPLE_FREQ_MASK_88_2) + oss << "88.2,"; + if (sad.sample_freq_mask & SAD_SAMPLE_FREQ_MASK_96) + oss << "96,"; + if (sad.sample_freq_mask & SAD_SAMPLE_FREQ_MASK_176_4) + oss << "176.4,"; + if (sad.sample_freq_mask & SAD_SAMPLE_FREQ_MASK_192) + oss << "192,"; + if (sad.sample_freq_mask & (1 << 7)) + oss << "Reserved,"; + oss << "\b \b"; + + if (sad.format_code == SAD_FMT_CODE_LPCM || + (sad.format_code == SAD_FMT_CODE_EXTENDED && + sad.extension_type_code == SAD_EXT_TYPE_LPCM_3D_AUDIO)) { + oss << ", bit depth: "; + if (sad.bit_depth_mask & SAD_BIT_DEPTH_MASK_16) + oss << "16,"; + if (sad.bit_depth_mask & SAD_BIT_DEPTH_MASK_20) + oss << "20,"; + if (sad.bit_depth_mask & SAD_BIT_DEPTH_MASK_24) + oss << "24,"; + oss << "\b \b"; + } else if (sad.format_code >= 2 && sad.format_code <= 8) + oss << " max bitrate (kbit/s): " << 8 * sad.max_bitrate; + + if (sad.format_code == SAD_FMT_CODE_EXTENDED) { + switch (sad.extension_type_code) { + case 4: + case 5: + case 6: + case 8: + case 10: + oss << ", frame length: "; + if (sad.frame_length_mask & SAD_FRAME_LENGTH_MASK_960) + oss << "960,"; + if (sad.frame_length_mask & SAD_FRAME_LENGTH_MASK_1024) + oss << "1024,"; + oss << "\b"; + break; + } + + if (sad.extension_type_code == 8 || sad.extension_type_code == 10) + oss << ", MPS"; + } + + return oss.str(); +} + +void sad_decode(struct short_audio_desc *sad, __u32 descriptor) +{ + __u8 b1 = (descriptor >> 16) & 0xff; + __u8 b2 = (descriptor >> 8) & 0xff; + __u8 b3 = descriptor & 0xff; + + sad->num_channels = (b1 & 0x07) + 1; + sad->format_code = (b1 >> 3) & 0x0f; + sad->sample_freq_mask = b2; + + switch (sad->format_code) { + case SAD_FMT_CODE_LPCM: + sad->bit_depth_mask = b3 & 0x07; + break; + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + sad->max_bitrate = b3; + break; + case 9: + case 10: + case 11: + case 12: + case 13: + sad->format_dependent = b3; + break; + case SAD_FMT_CODE_WMA_PRO: + sad->wma_profile = b3 & 0x03; + break; + case SAD_FMT_CODE_EXTENDED: + sad->extension_type_code = (b3 >> 3) & 0x1f; + + switch (sad->extension_type_code) { + case 4: + case 5: + case 6: + sad->frame_length_mask = (b3 >> 1) & 0x03; + break; + case 8: + case 10: + sad->frame_length_mask = (b3 >> 1) & 0x03; + sad->mps = b3 & 1; + break; + case SAD_EXT_TYPE_MPEG_H_3D_AUDIO: + case SAD_EXT_TYPE_AC_4: + sad->format_dependent = b3 & 0x07; + case SAD_EXT_TYPE_LPCM_3D_AUDIO: + sad->bit_depth_mask = b3 & 0x07; + break; + } + break; + } +} + +const char *bcast_system2s(__u8 bcast_system) +{ + switch (bcast_system) { + case CEC_OP_BCAST_SYSTEM_PAL_BG: + return "PAL B/G"; + case CEC_OP_BCAST_SYSTEM_SECAM_LQ: + return "SECAM L'"; + case CEC_OP_BCAST_SYSTEM_PAL_M: + return "PAL M"; + case CEC_OP_BCAST_SYSTEM_NTSC_M: + return "NTSC M"; + case CEC_OP_BCAST_SYSTEM_PAL_I: + return "PAL I"; + case CEC_OP_BCAST_SYSTEM_SECAM_DK: + return "SECAM DK"; + case CEC_OP_BCAST_SYSTEM_SECAM_BG: + return "SECAM B/G"; + case CEC_OP_BCAST_SYSTEM_SECAM_L: + return "SECAM L"; + case CEC_OP_BCAST_SYSTEM_PAL_DK: + return "PAL DK"; + case 31: + return "Other System"; + default: + return "Future use"; + } +} + +const char *dig_bcast_system2s(__u8 bcast_system) +{ + switch (bcast_system) { + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ARIB_GEN: + return "ARIB generic"; + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ATSC_GEN: + return "ATSC generic"; + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_DVB_GEN: + return "DVB generic"; + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ARIB_BS: + return "ARIB-BS"; + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ARIB_CS: + return "ARIB-CS"; + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ARIB_T: + return "ARIB-T"; + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ATSC_CABLE: + return "ATSC Cable"; + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ATSC_SAT: + return "ATSC Satellite"; + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ATSC_T: + return "ATSC Terrestrial"; + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_DVB_C: + return "DVB-C"; + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_DVB_S: + return "DVB-S"; + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_DVB_S2: + return "DVB S2"; + case CEC_OP_DIG_SERVICE_BCAST_SYSTEM_DVB_T: + return "DVB-T"; + default: + return "Invalid"; + } +} + +const char *hec_func_state2s(__u8 hfs) +{ + switch (hfs) { + case CEC_OP_HEC_FUNC_STATE_NOT_SUPPORTED: + return "HEC Not Supported"; + case CEC_OP_HEC_FUNC_STATE_INACTIVE: + return "HEC Inactive"; + case CEC_OP_HEC_FUNC_STATE_ACTIVE: + return "HEC Active"; + case CEC_OP_HEC_FUNC_STATE_ACTIVATION_FIELD: + return "HEC Activation Field"; + default: + return "Unknown"; + } +} + +const char *host_func_state2s(__u8 hfs) +{ + switch (hfs) { + case CEC_OP_HOST_FUNC_STATE_NOT_SUPPORTED: + return "Host Not Supported"; + case CEC_OP_HOST_FUNC_STATE_INACTIVE: + return "Host Inactive"; + case CEC_OP_HOST_FUNC_STATE_ACTIVE: + return "Host Active"; + default: + return "Unknown"; + } +} + +const char *enc_func_state2s(__u8 efs) +{ + switch (efs) { + case CEC_OP_ENC_FUNC_STATE_EXT_CON_NOT_SUPPORTED: + return "Ext Con Not Supported"; + case CEC_OP_ENC_FUNC_STATE_EXT_CON_INACTIVE: + return "Ext Con Inactive"; + case CEC_OP_ENC_FUNC_STATE_EXT_CON_ACTIVE: + return "Ext Con Active"; + default: + return "Unknown"; + } +} + +const char *cdc_errcode2s(__u8 cdc_errcode) +{ + switch (cdc_errcode) { + case CEC_OP_CDC_ERROR_CODE_NONE: + return "No error"; + case CEC_OP_CDC_ERROR_CODE_CAP_UNSUPPORTED: + return "Initiator does not have requested capability"; + case CEC_OP_CDC_ERROR_CODE_WRONG_STATE: + return "Initiator is in wrong state"; + case CEC_OP_CDC_ERROR_CODE_OTHER: + return "Other error"; + default: + return "Unknown"; + } +} + +std::string opcode2s(const struct cec_msg *msg) +{ + std::stringstream oss; + __u8 opcode = msg->msg[2]; + + if (opcode == CEC_MSG_CDC_MESSAGE) { + __u8 cdc_opcode = msg->msg[4]; + + for (unsigned i = 0; i < ARRAY_SIZE(cdcmsgtable); i++) { + if (cdcmsgtable[i].opcode == cdc_opcode) + return cdcmsgtable[i].name; + } + oss << "CDC: 0x" << std::hex << cdc_opcode; + return oss.str(); + } + + for (unsigned i = 0; i < ARRAY_SIZE(msgtable); i++) { + if (msgtable[i].opcode == opcode) + return msgtable[i].name; + } + oss << "0x" << std::hex << opcode; + return oss.str(); +} + +int cec_named_ioctl(struct node *node, const char *name, + unsigned long int request, void *parm) +{ + int retval; + int e; + struct cec_msg *msg = (struct cec_msg *)parm; + __u8 opcode = 0; + + if (request == CEC_TRANSMIT) + opcode = cec_msg_opcode(msg); + + retval = ioctl(node->fd, request, parm); + + 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; + + if (!retval) { + /* TODO: The logic here might need to be re-evaluated. + + Currently a message is registered as recognized if + - We receive a reply that is not Feature Abort with + [Unrecognized Opcode] or [Undetermined] + - We did not receive a reply, but also did not + expect one (other than Feature Abort) + - We manually receive (CEC_RECEIVE) and get a Feature Abort + with reason different than [Unrecognized Opcode] or + [Undetermined] */ + if (request == CEC_TRANSMIT && msg->timeout > 0 && + cec_msg_initiator(msg) != CEC_LOG_ADDR_UNREGISTERED && + cec_msg_destination(msg) != CEC_LOG_ADDR_BROADCAST && + (msg->tx_status & CEC_TX_STATUS_OK)) { + if (msg->rx_status & CEC_RX_STATUS_OK) { + __u8 la = cec_msg_initiator(msg); + + if (cec_msg_status_is_abort(msg) && + (abort_reason(msg) == CEC_OP_ABORT_UNRECOGNIZED_OP || + abort_reason(msg) == CEC_OP_ABORT_UNDETERMINED)) + node->remote[la].unrecognized_op[opcode] = true; + else + node->remote[la].recognized_op[opcode] = true; + } + else { + __u8 la = cec_msg_destination(msg); + + if (msg->reply > 0) + node->remote[la].unrecognized_op[opcode] = true; + else + node->remote[la].recognized_op[opcode] = true; + } + } + + if (request == CEC_RECEIVE && + cec_msg_initiator(msg) != CEC_LOG_ADDR_UNREGISTERED && + cec_msg_status_is_abort(msg)) { + __u8 la = cec_msg_initiator(msg); + __u8 abort_msg = msg->msg[2]; + + if (abort_reason(msg) == CEC_OP_ABORT_UNRECOGNIZED_OP || + abort_reason(msg) == CEC_OP_ABORT_UNDETERMINED) + node->remote[la].unrecognized_op[abort_msg] = true; + else + node->remote[la].recognized_op[abort_msg] = true; + } + } + + return retval == -1 ? e : (retval ? -1 : 0); +} + +const char *ok(int res) +{ + static char buf[100]; + + if (res == NOTSUPPORTED) { + 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 poll_remote_devs(struct node *node) +{ + node->remote_la_mask = 0; + if (!(node->caps & CEC_CAP_TRANSMIT)) + return 0; + + for (unsigned i = 0; i < 15; i++) { + struct cec_msg msg; + + cec_msg_init(&msg, 0xf, i); + + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + if (msg.tx_status & CEC_TX_STATUS_OK) + node->remote_la_mask |= 1 << i; + } + return 0; +} + +static void topology_probe_device(struct node *node, unsigned i, unsigned la) +{ + struct cec_msg msg = { }; + + 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); + printf("\t\tCEC Version : "); + if (!transmit_timeout(node, &msg) || timed_out_or_abort(&msg)) { + printf("%s\n", status2s(msg).c_str()); + node->remote[i].cec_version = CEC_OP_CEC_VERSION_1_4; + } + /* This needs to be kept in sync with newer CEC versions */ + else { + node->remote[i].cec_version = msg.msg[2]; + if (msg.msg[2] < CEC_OP_CEC_VERSION_1_3A) { + printf("< 1.3a (%x)\n", msg.msg[2]); + warn("The reported CEC version is less than 1.3a. The device will be tested as a CEC 1.3a compliant device.\n"); + } + else if (msg.msg[2] > CEC_OP_CEC_VERSION_2_0) { + printf("> 2.0 (%x)\n", msg.msg[2]); + warn("The reported CEC version is greater than 2.0. The device will be tested as a CEC 2.0 compliant device.\n"); + } + else + printf("%s\n", version2s(msg.msg[2])); + } + + cec_msg_init(&msg, la, i); + cec_msg_give_physical_addr(&msg, true); + printf("\t\tPhysical Address : "); + if (!transmit_timeout(node, &msg) || timed_out_or_abort(&msg)) { + printf("%s\n", status2s(msg).c_str()); + node->remote[i].phys_addr = CEC_PHYS_ADDR_INVALID; + } + else { + node->remote[i].phys_addr = (msg.msg[2] << 8) | msg.msg[3]; + printf("%x.%x.%x.%x\n", + cec_phys_addr_exp(node->remote[i].phys_addr)); + node->remote[i].prim_type = msg.msg[4]; + printf("\t\tPrimary Device Type : %s\n", + prim_type2s(node->remote[i].prim_type)); + } + + cec_msg_init(&msg, la, i); + cec_msg_give_device_vendor_id(&msg, true); + printf("\t\tVendor ID : "); + if (!transmit_timeout(node, &msg) || timed_out_or_abort(&msg)) + printf("%s\n", status2s(msg).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_osd_name(&msg, true); + printf("\t\tOSD Name : "); + if (!transmit_timeout(node, &msg) || timed_out_or_abort(&msg)) + printf("%s\n", status2s(msg).c_str()); + else { + char osd_name[15]; + + cec_ops_set_osd_name(&msg, osd_name); + printf("'%s'\n", osd_name); + } + + cec_msg_init(&msg, la, i); + cec_msg_get_menu_language(&msg, true); + if (transmit_timeout(node, &msg) && !timed_out_or_abort(&msg)) { + char language[4]; + + cec_ops_set_menu_language(&msg, language); + language[3] = 0; + printf("\t\tMenu Language : %s\n", language); + } + + cec_msg_init(&msg, la, i); + cec_msg_give_device_power_status(&msg, true); + printf("\t\tPower Status : "); + if (!transmit_timeout(node, &msg) || timed_out_or_abort(&msg)) + printf("%s\n", status2s(msg).c_str()); + else { + __u8 pwr; + + cec_ops_report_power_status(&msg, &pwr); + if (pwr >= 4) + printf("Invalid\n"); + else { + node->remote[i].has_power_status = true; + node->remote[i].in_standby = pwr != CEC_OP_POWER_STATUS_ON; + printf("%s\n", power_status2s(pwr)); + } + } + + if (node->remote[i].cec_version < CEC_OP_CEC_VERSION_2_0) + return; + cec_msg_init(&msg, la, i); + cec_msg_give_features(&msg, true); + if (transmit_timeout(node, &msg) && !timed_out_or_abort(&msg)) { + /* RC Profile and Device Features are assumed to be 1 byte. As of CEC 2.0 only + 1 byte is used, but this might be extended in future versions. */ + __u8 cec_version, all_device_types; + const __u8 *rc_profile, *dev_features; + + cec_ops_report_features(&msg, &cec_version, &all_device_types, + &rc_profile, &dev_features); + if (rc_profile == NULL || dev_features == NULL) + return; + node->remote[i].rc_profile = *rc_profile; + node->remote[i].dev_features = *dev_features; + node->remote[i].has_arc_rx = + (*dev_features & CEC_OP_FEAT_DEV_SOURCE_HAS_ARC_RX) != 0; + node->remote[i].has_arc_tx = + (*dev_features & CEC_OP_FEAT_DEV_SINK_HAS_ARC_TX) != 0; + node->remote[i].has_aud_rate = + (*dev_features & CEC_OP_FEAT_DEV_HAS_SET_AUDIO_RATE) != 0; + node->remote[i].has_deck_ctl = + (*dev_features & CEC_OP_FEAT_DEV_HAS_DECK_CONTROL) != 0; + node->remote[i].has_rec_tv = + (*dev_features & CEC_OP_FEAT_DEV_HAS_RECORD_TV_SCREEN) != 0; + } +} + +int main(int argc, char **argv) +{ + const char *device = "/dev/cec0"; /* -d device */ + char short_options[26 * 2 * 2 + 1]; + int remote_la = -1; + bool test_remote = false; + unsigned test_tags = 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++] = ':'; + } else if (long_options[i].has_arg == optional_argument) { + short_options[idx++] = ':'; + 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 OptRemote: + if (optarg) { + remote_la = strtoul(optarg, NULL, 0); + if (remote_la < 0 || remote_la > 15) { + fprintf(stderr, "--test: invalid remote logical address\n"); + usage(); + return 1; + } + } + test_remote = true; + break; + case OptVerbose: + show_info = true; + 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; + + if (options[OptTestAudioRateControl]) + test_tags |= TAG_AUDIO_RATE_CONTROL; + if (options[OptTestARCControl]) + test_tags |= TAG_ARC_CONTROL; + if (options[OptTestCapDiscoveryControl]) + test_tags |= TAG_CAP_DISCOVERY_CONTROL; + if (options[OptTestDeckControl]) + test_tags |= TAG_DECK_CONTROL; + if (options[OptTestDeviceMenuControl]) + test_tags |= TAG_DEVICE_MENU_CONTROL; + if (options[OptTestDeviceOSDTransfer]) + test_tags |= TAG_DEVICE_OSD_TRANSFER; + if (options[OptTestDynamicAutoLipsync]) + test_tags |= TAG_DYNAMIC_AUTO_LIPSYNC; + if (options[OptTestOSDDisplay]) + test_tags |= TAG_OSD_DISPLAY; + if (options[OptTestOneTouchPlay]) + test_tags |= TAG_ONE_TOUCH_PLAY; + if (options[OptTestOneTouchRecord]) + test_tags |= TAG_ONE_TOUCH_RECORD; + if (options[OptTestPowerStatus]) + test_tags |= TAG_POWER_STATUS; + if (options[OptTestRemoteControlPassthrough]) + test_tags |= TAG_REMOTE_CONTROL_PASSTHROUGH; + if (options[OptTestRoutingControl]) + test_tags |= TAG_ROUTING_CONTROL; + if (options[OptTestSystemAudioControl]) + test_tags |= TAG_SYSTEM_AUDIO_CONTROL; + if (options[OptTestSystemInformation]) + test_tags |= TAG_SYSTEM_INFORMATION; + if (options[OptTestTimerProgramming]) + test_tags |= TAG_TIMER_PROGRAMMING; + if (options[OptTestTunerControl]) + test_tags |= TAG_TUNER_CONTROL; + if (options[OptTestVendorSpecificCommands]) + test_tags |= TAG_VENDOR_SPECIFIC_COMMANDS; + /* When code is added to the Standby/Resume test for waking up + other devices than TVs, the necessary tags should be added + here (probably Routing Control and/or RC Passthrough) */ + if (options[OptTestStandbyResume]) + test_tags |= TAG_POWER_STATUS | TAG_STANDBY_RESUME; + + if (!test_tags && !options[OptTestCore]) + test_tags = TAG_ALL; + + if (options[OptInteractive]) + test_tags |= TAG_INTERACTIVE; + + printf("Driver Info:\n"); + printf("\tDriver Name : %s\n", caps.driver); + printf("\tAdapter Name : %s\n", caps.name); + printf("\tCapabilities : 0x%08x\n", caps.capabilities); + printf("%s", caps2s(caps.capabilities).c_str()); + printf("\tDriver version : %d.%d.%d\n", + caps.version >> 16, + (caps.version >> 8) & 0xff, + caps.version & 0xff); + printf("\tAvailable Logical Addresses: %u\n", + caps.available_log_addrs); + + node.phys_addr = CEC_PHYS_ADDR_INVALID; + doioctl(&node, CEC_ADAP_G_PHYS_ADDR, &node.phys_addr); + + printf("\tPhysical Address : %x.%x.%x.%x\n", + cec_phys_addr_exp(node.phys_addr)); + + struct cec_log_addrs laddrs = { }; + doioctl(&node, CEC_ADAP_G_LOG_ADDRS, &laddrs); + + printf("\tLogical Address Mask : 0x%04x\n", laddrs.log_addr_mask); + printf("\tCEC Version : %s\n", version2s(laddrs.cec_version)); + if (laddrs.vendor_id != CEC_VENDOR_ID_NONE) + printf("\tVendor ID : 0x%06x\n", laddrs.vendor_id); + printf("\tLogical Addresses : %u\n", laddrs.num_log_addrs); + for (unsigned i = 0; i < laddrs.num_log_addrs; i++) { + if (laddrs.log_addr[i] == CEC_LOG_ADDR_INVALID) + printf("\n\t Logical Address : Not Allocated\n"); + else + printf("\n\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", + 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", + dev_feat2s(byte).c_str()); + } + if (byte & CEC_OP_FEAT_EXT) + continue; + if (!is_dev_feat) + is_dev_feat = true; + else + break; + } + } + + bool missing_pa = node.phys_addr == CEC_PHYS_ADDR_INVALID && (node.caps & CEC_CAP_PHYS_ADDR); + bool missing_la = laddrs.num_log_addrs == 0 && (node.caps & CEC_CAP_LOG_ADDRS); + + if (missing_la || missing_pa) + printf("\n"); + if (missing_pa) + fprintf(stderr, "FAIL: missing physical address, use cec-ctl to configure this\n"); + if (missing_la) + fprintf(stderr, "FAIL: missing logical address(es), use cec-ctl to configure this\n"); + if (missing_la || missing_pa) + exit(-1); + + printf("\nCompliance test for device %s:\n\n", device); + printf(" The test results mean the following:\n" + " OK Supported correctly by the device.\n" + " OK (Not Supported) Not supported and not mandatory for the device.\n" + " OK (Presumed) Presumably supported. Manually check to confirm.\n" + " OK (Unexpected) Supported correctly but is not expected to be supported for this device.\n" + " OK (Refused) Supported by the device, but was refused.\n" + " FAIL Failed and was expected to be supported by this device.\n\n"); + + node.has_cec20 = laddrs.cec_version >= CEC_OP_CEC_VERSION_2_0; + node.num_log_addrs = laddrs.num_log_addrs; + memcpy(node.log_addr, laddrs.log_addr, laddrs.num_log_addrs); + node.adap_la_mask = laddrs.log_addr_mask; + + printf("Find remote devices:\n"); + printf("\tPolling: %s\n", ok(poll_remote_devs(&node))); + + if (options[OptTestAdapter]) { + /* Required ioctls */ + + printf("\nCEC API:\n"); + printf("\tCEC_ADAP_G_CAPS: %s\n", ok(testCap(&node))); + printf("\tCEC_DQEVENT: %s\n", ok(testDQEvent(&node))); + printf("\tCEC_ADAP_G/S_PHYS_ADDR: %s\n", ok(testAdapPhysAddr(&node))); + if (node.caps & CEC_CAP_PHYS_ADDR) + doioctl(&node, CEC_ADAP_S_PHYS_ADDR, &node.phys_addr); + printf("\tCEC_ADAP_G/S_LOG_ADDRS: %s\n", ok(testAdapLogAddrs(&node))); + fcntl(node.fd, F_SETFL, fcntl(node.fd, F_GETFL) & ~O_NONBLOCK); + if (node.caps & CEC_CAP_LOG_ADDRS) { + struct cec_log_addrs clear = { }; + + doioctl(&node, CEC_ADAP_S_LOG_ADDRS, &clear); + doioctl(&node, CEC_ADAP_S_LOG_ADDRS, &laddrs); + } + doioctl(&node, CEC_ADAP_G_LOG_ADDRS, &laddrs); + if (laddrs.log_addr_mask != node.adap_la_mask) + printf("\tNew Logical Address Mask : 0x%04x\n", laddrs.log_addr_mask); + // The LAs may have changed after these tests, so update these node fields + node.num_log_addrs = laddrs.num_log_addrs; + memcpy(node.log_addr, laddrs.log_addr, laddrs.num_log_addrs); + node.adap_la_mask = laddrs.log_addr_mask; + + printf("\tCEC_TRANSMIT: %s\n", ok(testTransmit(&node))); + printf("\tCEC_RECEIVE: %s\n", ok(testReceive(&node))); + __u32 mode = CEC_MODE_INITIATOR; + doioctl(&node, CEC_S_MODE, &mode); + printf("\tCEC_TRANSMIT/RECEIVE (non-blocking): %s\n", ok(testNonBlocking(&node))); + fcntl(node.fd, F_SETFL, fcntl(node.fd, F_GETFL) & ~O_NONBLOCK); + doioctl(&node, CEC_S_MODE, &mode); + + struct node node2 = node; + + if ((node2.fd = open(device, O_RDWR)) < 0) { + fprintf(stderr, "Failed to open %s: %s\n", device, + strerror(errno)); + exit(1); + } + + printf("\tCEC_G/S_MODE: %s\n", ok(testModes(&node, &node2))); + close(node2.fd); + doioctl(&node, CEC_S_MODE, &mode); + printf("\tCEC_EVENT_LOST_MSGS: %s\n", ok(testLostMsgs(&node))); + fcntl(node.fd, F_SETFL, fcntl(node.fd, F_GETFL) & ~O_NONBLOCK); + } + printf("\n"); + + printf("Network topology:\n"); + for (unsigned i = 0; i < 15; i++) + if (node.remote_la_mask & (1 << i)) + topology_probe_device(&node, i, node.log_addr[0]); + printf("\n"); + + unsigned remote_la_mask = node.remote_la_mask; + + if (remote_la >= 0) + remote_la_mask = 1 << remote_la; + + if (test_remote) { + for (unsigned from = 0; from <= 15; from++) { + if (!(node.adap_la_mask & (1 << from))) + continue; + for (unsigned to = 0; to <= 15; to++) + if ((remote_la_mask & (1 << to)) && + node.remote[to].phys_addr != CEC_PHYS_ADDR_INVALID) + testRemote(&node, from, to, test_tags, options[OptInteractive]); + } + } + + /* 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 00000000..6ee74029 --- /dev/null +++ b/utils/cec-compliance/cec-compliance.h @@ -0,0 +1,459 @@ +/* + * CEC API compliance test tool. + * + * Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved. + * + * This program is free software; you may redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef _CEC_COMPLIANCE_H_ +#define _CEC_COMPLIANCE_H_ + +#include <stdarg.h> +#include <cerrno> +#include <string> +#include <linux/cec-funcs.h> + +#ifdef ANDROID +#include <android-config.h> +#else +#include <config.h> +#endif + +#define TAG_AUDIO_RATE_CONTROL 1 +#define TAG_ARC_CONTROL (1 << 1) +#define TAG_CAP_DISCOVERY_CONTROL (1 << 2) +#define TAG_DECK_CONTROL (1 << 3) +#define TAG_DEVICE_MENU_CONTROL (1 << 4) +#define TAG_DEVICE_OSD_TRANSFER (1 << 5) +#define TAG_DYNAMIC_AUTO_LIPSYNC (1 << 6) +#define TAG_OSD_DISPLAY (1 << 7) +#define TAG_ONE_TOUCH_PLAY (1 << 8) +#define TAG_ONE_TOUCH_RECORD (1 << 9) +#define TAG_POWER_STATUS (1 << 10) +#define TAG_REMOTE_CONTROL_PASSTHROUGH (1 << 11) +#define TAG_ROUTING_CONTROL (1 << 12) +#define TAG_SYSTEM_AUDIO_CONTROL (1 << 13) +#define TAG_SYSTEM_INFORMATION (1 << 14) +#define TAG_TIMER_PROGRAMMING (1 << 15) +#define TAG_TUNER_CONTROL (1 << 16) +#define TAG_VENDOR_SPECIFIC_COMMANDS (1 << 17) +#define TAG_STANDBY_RESUME (1 << 18) + +#define TAG_INTERACTIVE (1 << 19) +#define TAG_CORE 0 +#define TAG_ALL (~TAG_INTERACTIVE) + +#define NUM_TAGS 21 + +struct short_audio_desc { + /* Byte 1 */ + __u8 num_channels; + __u8 format_code; + + /* Byte 2 */ + __u8 sample_freq_mask; + + /* Byte 3 */ + union { + __u8 bit_depth_mask; // LPCM + __u8 max_bitrate; // Format codes 2-8 + __u8 format_dependent; // Format codes 9-13 + __u8 wma_profile; // WMA Pro + __u8 frame_length_mask; // Extension type codes 4-6, 8-10 + }; + __u8 mps; // Format codes 8-10 + __u8 extension_type_code; +}; + +#define SAD_FMT_CODE_LPCM 1 +#define SAD_FMT_CODE_AC3 2 +#define SAD_FMT_CODE_MPEG1 3 +#define SAD_FMT_CODE_MP3 4 +#define SAD_FMT_CODE_MPEG2 5 +#define SAD_FMT_CODE_AAC_LC 6 +#define SAD_FMT_CODE_DTS 7 +#define SAD_FMT_CODE_ATRAC 8 +#define SAD_FMT_CODE_ONE_BIT_AUDIO 9 +#define SAD_FMT_CODE_ENHANCED_AC3 10 +#define SAD_FMT_CODE_DTS_HD 11 +#define SAD_FMT_CODE_MAT 12 +#define SAD_FMT_CODE_DST 13 +#define SAD_FMT_CODE_WMA_PRO 14 +#define SAD_FMT_CODE_EXTENDED 15 + +#define SAD_BIT_DEPTH_MASK_16 1 +#define SAD_BIT_DEPTH_MASK_20 (1 << 1) +#define SAD_BIT_DEPTH_MASK_24 (1 << 2) + +#define SAD_SAMPLE_FREQ_MASK_32 1 +#define SAD_SAMPLE_FREQ_MASK_44_1 (1 << 1) +#define SAD_SAMPLE_FREQ_MASK_48 (1 << 2) +#define SAD_SAMPLE_FREQ_MASK_88_2 (1 << 3) +#define SAD_SAMPLE_FREQ_MASK_96 (1 << 4) +#define SAD_SAMPLE_FREQ_MASK_176_4 (1 << 5) +#define SAD_SAMPLE_FREQ_MASK_192 (1 << 6) + +#define SAD_FRAME_LENGTH_MASK_960 1 +#define SAD_FRAME_LENGTH_MASK_1024 (1 << 1) + +#define SAD_EXT_TYPE_MPEG4_HE_AAC 4 +#define SAD_EXT_TYPE_MPEG4_HE_AACv2 5 +#define SAD_EXT_TYPE_MPEG4_AAC_LC 6 +#define SAD_EXT_TYPE_DRA 7 +#define SAD_EXT_TYPE_MPEG4_HE_AAC_SURROUND 8 +#define SAD_EXT_TYPE_MPEG4_AAC_LC_SURROUND 10 +#define SAD_EXT_TYPE_MPEG_H_3D_AUDIO 11 +#define SAD_EXT_TYPE_AC_4 12 +#define SAD_EXT_TYPE_LPCM_3D_AUDIO 13 + +extern bool show_info; +extern bool show_warnings; +extern unsigned warnings; + +struct remote { + bool recognized_op[256]; + bool unrecognized_op[256]; + unsigned prim_type; + __u16 phys_addr; + __u8 cec_version; + __u8 rc_profile; + __u8 dev_features; + bool has_osd; + bool has_power_status; + bool has_image_view_on; + bool has_text_view_on; + bool in_standby; + bool has_remote_control_passthrough; + bool has_arc_rx; + bool has_arc_tx; + bool arc_initiated; + bool has_sys_audio_mode_req; + bool has_set_sys_audio_mode; + bool has_sad; + __u8 supp_format_id; + __u8 supp_format_code; + __u8 volume; + __u8 mute; + bool has_aud_rate; + bool has_deck_ctl; + __u8 bcast_sys; + __u8 dig_bcast_sys; + bool has_rec_tv; + bool has_cdc; +}; + +struct node { + int fd; + const char *device; + bool has_cec20; + bool test_cec20; + unsigned caps; + unsigned available_log_addrs; + unsigned num_log_addrs; + unsigned adap_la_mask; + __u8 log_addr[CEC_MAX_LOG_ADDRS]; + unsigned remote_la_mask; + struct remote remote[16]; + __u16 phys_addr; +}; + +struct remote_subtest { + const char *name; + const __u16 la_mask; + int (*const test_fn)(struct node *node, unsigned me, unsigned la, bool interactive); +}; + +#define PRESUMED_OK 1 +#define FAIL 2 +#define FAIL_CRITICAL 3 +#define NOTSUPPORTED 4 +#define NOTAPPLICABLE 5 +#define REFUSED 6 + +#define CEC_LOG_ADDR_MASK_ALL 0xffff + +#define ARRAY_SIZE(a) \ + (sizeof(a) / sizeof(*a)) + +#define cec_phys_addr_exp(pa) \ + ((pa) >> 12), ((pa) >> 8) & 0xf, ((pa) >> 4) & 0xf, (pa) & 0xf + +#define info(fmt, args...) \ + do { \ + if (show_info) \ + printf("\t\tinfo: " fmt, ##args); \ + } while (0) + +#define announce(fmt, args...) \ + do { \ + printf("\t\t>>> " fmt "\n", ##args); \ + } while (0) + +#define interactive_info(block, fmt, args...) \ + do { \ + if (interactive) { \ + printf("\t\t>>> " fmt "\n", ##args); \ + if (block) { \ + printf("\t\t>>> Press ENTER to proceed.\n"); \ + getchar(); \ + } \ + } \ + } 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); \ + FAIL; \ +}) + +#define fail_on_test(test) \ + do { \ + if (test) \ + return fail("%s\n", #test); \ + } while (0) + +#define fail_on_test_v2(version, test) fail_on_test(version >= CEC_OP_CEC_VERSION_2_0 && (test)) + +#define fail_on_test_v2_warn(version, test) \ + do { \ + if (test) { \ + if (version >= CEC_OP_CEC_VERSION_2_0) \ + return fail("%s\n", #test); \ + else \ + warn("fails in CEC 2.0: %s\n", #test); \ + } \ + } while(0) + +static inline char get_yn() +{ + char c; + + while ((c = tolower(getchar())) != 'y' && c != 'n'); + getchar(); + return c; +} + +static inline bool question(const char* prompt) +{ + printf("\t\t>>> %s (y/n) ", prompt); + return get_yn() == 'y'; +} + +int cec_named_ioctl(struct node *node, const char *name, + unsigned long int request, void *parm); + +#define doioctl(n, r, p) cec_named_ioctl(n, #r, r, p) + +#define cec_phys_addr_exp(pa) \ + ((pa) >> 12), ((pa) >> 8) & 0xf, ((pa) >> 4) & 0xf, (pa) & 0xf + +std::string opcode2s(const struct cec_msg *msg); + +static inline bool is_tv(unsigned la, unsigned prim_type) +{ + return cec_has_tv(1 << la) || + (cec_has_specific(1 << la) && prim_type == CEC_OP_PRIM_DEVTYPE_TV); +} + +static inline bool cec_msg_status_is_abort(const struct cec_msg *msg) +{ + return msg->rx_status & CEC_RX_STATUS_FEATURE_ABORT; +} + +static inline __u8 abort_reason(const struct cec_msg *msg) +{ + return msg->msg[3]; +} + +static inline bool unrecognized_op(const struct cec_msg *msg) +{ + if (!cec_msg_status_is_abort(msg)) + return false; + if (abort_reason(msg) == CEC_OP_ABORT_UNRECOGNIZED_OP) + return true; + if (abort_reason(msg) == CEC_OP_ABORT_UNDETERMINED) { + warn("Opcode %x was undetermined and is treated as not supported.\n", msg->msg[2]); + return true; + } + return false; +} + +static inline bool refused(const struct cec_msg *msg) +{ + return cec_msg_status_is_abort(msg) && abort_reason(msg) == CEC_OP_ABORT_REFUSED; +} + +static inline bool timed_out(const struct cec_msg *msg) +{ + return msg->rx_status & CEC_RX_STATUS_TIMEOUT; +} + +static inline bool timed_out_or_abort(const struct cec_msg *msg) +{ + return timed_out(msg) || cec_msg_status_is_abort(msg); +} + +static inline unsigned response_time_ms(const struct cec_msg *msg) +{ + return (msg->rx_ts - msg->tx_ts) / 1000000; +} + +static inline bool transmit_timeout(struct node *node, struct cec_msg *msg, + unsigned timeout = 2000) +{ + bool broadcast = cec_msg_is_broadcast(msg); + + msg->timeout = timeout; + if (doioctl(node, CEC_TRANSMIT, msg) || + !(msg->tx_status & CEC_TX_STATUS_OK)) + return false; + + if (((msg->rx_status & CEC_RX_STATUS_OK) || (msg->rx_status & CEC_RX_STATUS_FEATURE_ABORT)) + && response_time_ms(msg) > 1000) + warn("Waited %4ums for reply.\n", response_time_ms(msg)); + + if (!cec_msg_status_is_abort(msg)) + return true; + + if (broadcast) { + fail("Received Feature Abort in reply to broadcast message\n"); + return false; + } + + const char *reason; + + switch (abort_reason(msg)) { + case CEC_OP_ABORT_UNRECOGNIZED_OP: + case CEC_OP_ABORT_UNDETERMINED: + return true; + case CEC_OP_ABORT_INVALID_OP: + reason = "Invalid operand"; + break; + case CEC_OP_ABORT_NO_SOURCE: + reason = "Cannot provide source"; + break; + case CEC_OP_ABORT_REFUSED: + reason = "Refused"; + break; + case CEC_OP_ABORT_INCORRECT_MODE: + reason = "Incorrect mode"; + break; + default: + reason = "Unknown"; + break; + } + info("Opcode %s was replied to with Feature Abort [%s]\n", + opcode2s(msg).c_str(), reason); + + return true; +} + +static inline bool transmit(struct node *node, struct cec_msg *msg) +{ + return transmit_timeout(node, msg, 0); +} + +static inline unsigned get_ts_ms() +{ + struct timespec timespec; + + clock_gettime(CLOCK_MONOTONIC, ×pec); + return timespec.tv_sec * 1000ull + timespec.tv_nsec / 1000000; +} + +const char *ok(int res); +const char *la2s(unsigned la); +const char *la_type2s(unsigned type); +const char *prim_type2s(unsigned type); +const char *version2s(unsigned version); +std::string status2s(const struct cec_msg &msg); +std::string all_dev_types2s(unsigned types); +std::string rc_src_prof2s(unsigned prof); +std::string dev_feat2s(unsigned feat); +const char *power_status2s(__u8 power_status); +std::string short_audio_desc2s(const struct short_audio_desc &sad); +void sad_decode(struct short_audio_desc *sad, __u32 descriptor); +const char *bcast_system2s(__u8 bcast_system); +const char *dig_bcast_system2s(__u8 bcast_system); +const char *hec_func_state2s(__u8 hfs); +const char *host_func_state2s(__u8 hfs); +const char *enc_func_state2s(__u8 efs); +const char *cdc_errcode2s(__u8 cdc_errcode); +int check_0(const void *p, int len); + +// CEC adapter tests +int testCap(struct node *node); +int testDQEvent(struct node *node); +int testAdapPhysAddr(struct node *node); +int testAdapLogAddrs(struct node *node); +int testTransmit(struct node *node); +int testReceive(struct node *node); +int testNonBlocking(struct node *node); +int testModes(struct node *node, struct node *node2); +int testLostMsgs(struct node *node); + +// CEC core tests +int testCore(struct node *node); + +// CEC processing +int testProcessing(struct node *node, unsigned me); + +// CEC testing +void testRemote(struct node *node, unsigned me, unsigned la, unsigned test_tags, + bool interactive); + +// cec-audio.cpp +extern struct remote_subtest sac_subtests[]; +extern const unsigned sac_subtests_size; +extern struct remote_subtest dal_subtests[]; +extern const unsigned dal_subtests_size; +extern struct remote_subtest arc_subtests[]; +extern const unsigned arc_subtests_size; +extern struct remote_subtest audio_rate_ctl_subtests[]; +extern const unsigned audio_rate_ctl_subtests_size; + +// cec-power.cpp +bool util_interactive_ensure_power_state(struct node *node, unsigned me, unsigned la, bool interactive, + __u8 target_pwr); +extern struct remote_subtest standby_subtests[]; +extern const unsigned standby_subtests_size; +extern struct remote_subtest one_touch_play_subtests[]; +extern const unsigned one_touch_play_subtests_size; +extern struct remote_subtest power_status_subtests[]; +extern const unsigned power_status_subtests_size; +extern struct remote_subtest standby_resume_subtests[]; +extern const unsigned standby_resume_subtests_size; + +#endif diff --git a/utils/cec-compliance/cec-test-adapter.cpp b/utils/cec-compliance/cec-test-adapter.cpp new file mode 100644 index 00000000..58bbf462 --- /dev/null +++ b/utils/cec-compliance/cec-test-adapter.cpp @@ -0,0 +1,916 @@ +/* + * Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved. + * + * This program is free software; you may redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#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 <sys/wait.h> +#include <sstream> + +#include "cec-compliance.h" + +static const __u8 tx_ok_retry_mask = CEC_TX_STATUS_OK | CEC_TX_STATUS_MAX_RETRIES; + +// Flush any pending messages +static int flush_pending_msgs(struct node *node) +{ + int res; + + do { + struct cec_msg msg; + + memset(&msg, 0xff, sizeof(msg)); + msg.timeout = 1; + res = doioctl(node, CEC_RECEIVE, &msg); + fail_on_test(res && res != ETIMEDOUT); + } while (res == 0); + return 0; +} + +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(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_TRANSMIT)); + return 0; +} + +int testDQEvent(struct node *node) +{ + struct cec_event ev; + + memset(&ev, 0xff, sizeof(ev)); + fail_on_test(doioctl(node, CEC_DQEVENT, &ev)); + fail_on_test(!(ev.flags & CEC_EVENT_FL_INITIAL_STATE)); + fail_on_test(ev.flags & ~CEC_EVENT_FL_INITIAL_STATE); + fail_on_test(ev.ts == 0 || ev.ts == ~0ULL); + fail_on_test(ev.event != CEC_EVENT_STATE_CHANGE); + fail_on_test(ev.state_change.log_addr_mask == 0xffff); + memset(&ev.state_change, 0, sizeof(ev.state_change)); + fail_on_test(check_0(ev.raw, sizeof(ev.raw))); + return 0; +} + +int testAdapPhysAddr(struct node *node) +{ + __u16 old_pa = 0xefff; + __u16 pa = 0x1000; + + fail_on_test(doioctl(node, CEC_ADAP_G_PHYS_ADDR, &old_pa)); + fail_on_test(old_pa == 0xefff); + if (node->caps & CEC_CAP_PHYS_ADDR) { + fail_on_test(doioctl(node, CEC_ADAP_S_PHYS_ADDR, &pa)); + fail_on_test(doioctl(node, CEC_ADAP_G_PHYS_ADDR, &pa)); + fail_on_test(pa != 0x1000); + + fail_on_test(doioctl(node, CEC_ADAP_S_PHYS_ADDR, &old_pa)); + fail_on_test(doioctl(node, CEC_ADAP_G_PHYS_ADDR, &pa)); + fail_on_test(pa != old_pa); + } else { + fail_on_test(doioctl(node, CEC_ADAP_S_PHYS_ADDR, &pa) != ENOTTY); + } + return 0; +} + +int testAdapLogAddrs(struct node *node) +{ + static const __u8 la_types[] = { + CEC_LOG_ADDR_TYPE_TV, + CEC_LOG_ADDR_TYPE_RECORD, + CEC_LOG_ADDR_TYPE_TUNER, + CEC_LOG_ADDR_TYPE_PLAYBACK + }; + static const __u8 prim_dev_types[] = { + CEC_OP_PRIM_DEVTYPE_TV, + CEC_OP_PRIM_DEVTYPE_RECORD, + CEC_OP_PRIM_DEVTYPE_TUNER, + CEC_OP_PRIM_DEVTYPE_PLAYBACK + }; + static const __u8 all_dev_types = + CEC_OP_ALL_DEVTYPE_TV | CEC_OP_ALL_DEVTYPE_RECORD; + static const __u8 features[12] = { + 0x90, 0x00, 0x8e, 0x00, + 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff + }; + struct cec_log_addrs clear = { }; + struct cec_log_addrs laddrs; + struct cec_event ev; + int res; + + memset(&laddrs, 0xff, sizeof(laddrs)); + fail_on_test(doioctl(node, CEC_ADAP_G_LOG_ADDRS, &laddrs)); + fail_on_test(laddrs.vendor_id != CEC_VENDOR_ID_NONE && + (laddrs.vendor_id & 0xff000000)); + 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->phys_addr == CEC_PHYS_ADDR_INVALID || laddrs.num_log_addrs == 0) + fail_on_test(laddrs.log_addr_mask); + else + fail_on_test(!laddrs.log_addr_mask); + + if (!(node->caps & CEC_CAP_LOG_ADDRS)) { + fail_on_test(!laddrs.num_log_addrs); + fail_on_test(doioctl(node, CEC_ADAP_S_LOG_ADDRS, &laddrs) != ENOTTY); + return 0; + } + + fail_on_test(doioctl(node, CEC_ADAP_S_LOG_ADDRS, &clear)); + fail_on_test(clear.num_log_addrs != 0); + fail_on_test(clear.log_addr_mask != 0); + fail_on_test(doioctl(node, CEC_ADAP_G_LOG_ADDRS, &laddrs)); + fail_on_test(laddrs.num_log_addrs != 0); + fail_on_test(laddrs.log_addr_mask != 0); + + __u16 pa; + + fail_on_test(doioctl(node, CEC_ADAP_G_PHYS_ADDR, &pa)); + fail_on_test(pa != node->phys_addr); + + memset(&laddrs, 0, sizeof(laddrs)); + strcpy(laddrs.osd_name, "Compliance"); + laddrs.num_log_addrs = node->available_log_addrs; + laddrs.cec_version = laddrs.num_log_addrs > 2 ? + CEC_OP_CEC_VERSION_1_4: CEC_OP_CEC_VERSION_2_0; + for (unsigned i = 0; i < CEC_MAX_LOG_ADDRS; i++) { + laddrs.log_addr_type[i] = la_types[i]; + laddrs.primary_device_type[i] = prim_dev_types[i]; + laddrs.all_device_types[i] = all_dev_types; + memcpy(laddrs.features[i], features, sizeof(features)); + } + + fail_on_test(doioctl(node, CEC_ADAP_S_LOG_ADDRS, &laddrs)); + fail_on_test(laddrs.num_log_addrs != node->available_log_addrs); + fail_on_test(laddrs.log_addr_mask == 0); + for (unsigned i = 0; i < laddrs.num_log_addrs; i++) { + fail_on_test(laddrs.log_addr[i] == CEC_LOG_ADDR_INVALID); + fail_on_test(memcmp(laddrs.features[i], features, 4)); + fail_on_test(check_0(laddrs.features[i] + 4, 8)); + } + for (unsigned i = laddrs.num_log_addrs; i < CEC_MAX_LOG_ADDRS; i++) { + fail_on_test(laddrs.log_addr_type[i] || + laddrs.primary_device_type[i] || + laddrs.all_device_types[i]); + fail_on_test(check_0(laddrs.features[i], sizeof(laddrs.features[i]))); + } + fail_on_test(doioctl(node, CEC_ADAP_S_LOG_ADDRS, &laddrs) != EBUSY); + + fcntl(node->fd, F_SETFL, fcntl(node->fd, F_GETFL) | O_NONBLOCK); + do { + res = doioctl(node, CEC_DQEVENT, &ev); + fail_on_test(res && res != EAGAIN); + if (!res) { + struct timeval tv = { 0, 10000 }; // 10 ms + + fail_on_test(ev.flags & CEC_EVENT_FL_INITIAL_STATE); + select(0, NULL, NULL, NULL, &tv); + } + } while (!res); + fail_on_test(doioctl(node, CEC_ADAP_S_LOG_ADDRS, &clear)); + do { + struct timeval tv = { 0, 10000 }; // 10 ms + + res = doioctl(node, CEC_DQEVENT, &ev); + fail_on_test(res && res != EAGAIN); + if (res) + select(0, NULL, NULL, NULL, &tv); + } while (res); + fail_on_test(ev.flags & CEC_EVENT_FL_INITIAL_STATE); + fail_on_test(ev.ts == 0); + fail_on_test(ev.event != CEC_EVENT_STATE_CHANGE); + fail_on_test(ev.state_change.phys_addr != node->phys_addr); + fail_on_test(ev.state_change.log_addr_mask); + + fail_on_test(doioctl(node, CEC_ADAP_S_LOG_ADDRS, &laddrs)); + do { + struct timeval tv = { 0, 10000 }; // 10 ms + + res = doioctl(node, CEC_DQEVENT, &ev); + fail_on_test(res && res != EAGAIN); + if (res) + select(0, NULL, NULL, NULL, &tv); + } while (res); + fail_on_test(ev.flags & CEC_EVENT_FL_INITIAL_STATE); + fail_on_test(ev.ts == 0); + fail_on_test(ev.event != CEC_EVENT_STATE_CHANGE); + fail_on_test(ev.state_change.phys_addr != node->phys_addr); + fail_on_test(ev.state_change.log_addr_mask == 0); + return 0; +} + +int testTransmit(struct node *node) +{ + struct cec_msg msg = { }; + unsigned i, la = node->log_addr[0]; + bool tested_self = false; + bool tested_valid_la = false; + bool tested_invalid_la = false; + + if (!(node->caps & CEC_CAP_TRANSMIT)) { + cec_msg_init(&msg, 0xf, 0); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg) != ENOTTY); + return NOTSUPPORTED; + } + + /* Check invalid messages */ + cec_msg_init(&msg, 0xf, 0xf); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg) != EINVAL); + + cec_msg_init(&msg, 0xf, 0); + msg.timeout = 1000; + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg) != EINVAL); + + cec_msg_init(&msg, 0xf, 0); + msg.len = 0; + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg) != EINVAL); + + cec_msg_init(&msg, 0xf, 0); + msg.len = CEC_MAX_MSG_SIZE + 1; + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg) != EINVAL); + + cec_msg_init(&msg, 0xf, 0); + msg.reply = CEC_MSG_CEC_VERSION; + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg) != EINVAL); + + for (i = 0; i < 15; i++) { + if (tested_self && (node->adap_la_mask & (1 << i))) + continue; + + memset(&msg, 0xff, sizeof(msg)); + msg.msg[0] = 0xf0 | i; + msg.len = 1; + msg.timeout = 0; + msg.reply = 0; + + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + + fail_on_test(msg.len != 1); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.timeout || msg.reply); + fail_on_test(msg.flags || msg.rx_status || msg.rx_ts); + fail_on_test(msg.tx_status == 0 || msg.tx_ts == 0 || msg.tx_ts == ~0ULL); + fail_on_test(msg.msg[0] != (0xf0 | i)); + fail_on_test(msg.sequence == 0 || msg.sequence == ~0U); + fail_on_test(msg.tx_status & ~0x3f); + fail_on_test((msg.tx_status & tx_ok_retry_mask) == tx_ok_retry_mask); + fail_on_test(!(msg.tx_status & tx_ok_retry_mask)); + + if (node->adap_la_mask & (1 << i)) { + // Send message to yourself + fail_on_test(msg.tx_status != + (CEC_TX_STATUS_NACK | CEC_TX_STATUS_MAX_RETRIES)); + fail_on_test(msg.tx_nack_cnt != 1); + fail_on_test(msg.tx_arb_lost_cnt); + fail_on_test(msg.tx_low_drive_cnt); + fail_on_test(msg.tx_error_cnt); + + cec_msg_init(&msg, i, i); + cec_msg_give_physical_addr(&msg, true); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg) != EINVAL); + } else if (msg.tx_status & CEC_TX_STATUS_OK) { + if (tested_valid_la) + continue; + tested_valid_la = true; + // Send message to a remote LA + memset(&msg, 0xff, sizeof(msg)); + msg.msg[0] = (la << 4) | i; + msg.timeout = 1001; + cec_msg_give_physical_addr(&msg, true); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + fail_on_test(msg.len != 5); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.timeout != 1001); + fail_on_test(msg.sequence == 0 || msg.sequence == ~0U); + fail_on_test(msg.reply != CEC_MSG_REPORT_PHYSICAL_ADDR); + fail_on_test(msg.rx_ts == 0 || msg.rx_ts == ~0ULL); + fail_on_test(msg.rx_status == 0 || msg.flags); + fail_on_test(msg.rx_status & ~0x07); + fail_on_test(msg.tx_ts == 0 || msg.tx_ts == ~0ULL); + fail_on_test(msg.rx_ts <= msg.tx_ts); + fail_on_test(!(msg.tx_status & CEC_TX_STATUS_OK)); + fail_on_test((msg.tx_status & tx_ok_retry_mask) == tx_ok_retry_mask); + fail_on_test(msg.tx_nack_cnt == 0xff); + fail_on_test(msg.tx_arb_lost_cnt == 0xff); + fail_on_test(msg.tx_low_drive_cnt == 0xff); + fail_on_test(msg.tx_error_cnt == 0xff); + + memset(&msg, 0xff, sizeof(msg)); + msg.msg[0] = (la << 4) | i; + msg.timeout = 0; + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + fail_on_test(msg.timeout); + fail_on_test(msg.sequence == 0 || msg.sequence == ~0U); + fail_on_test(msg.reply); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.rx_status || msg.rx_ts || msg.flags); + fail_on_test(msg.tx_ts == 0 || msg.tx_ts == ~0ULL); + fail_on_test(!(msg.tx_status & CEC_TX_STATUS_OK)); + fail_on_test((msg.tx_status & tx_ok_retry_mask) == tx_ok_retry_mask); + fail_on_test(msg.tx_nack_cnt == 0xff); + fail_on_test(msg.tx_arb_lost_cnt == 0xff); + fail_on_test(msg.tx_low_drive_cnt == 0xff); + fail_on_test(msg.tx_error_cnt == 0xff); + } else { + if (tested_invalid_la) + continue; + tested_invalid_la = true; + // Send message to a remote non-existent LA + memset(&msg, 0xff, sizeof(msg)); + msg.msg[0] = (la << 4) | i; + msg.timeout = 1002; + cec_msg_give_physical_addr(&msg, true); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + fail_on_test(msg.timeout != 1002); + fail_on_test(msg.sequence == 0 || msg.sequence == ~0U); + fail_on_test(msg.len != 2); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.flags); + fail_on_test(msg.reply != CEC_MSG_REPORT_PHYSICAL_ADDR); + fail_on_test(!(msg.tx_status & CEC_TX_STATUS_MAX_RETRIES)); + fail_on_test((msg.tx_status & tx_ok_retry_mask) == tx_ok_retry_mask); + fail_on_test(msg.rx_ts || msg.rx_status); + fail_on_test(msg.tx_ts == 0 || msg.tx_ts == ~0ULL); + fail_on_test(msg.tx_nack_cnt == 0xff); + fail_on_test(msg.tx_arb_lost_cnt == 0xff); + fail_on_test(msg.tx_low_drive_cnt == 0xff); + fail_on_test(msg.tx_error_cnt == 0xff); + + memset(&msg, 0xff, sizeof(msg)); + msg.msg[0] = (la << 4) | i; + msg.timeout = 0; + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + fail_on_test(msg.timeout); + fail_on_test(msg.sequence == 0 || msg.sequence == ~0U); + fail_on_test(msg.reply); + fail_on_test(msg.len != 2); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.rx_status || msg.rx_ts || msg.flags); + fail_on_test(msg.tx_ts == 0 || msg.tx_ts == ~0ULL); + fail_on_test(!(msg.tx_status & CEC_TX_STATUS_MAX_RETRIES)); + fail_on_test((msg.tx_status & tx_ok_retry_mask) == tx_ok_retry_mask); + fail_on_test(msg.tx_nack_cnt == 0xff); + fail_on_test(msg.tx_arb_lost_cnt == 0xff); + fail_on_test(msg.tx_low_drive_cnt == 0xff); + fail_on_test(msg.tx_error_cnt == 0xff); + } + } + + return 0; +} + +int testReceive(struct node *node) +{ + unsigned la = node->log_addr[0], remote_la = 0; + struct cec_msg msg; + + if (!(node->caps & CEC_CAP_TRANSMIT)) { + fail_on_test(doioctl(node, CEC_RECEIVE, &msg) != ENOTTY); + return NOTSUPPORTED; + } + + for (unsigned i = 0; i < 15; i++) { + if (node->remote_la_mask & (1 << i)) + break; + remote_la++; + } + + fail_on_test(flush_pending_msgs(node)); + + if (remote_la == 15) + return PRESUMED_OK; + + cec_msg_init(&msg, la, remote_la); + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + + msg.timeout = 1500; + fail_on_test(doioctl(node, CEC_RECEIVE, &msg) != ETIMEDOUT); + fail_on_test(msg.timeout != 1500); + + __u32 mode = CEC_MODE_INITIATOR | CEC_MODE_FOLLOWER; + + fail_on_test(doioctl(node, CEC_S_MODE, &mode)); + + cec_msg_init(&msg, la, remote_la); + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + + memset(&msg, 0xff, sizeof(msg)); + msg.timeout = 1500; + fail_on_test(doioctl(node, CEC_RECEIVE, &msg)); + + fail_on_test(msg.tx_ts || msg.tx_status || msg.tx_arb_lost_cnt || + msg.tx_nack_cnt || msg.tx_low_drive_cnt || msg.tx_error_cnt); + fail_on_test(msg.timeout != 1500); + fail_on_test(msg.len != 5 || msg.msg[1] != CEC_MSG_REPORT_PHYSICAL_ADDR); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.msg[0] != ((remote_la << 4) | 0xf)); + fail_on_test(msg.sequence); + fail_on_test(msg.rx_ts == 0); + fail_on_test(msg.flags || msg.reply); + fail_on_test(msg.rx_status != CEC_RX_STATUS_OK); + + return 0; +} + +int testNonBlocking(struct node *node) +{ + unsigned la = node->log_addr[0], remote_la = 0, invalid_remote = 0xf; + struct cec_msg msg; + + if (!(node->caps & CEC_CAP_TRANSMIT)) + return NOTSUPPORTED; + + for (unsigned i = 0; i < 15; i++) { + if (node->remote_la_mask & (1 << i)) + break; + if (invalid_remote == 0xf && !(node->adap_la_mask & (1 << i))) + invalid_remote = i; + remote_la++; + } + + fail_on_test(flush_pending_msgs(node)); + + fcntl(node->fd, F_SETFL, fcntl(node->fd, F_GETFL) | O_NONBLOCK); + + fail_on_test(doioctl(node, CEC_RECEIVE, &msg) != EAGAIN); + + cec_msg_init(&msg, 0xf, la); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + fail_on_test(msg.tx_status != (CEC_TX_STATUS_NACK | CEC_TX_STATUS_MAX_RETRIES)); + + if (invalid_remote < 15) { + __u32 seq; + + // Send non-blocking non-reply message to invalid remote LA + memset(&msg, 0xff, sizeof(msg)); + msg.msg[0] = (la << 4) | invalid_remote; + msg.timeout = 0; + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + fail_on_test(msg.len != 2); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.msg[0] != ((la << 4) | invalid_remote)); + fail_on_test(msg.msg[1] != CEC_MSG_GIVE_PHYSICAL_ADDR); + fail_on_test(msg.timeout || msg.flags); + fail_on_test(msg.sequence == 0 || msg.sequence == ~0U); + fail_on_test(msg.reply); + fail_on_test(msg.tx_status); + fail_on_test(msg.rx_ts || msg.rx_status); + fail_on_test(msg.tx_ts || msg.tx_nack_cnt || msg.tx_arb_lost_cnt || + msg.tx_low_drive_cnt || msg.tx_error_cnt); + seq = msg.sequence; + + sleep(1); + while (true) { + memset(&msg, 0xff, sizeof(msg)); + msg.timeout = 1500; + fail_on_test(doioctl(node, CEC_RECEIVE, &msg)); + if (msg.sequence == 0) + continue; + fail_on_test(msg.sequence != seq); + fail_on_test(msg.len != 2); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.msg[0] != ((la << 4) | invalid_remote)); + fail_on_test(msg.msg[1] != CEC_MSG_GIVE_PHYSICAL_ADDR); + fail_on_test(msg.timeout != 1500 || msg.flags); + fail_on_test(msg.reply); + fail_on_test(!(msg.tx_status & CEC_TX_STATUS_MAX_RETRIES)); + fail_on_test((msg.tx_status & tx_ok_retry_mask) == tx_ok_retry_mask); + fail_on_test(msg.tx_nack_cnt == 0xff); + fail_on_test(msg.tx_arb_lost_cnt == 0xff); + fail_on_test(msg.tx_low_drive_cnt == 0xff); + fail_on_test(msg.tx_error_cnt == 0xff); + fail_on_test(msg.rx_ts || msg.rx_status); + break; + } + + // Send non-blocking reply message to invalid remote LA + memset(&msg, 0xff, sizeof(msg)); + msg.msg[0] = (la << 4) | invalid_remote; + msg.timeout = 0; + cec_msg_give_physical_addr(&msg, true); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + fail_on_test(msg.len != 2); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.msg[0] != ((la << 4) | invalid_remote)); + fail_on_test(msg.msg[1] != CEC_MSG_GIVE_PHYSICAL_ADDR); + fail_on_test(msg.timeout != 1000 || msg.flags); + fail_on_test(msg.sequence == 0 || msg.sequence == ~0U); + fail_on_test(msg.reply != CEC_MSG_REPORT_PHYSICAL_ADDR); + fail_on_test(msg.tx_status); + fail_on_test(msg.rx_ts || msg.rx_status); + fail_on_test(msg.tx_ts || msg.tx_nack_cnt || msg.tx_arb_lost_cnt || + msg.tx_low_drive_cnt || msg.tx_error_cnt); + seq = msg.sequence; + + sleep(1); + while (true) { + memset(&msg, 0xff, sizeof(msg)); + msg.timeout = 1500; + fail_on_test(doioctl(node, CEC_RECEIVE, &msg)); + if (msg.sequence == 0) + continue; + fail_on_test(msg.sequence != seq); + fail_on_test(msg.len != 2); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.msg[0] != ((la << 4) | invalid_remote)); + fail_on_test(msg.msg[1] != CEC_MSG_GIVE_PHYSICAL_ADDR); + fail_on_test(msg.timeout != 1500 || msg.flags); + fail_on_test(msg.reply != CEC_MSG_REPORT_PHYSICAL_ADDR); + fail_on_test(!(msg.tx_status & CEC_TX_STATUS_MAX_RETRIES)); + fail_on_test((msg.tx_status & tx_ok_retry_mask) == tx_ok_retry_mask); + fail_on_test(msg.tx_nack_cnt == 0xff); + fail_on_test(msg.tx_arb_lost_cnt == 0xff); + fail_on_test(msg.tx_low_drive_cnt == 0xff); + fail_on_test(msg.tx_error_cnt == 0xff); + fail_on_test(msg.rx_ts || msg.rx_status); + break; + } + + __u32 mode = CEC_MODE_INITIATOR | CEC_MODE_FOLLOWER; + + fail_on_test(doioctl(node, CEC_S_MODE, &mode)); + } + if (remote_la < 15) { + __u32 seq; + + // Send non-blocking non-reply message to valid remote LA + memset(&msg, 0xff, sizeof(msg)); + msg.msg[0] = (la << 4) | remote_la; + msg.timeout = 0; + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + fail_on_test(msg.len != 2); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.msg[0] != ((la << 4) | remote_la)); + fail_on_test(msg.msg[1] != CEC_MSG_GIVE_PHYSICAL_ADDR); + fail_on_test(msg.timeout || msg.flags); + fail_on_test(msg.sequence == 0 || msg.sequence == ~0U); + fail_on_test(msg.reply); + fail_on_test(msg.tx_status); + fail_on_test(msg.rx_ts || msg.rx_status); + fail_on_test(msg.tx_ts || msg.tx_nack_cnt || msg.tx_arb_lost_cnt || + msg.tx_low_drive_cnt || msg.tx_error_cnt); + seq = msg.sequence; + + sleep(1); + while (true) { + memset(&msg, 0xff, sizeof(msg)); + msg.timeout = 1500; + fail_on_test(doioctl(node, CEC_RECEIVE, &msg)); + if (msg.sequence == 0) + continue; + fail_on_test(msg.sequence != seq); + fail_on_test(msg.len != 2); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.msg[0] != ((la << 4) | remote_la)); + fail_on_test(msg.msg[1] != CEC_MSG_GIVE_PHYSICAL_ADDR); + fail_on_test(msg.timeout != 1500 || msg.flags); + fail_on_test(msg.reply); + fail_on_test(!(msg.tx_status & CEC_TX_STATUS_OK)); + fail_on_test((msg.tx_status & tx_ok_retry_mask) == tx_ok_retry_mask); + fail_on_test(msg.tx_nack_cnt == 0xff); + fail_on_test(msg.tx_arb_lost_cnt == 0xff); + fail_on_test(msg.tx_low_drive_cnt == 0xff); + fail_on_test(msg.tx_error_cnt == 0xff); + fail_on_test(msg.rx_ts || msg.rx_status); + break; + } + + // Send non-blocking reply message to valid remote LA + memset(&msg, 0xff, sizeof(msg)); + msg.msg[0] = (la << 4) | remote_la; + msg.timeout = 0; + cec_msg_give_physical_addr(&msg, true); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + fail_on_test(msg.len != 2); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.msg[0] != ((la << 4) | remote_la)); + fail_on_test(msg.msg[1] != CEC_MSG_GIVE_PHYSICAL_ADDR); + fail_on_test(msg.timeout != 1000 || msg.flags); + fail_on_test(msg.sequence == 0 || msg.sequence == ~0U); + fail_on_test(msg.reply != CEC_MSG_REPORT_PHYSICAL_ADDR); + fail_on_test(msg.tx_status); + fail_on_test(msg.rx_ts || msg.rx_status); + fail_on_test(msg.tx_ts || msg.tx_nack_cnt || msg.tx_arb_lost_cnt || + msg.tx_low_drive_cnt || msg.tx_error_cnt); + seq = msg.sequence; + + sleep(1); + while (true) { + memset(&msg, 0xff, sizeof(msg)); + msg.timeout = 1500; + fail_on_test(doioctl(node, CEC_RECEIVE, &msg)); + if (msg.sequence == 0) + continue; + fail_on_test(msg.sequence != seq); + fail_on_test(msg.len != 5); + fail_on_test(check_0(msg.msg + msg.len, sizeof(msg.msg) - msg.len)); + fail_on_test(msg.msg[0] != ((remote_la << 4) | 0xf)); + fail_on_test(msg.msg[1] != CEC_MSG_REPORT_PHYSICAL_ADDR); + fail_on_test(msg.timeout != 1500 || msg.flags); + fail_on_test(msg.reply != CEC_MSG_REPORT_PHYSICAL_ADDR); + fail_on_test(!(msg.tx_status & CEC_TX_STATUS_OK)); + fail_on_test((msg.tx_status & tx_ok_retry_mask) == tx_ok_retry_mask); + fail_on_test(msg.tx_nack_cnt == 0xff); + fail_on_test(msg.tx_arb_lost_cnt == 0xff); + fail_on_test(msg.tx_low_drive_cnt == 0xff); + fail_on_test(msg.tx_error_cnt == 0xff); + fail_on_test(msg.rx_ts == 0 || msg.rx_ts == ~0ULL || msg.rx_status != CEC_RX_STATUS_OK); + fail_on_test(msg.rx_ts < msg.tx_ts); + break; + } + + __u32 mode = CEC_MODE_INITIATOR | CEC_MODE_FOLLOWER; + + fail_on_test(doioctl(node, CEC_S_MODE, &mode)); + } + if (remote_la == 15 || invalid_remote == 15) + return PRESUMED_OK; + return 0; +} + +int testModes(struct node *node, struct node *node2) +{ + struct cec_msg msg; + __u8 me = node->log_addr[0]; + __u8 remote = CEC_LOG_ADDR_INVALID; + __u32 mode = 0, m; + + for (unsigned i = 0; i < 15; i++) { + if (node->remote_la_mask & (1 << i)) { + remote = i; + break; + } + } + + cec_msg_init(&msg, 0xf, 0); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + fail_on_test(doioctl(node2, CEC_TRANSMIT, &msg)); + + fail_on_test(doioctl(node, CEC_G_MODE, &m)); + fail_on_test(m != CEC_MODE_INITIATOR); + fail_on_test(doioctl(node2, CEC_G_MODE, &m)); + fail_on_test(m != CEC_MODE_INITIATOR); + + fail_on_test(doioctl(node, CEC_S_MODE, &mode)); + fail_on_test(doioctl(node, CEC_G_MODE, &m)); + fail_on_test(m != mode); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg) != EBUSY); + fail_on_test(doioctl(node2, CEC_TRANSMIT, &msg)); + + mode = CEC_MODE_INITIATOR; + fail_on_test(doioctl(node, CEC_S_MODE, &mode)); + fail_on_test(doioctl(node, CEC_G_MODE, &m)); + fail_on_test(m != mode); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + + mode = CEC_MODE_EXCL_INITIATOR; + fail_on_test(doioctl(node, CEC_S_MODE, &mode)); + fail_on_test(doioctl(node, CEC_G_MODE, &m)); + fail_on_test(m != mode); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + fail_on_test(doioctl(node2, CEC_TRANSMIT, &msg) != EBUSY); + + mode = CEC_MODE_INITIATOR; + fail_on_test(doioctl(node, CEC_S_MODE, &mode)); + fail_on_test(doioctl(node, CEC_G_MODE, &m)); + fail_on_test(m != mode); + + if (remote == CEC_LOG_ADDR_INVALID) + return 0; + + fail_on_test(flush_pending_msgs(node)); + cec_msg_init(&msg, me, remote); + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + msg.timeout = 1200; + fail_on_test(doioctl(node, CEC_RECEIVE, &msg) != ETIMEDOUT); + + mode = CEC_MODE_INITIATOR | CEC_MODE_FOLLOWER; + fail_on_test(doioctl(node, CEC_S_MODE, &mode)); + fail_on_test(doioctl(node, CEC_G_MODE, &m)); + fail_on_test(m != mode); + + fail_on_test(flush_pending_msgs(node)); + cec_msg_init(&msg, me, remote); + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + msg.timeout = 1200; + fail_on_test(doioctl(node, CEC_RECEIVE, &msg)); + fail_on_test(msg.msg[1] != CEC_MSG_REPORT_PHYSICAL_ADDR); + + mode = CEC_MODE_INITIATOR | CEC_MODE_FOLLOWER; + fail_on_test(doioctl(node2, CEC_S_MODE, &mode)); + fail_on_test(doioctl(node2, CEC_G_MODE, &m)); + fail_on_test(m != mode); + + fail_on_test(flush_pending_msgs(node)); + fail_on_test(flush_pending_msgs(node2)); + cec_msg_init(&msg, me, remote); + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + msg.timeout = 1200; + fail_on_test(doioctl(node, CEC_RECEIVE, &msg)); + fail_on_test(msg.msg[1] != CEC_MSG_REPORT_PHYSICAL_ADDR); + msg.timeout = 1; + fail_on_test(doioctl(node2, CEC_RECEIVE, &msg)); + fail_on_test(msg.msg[1] != CEC_MSG_REPORT_PHYSICAL_ADDR); + + mode = CEC_MODE_INITIATOR | CEC_MODE_EXCL_FOLLOWER; + fail_on_test(doioctl(node, CEC_S_MODE, &mode)); + fail_on_test(doioctl(node, CEC_G_MODE, &m)); + fail_on_test(m != mode); + + fail_on_test(flush_pending_msgs(node)); + fail_on_test(flush_pending_msgs(node2)); + cec_msg_init(&msg, me, remote); + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + msg.timeout = 1200; + fail_on_test(doioctl(node, CEC_RECEIVE, &msg)); + fail_on_test(msg.msg[1] != CEC_MSG_REPORT_PHYSICAL_ADDR); + msg.timeout = 1; + fail_on_test(doioctl(node2, CEC_RECEIVE, &msg) != ETIMEDOUT); + + mode = CEC_MODE_INITIATOR | CEC_MODE_EXCL_FOLLOWER_PASSTHRU; + fail_on_test(doioctl(node, CEC_S_MODE, &mode)); + fail_on_test(doioctl(node, CEC_G_MODE, &m)); + fail_on_test(m != mode); + + // Note: this test is the same as for CEC_MODE_EXCL_FOLLOWER. + // Ideally the remote device would send us a passthrough message, + // but there is no way to trigger that. + fail_on_test(flush_pending_msgs(node)); + fail_on_test(flush_pending_msgs(node2)); + cec_msg_init(&msg, me, remote); + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + msg.timeout = 1200; + fail_on_test(doioctl(node, CEC_RECEIVE, &msg)); + fail_on_test(msg.msg[1] != CEC_MSG_REPORT_PHYSICAL_ADDR); + msg.timeout = 1; + fail_on_test(doioctl(node2, CEC_RECEIVE, &msg) != ETIMEDOUT); + + mode = CEC_MODE_INITIATOR; + fail_on_test(doioctl(node, CEC_S_MODE, &mode)); + fail_on_test(doioctl(node, CEC_G_MODE, &m)); + fail_on_test(m != mode); + + mode = CEC_MODE_INITIATOR | CEC_MODE_MONITOR; + fail_on_test(doioctl(node2, CEC_S_MODE, &mode) != EINVAL); + fail_on_test(doioctl(node2, CEC_G_MODE, &m)); + fail_on_test(m != (CEC_MODE_INITIATOR | CEC_MODE_FOLLOWER)); + + bool is_root = getuid() == 0; + + mode = CEC_MODE_MONITOR; + fail_on_test(doioctl(node2, CEC_S_MODE, &mode) != (is_root ? 0 : EPERM)); + + if (is_root) { + fail_on_test(doioctl(node2, CEC_G_MODE, &m)); + fail_on_test(m != mode); + fail_on_test(flush_pending_msgs(node2)); + cec_msg_init(&msg, me, remote); + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + msg.timeout = 1200; + fail_on_test(doioctl(node2, CEC_RECEIVE, &msg)); + fail_on_test(msg.msg[1] != CEC_MSG_GIVE_PHYSICAL_ADDR); + while (true) { + fail_on_test(doioctl(node2, CEC_RECEIVE, &msg)); + if (msg.msg[1] == CEC_MSG_REPORT_PHYSICAL_ADDR) + break; + fail_on_test(!cec_msg_is_broadcast(&msg) && + !(node2->adap_la_mask & (1 << cec_msg_destination(&msg)))); + } + } + + mode = CEC_MODE_MONITOR_ALL; + int res = doioctl(node2, CEC_S_MODE, &mode); + if (node2->caps & CEC_CAP_MONITOR_ALL) + fail_on_test(res != (is_root ? 0 : EPERM)); + else + fail_on_test(res != EINVAL); + + if (is_root) { + fail_on_test(doioctl(node2, CEC_G_MODE, &m)); + fail_on_test(m != mode); + fail_on_test(flush_pending_msgs(node2)); + cec_msg_init(&msg, me, remote); + cec_msg_give_physical_addr(&msg, false); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + msg.timeout = 1200; + fail_on_test(doioctl(node2, CEC_RECEIVE, &msg)); + fail_on_test(msg.msg[1] != CEC_MSG_GIVE_PHYSICAL_ADDR); + while (true) { + fail_on_test(doioctl(node2, CEC_RECEIVE, &msg)); + if (msg.msg[1] == CEC_MSG_REPORT_PHYSICAL_ADDR) + break; + } + } + return 0; +} + +int testLostMsgs(struct node *node) +{ + struct cec_msg msg; + struct cec_event ev; + __u8 me = node->log_addr[0]; + __u8 remote = CEC_LOG_ADDR_INVALID; + + for (unsigned i = 0; i < 15; i++) { + if (node->remote_la_mask & (1 << i)) { + remote = i; + break; + } + } + + fail_on_test(flush_pending_msgs(node)); + fcntl(node->fd, F_SETFL, fcntl(node->fd, F_GETFL) | O_NONBLOCK); + cec_msg_init(&msg, me, remote); + cec_msg_get_cec_version(&msg, true); + + // flush pending events + while (doioctl(node, CEC_DQEVENT, &ev) == 0) { } + + bool got_busy = false; + unsigned xfer_cnt = 0; + + do { + int res; + + do { + res = doioctl(node, CEC_TRANSMIT, &msg); + + fail_on_test(res && res != EBUSY); + if (res == EBUSY) { + struct timeval tv = { 0, 10000 }; // 10 ms + + select(0, NULL, NULL, NULL, &tv); + got_busy = true; + } else if (!got_busy) { + xfer_cnt++; + } + } while (res == EBUSY); + // Alternate between wait for reply and just transmit + msg.timeout = msg.timeout ? 0 : 1000; + msg.reply = CEC_MSG_CEC_VERSION; + } while (doioctl(node, CEC_DQEVENT, &ev)); + fail_on_test(ev.event != CEC_EVENT_LOST_MSGS); + fail_on_test(!got_busy); + + /* + * No more than max 18 transmits can be queued, but one message + * might be finished transmitting before the queue fills up, so + * check for 19 instead. + */ + fail_on_test(xfer_cnt == 0 || xfer_cnt > 19); + + unsigned pending_msgs = 0; + + fcntl(node->fd, F_SETFL, fcntl(node->fd, F_GETFL) & ~O_NONBLOCK); + msg.timeout = 1000; + + while (!doioctl(node, CEC_RECEIVE, &msg)) + pending_msgs++; + + /* Should be at least the size of the internal message queue */ + fail_on_test(pending_msgs < 18 * 3); + + return 0; +} diff --git a/utils/cec-compliance/cec-test-audio.cpp b/utils/cec-compliance/cec-test-audio.cpp new file mode 100644 index 00000000..42aa2e47 --- /dev/null +++ b/utils/cec-compliance/cec-test-audio.cpp @@ -0,0 +1,831 @@ +/* + * Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved. + * + * This program is free software; you may redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include <unistd.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <inttypes.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" + + +/* Dynamic Auto Lipsync */ + +static int dal_request_current_latency(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_request_current_latency(&msg, true, node->remote[la].phys_addr); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test_v2(node->remote[la].cec_version, + timed_out(&msg) && is_tv(la, node->remote[la].prim_type)); + if (timed_out(&msg)) + return NOTSUPPORTED; + + /* When the device supports Dynamic Auto Lipsync but does not implement + CEC 1.4b, or 2.0, a very strict subset of CEC can be supported. If we + get here and the version is < 1.4, we know that the device is not + complying to this specification. */ + if (node->remote[la].cec_version < CEC_OP_CEC_VERSION_1_4) { + warn("CEC 2.0 specifies that devices with CEC version < 1.4 which implement\n"); + warn("Dynamic Auto Lipsync shall only implement a very strict subset of CEC.\n"); + } + + __u16 phys_addr; + __u8 video_latency, low_latency_mode, audio_out_compensated, audio_out_delay; + + cec_ops_report_current_latency(&msg, &phys_addr, &video_latency, &low_latency_mode, + &audio_out_compensated, &audio_out_delay); + fail_on_test(phys_addr != node->remote[la].phys_addr); + info("Video latency: %d\n", video_latency); + info("Low latency mode: %d\n", low_latency_mode); + info("Audio output compensation: %d\n", audio_out_compensated); + info("Audio out delay: %d\n", audio_out_delay); + + return 0; +} + +static int dal_req_current_latency_invalid(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + /* Test that there is no reply when the physical address operand is not the + physical address of the remote device. */ + cec_msg_init(&msg, me, la); + cec_msg_request_current_latency(&msg, true, CEC_PHYS_ADDR_INVALID); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(!timed_out(&msg)); + + return 0; +} + +struct remote_subtest dal_subtests[] = { + { "Request Current Latency", CEC_LOG_ADDR_MASK_ALL, dal_request_current_latency }, + { "Request Current Latency with invalid PA", CEC_LOG_ADDR_MASK_ALL, dal_req_current_latency_invalid }, +}; + +const unsigned dal_subtests_size = ARRAY_SIZE(dal_subtests); + + +/* Audio Return Channel Control */ + +static __u16 pa_common_mask(__u16 pa1, __u16 pa2) +{ + __u16 mask = 0xf000; + + for (int i = 0; i < 3; i++) { + if ((pa1 & mask) != (pa2 & mask)) + break; + mask = (mask >> 4) | 0xf000; + } + return mask << 4; +} +static bool pa_are_adjacent(__u16 pa1, __u16 pa2) +{ + const __u16 mask = pa_common_mask(pa1, pa2); + const __u16 trail_mask = ((~mask) & 0xffff) >> 4; + + if (pa1 == CEC_PHYS_ADDR_INVALID || pa2 == CEC_PHYS_ADDR_INVALID || pa1 == pa2) + return false; + if ((pa1 & trail_mask) || (pa2 & trail_mask)) + return false; + if (!((pa1 & ~mask) && (pa2 & ~mask))) + return true; + return false; +} + +static bool pa_is_upstream_from(__u16 pa1, __u16 pa2) +{ + const __u16 mask = pa_common_mask(pa1, pa2); + + if (pa1 == CEC_PHYS_ADDR_INVALID || pa2 == CEC_PHYS_ADDR_INVALID) + return false; + if (!(pa1 & ~mask) && (pa2 & ~mask)) + return true; + return false; +} + +static bool util_receive(struct node *node, unsigned la, struct cec_msg *msg, + __u8 sent_msg, __u8 reply1, __u8 reply2 = 0) +{ + while (1) { + memset(msg, 0, sizeof(*msg)); + msg->timeout = 1; + if (doioctl(node, CEC_RECEIVE, msg)) + break; + if (cec_msg_initiator(msg) != la) + continue; + + if (msg->msg[1] == CEC_MSG_FEATURE_ABORT) { + __u8 reason, abort_msg; + + cec_ops_feature_abort(msg, &abort_msg, &reason); + if (abort_msg != sent_msg) + continue; + return true; + } + + if (msg->msg[1] == reply1 || (reply2 && msg->msg[1] == reply2)) + return true; + } + + return false; +} + +static int arc_initiate_tx(struct node *node, unsigned me, unsigned la, bool interactive) +{ + /* Check if we are upstream from the device. If we are, then the device is + an HDMI source, which means that it is an ARC receiver, not a transmitter. */ + if (pa_is_upstream_from(node->phys_addr, node->remote[la].phys_addr)) + return NOTAPPLICABLE; + + struct cec_msg msg = {}; + bool got_response = false; + + /* TODO: CEC framework should perhaps support more than one possible + reply opcode for a message. + + An ARC Tx device can reply to the Initiate ARC message with either + Report ARC Initiated or Report ARC Terminated, in addition to Feature + Abort. The CEC framework currently doesn't handle several possible + reply messages, so when setting it to expect a reply, only Report ARC + Initiated will be recognized as a reply, and it will time out when + receiving Report ARC Terminated. + + For now, this is handled manually by looping over the received messages + after sending Initiate ARC. */ + __u32 mode = CEC_MODE_INITIATOR | CEC_MODE_FOLLOWER; + + doioctl(node, CEC_S_MODE, &mode); + cec_msg_init(&msg, me, la); + cec_msg_initiate_arc(&msg, false); + fail_on_test(!transmit_timeout(node, &msg)); + got_response = util_receive(node, la, &msg, CEC_MSG_INITIATE_ARC, + CEC_MSG_REPORT_ARC_INITIATED, CEC_MSG_REPORT_ARC_TERMINATED); + mode = CEC_MODE_INITIATOR; + doioctl(node, CEC_S_MODE, &mode); + + if (!got_response || (got_response && unrecognized_op(&msg))) { + fail_on_test_v2(node->remote[la].cec_version, node->remote[la].has_arc_tx); + return NOTSUPPORTED; + } + if (cec_msg_opcode(&msg) == CEC_MSG_REPORT_ARC_INITIATED) { + fail_on_test(!pa_are_adjacent(node->phys_addr, node->remote[la].phys_addr)); + fail_on_test_v2(node->remote[la].cec_version, !node->remote[la].has_arc_tx); + node->remote[la].arc_initiated = true; + } + else if (cec_msg_opcode(&msg) == CEC_MSG_REPORT_ARC_TERMINATED) + announce("Device supports ARC but is not ready to initiate."); + else if (refused(&msg)) + return REFUSED; + else if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int arc_terminate_tx(struct node *node, unsigned me, unsigned la, bool interactive) +{ + /* Check if we are upstream from the device. If we are, then the device is + an HDMI source, which means that it is an ARC receiver, not a transmitter. */ + if (pa_is_upstream_from(node->phys_addr, node->remote[la].phys_addr)) + return NOTAPPLICABLE; + if (!node->remote[la].arc_initiated) + return NOTAPPLICABLE; + + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_terminate_arc(&msg, true); + fail_on_test(!transmit_timeout(node, &msg)); + if (timed_out(&msg)) { + warn("Timed out waiting for Report ARC Terminated.\n"); + return PRESUMED_OK; + } + fail_on_test(unrecognized_op(&msg)); + if (cec_msg_status_is_abort(&msg)) { + warn("Received Feature Abort for Terminate ARC (but the message was recognized)."); + if (refused(&msg)) + return REFUSED; + return PRESUMED_OK; + } + + return 0; +} + +static int arc_initiate_rx(struct node *node, unsigned me, unsigned la, bool interactive) +{ + /* Check if the DUT is upstream from us. If it is, then it is an + HDMI sink, which means that it is an ARC transmitter, not receiver. */ + if (pa_is_upstream_from(node->remote[la].phys_addr, node->phys_addr)) + return NOTAPPLICABLE; + + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_request_arc_initiation(&msg, true); + + bool unsupported = false; + + fail_on_test(!transmit_timeout(node, &msg)); + if (timed_out(&msg) || unrecognized_op(&msg)) + unsupported = true; + else if (cec_msg_status_is_abort(&msg)) { + __u8 abort_msg, reason; + + cec_ops_feature_abort(&msg, &abort_msg, &reason); + if (reason == CEC_OP_ABORT_INCORRECT_MODE) { + announce("The device supports ARC but is not ready to initiate."); + return 0; + } + else { + warn("Device responded Feature Abort with unexpected abort reason. Assuming no ARC support.\n"); + unsupported = true; + } + } + if (unsupported) { + fail_on_test_v2(node->remote[la].cec_version, node->remote[la].has_arc_rx); + return NOTSUPPORTED; + } + fail_on_test(!pa_are_adjacent(node->phys_addr, node->remote[la].phys_addr)); + fail_on_test_v2(node->remote[la].cec_version, !node->remote[la].has_arc_rx); + + cec_msg_init(&msg, me, la); + cec_msg_report_arc_initiated(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(unrecognized_op(&msg)); + node->remote[la].arc_initiated = true; + + return 0; +} + +static int arc_terminate_rx(struct node *node, unsigned me, unsigned la, bool interactive) +{ + /* Check if the DUT is upstream from us. If it is, then it is an + HDMI sink, which means that it is an ARC transmitter, not receiver. */ + if (pa_is_upstream_from(node->remote[la].phys_addr, node->phys_addr)) + return NOTAPPLICABLE; + if (!node->remote[la].arc_initiated) + return NOTAPPLICABLE; + + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_request_arc_termination(&msg, true); + fail_on_test(!transmit_timeout(node, &msg)); + if (timed_out(&msg)) { + warn("Timed out waiting for Terminate ARC.\n"); + return PRESUMED_OK; + } + fail_on_test(unrecognized_op(&msg)); + if (cec_msg_status_is_abort(&msg)) { + warn("Received Feature Abort for Request ARC Termination (but the message was recognized)."); + if (refused(&msg)) + return REFUSED; + return PRESUMED_OK; + } + + cec_msg_init(&msg, me, la); + cec_msg_report_arc_terminated(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(unrecognized_op(&msg)); + + return 0; +} + +struct remote_subtest arc_subtests[] = { + { "Initiate ARC (RX)", CEC_LOG_ADDR_MASK_ALL, arc_initiate_rx }, + { "Terminate ARC (RX)", CEC_LOG_ADDR_MASK_ALL, arc_terminate_rx }, + { "Initiate ARC (TX)", CEC_LOG_ADDR_MASK_ALL, arc_initiate_tx }, + { "Terminate ARC (TX)", CEC_LOG_ADDR_MASK_ALL, arc_terminate_tx }, +}; + +const unsigned arc_subtests_size = ARRAY_SIZE(arc_subtests); + + +/* System Audio Control */ + +static int sac_request_sad_probe(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + __u8 audio_format_id = 0; + __u8 audio_format_code = 1; + + cec_msg_init(&msg, me, la); + cec_msg_request_short_audio_descriptor(&msg, true, 1, &audio_format_id, &audio_format_code); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + node->remote[la].has_sad = true; + + return 0; +} + +static int sac_request_sad_invalid(struct node *node, unsigned me, unsigned la, bool interactive) +{ + if (!node->remote[la].has_sad) + return NOTAPPLICABLE; + + struct cec_msg msg = {}; + __u8 audio_format_id = CEC_OP_AUD_FMT_ID_CEA861; + __u8 audio_format_code = 63; // This is outside the range of CEA861-F + + cec_msg_init(&msg, me, la); + cec_msg_request_short_audio_descriptor(&msg, true, 1, &audio_format_id, &audio_format_code); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test(!cec_msg_status_is_abort(&msg)); + if (abort_reason(&msg) != CEC_OP_ABORT_INVALID_OP) { + warn("Expected Feature Abort [Invalid operand] in reply to Request Short\n"); + warn("Audio Descriptor with invalid audio format as operand.\n"); + } + + return 0; +} + +static int sac_sad_format_check(struct node *node, unsigned me, unsigned la, bool interactive) +{ + if (!node->remote[la].has_sad) + return NOTAPPLICABLE; + + struct cec_msg msg = {}; + __u8 audio_format_id; + __u8 audio_format_code; + + for (unsigned int id = 0; id <= 1; id++) { + audio_format_id = id; + for (unsigned int fmt_code = 1; fmt_code <= 14; fmt_code++) { + audio_format_code = fmt_code; + + cec_msg_init(&msg, me, la); + cec_msg_request_short_audio_descriptor(&msg, true, 1, &audio_format_id, &audio_format_code); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test(unrecognized_op(&msg) || refused(&msg)); + + if (cec_msg_status_is_abort(&msg) && + abort_reason(&msg) == CEC_OP_ABORT_INVALID_OP) + continue; + + __u8 num_descriptors; + __u32 descriptors[4] = {}; + + cec_ops_report_short_audio_descriptor(&msg, &num_descriptors, descriptors); + fail_on_test(num_descriptors == 0); + if (id == 1 && node->remote[la].cec_version < CEC_OP_CEC_VERSION_2_0) + warn("The device has CEC version < 2.0 but reports audio format(s) introduced in CEC 2.0.\n"); + + for (int j = 0; j < num_descriptors; j++) { + struct short_audio_desc sad; + + sad_decode(&sad, descriptors[j]); + if ((id == 0 && sad.format_code != fmt_code) || + (id == 1 && sad.extension_type_code != fmt_code)) + return fail("Different audio format code reported than requested.\n"); + info("Supports format %s\n", short_audio_desc2s(sad).c_str()); + + /* We need to store the ID and Code for one of the audio formats found, + for use in later test(s) */ + node->remote[la].supp_format_id = audio_format_id; + node->remote[la].supp_format_code = audio_format_code; + } + } + } + + return 0; +} + +static int sac_sad_req_multiple(struct node *node, unsigned me, unsigned la, bool interactive) +{ + if (!node->remote[la].has_sad || node->remote[la].supp_format_code == 0) + return NOTAPPLICABLE; + + /* Check that if we got a response to a Request Short Audio Descriptor + with a single format, we also get a response when the same audio format + occurs in a request together with other formats. */ + struct cec_msg msg = {}; + __u8 audio_format_id[4]; + __u8 audio_format_code[4]; + + for (int i = 0; i < 4; i++) { + if (node->remote[la].supp_format_code <= 12) + audio_format_code[i] = node->remote[la].supp_format_code - 1 + i; + else + audio_format_code[i] = node->remote[la].supp_format_code - 1 - i; + } + cec_msg_init(&msg, me, la); + cec_msg_request_short_audio_descriptor(&msg, true, 4, audio_format_id, audio_format_code); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test(cec_msg_status_is_abort(&msg)); + + return 0; +} + +static int sac_set_system_audio_mode_direct(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_set_system_audio_mode(&msg, CEC_OP_SYS_AUD_STATUS_ON); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test_v2(node->remote[la].cec_version, + unrecognized_op(&msg) && is_tv(la, node->remote[la].prim_type)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + node->remote[la].has_set_sys_audio_mode = true; + + return PRESUMED_OK; +} + +static int sac_set_system_audio_mode_broadcast_on(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, CEC_LOG_ADDR_BROADCAST); + cec_msg_set_system_audio_mode(&msg, CEC_OP_SYS_AUD_STATUS_ON); + fail_on_test(!transmit_timeout(node, &msg)); + + return PRESUMED_OK; +} + +static int sac_system_audio_mode_status(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + /* The device shall not feature abort System Audio Status if it did not + feature abort Set System Audio Mode. + + The message is mandatory for TVs in CEC 2.0. */ + cec_msg_init(&msg, me, la); + cec_msg_system_audio_mode_status(&msg, CEC_OP_SYS_AUD_STATUS_ON); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test_v2(node->remote[la].cec_version, + is_tv(la, node->remote[la].prim_type) && unrecognized_op(&msg)); + if (unrecognized_op(&msg) && !node->remote[la].has_set_sys_audio_mode) + return NOTSUPPORTED; + fail_on_test(unrecognized_op(&msg)); + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int sac_set_system_audio_mode_broadcast_off(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, CEC_LOG_ADDR_BROADCAST); + cec_msg_set_system_audio_mode(&msg, CEC_OP_SYS_AUD_STATUS_OFF); + fail_on_test(!transmit_timeout(node, &msg)); + + return PRESUMED_OK; +} + +static int sac_system_audio_mode_req_on(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + __u8 status; + + /* Send a System Audio Mode Request to the audio system. This notifies the + audio system that our device has SAC capabilities, so it should enable + the feature right away by sending Set System Audio Mode with On as status. + + The message is mandatory for audio systems in CEC 2.0. */ + cec_msg_init(&msg, me, la); + cec_msg_system_audio_mode_request(&msg, true, node->phys_addr); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test_v2(node->remote[la].cec_version, + cec_has_audiosystem(1 << la) && unrecognized_op(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + node->remote[la].has_sys_audio_mode_req = true; + cec_ops_set_system_audio_mode(&msg, &status); + fail_on_test(status != CEC_OP_SYS_AUD_STATUS_ON); + + return 0; +} + +static int sac_give_system_audio_mode_status(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + __u8 system_audio_status; + + /* The device shall not feature abort Give System Audio Mode Status if it did not + feature abort System Audio Mode Request. + + The message is mandatory for audio systems in CEC 2.0. */ + cec_msg_init(&msg, me, la); + cec_msg_give_system_audio_mode_status(&msg, true); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test_v2(node->remote[la].cec_version, + cec_has_audiosystem(1 << la) && unrecognized_op(&msg)); + if (unrecognized_op(&msg) && !node->remote[la].has_sys_audio_mode_req) + return NOTSUPPORTED; + fail_on_test(unrecognized_op(&msg)); + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + cec_ops_system_audio_mode_status(&msg, &system_audio_status); + fail_on_test(system_audio_status != CEC_OP_SYS_AUD_STATUS_ON); + + return 0; +} + +static int sac_give_audio_status(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + /* Give Audio Status is mandatory for audio systems in CEC 2.0, except + for systems that lack external controls for volume/mute status. */ + cec_msg_init(&msg, me, la); + cec_msg_give_audio_status(&msg, true); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test_v2(node->remote[la].cec_version, + cec_has_audiosystem(1 << la) && unrecognized_op(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + cec_ops_report_audio_status(&msg, &node->remote[la].mute, &node->remote[la].volume); + fail_on_test(node->remote[la].volume > 100); + info("Volume: %d %s\n", node->remote[la].volume, node->remote[la].mute ? "(muted)" : ""); + + return 0; +} + +static int sac_util_send_user_control_press(struct node *node, unsigned me, unsigned la, __u8 ui_cmd) +{ + struct cec_msg msg = {}; + struct cec_op_ui_command rc_press = {}; + + /* The device shall not feature abort + - User Control Pressed ["Volume Up"] + - User Control Pressed ["Volume Down"] + - User Control Pressed ["Mute"] + if it did not feature abort System Audio Mode Request. + + The messages are mandatory for audio systems and TVs in CEC 2.0, + and it is mandatory for audio systems to send Report Audio Status + back to the TV in CEC 2.0. + + It is recommended for devices to not send Report Audio Status back + more often than once every 500ms. We therefore sleep a second before + each User Control Pressed is sent. */ + __u32 mode = CEC_MODE_INITIATOR | CEC_MODE_FOLLOWER; + bool got_response; + + sleep(1); + doioctl(node, CEC_S_MODE, &mode); + cec_msg_init(&msg, me, la); + rc_press.ui_cmd = ui_cmd; + cec_msg_user_control_pressed(&msg, &rc_press); + fail_on_test(!transmit(node, &msg)); + cec_msg_init(&msg, me, la); + cec_msg_user_control_released(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + got_response = util_receive(node, la, &msg, CEC_MSG_USER_CONTROL_PRESSED, CEC_MSG_REPORT_AUDIO_STATUS); + mode = CEC_MODE_INITIATOR; + doioctl(node, CEC_S_MODE, &mode); + + fail_on_test_v2(node->remote[la].cec_version, !got_response && + cec_has_audiosystem(1 << la)); + fail_on_test_v2(node->remote[la].cec_version, unrecognized_op(&msg) && + (is_tv(la, node->remote[la].prim_type) || cec_has_audiosystem(1 << la))); + if (unrecognized_op(&msg) && !node->remote[la].has_sys_audio_mode_req) + return NOTSUPPORTED; + fail_on_test(unrecognized_op(&msg)); + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + if (got_response) { + cec_ops_report_audio_status(&msg, &node->remote[la].mute, &node->remote[la].volume); + return 0; + } + + return PRESUMED_OK; +} + +static int sac_user_control_press_vol_up(struct node *node, unsigned me, unsigned la, bool interactive) +{ + __u8 ret, old_volume = node->remote[la].volume; + + if ((ret = sac_util_send_user_control_press(node, me, la, 0x41))) + return ret; + /* Check that if not already at the highest, the volume was increased. */ + fail_on_test_v2(node->remote[la].cec_version, + la == CEC_LOG_ADDR_AUDIOSYSTEM && + old_volume < 100 && node->remote[la].volume <= old_volume); + + return 0; +} + +static int sac_user_control_press_vol_down(struct node *node, unsigned me, unsigned la, bool interactive) +{ + __u8 ret, old_volume = node->remote[la].volume; + + if ((ret = sac_util_send_user_control_press(node, me, la, 0x42))) + return ret; + /* Check that if not already at the lowest, the volume was lowered. */ + fail_on_test_v2(node->remote[la].cec_version, + la == CEC_LOG_ADDR_AUDIOSYSTEM && + old_volume > 0 && node->remote[la].volume >= old_volume); + + return 0; +} + +static int sac_user_control_press_mute(struct node *node, unsigned me, unsigned la, bool interactive) +{ + __u8 ret, old_mute = node->remote[la].mute; + + if ((ret = sac_util_send_user_control_press(node, me, la, 0x43))) + return ret; + /* Check that mute has been toggled from what it was before. */ + fail_on_test_v2(node->remote[la].cec_version, + la == CEC_LOG_ADDR_AUDIOSYSTEM && + node->remote[la].mute == old_mute); + + return 0; +} + +static int sac_user_control_release(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + /* The device shall not feature abort User Control Released if it did not + feature abort System Audio Mode Request + + The message is mandatory for audio systems and TVs in CEC 2.0. */ + cec_msg_init(&msg, me, la); + cec_msg_user_control_released(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test_v2(node->remote[la].cec_version, unrecognized_op(&msg) && + (is_tv(la, node->remote[la].prim_type) || cec_has_audiosystem(1 << la))); + if (unrecognized_op(&msg) && !node->remote[la].has_sys_audio_mode_req) + return NOTSUPPORTED; + fail_on_test(unrecognized_op(&msg)); + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return PRESUMED_OK; +} + +static int sac_system_audio_mode_req_off(struct node *node, unsigned me, unsigned la, bool interactive) +{ + if (!node->remote[la].has_sys_audio_mode_req) + return NOTAPPLICABLE; + + struct cec_msg msg = {}; + __u8 status; + + cec_msg_init(&msg, me, la); + cec_msg_system_audio_mode_request(&msg, true, CEC_PHYS_ADDR_INVALID); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test_v2(node->remote[la].cec_version, + cec_has_audiosystem(1 << la) && unrecognized_op(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + cec_ops_set_system_audio_mode(&msg, &status); + fail_on_test(status != CEC_OP_SYS_AUD_STATUS_OFF); + + return 0; +} + +struct remote_subtest sac_subtests[] = { + { "Request Short Audio Descriptor", + CEC_LOG_ADDR_MASK_AUDIOSYSTEM, + sac_request_sad_probe }, + { "Request Short Audio Descriptor, invalid", + CEC_LOG_ADDR_MASK_AUDIOSYSTEM, + sac_request_sad_invalid }, + { "Report Short Audio Descriptor consistency", + CEC_LOG_ADDR_MASK_AUDIOSYSTEM, + sac_sad_format_check }, + { "Report Short Audio Descriptor, multiple requests in one", + CEC_LOG_ADDR_MASK_AUDIOSYSTEM, + sac_sad_req_multiple }, + { "Set System Audio Mode (directly addressed)", + CEC_LOG_ADDR_MASK_TV, + sac_set_system_audio_mode_direct }, + { "Set System Audio Mode (broadcast on)", + CEC_LOG_ADDR_MASK_TV, + sac_set_system_audio_mode_broadcast_on }, + { "System Audio Mode Status", + CEC_LOG_ADDR_MASK_TV, + sac_system_audio_mode_status }, + { "System Audio Mode Request (on)", + CEC_LOG_ADDR_MASK_AUDIOSYSTEM, + sac_system_audio_mode_req_on }, + { "Give System Audio Mode Status", + CEC_LOG_ADDR_MASK_AUDIOSYSTEM, + sac_give_system_audio_mode_status }, + { "Give Audio Status", + CEC_LOG_ADDR_MASK_AUDIOSYSTEM, + sac_give_audio_status }, + { "User Control Pressed (Volume Up)", + CEC_LOG_ADDR_MASK_AUDIOSYSTEM | CEC_LOG_ADDR_MASK_TV, + sac_user_control_press_vol_up }, + { "User Control Pressed (Volume Down)", + CEC_LOG_ADDR_MASK_AUDIOSYSTEM | CEC_LOG_ADDR_MASK_TV, + sac_user_control_press_vol_down }, + { "User Control Pressed (Mute)", + CEC_LOG_ADDR_MASK_AUDIOSYSTEM | CEC_LOG_ADDR_MASK_TV, + sac_user_control_press_mute }, + { "User Control Released", + CEC_LOG_ADDR_MASK_AUDIOSYSTEM | CEC_LOG_ADDR_MASK_TV, + sac_user_control_release }, + { "Set System Audio Mode (broadcast off)", + CEC_LOG_ADDR_MASK_TV, + sac_set_system_audio_mode_broadcast_off }, + { "System Audio Mode Request (off)", + CEC_LOG_ADDR_MASK_AUDIOSYSTEM, + sac_system_audio_mode_req_off }, +}; + +const unsigned sac_subtests_size = ARRAY_SIZE(sac_subtests); + + +/* Audio Rate Control */ + +/* + TODO: These are very rudimentary tests which should be expanded. + */ + +static int audio_rate_ctl_set_audio_rate(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_set_audio_rate(&msg, CEC_OP_AUD_RATE_WIDE_STD); + fail_on_test(!transmit_timeout(node, &msg)); + /* CEC 2.0: Devices shall use the device feature bit to indicate support. */ + fail_on_test_v2(node->remote[la].cec_version, + node->remote[la].has_aud_rate && unrecognized_op(&msg)); + fail_on_test_v2(node->remote[la].cec_version, + !node->remote[la].has_aud_rate && !unrecognized_op(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + + return PRESUMED_OK; +} + +struct remote_subtest audio_rate_ctl_subtests[] = { + { "Set Audio Rate", + CEC_LOG_ADDR_MASK_PLAYBACK | CEC_LOG_ADDR_MASK_RECORD | CEC_LOG_ADDR_MASK_TUNER, + audio_rate_ctl_set_audio_rate }, +}; + +const unsigned audio_rate_ctl_subtests_size = ARRAY_SIZE(audio_rate_ctl_subtests); diff --git a/utils/cec-compliance/cec-test-power.cpp b/utils/cec-compliance/cec-test-power.cpp new file mode 100644 index 00000000..e88e3387 --- /dev/null +++ b/utils/cec-compliance/cec-test-power.cpp @@ -0,0 +1,441 @@ +/* + * Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved. + * + * This program is free software; you may redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include <unistd.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <inttypes.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" + + +static bool get_power_status(struct node *node, unsigned me, unsigned la, __u8 &power_status) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_give_device_power_status(&msg, true); + if (!transmit_timeout(node, &msg) || timed_out_or_abort(&msg)) + return false; + cec_ops_report_power_status(&msg, &power_status); + return true; +} + +bool util_interactive_ensure_power_state(struct node *node, unsigned me, unsigned la, bool interactive, + __u8 target_pwr) +{ + interactive_info(true, "Please ensure that the device is in state %s.", + power_status2s(target_pwr)); + + if (!node->remote[la].has_power_status) + return true; + + while (interactive) { + __u8 pwr; + + if (!get_power_status(node, me, la, pwr)) + announce("Failed to retrieve power status."); + else if (pwr == target_pwr) + return true; + else + announce("The device reported power status %s.", power_status2s(pwr)); + fail_on_test(!question("Retry?")); + } + + return true; +} + + +/* Give Device Power Status */ + +static int power_status_give(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = { }; + + cec_msg_init(&msg, me, la); + cec_msg_give_device_power_status(&msg, true); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test(unrecognized_op(&msg)); + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + __u8 power_status; + cec_ops_report_power_status(&msg, &power_status); + fail_on_test(power_status >= 4); + + return 0; +} + +static int power_status_report(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_report_power_status(&msg, CEC_OP_POWER_STATUS_ON); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + + return PRESUMED_OK; +} + +struct remote_subtest power_status_subtests[] = { + { "Give Device Power Status", CEC_LOG_ADDR_MASK_ALL, power_status_give }, + { "Report Device Power Status", CEC_LOG_ADDR_MASK_ALL, power_status_report }, +}; + +const unsigned power_status_subtests_size = ARRAY_SIZE(power_status_subtests); + + +/* One Touch Play */ + +static int one_touch_play_image_view_on(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + fail_on_test(!util_interactive_ensure_power_state(node, me, la, interactive, CEC_OP_POWER_STATUS_STANDBY)); + + cec_msg_init(&msg, me, la); + cec_msg_image_view_on(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(is_tv(la, node->remote[la].prim_type) && unrecognized_op(&msg)); + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + fail_on_test(interactive && !question("Did the TV turn on?")); + node->remote[la].has_image_view_on = true; + + if (interactive) + return 0; + else + return PRESUMED_OK; +} + +static int one_touch_play_text_view_on(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + fail_on_test(!util_interactive_ensure_power_state(node, me, la, interactive, CEC_OP_POWER_STATUS_STANDBY)); + + cec_msg_init(&msg, me, la); + cec_msg_text_view_on(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(is_tv(la, node->remote[la].prim_type) && unrecognized_op(&msg)); + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + fail_on_test(interactive && !question("Did the TV turn on?")); + node->remote[la].has_text_view_on = true; + + if (interactive) + return 0; + else + return PRESUMED_OK; +} + +static int one_touch_play_active_source(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + while (interactive) { + __u8 pwr; + + interactive_info(true, "Please switch the TV to another source."); + fail_on_test(!get_power_status(node, me, la, pwr)); + if (pwr == CEC_OP_POWER_STATUS_ON) + break; + announce("The device reported power status %s.", power_status2s(pwr)); + fail_on_test(!question("Retry?")); + } + + cec_msg_init(&msg, me, la); + cec_msg_active_source(&msg, node->phys_addr); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(interactive && !question("Did the TV switch to this source?")); + + if (interactive) + return 0; + else + return PRESUMED_OK; +} + +static int one_touch_play_req_active_source(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + /* We have now said that we are active source, so receiving a reply to + Request Active Source should fail the test. */ + cec_msg_init(&msg, me, la); + cec_msg_request_active_source(&msg, true); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(!timed_out(&msg)); + + return 0; +} + +struct remote_subtest one_touch_play_subtests[] = { + { "Image View On", CEC_LOG_ADDR_MASK_TV, one_touch_play_image_view_on }, + { "Text View On", CEC_LOG_ADDR_MASK_TV, one_touch_play_text_view_on }, + { "Active Source", CEC_LOG_ADDR_MASK_TV | CEC_LOG_ADDR_MASK_UNREGISTERED, one_touch_play_active_source }, + { "Request Active Source", (__u16)~CEC_LOG_ADDR_MASK_TV, one_touch_play_req_active_source }, +}; + +const unsigned one_touch_play_subtests_size = ARRAY_SIZE(one_touch_play_subtests); + + +/* Standby / Resume */ + +/* The default sleep time between power status requests. */ +#define SLEEP_POLL_POWER_STATUS 2 + +static bool wait_changing_power_status(struct node *node, unsigned me, unsigned la, __u8 &new_status, + unsigned &unresponsive_time) +{ + __u8 old_status; + + announce("Checking for power status change. This may take up to 60 s."); + if (!get_power_status(node, me, la, old_status)) + return false; + for (unsigned i = 0; i < 60 / SLEEP_POLL_POWER_STATUS; i++) { + __u8 power_status; + + if (!get_power_status(node, me, la, power_status)) { + /* Some TVs become completely unresponsive when transitioning + between power modes. Register that this happens, but continue + the test. */ + unresponsive_time = i * SLEEP_POLL_POWER_STATUS; + } else if (old_status != power_status) { + new_status = power_status; + return true; + } + sleep(SLEEP_POLL_POWER_STATUS); + } + new_status = old_status; + return false; +} + +static bool poll_stable_power_status(struct node *node, unsigned me, unsigned la, + __u8 expected_status, unsigned &unresponsive_time) +{ + bool transient = false; + unsigned time_to_transient = 0; + + /* Some devices can use several seconds to transition from one power + state to another, so the power state must be repeatedly polled */ + announce("Waiting for new stable power status. This may take up to 60 s."); + for (unsigned tries = 0; tries < 60 / SLEEP_POLL_POWER_STATUS; tries++) { + __u8 power_status; + + if (!get_power_status(node, me, la, power_status)) { + /* Some TVs become completely unresponsive when transitioning + between power modes. Register that this happens, but continue + the test. */ + unresponsive_time = tries * SLEEP_POLL_POWER_STATUS; + } + if (!transient && (power_status == CEC_OP_POWER_STATUS_TO_ON || + power_status == CEC_OP_POWER_STATUS_TO_STANDBY)) { + time_to_transient = tries * SLEEP_POLL_POWER_STATUS; + transient = true; + } + if (power_status == expected_status) { + announce("Transient state after %d s, stable state %s after %d s", + time_to_transient, power_status2s(power_status), tries * SLEEP_POLL_POWER_STATUS); + return true; + } + sleep(SLEEP_POLL_POWER_STATUS); + } + return false; +} + +static int standby_resume_standby(struct node *node, unsigned me, unsigned la, bool interactive) +{ + if (!node->remote[la].has_power_status) + return NOTAPPLICABLE; + + struct cec_msg msg = {}; + unsigned unresponsive_time = 0; + + fail_on_test(!util_interactive_ensure_power_state(node, me, la, interactive, CEC_OP_POWER_STATUS_ON)); + + announce("Sending Standby message."); + cec_msg_init(&msg, me, la); + cec_msg_standby(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(cec_msg_status_is_abort(&msg)); + fail_on_test(!poll_stable_power_status(node, me, la, CEC_OP_POWER_STATUS_STANDBY, unresponsive_time)); + fail_on_test(interactive && !question("Is the device in standby?")); + node->remote[la].in_standby = true; + + if (unresponsive_time > 0) + return fail("The device went correctly into standby, but became unresponsive for %d s during the transition.\n", + unresponsive_time); + + return 0; +} + +static int standby_resume_standby_toggle(struct node *node, unsigned me, unsigned la, bool interactive) +{ + if (!node->remote[la].in_standby) + return NOTAPPLICABLE; + + struct cec_msg msg = {}; + unsigned unresponsive_time = 0; + __u8 new_status; + + node->remote[la].in_standby = false; + + /* Send Standby again to test that it is not acting like a toggle */ + announce("Sending Standby message."); + cec_msg_init(&msg, me, la); + cec_msg_standby(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(cec_msg_status_is_abort(&msg)); + fail_on_test(wait_changing_power_status(node, me, la, new_status, unresponsive_time)); + fail_on_test(new_status != CEC_OP_POWER_STATUS_STANDBY); + fail_on_test(interactive && !question("Is the device still in standby?")); + node->remote[la].in_standby = true; + if (unresponsive_time > 0) + return fail("The device went correctly into standby, but became unresponsive for %d s during the transition.\n", + unresponsive_time); + + return 0; +} + +static int standby_resume_active_source_nowake(struct node *node, unsigned me, unsigned la, bool interactive) +{ + if (!node->remote[la].in_standby) + return NOTAPPLICABLE; + + struct cec_msg msg = {}; + unsigned unresponsive_time = 0; + __u8 new_status; + + node->remote[la].in_standby = false; + + /* In CEC 2.0 it is specified that a device shall not go out of standby + if an Active Source message is received. */ + announce("Sending Active Source message."); + cec_msg_init(&msg, me, la); + cec_msg_active_source(&msg, node->phys_addr); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(wait_changing_power_status(node, me, la, new_status, unresponsive_time)); + fail_on_test_v2_warn(node->remote[la].cec_version, new_status != CEC_OP_POWER_STATUS_STANDBY); + node->remote[la].in_standby = true; + if (unresponsive_time > 0) + return fail("The device stayed correctly in standby, but became unresponsive for %d s.\n", + unresponsive_time); + + return 0; +} + +static int wakeup_rc(struct node *node, unsigned me, unsigned la) +{ + struct cec_msg msg = {}; + struct cec_op_ui_command rc_press = {}; + + /* Todo: A release should be sent after this */ + cec_msg_init(&msg, me, la); + rc_press.ui_cmd = 0x6D; /* Power On Function */ + cec_msg_user_control_pressed(&msg, &rc_press); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(cec_msg_status_is_abort(&msg)); + + return 0; +} + +static int wakeup_tv(struct node *node, unsigned me, unsigned la) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_image_view_on(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + if (!cec_msg_status_is_abort(&msg)) + return 0; + + cec_msg_init(&msg, me, la); + cec_msg_text_view_on(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + if (!cec_msg_status_is_abort(&msg)) + return 0; + + return wakeup_rc(node, me, la); +} + +static int wakeup_source(struct node *node, unsigned me, unsigned la) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_set_stream_path(&msg, node->remote[la].phys_addr); + fail_on_test(!transmit_timeout(node, &msg)); + if (!cec_msg_status_is_abort(&msg)) + return 0; + + return wakeup_rc(node, me, la); +} + +static int standby_resume_wakeup(struct node *node, unsigned me, unsigned la, bool interactive) +{ + if (!node->remote[la].in_standby) + return NOTAPPLICABLE; + + int ret; + + if (is_tv(la, node->remote[la].prim_type)) + ret = wakeup_tv(node, me, la); + else + ret = wakeup_source(node, me, la); + if (ret) + return ret; + + unsigned unresponsive_time = 0; + + announce("Device is woken up"); + fail_on_test(!poll_stable_power_status(node, me, la, CEC_OP_POWER_STATUS_ON, unresponsive_time)); + fail_on_test(interactive && !question("Is the device in On state?")); + + if (unresponsive_time > 0) + return fail("The device went correctly out of standby, but became unresponsive for %d s during the transition.", + unresponsive_time); + + return 0; +} + +struct remote_subtest standby_resume_subtests[] = { + { "Standby", CEC_LOG_ADDR_MASK_ALL, standby_resume_standby }, + { "Repeated Standby message does not wake up", CEC_LOG_ADDR_MASK_ALL, standby_resume_standby_toggle }, + { "No wakeup on Active Source", CEC_LOG_ADDR_MASK_ALL, standby_resume_active_source_nowake }, + { "Wake up", CEC_LOG_ADDR_MASK_ALL, standby_resume_wakeup }, +}; + +const unsigned standby_resume_subtests_size = ARRAY_SIZE(standby_resume_subtests); diff --git a/utils/cec-compliance/cec-test.cpp b/utils/cec-compliance/cec-test.cpp new file mode 100644 index 00000000..5fac04a9 --- /dev/null +++ b/utils/cec-compliance/cec-test.cpp @@ -0,0 +1,1493 @@ +/* + * Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved. + * + * This program is free software; you may redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include <unistd.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <inttypes.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 <sstream> + +#include "cec-compliance.h" + +/* + * cec-funcs.h doesn't have the reply argument for these CEC messages. + * A fix is pending but not yet merged. Once it is merged this workaround + * can be removed. + */ +static inline void cec_msg_record_on_reply(struct cec_msg *msg, + bool reply, + const struct cec_op_record_src *rec_src) +{ + switch (rec_src->type) { + case CEC_OP_RECORD_SRC_OWN: + cec_msg_record_on_own(msg); + break; + case CEC_OP_RECORD_SRC_DIGITAL: + cec_msg_record_on_digital(msg, &rec_src->digital); + break; + case CEC_OP_RECORD_SRC_ANALOG: + cec_msg_record_on_analog(msg, + rec_src->analog.ana_bcast_type, + rec_src->analog.ana_freq, + rec_src->analog.bcast_system); + break; + case CEC_OP_RECORD_SRC_EXT_PLUG: + cec_msg_record_on_plug(msg, rec_src->ext_plug.plug); + break; + case CEC_OP_RECORD_SRC_EXT_PHYS_ADDR: + cec_msg_record_on_phys_addr(msg, + rec_src->ext_phys_addr.phys_addr); + break; + } + msg->reply = reply ? CEC_MSG_RECORD_STATUS : 0; +} +#define cec_msg_record_on cec_msg_record_on_reply + +static inline void cec_msg_record_off_reply(struct cec_msg *msg, bool reply) +{ + msg->len = 2; + msg->msg[1] = CEC_MSG_RECORD_OFF; + msg->reply = reply ? CEC_MSG_RECORD_STATUS : 0; +} +#define cec_msg_record_off cec_msg_record_off_reply + +#define test_case(name, tags, subtests) {name, tags, subtests, ARRAY_SIZE(subtests)} +#define test_case_ext(name, tags, subtests) {name, tags, subtests, subtests##_size} + +struct remote_test { + const char *name; + const unsigned tags; + struct remote_subtest *subtests; + unsigned num_subtests; +}; + + +/* System Information */ + +static int system_info_polling(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = { }; + + cec_msg_init(&msg, 0xf, la); + fail_on_test(doioctl(node, CEC_TRANSMIT, &msg)); + if (node->remote_la_mask & (1 << la)) { + if (!cec_msg_status_is_ok(&msg)) + return FAIL_CRITICAL; + } else { + if (cec_msg_status_is_ok(&msg)) + return FAIL_CRITICAL; + return NOTSUPPORTED; + } + + return 0; +} + +static int system_info_phys_addr(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = { }; + + cec_msg_init(&msg, me, la); + cec_msg_give_physical_addr(&msg, true); + if (!transmit_timeout(node, &msg) || timed_out_or_abort(&msg)) + return FAIL_CRITICAL; + + return 0; +} + +static int system_info_version(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_get_cec_version(&msg, true); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + + /* This needs to be kept in sync with newer CEC versions */ + fail_on_test(msg.msg[2] < CEC_OP_CEC_VERSION_1_3A || + msg.msg[2] > CEC_OP_CEC_VERSION_2_0); + + return 0; +} + +static int system_info_get_menu_lang(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + char language[4]; + + cec_msg_init(&msg, me, la); + cec_msg_get_menu_language(&msg, true); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + + /* Devices other than TVs shall send Feature Abort [Unregcognized Opcode] + in reply to Get Menu Language. */ + fail_on_test(!is_tv(la, node->remote[la].prim_type) && !unrecognized_op(&msg)); + + if (unrecognized_op(&msg)) { + if (is_tv(la, node->remote[la].prim_type)) + warn("TV did not respond to Get Menu Language.\n"); + return NOTSUPPORTED; + } + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + cec_ops_set_menu_language(&msg, language); + language[3] = 0; + + return 0; +} + +static int system_info_set_menu_lang(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_set_menu_language(&msg, "eng"); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + + return PRESUMED_OK; +} + +static int system_info_give_features(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = { }; + + cec_msg_init(&msg, me, la); + cec_msg_give_features(&msg, true); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + if (unrecognized_op(&msg)) { + if (node->remote[la].cec_version < CEC_OP_CEC_VERSION_2_0) + return NOTSUPPORTED; + fail_on_test_v2(node->remote[la].cec_version, true); + } + if (refused(&msg)) + return REFUSED; + if (node->remote[la].cec_version < CEC_OP_CEC_VERSION_2_0) + info("Device has CEC Version < 2.0 but supports Give Features.\n"); + + /* RC Profile and Device Features are assumed to be 1 byte. As of CEC 2.0 only + 1 byte is used, but this might be extended in future versions. */ + __u8 cec_version, all_device_types; + const __u8 *rc_profile, *dev_features; + + cec_ops_report_features(&msg, &cec_version, &all_device_types, &rc_profile, &dev_features); + fail_on_test(cec_version != node->remote[la].cec_version); + fail_on_test(rc_profile == NULL || dev_features == NULL); + info("All Device Types: \t\t%s\n", all_dev_types2s(all_device_types).c_str()); + info("RC Profile: \t%s", rc_src_prof2s(*rc_profile).c_str()); + info("Device Features: \t%s", dev_feat2s(*dev_features).c_str()); + + if (!(cec_has_playback(1 << la) || cec_has_record(1 << la) || cec_has_tuner(1 << la)) && + node->remote[la].has_aud_rate) { + return fail("Only Playback, Recording or Tuner devices shall set the Set Audio Rate bit"); + } + if (!(cec_has_playback(1 << la) || cec_has_record(1 << la)) && node->remote[la].has_deck_ctl) + return fail("Only Playback and Recording devices shall set the Supports Deck Control bit"); + if (!cec_has_tv(1 << la) && node->remote[la].has_rec_tv) + return fail("Only TVs shall set the Record TV Screen bit"); + + return 0; +} + +static struct remote_subtest system_info_subtests[] = { + { "Polling Message", CEC_LOG_ADDR_MASK_ALL, system_info_polling }, + { "Give Physical Address", CEC_LOG_ADDR_MASK_ALL, system_info_phys_addr }, + { "Give CEC Version", CEC_LOG_ADDR_MASK_ALL, system_info_version }, + { "Get Menu Language", CEC_LOG_ADDR_MASK_ALL, system_info_get_menu_lang }, + { "Set Menu Language", CEC_LOG_ADDR_MASK_ALL, system_info_set_menu_lang }, + { "Give Device Features", CEC_LOG_ADDR_MASK_ALL, system_info_give_features }, +}; + + +/* Core behavior */ + +static int core_unknown(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = { }; + + /* Unknown opcodes should be responded to with Feature Abort, with abort + reason Unknown Opcode. + + For CEC 2.0 and before, 0xfe is an unused opcode. The test possibly + needs to be updated for future CEC versions. */ + cec_msg_init(&msg, me, la); + msg.len = 2; + msg.msg[1] = 0xfe; + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test(!cec_msg_status_is_abort(&msg)); + + __u8 abort_msg, reason; + + cec_ops_feature_abort(&msg, &abort_msg, &reason); + fail_on_test(reason != CEC_OP_ABORT_UNRECOGNIZED_OP); + fail_on_test(abort_msg != 0xfe); + return 0; +} + +static int core_abort(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + /* The Abort message should always be responded to with Feature Abort + (with any abort reason) */ + cec_msg_init(&msg, me, la); + cec_msg_abort(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test(!cec_msg_status_is_abort(&msg)); + return 0; +} + +static struct remote_subtest core_subtests[] = { + { "Feature aborts unknown messages", CEC_LOG_ADDR_MASK_ALL, core_unknown }, + { "Feature aborts Abort message", CEC_LOG_ADDR_MASK_ALL, core_abort }, +}; + + +/* Vendor Specific Commands */ + +static int vendor_specific_commands_id(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_give_device_vendor_id(&msg, true); + fail_on_test(!transmit(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static struct remote_subtest vendor_specific_subtests[] = { + { "Give Device Vendor ID", CEC_LOG_ADDR_MASK_ALL, vendor_specific_commands_id }, +}; + + +/* Device OSD Transfer */ + +static int device_osd_transfer_set(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = { }; + + cec_msg_init(&msg, me, la); + cec_msg_set_osd_name(&msg, "Whatever"); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) { + if (is_tv(la, node->remote[la].prim_type) && + node->remote[la].cec_version >= CEC_OP_CEC_VERSION_2_0) + warn("TV feature aborted Set OSD Name\n"); + return NOTSUPPORTED; + } + if (refused(&msg)) + return REFUSED; + + return PRESUMED_OK; +} + +static int device_osd_transfer_give(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = { }; + + /* Todo: CEC 2.0: devices with several logical addresses shall report + the same for each logical address. */ + cec_msg_init(&msg, me, la); + cec_msg_give_osd_name(&msg, true); + fail_on_test(!transmit(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test(!is_tv(la, node->remote[la].prim_type) && unrecognized_op(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static struct remote_subtest device_osd_transfer_subtests[] = { + { "Set OSD Name", CEC_LOG_ADDR_MASK_TV, device_osd_transfer_set }, + { "Give OSD Name", CEC_LOG_ADDR_MASK_ALL, device_osd_transfer_give }, +}; + + +/* OSD Display */ + +static int osd_string_set_default(struct node *node, unsigned me, unsigned la, bool interactive) +{ + if (!node->remote[la].has_osd) + return NOTAPPLICABLE; + + struct cec_msg msg = { }; + char osd[14]; + bool unsuitable = false; + + sprintf(osd, "Rept %x from %x", la, me); + + interactive_info(true, "You should see \"%s\" appear on the screen for approximately one second.", osd); + cec_msg_init(&msg, me, la); + cec_msg_set_osd_string(&msg, CEC_OP_DISP_CTL_DEFAULT, osd); + fail_on_test(!transmit_timeout(node, &msg)); + /* In CEC 2.0 it is mandatory for a TV to support this if it reports so + in its Device Features. */ + fail_on_test_v2(node->remote[la].cec_version, + unrecognized_op(&msg) && + (node->remote[la].dev_features & CEC_OP_FEAT_DEV_HAS_SET_OSD_STRING)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + else if (refused(&msg)) + return REFUSED; + else { + warn("The device is in an unsuitable state or cannot display the complete message.\n"); + unsuitable = true; + } + + cec_msg_init(&msg, me, la); + cec_msg_set_osd_string(&msg, CEC_OP_DISP_CTL_CLEAR, ""); + fail_on_test(!transmit_timeout(node, &msg, 250)); + fail_on_test(cec_msg_status_is_abort(&msg)); + fail_on_test(!unsuitable && interactive && !question("Did the string appear?")); + + node->remote[la].has_osd = true; + if (interactive) + return 0; + else + return PRESUMED_OK; +} + +static int osd_string_set_until_clear(struct node *node, unsigned me, unsigned la, bool interactive) +{ + if (!node->remote[la].has_osd) + return NOTAPPLICABLE; + + struct cec_msg msg = { }; + char osd[14]; + bool unsuitable = false; + + strcpy(osd, "Appears 1 sec"); + // Make sure the string is the maximum possible length + fail_on_test(strlen(osd) != 13); + + interactive_info(true, "You should see \"%s\" appear on the screen for approximately three seconds.", osd); + cec_msg_init(&msg, me, la); + cec_msg_set_osd_string(&msg, CEC_OP_DISP_CTL_UNTIL_CLEARED, osd); + fail_on_test(!transmit(node, &msg)); + if (cec_msg_status_is_abort(&msg) && abort_reason(&msg) != CEC_OP_ABORT_UNRECOGNIZED_OP) { + warn("The device is in an unsuitable state or cannot display the complete message.\n"); + unsuitable = true; + } + sleep(3); + + cec_msg_init(&msg, me, la); + cec_msg_set_osd_string(&msg, CEC_OP_DISP_CTL_CLEAR, ""); + fail_on_test(!transmit_timeout(node, &msg, 250)); + fail_on_test(cec_msg_status_is_abort(&msg)); + fail_on_test(!unsuitable && interactive && !question("Did the string appear?")); + + if (interactive) + return 0; + else + return PRESUMED_OK; +} + +static int osd_string_invalid(struct node *node, unsigned me, unsigned la, bool interactive) +{ + if (!node->remote[la].has_osd) + return NOTAPPLICABLE; + + struct cec_msg msg = { }; + + /* Send Set OSD String with an Display Control operand. A Feature Abort is + expected in reply. */ + interactive_info(true, "You should observe no change on the on screen display"); + cec_msg_init(&msg, me, la); + cec_msg_set_osd_string(&msg, 0xff, ""); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test(!cec_msg_status_is_abort(&msg)); + fail_on_test(interactive && question("Did the display change?")); + + return 0; +} + +static struct remote_subtest osd_string_subtests[] = { + { "Set OSD String with default timeout", CEC_LOG_ADDR_MASK_TV, osd_string_set_default }, + { "Set OSD String with no timeout", CEC_LOG_ADDR_MASK_TV, osd_string_set_until_clear }, + { "Set OSD String with invalid operand", CEC_LOG_ADDR_MASK_TV, osd_string_invalid }, +}; + + +/* Routing Control */ + +static int routing_control_inactive_source(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + interactive_info(true, "Please make sure that the TV is currently viewing this source."); + cec_msg_init(&msg, me, la); + cec_msg_inactive_source(&msg, node->phys_addr); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + fail_on_test(interactive && !question("Did the TV switch away from or stop showing this source?")); + + if (interactive) + return 0; + else + return PRESUMED_OK; +} + +static int routing_control_active_source(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + interactive_info(true, "Please switch the TV to another source."); + cec_msg_init(&msg, me, la); + cec_msg_active_source(&msg, node->phys_addr); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(interactive && !question("Did the TV switch to this source?")); + + if (interactive) + return 0; + else + return PRESUMED_OK; +} + +static int routing_control_req_active_source(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + /* We have now said that we are active source, so receiving a reply to + Request Active Source should fail the test. */ + cec_msg_init(&msg, me, la); + cec_msg_request_active_source(&msg, true); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(!timed_out(&msg)); + + return 0; +} + +static int routing_control_set_stream_path(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + __u16 phys_addr; + + /* Send Set Stream Path with the remote physical address. We expect the + source to eventually send Active Source. The timeout of 60 seconds is + necessary because the device might have to wake up from standby. + + In CEC 2.0 it is mandatory for sources to send Active Source. */ + if (is_tv(la, node->remote[la].prim_type)) + interactive_info(true, "Please ensure that the device is in standby."); + announce("Sending Set Stream Path and waiting for reply. This may take up to 60 s."); + cec_msg_init(&msg, me, la); + cec_msg_set_stream_path(&msg, node->remote[la].phys_addr); + msg.reply = CEC_MSG_ACTIVE_SOURCE; + fail_on_test(!transmit_timeout(node, &msg, 60000)); + if (timed_out(&msg) && is_tv(la, node->remote[la].prim_type)) + return NOTSUPPORTED; + if (timed_out(&msg) && node->remote[la].cec_version < CEC_OP_CEC_VERSION_2_0) { + warn("Device did not respond to Set Stream Path.\n"); + return NOTSUPPORTED; + } + fail_on_test_v2(node->remote[la].cec_version, timed_out(&msg)); + cec_ops_active_source(&msg, &phys_addr); + fail_on_test(phys_addr != node->remote[la].phys_addr); + if (is_tv(la, node->remote[la].prim_type)) + fail_on_test(interactive && !question("Did the device go out of standby?")); + + if (interactive || node->remote[la].cec_version >= CEC_OP_CEC_VERSION_2_0) + return 0; + else + return PRESUMED_OK; + return 0; +} + +static struct remote_subtest routing_control_subtests[] = { + { "Inactive Source", CEC_LOG_ADDR_MASK_TV, routing_control_inactive_source }, + { "Active Source", CEC_LOG_ADDR_MASK_TV, routing_control_active_source }, + { "Request Active Source", CEC_LOG_ADDR_MASK_ALL, routing_control_req_active_source }, + { "Set Stream Path", CEC_LOG_ADDR_MASK_ALL, routing_control_set_stream_path }, +}; + + +/* Remote Control Passthrough */ + +static int rc_passthrough_user_ctrl_pressed(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + struct cec_op_ui_command rc_press; + + cec_msg_init(&msg, me, la); + rc_press.ui_cmd = 0x41; // Volume up key (the key is not crucial here) + cec_msg_user_control_pressed(&msg, &rc_press); + fail_on_test(!transmit_timeout(node, &msg)); + /* Mandatory for all except devices which have taken logical address 15 */ + fail_on_test_v2(node->remote[la].cec_version, + unrecognized_op(&msg) && !(cec_is_unregistered(1 << la))); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + + return PRESUMED_OK; +} + +static int rc_passthrough_user_ctrl_released(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_user_control_released(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test_v2(node->remote[la].cec_version, + cec_msg_status_is_abort(&msg) && !(la & CEC_LOG_ADDR_MASK_UNREGISTERED)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + node->remote[la].has_remote_control_passthrough = true; + + return PRESUMED_OK; +} + +static struct remote_subtest rc_passthrough_subtests[] = { + { "User Control Pressed", CEC_LOG_ADDR_MASK_ALL, rc_passthrough_user_ctrl_pressed }, + { "User Control Released", CEC_LOG_ADDR_MASK_ALL, rc_passthrough_user_ctrl_released }, +}; + + +/* Device Menu Control */ + +/* + TODO: These are very rudimentary tests which should be expanded. + */ + +static int dev_menu_ctl_request(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_menu_request(&msg, true, CEC_OP_MENU_REQUEST_QUERY); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + if (node->remote[la].cec_version >= CEC_OP_CEC_VERSION_2_0) + warn("The Device Menu Control feature is deprecated in CEC 2.0"); + + return 0; +} + +static int dev_menu_ctl_status(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_menu_status(&msg, CEC_OP_MENU_STATE_ACTIVATED); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + if (node->remote[la].cec_version >= CEC_OP_CEC_VERSION_2_0) + warn("The Device Menu Control feature is deprecated in CEC 2.0"); + + return 0; +} +static struct remote_subtest dev_menu_ctl_subtests[] = { + { "Menu Request", (__u16)~CEC_LOG_ADDR_MASK_TV, dev_menu_ctl_request }, + { "Menu Status", CEC_LOG_ADDR_MASK_TV, dev_menu_ctl_status }, + { "User Control Pressed", CEC_LOG_ADDR_MASK_ALL, rc_passthrough_user_ctrl_pressed }, + { "User Control Released", CEC_LOG_ADDR_MASK_ALL, rc_passthrough_user_ctrl_released }, +}; + + +/* Deck Control */ + +/* + TODO: These are very rudimentary tests which should be expanded. + */ + +static int deck_ctl_give_status(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_give_deck_status(&msg, true, CEC_OP_STATUS_REQ_ONCE); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test_v2(node->remote[la].cec_version, + node->remote[la].has_deck_ctl && cec_msg_status_is_abort(&msg)); + fail_on_test_v2(node->remote[la].cec_version, + !node->remote[la].has_deck_ctl && !unrecognized_op(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int deck_ctl_deck_status(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_deck_status(&msg, CEC_OP_DECK_INFO_STOP); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int deck_ctl_deck_ctl(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_deck_control(&msg, CEC_OP_DECK_CTL_MODE_STOP); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test_v2(node->remote[la].cec_version, + node->remote[la].has_deck_ctl && unrecognized_op(&msg)); + fail_on_test_v2(node->remote[la].cec_version, + !node->remote[la].has_deck_ctl && !unrecognized_op(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return PRESUMED_OK; +} + +static int deck_ctl_play(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_play(&msg, CEC_OP_PLAY_MODE_PLAY_STILL); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test_v2(node->remote[la].cec_version, + node->remote[la].has_deck_ctl && unrecognized_op(&msg)); + fail_on_test_v2(node->remote[la].cec_version, + !node->remote[la].has_deck_ctl && !unrecognized_op(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return PRESUMED_OK; +} + +static struct remote_subtest deck_ctl_subtests[] = { + { "Give Deck Status", + CEC_LOG_ADDR_MASK_PLAYBACK | CEC_LOG_ADDR_MASK_RECORD, + deck_ctl_give_status }, + { "Deck Status", + CEC_LOG_ADDR_MASK_ALL, + deck_ctl_deck_status }, + { "Deck Control", + CEC_LOG_ADDR_MASK_PLAYBACK | CEC_LOG_ADDR_MASK_RECORD, + deck_ctl_deck_ctl }, + { "Play", + CEC_LOG_ADDR_MASK_PLAYBACK | CEC_LOG_ADDR_MASK_RECORD, + deck_ctl_play }, +}; + + +/* Tuner Control */ + +/* + TODO: These are very rudimentary tests which should be expanded. + */ + +static int tuner_ctl_give_status(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_give_tuner_device_status(&msg, true, CEC_OP_STATUS_REQ_ONCE); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int tuner_ctl_sel_analog_service(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + node->remote[la].bcast_sys = ~0; + for (unsigned sys = 0; sys <= 8; sys++) { + cec_msg_init(&msg, me, la); + cec_msg_select_analogue_service(&msg, CEC_OP_ANA_BCAST_TYPE_CABLE, + 7668, sys); // 479.25 MHz analog frequency + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (abort_reason(&msg) == CEC_OP_ABORT_INVALID_OP) { + info("Tuner supports %s, but cannot select that service.\n", + bcast_system2s(sys)); + node->remote[la].bcast_sys = sys; + continue; + } + if (cec_msg_status_is_abort(&msg)) + continue; + info("Tuner supports %s\n", bcast_system2s(sys)); + node->remote[la].bcast_sys = sys; + } + + if (node->remote[la].bcast_sys == (__u8)~0) + warn("No analog broadcast format supported\n"); + else + return 0; + + return PRESUMED_OK; +} + +static int tuner_ctl_sel_digital_service(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + struct cec_op_digital_service_id digital_service_id = {}; + + digital_service_id.service_id_method = CEC_OP_SERVICE_ID_METHOD_BY_CHANNEL; + digital_service_id.channel.channel_number_fmt = CEC_OP_CHANNEL_NUMBER_FMT_1_PART; + digital_service_id.channel.minor = 1; + + __u8 bcast_systems[] = { + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ARIB_GEN, + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ATSC_GEN, + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_DVB_GEN, + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ARIB_BS, + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ARIB_CS, + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ARIB_T, + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ATSC_CABLE, + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ATSC_SAT, + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_ATSC_T, + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_DVB_C, + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_DVB_S, + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_DVB_S2, + CEC_OP_DIG_SERVICE_BCAST_SYSTEM_DVB_T, + }; + + node->remote[la].dig_bcast_sys = ~0; + for (unsigned i = 0; i < ARRAY_SIZE(bcast_systems); i++) { + __u8 sys = bcast_systems[i]; + + digital_service_id.dig_bcast_system = sys; + cec_msg_init(&msg, me, la); + cec_msg_select_digital_service(&msg, &digital_service_id); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (abort_reason(&msg) == CEC_OP_ABORT_INVALID_OP) { + info("Tuner supports %s, but cannot select that service.\n", + dig_bcast_system2s(sys)); + node->remote[la].dig_bcast_sys = sys; + continue; + } + if (cec_msg_status_is_abort(&msg)) + continue; + info("Tuner supports %s\n", dig_bcast_system2s(sys)); + node->remote[la].dig_bcast_sys = sys; + } + + if (node->remote[la].dig_bcast_sys == (__u8)~0) + warn("No digital broadcast system supported\n"); + else + return 0; + + return PRESUMED_OK; +} + +static int tuner_ctl_tuner_dev_status(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + struct cec_op_tuner_device_info tuner_dev_info = {}; + + tuner_dev_info.rec_flag = CEC_OP_REC_FLAG_NOT_USED; + tuner_dev_info.tuner_display_info = CEC_OP_TUNER_DISPLAY_INFO_NONE; + tuner_dev_info.is_analog = false; + tuner_dev_info.digital.service_id_method = CEC_OP_SERVICE_ID_METHOD_BY_CHANNEL; + tuner_dev_info.digital.dig_bcast_system = CEC_OP_DIG_SERVICE_BCAST_SYSTEM_DVB_C; + tuner_dev_info.digital.channel.channel_number_fmt = CEC_OP_CHANNEL_NUMBER_FMT_1_PART; + tuner_dev_info.digital.channel.minor = 1; + + cec_msg_init(&msg, me, la); + + cec_msg_tuner_device_status(&msg, &tuner_dev_info); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int tuner_ctl_step_dec(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_tuner_step_decrement(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + + return PRESUMED_OK; +} + +static int tuner_ctl_step_inc(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_tuner_step_increment(&msg); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + + return PRESUMED_OK; +} + +static struct remote_subtest tuner_ctl_subtests[] = { + { "Give Tuner Device Status", CEC_LOG_ADDR_MASK_TUNER, tuner_ctl_give_status }, + { "Select Analogue Service", CEC_LOG_ADDR_MASK_TUNER, tuner_ctl_sel_analog_service }, + { "Select Digital Service", CEC_LOG_ADDR_MASK_TUNER, tuner_ctl_sel_digital_service }, + { "Tuner Device Status", CEC_LOG_ADDR_MASK_ALL, tuner_ctl_tuner_dev_status }, + { "Tuner Step Decrement", CEC_LOG_ADDR_MASK_TUNER, tuner_ctl_step_dec }, + { "Tuner Step Increment", CEC_LOG_ADDR_MASK_TUNER, tuner_ctl_step_inc }, +}; + + +/* One Touch Record */ + +/* + TODO: These are very rudimentary tests which should be expanded. + + - The HDMI CEC 1.4b spec details that Standby shall not be acted upon while the + device is recording, but it should remember that it received Standby. + */ + +static int one_touch_rec_tv_screen(struct node *node, unsigned me, unsigned la, bool interactive) +{ + /* + TODO: + - Page 36 in HDMI CEC 1.4b spec lists additional behaviors that should be + checked for. + - The TV should ignore this message when received from other LA than Recording or + Reserved. + */ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_record_tv_screen(&msg, true); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test_v2(node->remote[la].cec_version, + node->remote[la].has_rec_tv && unrecognized_op(&msg)); + fail_on_test_v2(node->remote[la].cec_version, + !node->remote[la].has_rec_tv && !unrecognized_op(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int one_touch_rec_on(struct node *node, unsigned me, unsigned la, bool interactive) +{ + /* + TODO: Page 36 in HDMI CEC 1.4b spec lists additional behaviors that should be + checked for. + */ + struct cec_msg msg = {}; + struct cec_op_record_src rec_src = {}; + + rec_src.type = CEC_OP_RECORD_SRC_OWN; + cec_msg_init(&msg, me, la); + cec_msg_record_on(&msg, true, &rec_src); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(timed_out(&msg)); + fail_on_test(cec_has_record(1 << la) && unrecognized_op(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int one_touch_rec_off(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_record_off(&msg, false); + fail_on_test(!transmit_timeout(node, &msg)); + fail_on_test(cec_has_record(1 << la) && unrecognized_op(&msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + if (timed_out(&msg)) + return PRESUMED_OK; + else + return 0; +} + +static int one_touch_rec_status(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_record_status(&msg, CEC_OP_RECORD_STATUS_DIG_SERVICE); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static struct remote_subtest one_touch_rec_subtests[] = { + { "Record TV Screen", CEC_LOG_ADDR_MASK_TV, one_touch_rec_tv_screen }, + { "Record On", CEC_LOG_ADDR_MASK_RECORD, one_touch_rec_on }, + { "Record Off", CEC_LOG_ADDR_MASK_RECORD, one_touch_rec_off }, + { "Record Status", CEC_LOG_ADDR_MASK_ALL, one_touch_rec_status }, +}; + + +/* Timer Programming */ + +/* + TODO: These are very rudimentary tests which should be expanded. + */ + +static int timer_prog_set_analog_timer(struct node *node, unsigned me, unsigned la, bool interactive) +{ + /* TODO: Check the timer status for possible errors, etc. */ + + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_set_analogue_timer(&msg, true, 1, 1, 0, 0, 1, 0, CEC_OP_REC_SEQ_ONCE_ONLY, + CEC_OP_ANA_BCAST_TYPE_CABLE, + 7668, // 479.25 MHz + node->remote[la].bcast_sys); + fail_on_test(!transmit_timeout(node, &msg, 10000)); + if (timed_out(&msg)) { + warn("Timed out waiting for Timer Status. Assuming timer was set.\n"); + return PRESUMED_OK; + } + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int timer_prog_set_digital_timer(struct node *node, unsigned me, unsigned la, bool interactive) +{ + /* TODO: Check the timer status for possible errors, etc. */ + + struct cec_msg msg = {}; + struct cec_op_digital_service_id digital_service_id = {}; + + digital_service_id.service_id_method = CEC_OP_SERVICE_ID_METHOD_BY_CHANNEL; + digital_service_id.channel.channel_number_fmt = CEC_OP_CHANNEL_NUMBER_FMT_1_PART; + digital_service_id.channel.minor = 1; + digital_service_id.dig_bcast_system = node->remote[la].dig_bcast_sys; + cec_msg_init(&msg, me, la); + cec_msg_set_digital_timer(&msg, true, 1, 1, 0, 0, 1, 0, CEC_OP_REC_SEQ_ONCE_ONLY, + &digital_service_id); + fail_on_test(!transmit_timeout(node, &msg, 10000)); + if (timed_out(&msg)) { + warn("Timed out waiting for Timer Status. Assuming timer was set.\n"); + return PRESUMED_OK; + } + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int timer_prog_set_ext_timer(struct node *node, unsigned me, unsigned la, bool interactive) +{ + /* TODO: Check the timer status. */ + + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_set_ext_timer(&msg, true, 1, 1, 0, 0, 1, 0, CEC_OP_REC_SEQ_ONCE_ONLY, + CEC_OP_EXT_SRC_PHYS_ADDR, 0, node->phys_addr); + fail_on_test(!transmit_timeout(node, &msg, 10000)); + if (timed_out(&msg)) { + warn("Timed out waiting for Timer Status. Assuming timer was set.\n"); + return PRESUMED_OK; + } + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int timer_prog_clear_analog_timer(struct node *node, unsigned me, unsigned la, bool interactive) +{ + /* TODO: Check the timer cleared status. */ + + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_clear_analogue_timer(&msg, true, 1, 1, 0, 0, 1, 0, CEC_OP_REC_SEQ_ONCE_ONLY, + CEC_OP_ANA_BCAST_TYPE_CABLE, + 7668, // 479.25 MHz + node->remote[la].bcast_sys); + fail_on_test(!transmit_timeout(node, &msg, 10000)); + if (timed_out(&msg)) { + warn("Timed out waiting for Timer Cleared Status. Assuming timer was cleared.\n"); + return PRESUMED_OK; + } + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int timer_prog_clear_digital_timer(struct node *node, unsigned me, unsigned la, bool interactive) +{ + /* TODO: Check the timer cleared status. */ + + struct cec_msg msg = {}; + struct cec_op_digital_service_id digital_service_id = {}; + + digital_service_id.service_id_method = CEC_OP_SERVICE_ID_METHOD_BY_CHANNEL; + digital_service_id.channel.channel_number_fmt = CEC_OP_CHANNEL_NUMBER_FMT_1_PART; + digital_service_id.channel.minor = 1; + digital_service_id.dig_bcast_system = node->remote[la].dig_bcast_sys; + cec_msg_init(&msg, me, la); + cec_msg_clear_digital_timer(&msg, true, 1, 1, 0, 0, 1, 0, CEC_OP_REC_SEQ_ONCE_ONLY, + &digital_service_id); + fail_on_test(!transmit_timeout(node, &msg, 10000)); + if (timed_out(&msg)) { + warn("Timed out waiting for Timer Cleared Status. Assuming timer was cleared.\n"); + return PRESUMED_OK; + } + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int timer_prog_clear_ext_timer(struct node *node, unsigned me, unsigned la, bool interactive) +{ + /* TODO: Check the timer cleared status. */ + + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_clear_ext_timer(&msg, true, 1, 1, 0, 0, 1, 0, CEC_OP_REC_SEQ_ONCE_ONLY, + CEC_OP_EXT_SRC_PHYS_ADDR, 0, node->phys_addr); + fail_on_test(!transmit_timeout(node, &msg, 10000)); + if (timed_out(&msg)) { + warn("Timed out waiting for Timer Cleared Status. Assuming timer was cleared.\n"); + return PRESUMED_OK; + } + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + if (cec_msg_status_is_abort(&msg)) + return PRESUMED_OK; + + return 0; +} + +static int timer_prog_set_prog_title(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_set_timer_program_title(&msg, "Super-Hans II"); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + + return PRESUMED_OK; +} + +static int timer_prog_timer_status(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_timer_status(&msg, CEC_OP_TIMER_OVERLAP_WARNING_NO_OVERLAP, + CEC_OP_MEDIA_INFO_NO_MEDIA, + CEC_OP_PROG_INFO_ENOUGH_SPACE, + 0, 0, 0); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + + return PRESUMED_OK; +} + +static int timer_prog_timer_clear_status(struct node *node, unsigned me, unsigned la, bool interactive) +{ + struct cec_msg msg = {}; + + cec_msg_init(&msg, me, la); + cec_msg_timer_cleared_status(&msg, CEC_OP_TIMER_CLR_STAT_CLEARED); + fail_on_test(!transmit_timeout(node, &msg)); + if (unrecognized_op(&msg)) + return NOTSUPPORTED; + if (refused(&msg)) + return REFUSED; + + return PRESUMED_OK; +} + +static struct remote_subtest timer_prog_subtests[] = { + { "Set Analogue Timer", CEC_LOG_ADDR_MASK_RECORD, timer_prog_set_analog_timer }, + { "Set Digital Timer", CEC_LOG_ADDR_MASK_RECORD, timer_prog_set_digital_timer }, + { "Set Timer Program Title", CEC_LOG_ADDR_MASK_RECORD, timer_prog_set_prog_title }, + { "Set External Timer", CEC_LOG_ADDR_MASK_RECORD, timer_prog_set_ext_timer }, + { "Clear Analogue Timer", CEC_LOG_ADDR_MASK_RECORD, timer_prog_clear_analog_timer }, + { "Clear Digital Timer", CEC_LOG_ADDR_MASK_RECORD, timer_prog_clear_digital_timer }, + { "Clear External Timer", CEC_LOG_ADDR_MASK_RECORD, timer_prog_clear_ext_timer }, + { "Timer Status", CEC_LOG_ADDR_MASK_RECORD, timer_prog_timer_status }, + { "Timer Cleared Status", CEC_LOG_ADDR_MASK_RECORD, timer_prog_timer_clear_status }, +}; + +static int cdc_hec_discover(struct node *node, unsigned me, unsigned la, bool print) +{ + /* TODO: For future use cases, it might be necessary to store the results + from the HEC discovery to know which HECs are possible to form, etc. */ + struct cec_msg msg = {}; + __u32 mode = CEC_MODE_INITIATOR | CEC_MODE_FOLLOWER; + bool has_cdc = false; + + doioctl(node, CEC_S_MODE, &mode); + cec_msg_init(&msg, me, la); + cec_msg_cdc_hec_discover(&msg); + fail_on_test(!transmit(node, &msg)); + + /* The spec describes that we shall wait for messages + up to 1 second, and extend the deadline for every received + message. The maximum time to wait for incoming state reports + is 5 seconds. */ + unsigned ts_start = get_ts_ms(); + while (get_ts_ms() - ts_start < 5000) { + __u8 from; + + memset(&msg, 0, sizeof(msg)); + msg.timeout = 1000; + if (doioctl(node, CEC_RECEIVE, &msg)) + break; + from = cec_msg_initiator(&msg); + if (msg.msg[1] == CEC_MSG_FEATURE_ABORT) { + if (from == la) + return fail("Device replied Feature Abort to broadcast message\n"); + else + warn("Device %d replied Feature Abort to broadcast message\n", cec_msg_initiator(&msg)); + } + if (msg.msg[1] != CEC_MSG_CDC_MESSAGE) + continue; + if (msg.msg[4] != CEC_MSG_CDC_HEC_REPORT_STATE) + continue; + + __u16 phys_addr, target_phys_addr, hec_field; + __u8 hec_func_state, host_func_state, enc_func_state, cdc_errcode, has_field; + + cec_ops_cdc_hec_report_state(&msg, &phys_addr, &target_phys_addr, + &hec_func_state, &host_func_state, + &enc_func_state, &cdc_errcode, + &has_field, &hec_field); + + if (target_phys_addr != node->phys_addr) + continue; + if (phys_addr == node->remote[la].phys_addr) + has_cdc = true; + if (!print) + continue; + + from = cec_msg_initiator(&msg); + info("Received CDC HEC State report from device %d (%s):\n", from, la2s(from)); + info("Physical address : %x.%x.%x.%x\n", + cec_phys_addr_exp(phys_addr)); + info("Target physical address : %x.%x.%x.%x\n", + cec_phys_addr_exp(target_phys_addr)); + info("HEC Functionality State : %s\n", hec_func_state2s(hec_func_state)); + info("Host Functionality State : %s\n", host_func_state2s(host_func_state)); + info("ENC Functionality State : %s\n", enc_func_state2s(enc_func_state)); + info("CDC Error Code : %s\n", cdc_errcode2s(cdc_errcode)); + + if (has_field) { + std::ostringstream oss; + + /* Bit 14 indicates whether or not the device's HDMI + output has HEC support/is active. */ + if (!hec_field) + oss << "None"; + else { + if (hec_field & (1 << 14)) + oss << "out, "; + for (int i = 13; i >= 0; i--) { + if (hec_field & (1 << i)) + oss << "in" << (14 - i) << ", "; + } + oss << "\b\b "; + } + info("HEC Suppport Field : %s\n", oss.str().c_str()); + } + } + + mode = CEC_MODE_INITIATOR; + doioctl(node, CEC_S_MODE, &mode); + + if (has_cdc) + return 0; + return NOTSUPPORTED; +} + +static struct remote_subtest cdc_subtests[] = { + { "CDC_HEC_Discover", CEC_LOG_ADDR_MASK_ALL, cdc_hec_discover }, +}; + + +/* Post-test checks */ + +static int post_test_check_recognized(struct node *node, unsigned me, unsigned la, bool interactive) +{ + bool fail = false; + + for (unsigned i = 0; i < 256; i++) { + if (node->remote[la].recognized_op[i] && node->remote[la].unrecognized_op[i]) { + struct cec_msg msg = {}; + msg.msg[1] = i; + fail("Opcode %s has been both recognized by and has been replied\n", opcode2s(&msg).c_str()); + fail("Feature Abort [Unrecognized Opcode] to by the device.\n"); + fail = true; + } + } + fail_on_test(fail); + + return 0; +} + +static struct remote_subtest post_test_subtests[] = { + { "Recognized/unrecognized message consistency", CEC_LOG_ADDR_MASK_ALL, post_test_check_recognized }, +}; + + +static struct remote_test tests[] = { + test_case("Core", + TAG_CORE, + core_subtests), + test_case_ext("Give Device Power Status feature", + TAG_POWER_STATUS, + power_status_subtests), + test_case("System Information feature", + TAG_SYSTEM_INFORMATION, + system_info_subtests), + test_case("Vendor Specific Commands feature", + TAG_VENDOR_SPECIFIC_COMMANDS, + vendor_specific_subtests), + test_case("Device OSD Transfer feature", + TAG_DEVICE_OSD_TRANSFER, + device_osd_transfer_subtests), + test_case("OSD String feature", + TAG_OSD_DISPLAY, + osd_string_subtests), + test_case("Remote Control Passthrough feature", + TAG_REMOTE_CONTROL_PASSTHROUGH, + rc_passthrough_subtests), + test_case("Device Menu Control feature", + TAG_DEVICE_MENU_CONTROL, + dev_menu_ctl_subtests), + test_case("Deck Control feature", + TAG_DECK_CONTROL, + deck_ctl_subtests), + test_case("Tuner Control feature", + TAG_TUNER_CONTROL, + tuner_ctl_subtests), + test_case("One Touch Record feature", + TAG_ONE_TOUCH_RECORD, + one_touch_rec_subtests), + test_case("Timer Progrmaming feature", + TAG_TIMER_PROGRAMMING, + timer_prog_subtests), + test_case("Capability Discovery and Control feature", + TAG_CAP_DISCOVERY_CONTROL, + cdc_subtests), + test_case_ext("Dynamic Auto Lipsync feature", + TAG_DYNAMIC_AUTO_LIPSYNC, + dal_subtests), + test_case_ext("Audio Return Channel feature", + TAG_ARC_CONTROL, + arc_subtests), + test_case_ext("System Audio Control feature", + TAG_SYSTEM_AUDIO_CONTROL, + sac_subtests), + test_case_ext("Audio Rate Control feature", + TAG_AUDIO_RATE_CONTROL, + audio_rate_ctl_subtests), + test_case_ext("One Touch Play feature", + TAG_ONE_TOUCH_PLAY, + one_touch_play_subtests), + test_case("Routing Control feature", + TAG_ROUTING_CONTROL, + routing_control_subtests), + test_case_ext("Standby/Resume and Power Status", + TAG_POWER_STATUS | TAG_STANDBY_RESUME, + standby_resume_subtests), + test_case("Post-test checks", + TAG_CORE, + post_test_subtests), +}; + +static const unsigned num_tests = sizeof(tests) / sizeof(struct remote_test); + +void testRemote(struct node *node, unsigned me, unsigned la, unsigned test_tags, + bool interactive) +{ + printf("testing CEC local LA %d (%s) to remote LA %d (%s):\n", + me, la2s(me), la, la2s(la)); + + if (!util_interactive_ensure_power_state(node, me, la, interactive, CEC_OP_POWER_STATUS_ON)) + return; + if (node->remote[la].in_standby && !interactive) { + announce("The remote device is in standby. It should be powered on when testing. Aborting."); + return; + } + if (!node->remote[la].has_power_status) { + announce("The device didn't support Give Device Power Status."); + announce("Assuming that the device is powered on."); + } + + int ret = 0; + + for (unsigned i = 0; i < num_tests; i++) { + if ((tests[i].tags & test_tags) != tests[i].tags) + continue; + printf("\t%s:\n", tests[i].name); + for (unsigned j = 0; j < tests[i].num_subtests; j++) { + ret = tests[i].subtests[j].test_fn(node, me, la, interactive); + if (!(tests[i].subtests[j].la_mask & (1 << la)) && !ret) { + printf("\t %s: OK (Unexpected)\n", + tests[i].subtests[j].name); + } + else if (ret == PRESUMED_OK) + printf("\t %s: OK (Presumed)\n", tests[i].subtests[j].name); + else if (ret == REFUSED) + printf("\t %s: OK (Refused)\n", tests[i].subtests[j].name); + else if (ret != NOTAPPLICABLE) + printf("\t %s: %s\n", tests[i].subtests[j].name, ok(ret)); + if (ret == FAIL_CRITICAL) + return; + } + printf("\n"); + } +} |