| // SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause) |
| /* |
| * Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved. |
| */ |
| |
| #include <cstring> |
| #include <ctime> |
| #include <sstream> |
| |
| #include <fcntl.h> |
| #include <getopt.h> |
| #include <sys/ioctl.h> |
| |
| #include "cec-follower.h" |
| #include "compiler.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 { |
| OptSetAdapter = 'a', |
| OptSetDevice = 'd', |
| OptSetDriver = 'D', |
| OptHelp = 'h', |
| OptIgnore = 'i', |
| OptNoWarnings = 'n', |
| OptTrace = 'T', |
| OptVerbose = 'v', |
| OptShowMsgs = 'm', |
| OptShowState = 's', |
| OptWallClock = 'w', |
| OptServiceByDigID = 128, |
| OptStandby, |
| OptTogglePowerStatus, |
| OptVersion, |
| OptLast = 256 |
| }; |
| |
| static char options[OptLast]; |
| |
| bool show_info; |
| bool show_msgs; |
| bool show_state; |
| bool show_warnings = true; |
| unsigned warnings; |
| std::set<struct Timer> programmed_timers; |
| |
| static struct option long_options[] = { |
| { "device", required_argument, nullptr, OptSetDevice }, |
| { "adapter", required_argument, nullptr, OptSetAdapter }, |
| { "driver", required_argument, nullptr, OptSetDriver }, |
| { "help", no_argument, nullptr, OptHelp }, |
| { "no-warnings", no_argument, nullptr, OptNoWarnings }, |
| { "trace", no_argument, nullptr, OptTrace }, |
| { "verbose", no_argument, nullptr, OptVerbose }, |
| { "show-msgs", no_argument, nullptr, OptShowMsgs }, |
| { "show-state", no_argument, nullptr, OptShowState }, |
| { "wall-clock", no_argument, nullptr, OptWallClock }, |
| { "service-by-dig-id", no_argument, nullptr, OptServiceByDigID }, |
| { "standby", no_argument, nullptr, OptStandby }, |
| { "toggle-power-status", required_argument, nullptr, OptTogglePowerStatus }, |
| { "ignore", required_argument, nullptr, OptIgnore }, |
| { "version", no_argument, nullptr, OptVersion }, |
| |
| { nullptr, 0, nullptr, 0 } |
| }; |
| |
| #define STR(x) #x |
| #define STRING(x) STR(x) |
| |
| static void usage() |
| { |
| 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" |
| " -D, --driver <driver> Use a cec device with this driver name\n" |
| " -a, --adapter <adapter> Use a cec device with this adapter name\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" |
| " -w, --wall-clock Show timestamps as wall-clock time (implies -v)\n" |
| " -m, --show-msgs Show received messages\n" |
| " -s, --show-state Show state changes from the emulated device\n" |
| " --service-by-dig-id Report digital services by digital ID instead of by channel\n" |
| " --standby Start in Standby state\n" |
| " --toggle-power-status <secs>\n" |
| " Toggle the power status every <secs> seconds\n" |
| " -i, --ignore <la>,<opcode>\n" |
| " Ignore messages from logical address <la> and opcode\n" |
| " <opcode>. 'all' can be used for <la> or <opcode> to match\n" |
| " all logical addresses or opcodes.\n" |
| " --version Show version information\n" |
| ); |
| } |
| |
| void sad_encode(const struct short_audio_desc *sad, __u32 *descriptor) |
| { |
| __u8 b1, b2, b3 = 0; |
| |
| b1 = (sad->num_channels - 1) & 0x07; |
| b1 |= (sad->format_code & 0x0f) << 3; |
| |
| b2 = sad->sample_freq_mask; |
| |
| switch (sad->format_code) { |
| case SAD_FMT_CODE_LPCM: |
| b3 = sad->bit_depth_mask & 0x07; |
| break; |
| case 2: |
| case 3: |
| case 4: |
| case 5: |
| case 6: |
| case 7: |
| case 8: |
| b3 = sad->max_bitrate; |
| break; |
| case 9: |
| case 10: |
| case 11: |
| case 12: |
| case 13: |
| b3 = sad->format_dependent; |
| break; |
| case SAD_FMT_CODE_WMA_PRO: |
| b3 = sad->wma_profile & 0x03; |
| break; |
| case SAD_FMT_CODE_EXTENDED: |
| b3 = (sad->extension_type_code & 0x1f) << 3; |
| |
| switch (sad->extension_type_code) { |
| case 4: |
| case 5: |
| case 6: |
| b3 |= (sad->frame_length_mask & 0x03) << 1; |
| break; |
| case 8: |
| case 10: |
| b3 |= (sad->frame_length_mask & 0x03) << 1; |
| b3 |= sad->mps & 1; |
| break; |
| case SAD_EXT_TYPE_MPEG_H_3D_AUDIO: |
| case SAD_EXT_TYPE_AC_4: |
| b3 |= sad->format_dependent & 0x07; |
| fallthrough; |
| case SAD_EXT_TYPE_LPCM_3D_AUDIO: |
| b3 |= sad->bit_depth_mask & 0x07; |
| break; |
| } |
| break; |
| } |
| |
| *descriptor = (b1 << 16) | (b2 << 8) | b3; |
| } |
| |
| 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"; |
| } |
| } |
| |
| static 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 audio_format_id_code2s(__u8 audio_format_id, __u8 audio_format_code) |
| { |
| if (audio_format_id == 0) |
| return audio_format_code2s(audio_format_code); |
| if (audio_format_id == 1) |
| return extension_type_code2s(audio_format_code); |
| return "Invalid"; |
| } |
| |
| std::string opcode2s(const struct cec_msg *msg) |
| { |
| std::stringstream oss; |
| __u8 opcode = msg->msg[1]; |
| const char *name; |
| |
| if (opcode == CEC_MSG_CDC_MESSAGE) { |
| __u8 cdc_opcode = msg->msg[4]; |
| name = cec_cdc_opcode2s(cdc_opcode); |
| |
| if (name) |
| return name; |
| oss << "CDC: 0x" << std::hex << static_cast<unsigned>(cdc_opcode); |
| return oss.str(); |
| } |
| |
| name = cec_opcode2s(opcode); |
| |
| if (name) |
| return name; |
| oss << "0x" << std::hex << static_cast<unsigned>(opcode); |
| return oss.str(); |
| } |
| |
| int cec_named_ioctl(int fd, const char *name, |
| unsigned long int request, void *parm) |
| { |
| int retval; |
| int e; |
| |
| retval = ioctl(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) { |
| const auto msg = static_cast<const struct cec_msg *>(parm); |
| |
| /* Update the timestamp whenever we successfully transmit to an LA, |
| or whenever we receive something from the LA */ |
| if (request == CEC_TRANSMIT && (msg->tx_status & CEC_TX_STATUS_OK) && |
| !cec_msg_is_broadcast(msg)) { |
| if (msg->timeout) { |
| if (msg->rx_status & (CEC_RX_STATUS_OK | CEC_RX_STATUS_FEATURE_ABORT)) |
| la_info[cec_msg_initiator(msg)].ts = msg->rx_ts; |
| } else |
| la_info[cec_msg_destination(msg)].ts = msg->tx_ts; |
| } |
| if (request == CEC_RECEIVE && |
| cec_msg_initiator(msg) != CEC_LOG_ADDR_UNREGISTERED && |
| (msg->rx_status & CEC_RX_STATUS_OK)) |
| la_info[cec_msg_initiator(msg)].ts = msg->rx_ts; |
| } |
| |
| return retval == -1 ? e : (retval ? -1 : 0); |
| } |
| |
| void print_timers(struct node *node) |
| { |
| if (show_info) { |
| printf("Timers Set:\n"); |
| if (node->state.recording_controlled_by_timer) |
| printf("Deck is currently recording from the first timer.\n"); |
| if (node->state.one_touch_record_on && !node->state.recording_controlled_by_timer) |
| printf("Deck is currently recording independent of timers.\n"); |
| for (auto &t : programmed_timers) { |
| std::string start = ctime(&t.start_time); |
| time_t end_time = t.start_time + t.duration; |
| std::string end = ctime(&end_time); |
| /* Remove the seconds because timer is precise only to the minute. */ |
| start.erase(16, 3); |
| end.erase(16, 3); |
| /* Remove the new line characters. */ |
| start.erase(start.end() - 1); |
| end.erase(end.end() - 1); |
| /* Remove the start year if it is the same as the end year. */ |
| if ((start.compare(start.size() - 4, 5, end, end.size() - 4, 5) == 0)) |
| start.erase(start.size() - 5, 5); |
| printf("\t%s - %s, ", start.c_str(), end.c_str()); |
| /* Find and print the source. */ |
| std::string source; |
| switch (t.src.type) { |
| case CEC_OP_RECORD_SRC_OWN: |
| source = "own"; |
| break; |
| case CEC_OP_RECORD_SRC_DIGITAL: |
| source = "digital"; |
| break; |
| case CEC_OP_RECORD_SRC_ANALOG: |
| source = "analog"; |
| break; |
| case CEC_OP_RECORD_SRC_EXT_PLUG: |
| source = "ext plug"; |
| break; |
| case CEC_OP_RECORD_SRC_EXT_PHYS_ADDR: |
| source = "ext phy addr"; |
| break; |
| default: |
| break; |
| } |
| printf("source: %s, ", source.c_str()); |
| if (t.recording_seq) |
| printf("rec-seq: 0x%x, ", t.recording_seq); |
| printf("needs: %ld %s\n", t.duration, "MB."); /* 1MB per second. */ |
| } |
| printf("Total media space available for recording: "); |
| if (node->state.media_space_available >= 0) |
| printf("%d MB.\n\n", node->state.media_space_available); |
| else |
| printf("0 MB.\n\n"); |
| } |
| } |
| |
| void state_init(struct node &node) |
| { |
| if (options[OptStandby]) |
| node.state.power_status = CEC_OP_POWER_STATUS_STANDBY; |
| else |
| node.state.power_status = CEC_OP_POWER_STATUS_ON; |
| node.state.old_power_status = CEC_OP_POWER_STATUS_ON; |
| node.state.power_status_changed_time = 0; |
| strcpy(node.state.menu_language, "eng"); |
| node.state.video_latency = 10; |
| node.state.low_latency_mode = 1; |
| node.state.audio_out_compensated = 3; |
| node.state.audio_out_delay = 20; |
| node.state.arc_active = false; |
| node.state.sac_active = false; |
| node.state.volume = 50; |
| node.state.mute = false; |
| node.state.deck_report_changes = false; |
| node.state.deck_report_changes_to = 0; |
| node.state.deck_state = CEC_OP_DECK_INFO_STOP; |
| node.state.deck_skip_start = 0; |
| node.state.one_touch_record_on = false; |
| node.state.record_received_standby = false; |
| node.state.media_space_available = 36000; /* In MB; space for 10 hours @ 1MB/sec */ |
| node.state.recording_controlled_by_timer = false; |
| tuner_dev_info_init(&node.state); |
| node.state.last_aud_rate_rx_ts = 0; |
| } |
| |
| int main(int argc, char **argv) |
| { |
| std::string device; |
| struct node node = { }; |
| const char *driver = nullptr; |
| const char *adapter = nullptr; |
| unsigned toggle_power_status = 0; |
| char short_options[26 * 2 * 2 + 1]; |
| 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 (true) { |
| int option_index = 0; |
| |
| short_options[idx] = 0; |
| ch = getopt_long(argc, argv, short_options, |
| long_options, &option_index); |
| if (ch == -1) |
| break; |
| |
| options[ch] = 1; |
| if (!option_index) { |
| for (i = 0; long_options[i].val; i++) { |
| if (long_options[i].val == ch) { |
| option_index = i; |
| break; |
| } |
| } |
| } |
| if (long_options[option_index].has_arg == optional_argument && |
| !optarg && argv[optind] && argv[optind][0] != '-') |
| optarg = argv[optind++]; |
| |
| switch (ch) { |
| case OptHelp: |
| usage(); |
| return 0; |
| case OptSetDevice: |
| device = optarg; |
| if (device[0] >= '0' && device[0] <= '9' && device.length() <= 3) { |
| static char newdev[20]; |
| |
| sprintf(newdev, "/dev/cec%s", optarg); |
| device = newdev; |
| } |
| break; |
| case OptSetDriver: |
| driver = optarg; |
| break; |
| case OptSetAdapter: |
| adapter = optarg; |
| break; |
| case OptNoWarnings: |
| show_warnings = false; |
| break; |
| case OptShowMsgs: |
| show_msgs = true; |
| break; |
| case OptShowState: |
| show_state = true; |
| break; |
| case OptTogglePowerStatus: |
| toggle_power_status = strtoul(optarg, nullptr, 0); |
| break; |
| case OptIgnore: { |
| bool all_la = !strncmp(optarg, "all", 3); |
| bool all_opcodes = true; |
| const char *sep = std::strchr(optarg, ','); |
| unsigned la_mask = 0xffff, opcode, la = 0; |
| |
| if (sep) |
| all_opcodes = !strncmp(sep + 1, "all", 3); |
| if (!all_la) { |
| la = strtoul(optarg, nullptr, 0); |
| |
| if (la > 15) { |
| fprintf(stderr, "invalid logical address (> 15)\n"); |
| usage(); |
| return 1; |
| } |
| la_mask = 1 << la; |
| } |
| if (!all_opcodes) { |
| opcode = strtoul(sep + 1, nullptr, 0); |
| if (opcode > 255) { |
| fprintf(stderr, "invalid opcode (> 255)\n"); |
| usage(); |
| return 1; |
| } |
| node.ignore_opcode[opcode] |= la_mask; |
| break; |
| } |
| if (all_la && all_opcodes) { |
| fprintf(stderr, "all,all is invalid\n"); |
| usage(); |
| return 1; |
| } |
| node.ignore_la[la] = true; |
| break; |
| } |
| case OptWallClock: |
| case OptVerbose: |
| show_info = true; |
| show_msgs = true; |
| show_state = true; |
| break; |
| case OptVersion: |
| printf("cec-follower %s%s\n", PACKAGE_VERSION, STRING(GIT_COMMIT_CNT)); |
| if (strlen(STRING(GIT_SHA))) |
| printf("cec-follower SHA: %s %s\n", |
| STRING(GIT_SHA), STRING(GIT_COMMIT_DATE)); |
| std::exit(EXIT_SUCCESS); |
| 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 (device.empty() && (driver || adapter)) { |
| device = cec_device_find(driver, adapter); |
| if (device.empty()) { |
| fprintf(stderr, |
| "Could not find a CEC device for the given driver/adapter combination\n"); |
| std::exit(EXIT_FAILURE); |
| } |
| } |
| if (device.empty()) |
| device = "/dev/cec0"; |
| |
| if ((fd = open(device.c_str(), O_RDWR)) < 0) { |
| fprintf(stderr, "Failed to open %s: %s\n", device.c_str(), |
| strerror(errno)); |
| std::exit(EXIT_FAILURE); |
| } |
| |
| struct cec_caps caps = { }; |
| |
| node.fd = fd; |
| node.device = device.c_str(); |
| doioctl(&node, CEC_ADAP_G_CAPS, &caps); |
| node.caps = caps.capabilities; |
| node.available_log_addrs = caps.available_log_addrs; |
| node.state.service_by_dig_id = options[OptServiceByDigID]; |
| node.state.toggle_power_status = toggle_power_status; |
| state_init(node); |
| |
| if (strlen(STRING(GIT_SHA))) |
| printf("cec-follower SHA : %s %s\n", |
| STRING(GIT_SHA), STRING(GIT_COMMIT_DATE)); |
| |
| doioctl(&node, CEC_ADAP_G_PHYS_ADDR, &node.phys_addr); |
| |
| struct cec_log_addrs laddrs = { }; |
| doioctl(&node, CEC_ADAP_G_LOG_ADDRS, &laddrs); |
| node.adap_la_mask = laddrs.log_addr_mask; |
| node.cec_version = laddrs.cec_version; |
| |
| struct cec_connector_info conn_info = {}; |
| |
| doioctl(&node, CEC_ADAP_G_CONNECTOR_INFO, &conn_info); |
| |
| cec_driver_info(caps, laddrs, node.phys_addr, conn_info); |
| |
| /* |
| * For CEC 1.4, features of a logical address may still be |
| * filled in according to the CEC 2.0 guidelines even though |
| * the CEC framework won’t use the features in the CEC 2.0 |
| * CEC_MSG_REPORT_FEATURES. |
| */ |
| bool is_dev_feat = false; |
| |
| for (__u8 byte : laddrs.features[0]) { |
| if (is_dev_feat) { |
| node.source_has_arc_rx = (byte & CEC_OP_FEAT_DEV_SOURCE_HAS_ARC_RX) != 0; |
| node.sink_has_arc_tx = (byte & CEC_OP_FEAT_DEV_SINK_HAS_ARC_TX) != 0; |
| node.has_aud_rate = (byte & CEC_OP_FEAT_DEV_HAS_SET_AUDIO_RATE) != 0; |
| node.has_deck_ctl = (byte & CEC_OP_FEAT_DEV_HAS_DECK_CONTROL) != 0; |
| node.has_rec_tv = (byte & CEC_OP_FEAT_DEV_HAS_RECORD_TV_SCREEN) != 0; |
| node.has_osd_string = (byte & CEC_OP_FEAT_DEV_HAS_SET_OSD_STRING) != 0; |
| break; |
| } |
| if (byte & CEC_OP_FEAT_EXT) |
| continue; |
| if (!is_dev_feat) |
| is_dev_feat = true; |
| else |
| break; |
| } |
| printf("\n"); |
| |
| if (laddrs.num_log_addrs == 0 && (node.caps & CEC_CAP_LOG_ADDRS)) { |
| printf("\nFAIL: missing logical address(es), use cec-ctl to configure this\n"); |
| std::exit(EXIT_FAILURE); |
| } |
| |
| testProcessing(&node, options[OptWallClock]); |
| } |