/* * Copyright (C) 2019 BayLibre, SAS. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #define LOG_TAG "hdmi_cec" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct hdmicec_context { hdmi_cec_device_t device; /* must be first */ int cec_fd; unsigned int vendor_id; unsigned int type; unsigned int version; struct hdmi_port_info port_info; event_callback_t p_event_cb; void *cb_arg; pthread_t thread; int exit_fd; } hdmicec_context_t; static int hdmicec_add_logical_address(const struct hdmi_cec_device *dev, cec_logical_address_t addr) { struct hdmicec_context *ctx = (struct hdmicec_context *)dev; unsigned int la_type = CEC_LOG_ADDR_TYPE_UNREGISTERED; unsigned int all_dev_types = 0; unsigned int prim_type = 0xff; struct cec_log_addrs laddrs; int ret; ALOGD("%s: addr:%x\n", __func__, addr); if (addr >= CEC_ADDR_BROADCAST) return -1; ret = ioctl(ctx->cec_fd, CEC_ADAP_G_LOG_ADDRS, &laddrs); if (ret) return ret; memset(&laddrs, 0, sizeof(laddrs)); laddrs.cec_version = ctx->version; laddrs.vendor_id = ctx->vendor_id; switch (addr) { case CEC_LOG_ADDR_TV: prim_type = CEC_OP_PRIM_DEVTYPE_TV; la_type = CEC_LOG_ADDR_TYPE_TV; all_dev_types = CEC_OP_ALL_DEVTYPE_TV; break; case CEC_LOG_ADDR_RECORD_1: case CEC_LOG_ADDR_RECORD_2: case CEC_LOG_ADDR_RECORD_3: prim_type = CEC_OP_PRIM_DEVTYPE_RECORD; la_type = CEC_LOG_ADDR_TYPE_RECORD; all_dev_types = CEC_OP_ALL_DEVTYPE_RECORD; break; case CEC_LOG_ADDR_TUNER_1: case CEC_LOG_ADDR_TUNER_2: case CEC_LOG_ADDR_TUNER_3: case CEC_LOG_ADDR_TUNER_4: prim_type = CEC_OP_PRIM_DEVTYPE_TUNER; la_type = CEC_LOG_ADDR_TYPE_TUNER; all_dev_types = CEC_OP_ALL_DEVTYPE_TUNER; break; case CEC_LOG_ADDR_PLAYBACK_1: case CEC_LOG_ADDR_PLAYBACK_2: case CEC_LOG_ADDR_PLAYBACK_3: prim_type = CEC_OP_PRIM_DEVTYPE_PLAYBACK; la_type = CEC_LOG_ADDR_TYPE_PLAYBACK; all_dev_types = CEC_OP_ALL_DEVTYPE_PLAYBACK; laddrs.flags = CEC_LOG_ADDRS_FL_ALLOW_RC_PASSTHRU; break; case CEC_LOG_ADDR_AUDIOSYSTEM: prim_type = CEC_OP_PRIM_DEVTYPE_AUDIOSYSTEM; la_type = CEC_LOG_ADDR_TYPE_AUDIOSYSTEM; all_dev_types = CEC_OP_ALL_DEVTYPE_AUDIOSYSTEM; break; case CEC_LOG_ADDR_SPECIFIC: prim_type = CEC_OP_PRIM_DEVTYPE_PROCESSOR; la_type = CEC_LOG_ADDR_TYPE_SPECIFIC; all_dev_types = CEC_OP_ALL_DEVTYPE_SWITCH; break; case CEC_ADDR_RESERVED_1: case CEC_ADDR_RESERVED_2: case CEC_ADDR_UNREGISTERED: laddrs.flags = CEC_LOG_ADDRS_FL_ALLOW_UNREG_FALLBACK; break; } laddrs.num_log_addrs = 1; laddrs.log_addr[0] = addr; laddrs.log_addr_type[0] = la_type; laddrs.primary_device_type[0] = prim_type; laddrs.all_device_types[0] = all_dev_types; laddrs.features[0][0] = 0; laddrs.features[0][1] = 0; ret = ioctl(ctx->cec_fd, CEC_ADAP_S_LOG_ADDRS, &laddrs); if (ret) { ALOGD("%s: %m\n", __func__); return ret; } ALOGD("%s: log_addr_mask=%x\n", __func__, laddrs.log_addr_mask); return 0; } static void hdmicec_clear_logical_address(const struct hdmi_cec_device *dev) { struct hdmicec_context *ctx = (struct hdmicec_context *)dev; struct cec_log_addrs laddrs; int ret; memset(&laddrs, 0, sizeof(laddrs)); ret = ioctl(ctx->cec_fd, CEC_ADAP_S_LOG_ADDRS, &laddrs); if (ret) ALOGD("%s: %m\n", __func__); } static int hdmicec_get_physical_address(const struct hdmi_cec_device *dev, uint16_t *addr) { struct hdmicec_context *ctx = (struct hdmicec_context *)dev; int ret = ioctl(ctx->cec_fd, CEC_ADAP_G_PHYS_ADDR, addr); if (ret) ALOGD("%s: %m\n", __func__); return ret; } static int hdmicec_send_message(const struct hdmi_cec_device *dev, const cec_message_t *msg) { struct hdmicec_context *ctx = (struct hdmicec_context *)dev; struct cec_msg cec_msg; int ret; ALOGD("%s: len=%u\n", __func__, (unsigned int)msg->length); memset(&cec_msg, 0, sizeof(cec_msg)); cec_msg.msg[0] = (msg->initiator << 4) | msg->destination; memcpy(&cec_msg.msg[1], msg->body, msg->length); cec_msg.len = msg->length + 1; ret = ioctl(ctx->cec_fd, CEC_TRANSMIT, &cec_msg); if (ret) { ALOGD("%s: %m\n", __func__); return HDMI_RESULT_FAIL; } if (cec_msg.tx_status != CEC_TX_STATUS_OK) ALOGD("%s: tx_status=%d\n", __func__, cec_msg.tx_status); switch (cec_msg.tx_status) { case CEC_TX_STATUS_OK: return HDMI_RESULT_SUCCESS; case CEC_TX_STATUS_ARB_LOST: return HDMI_RESULT_BUSY; case CEC_TX_STATUS_NACK: return HDMI_RESULT_NACK; default: return HDMI_RESULT_FAIL; } } static void hdmicec_register_event_callback(const struct hdmi_cec_device *dev, event_callback_t callback, void *arg) { struct hdmicec_context *ctx = (struct hdmicec_context *)dev; ctx->p_event_cb = callback; ctx->cb_arg = arg; } static void hdmicec_get_version(const struct hdmi_cec_device *dev, int *version) { struct hdmicec_context *ctx = (struct hdmicec_context *)dev; *version = ctx->version; } static void hdmicec_get_vendor_id(const struct hdmi_cec_device *dev, uint32_t *vendor_id) { struct hdmicec_context *ctx = (struct hdmicec_context *)dev; *vendor_id = ctx->vendor_id; } static void hdmicec_get_port_info(const struct hdmi_cec_device *dev, struct hdmi_port_info *list[], int *total) { struct hdmicec_context *ctx = (struct hdmicec_context *)dev; int ret; ret = ioctl(ctx->cec_fd, CEC_ADAP_G_PHYS_ADDR, &ctx->port_info.physical_address); if (ret) ALOGD("%s: %m\n", __func__); ALOGD("type:%s, id:%d, cec support:%d, arc support:%d, physical address:%x", ctx->port_info.type ? "output" : "input", ctx->port_info.port_id, ctx->port_info.cec_supported, ctx->port_info.arc_supported, ctx->port_info.physical_address); *list = &ctx->port_info; *total = 1; } static void hdmicec_set_option(const struct hdmi_cec_device *dev, int flag, int value) { (void)dev; ALOGD("%s: flag=%d, value=%d", __func__, flag, value); switch (flag) { case HDMI_OPTION_ENABLE_CEC: case HDMI_OPTION_WAKEUP: case HDMI_OPTION_SYSTEM_CEC_CONTROL: /* TOFIX */ break; } } static int hdmicec_is_connected(const struct hdmi_cec_device *dev, int port_id) { struct hdmicec_context *ctx = (struct hdmicec_context *)dev; int ret; (void)port_id; ret = ioctl(ctx->cec_fd, CEC_ADAP_G_PHYS_ADDR, &ctx->port_info.physical_address); if (ret) { ALOGD("%s: %m\n", __func__); return ret; } if (ctx->port_info.physical_address == CEC_PHYS_ADDR_INVALID) return false; return true; } static void *event_thread(void *arg) { struct hdmicec_context *ctx = (struct hdmicec_context *)arg; int ret; struct pollfd ufds[3] = { { ctx->cec_fd, POLLIN, 0 }, { ctx->cec_fd, POLLERR, 0 }, { ctx->exit_fd, POLLIN, 0 }, }; ALOGI("%s start!", __func__); while (1) { ufds[0].revents = 0; ufds[1].revents = 0; ufds[2].revents = 0; ret = poll(ufds, 3, -1); if (ret <= 0) continue; if (ufds[2].revents == POLLIN) /* Exit */ break; if (ufds[1].revents == POLLERR) { /* CEC Event */ hdmi_event_t event = { }; struct cec_event ev; ret = ioctl(ctx->cec_fd, CEC_DQEVENT, &ev); if (ret) continue; if (ev.event == CEC_EVENT_STATE_CHANGE) { event.type = HDMI_EVENT_HOT_PLUG; event.dev = &ctx->device; event.hotplug.port_id = 1; if (ev.state_change.phys_addr == CEC_PHYS_ADDR_INVALID) event.hotplug.connected = false; else event.hotplug.connected = true; if (ctx->p_event_cb != NULL) { ctx->p_event_cb(&event, ctx->cb_arg); } else { ALOGE("no event callback for hotplug\n"); } } } if (ufds[0].revents == POLLIN) { /* CEC Driver */ struct cec_msg msg = { }; hdmi_event_t event = { }; ret = ioctl(ctx->cec_fd, CEC_RECEIVE, &msg); if (ret) { ALOGE("%s: CEC_RECEIVE error (%m)\n", __func__); continue; } if (msg.rx_status != CEC_RX_STATUS_OK) { ALOGD("%s: rx_status=%d\n", __func__, msg.rx_status); continue; } if (ctx->p_event_cb != NULL) { event.type = HDMI_EVENT_CEC_MESSAGE; event.dev = &ctx->device; event.cec.initiator = msg.msg[0] >> 4; event.cec.destination = msg.msg[0] & 0xf; event.cec.length = msg.len - 1; memcpy(event.cec.body, &msg.msg[1], msg.len - 1); ctx->p_event_cb(&event, ctx->cb_arg); } else { ALOGE("no event callback for msg\n"); } } } ALOGI("%s exit!", __func__); return NULL; } static void hdmicec_set_arc(const struct hdmi_cec_device *dev, int port_id, int flag) { (void)dev; (void)port_id; (void)flag; /* Not supported */ } static int hdmicec_close(struct hdmi_cec_device *dev) { struct hdmicec_context *ctx = (struct hdmicec_context *)dev; uint64_t tmp = 1; ALOGD("%s\n", __func__); if (ctx->exit_fd > 0) { write(ctx->exit_fd, &tmp, sizeof(tmp)); pthread_join(ctx->thread, NULL); } if (ctx->cec_fd > 0) close(ctx->cec_fd); if (ctx->exit_fd > 0) close(ctx->exit_fd); free(ctx); return 0; } static int cec_init(struct hdmicec_context *ctx) { struct cec_log_addrs laddrs = {}; struct cec_caps caps = {}; uint32_t mode; int ret; // Ensure the CEC device supports required capabilities ret = ioctl(ctx->cec_fd, CEC_ADAP_G_CAPS, &caps); if (ret) return ret; if (!(caps.capabilities & (CEC_CAP_LOG_ADDRS | CEC_CAP_TRANSMIT | CEC_CAP_PASSTHROUGH))) { ALOGE("%s: wrong cec adapter capabilities %x\n", __func__, caps.capabilities); return -1; } // This is an exclusive follower, in addition put the CEC device into passthrough mode mode = CEC_MODE_INITIATOR | CEC_MODE_EXCL_FOLLOWER_PASSTHRU; ret = ioctl(ctx->cec_fd, CEC_S_MODE, &mode); if (ret) return ret; ctx->type = property_get_int32("ro.hdmi.device_type", CEC_DEVICE_PLAYBACK); ctx->vendor_id = property_get_int32("ro.hdmi.vendor_id", 0x000c03 /* HDMI LLC vendor ID */); ctx->version = property_get_bool("ro.hdmi.cec_version", CEC_OP_CEC_VERSION_1_4); ctx->port_info.type = ctx->type == CEC_DEVICE_TV ? HDMI_INPUT : HDMI_OUTPUT; ctx->port_info.port_id = 1; ctx->port_info.cec_supported = 1; ctx->port_info.arc_supported = 0; ALOGD("%s: type=%d\n", __func__, ctx->type); ALOGD("%s: vendor_id=%04x\n", __func__, ctx->vendor_id); ALOGD("%s: version=%d\n", __func__, ctx->version); memset(&laddrs, 0, sizeof(laddrs)); ret = ioctl(ctx->cec_fd, CEC_ADAP_S_LOG_ADDRS, &laddrs); if (ret) return ret; ALOGD("%s: initialized CEC controller\n", __func__); return ret; } static int open_hdmi_cec(const struct hw_module_t *module, const char *id, struct hw_device_t **device) { char *path = "/dev/cec0"; hdmicec_context_t *ctx; int ret; ALOGD("%s: id=%s\n", __func__, id); ctx = malloc(sizeof(struct hdmicec_context)); if (!ctx) return -ENOMEM; memset(ctx, 0, sizeof(*ctx)); ctx->cec_fd = open(path, O_RDWR); if (ctx->cec_fd < 0) { ALOGE("faild to open %s, ret=%s\n", path, strerror(errno)); goto fail; } ctx->exit_fd = eventfd(0, EFD_NONBLOCK); if (ctx->exit_fd < 0) { ALOGE("faild to open eventfd, ret = %d\n", errno); goto fail; } ctx->device.common.tag = HARDWARE_DEVICE_TAG; ctx->device.common.version = HDMI_CEC_DEVICE_API_VERSION_1_0; ctx->device.common.module = (struct hw_module_t *)module; ctx->device.common.close = (int (*)(struct hw_device_t* device))hdmicec_close; ctx->device.add_logical_address = hdmicec_add_logical_address; ctx->device.clear_logical_address = hdmicec_clear_logical_address; ctx->device.get_physical_address = hdmicec_get_physical_address; ctx->device.send_message = hdmicec_send_message; ctx->device.register_event_callback = hdmicec_register_event_callback; ctx->device.get_version = hdmicec_get_version; ctx->device.get_vendor_id = hdmicec_get_vendor_id; ctx->device.get_port_info = hdmicec_get_port_info; ctx->device.set_option = hdmicec_set_option; ctx->device.set_audio_return_channel = hdmicec_set_arc; ctx->device.is_connected = hdmicec_is_connected; /* init status */ ret = cec_init(ctx); if (ret) goto fail; *device = &ctx->device.common; /* thread loop for receiving cec msg */ if (pthread_create(&ctx->thread, NULL, event_thread, ctx)) { ALOGE("Can't create event thread: %s\n", strerror(errno)); goto fail; } return 0; fail: hdmicec_close((struct hdmi_cec_device *)ctx); return -errno; } /* module method */ static struct hw_module_methods_t hdmi_cec_module_methods = { .open = open_hdmi_cec, }; /* hdmi_cec module */ struct hw_module_t HAL_MODULE_INFO_SYM = { .tag = HARDWARE_MODULE_TAG, .version_major = 1, .version_minor = 0, .id = HDMI_CEC_HARDWARE_MODULE_ID, .name = "YUKAWA HDMI CEC module", .author = "The Android Open Source Project", .methods = &hdmi_cec_module_methods, };