// SPDX-License-Identifier: GPL-2.0-only /* * Copyright 2021-2024 Cisco Systems, Inc. and/or its affiliates. All rights reserved. */ #include #include "cec-splitter.h" /* * Helper function to reply to a received message with a Feature Abort * message. */ static int cec_feature_abort_reason(struct cec_adapter *adap, struct cec_msg *msg, u8 reason) { struct cec_msg tx_msg = { }; /* * Don't reply with CEC_MSG_FEATURE_ABORT to a CEC_MSG_FEATURE_ABORT * message! */ if (msg->msg[1] == CEC_MSG_FEATURE_ABORT) return 0; /* Don't Feature Abort messages from 'Unregistered' */ if (cec_msg_initiator(msg) == CEC_LOG_ADDR_UNREGISTERED) return 0; cec_msg_set_reply_to(&tx_msg, msg); cec_msg_feature_abort(&tx_msg, msg->msg[1], reason); return cec_transmit_msg(adap, &tx_msg, false); } /* Transmit an Active Source message from this output port to a sink */ static void cec_port_out_active_source(struct cec_splitter_port *p) { struct cec_adapter *adap = p->adap; struct cec_msg msg; if (!adap->is_configured) return; p->is_active_source = true; cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0); cec_msg_active_source(&msg, adap->phys_addr); cec_transmit_msg(adap, &msg, false); } /* Transmit Active Source messages from all output ports to the sinks */ static void cec_out_active_source(struct cec_splitter *splitter) { unsigned int i; for (i = 0; i < splitter->num_out_ports; i++) cec_port_out_active_source(splitter->ports[i]); } /* Transmit a Standby message from this output port to a sink */ static void cec_port_out_standby(struct cec_splitter_port *p) { struct cec_adapter *adap = p->adap; struct cec_msg msg; if (!adap->is_configured) return; cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0); cec_msg_standby(&msg); cec_transmit_msg(adap, &msg, false); } /* Transmit Standby messages from all output ports to the sinks */ static void cec_out_standby(struct cec_splitter *splitter) { unsigned int i; for (i = 0; i < splitter->num_out_ports; i++) cec_port_out_standby(splitter->ports[i]); } /* Transmit an Image/Text View On message from this output port to a sink */ static void cec_port_out_wakeup(struct cec_splitter_port *p, u8 opcode) { struct cec_adapter *adap = p->adap; u8 la = adap->log_addrs.log_addr[0]; struct cec_msg msg; if (la == CEC_LOG_ADDR_INVALID) la = CEC_LOG_ADDR_UNREGISTERED; cec_msg_init(&msg, la, 0); msg.len = 2; msg.msg[1] = opcode; cec_transmit_msg(adap, &msg, false); } /* Transmit Image/Text View On messages from all output ports to the sinks */ static void cec_out_wakeup(struct cec_splitter *splitter, u8 opcode) { unsigned int i; for (i = 0; i < splitter->num_out_ports; i++) cec_port_out_wakeup(splitter->ports[i], opcode); } /* * Update the power state of the unconfigured CEC device to either * Off or On depending on the current state of the splitter. * This keeps the outputs in a consistent state. */ void cec_splitter_unconfigured_output(struct cec_splitter_port *p) { p->video_latency = 1; p->power_status = p->splitter->is_standby ? CEC_OP_POWER_STATUS_TO_STANDBY : CEC_OP_POWER_STATUS_TO_ON; /* The adapter was unconfigured, so clear the sequence and ts values */ p->out_give_device_power_status_seq = 0; p->out_give_device_power_status_ts = ktime_set(0, 0); p->out_request_current_latency_seq = 0; p->out_request_current_latency_ts = ktime_set(0, 0); } /* * Update the power state of the newly configured CEC device to either * Off or On depending on the current state of the splitter. * This keeps the outputs in a consistent state. */ void cec_splitter_configured_output(struct cec_splitter_port *p) { p->video_latency = 1; p->power_status = p->splitter->is_standby ? CEC_OP_POWER_STATUS_TO_STANDBY : CEC_OP_POWER_STATUS_TO_ON; if (p->splitter->is_standby) { /* * Some sinks only obey Standby if it comes from the * active source. */ cec_port_out_active_source(p); cec_port_out_standby(p); } else { cec_port_out_wakeup(p, CEC_MSG_IMAGE_VIEW_ON); } } /* Pass the in_msg on to all output ports */ static void cec_out_passthrough(struct cec_splitter *splitter, const struct cec_msg *in_msg) { unsigned int i; for (i = 0; i < splitter->num_out_ports; i++) { struct cec_splitter_port *p = splitter->ports[i]; struct cec_adapter *adap = p->adap; struct cec_msg msg; if (!adap->is_configured) continue; cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0); msg.len = in_msg->len; memcpy(msg.msg + 1, in_msg->msg + 1, msg.len - 1); cec_transmit_msg(adap, &msg, false); } } /* * See if all output ports received the Report Current Latency message, * and if so, transmit the result from the input port to the video source. */ static void cec_out_report_current_latency(struct cec_splitter *splitter, struct cec_adapter *input_adap) { struct cec_msg reply = {}; unsigned int reply_lat = 0; unsigned int cnt = 0; unsigned int i; for (i = 0; i < splitter->num_out_ports; i++) { struct cec_splitter_port *p = splitter->ports[i]; struct cec_adapter *adap = p->adap; /* Skip unconfigured ports */ if (!adap->is_configured) continue; /* Return if a port is still waiting for a reply */ if (p->out_request_current_latency_seq) return; reply_lat += p->video_latency - 1; cnt++; } /* * All ports that can reply, replied, so clear the sequence * and timestamp values. */ for (i = 0; i < splitter->num_out_ports; i++) { struct cec_splitter_port *p = splitter->ports[i]; p->out_request_current_latency_seq = 0; p->out_request_current_latency_ts = ktime_set(0, 0); } /* * Return if there were no replies or the input port is no longer * configured. */ if (!cnt || !input_adap->is_configured) return; /* Reply with the average latency */ reply_lat = 1 + reply_lat / cnt; cec_msg_init(&reply, input_adap->log_addrs.log_addr[0], splitter->request_current_latency_dest); cec_msg_report_current_latency(&reply, input_adap->phys_addr, reply_lat, 1, 1, 1); cec_transmit_msg(input_adap, &reply, false); } /* Transmit Request Current Latency to all output ports */ static int cec_out_request_current_latency(struct cec_splitter *splitter) { ktime_t now = ktime_get(); bool error = true; unsigned int i; for (i = 0; i < splitter->num_out_ports; i++) { struct cec_splitter_port *p = splitter->ports[i]; struct cec_adapter *adap = p->adap; if (!adap->is_configured) { /* Clear if not configured */ p->out_request_current_latency_seq = 0; p->out_request_current_latency_ts = ktime_set(0, 0); } else if (!p->out_request_current_latency_seq) { /* * Keep the old ts if an earlier request is still * pending. This ensures that the request will * eventually time out based on the timestamp of * the first request if the sink is unresponsive. */ p->out_request_current_latency_ts = now; } } for (i = 0; i < splitter->num_out_ports; i++) { struct cec_splitter_port *p = splitter->ports[i]; struct cec_adapter *adap = p->adap; struct cec_msg msg; if (!adap->is_configured) continue; cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0); cec_msg_request_current_latency(&msg, true, adap->phys_addr); if (cec_transmit_msg(adap, &msg, false)) continue; p->out_request_current_latency_seq = msg.sequence | (1U << 31); error = false; } return error ? -ENODEV : 0; } /* * See if all output ports received the Report Power Status message, * and if so, transmit the result from the input port to the video source. */ static void cec_out_report_power_status(struct cec_splitter *splitter, struct cec_adapter *input_adap) { struct cec_msg reply = {}; /* The target power status of the splitter itself */ u8 splitter_pwr = splitter->is_standby ? CEC_OP_POWER_STATUS_STANDBY : CEC_OP_POWER_STATUS_ON; /* * The transient power status of the splitter, used if not all * output report the target power status. */ u8 splitter_transient_pwr = splitter->is_standby ? CEC_OP_POWER_STATUS_TO_STANDBY : CEC_OP_POWER_STATUS_TO_ON; u8 reply_pwr = splitter_pwr; unsigned int i; for (i = 0; i < splitter->num_out_ports; i++) { struct cec_splitter_port *p = splitter->ports[i]; /* Skip if no sink was found (HPD was low for more than 5s) */ if (!p->found_sink) continue; /* Return if a port is still waiting for a reply */ if (p->out_give_device_power_status_seq) return; if (p->power_status != splitter_pwr) reply_pwr = splitter_transient_pwr; } /* * All ports that can reply, replied, so clear the sequence * and timestamp values. */ for (i = 0; i < splitter->num_out_ports; i++) { struct cec_splitter_port *p = splitter->ports[i]; p->out_give_device_power_status_seq = 0; p->out_give_device_power_status_ts = ktime_set(0, 0); } /* Return if the input port is no longer configured. */ if (!input_adap->is_configured) return; /* Reply with the new power status */ cec_msg_init(&reply, input_adap->log_addrs.log_addr[0], splitter->give_device_power_status_dest); cec_msg_report_power_status(&reply, reply_pwr); cec_transmit_msg(input_adap, &reply, false); } /* Transmit Give Device Power Status to all output ports */ static int cec_out_give_device_power_status(struct cec_splitter *splitter) { ktime_t now = ktime_get(); bool error = true; unsigned int i; for (i = 0; i < splitter->num_out_ports; i++) { struct cec_splitter_port *p = splitter->ports[i]; struct cec_adapter *adap = p->adap; /* * Keep the old ts if an earlier request is still * pending. This ensures that the request will * eventually time out based on the timestamp of * the first request if the sink is unresponsive. */ if (adap->is_configured && !p->out_give_device_power_status_seq) p->out_give_device_power_status_ts = now; } for (i = 0; i < splitter->num_out_ports; i++) { struct cec_splitter_port *p = splitter->ports[i]; struct cec_adapter *adap = p->adap; struct cec_msg msg; if (!adap->is_configured) continue; cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0); cec_msg_give_device_power_status(&msg, true); if (cec_transmit_msg(adap, &msg, false)) continue; p->out_give_device_power_status_seq = msg.sequence | (1U << 31); error = false; } return error ? -ENODEV : 0; } /* * CEC messages received on the HDMI input of the splitter are * forwarded (if relevant) to the HDMI outputs of the splitter. */ int cec_splitter_received_input(struct cec_splitter_port *p, struct cec_msg *msg) { if (!cec_msg_status_is_ok(msg)) return 0; if (msg->len < 2) return -ENOMSG; switch (msg->msg[1]) { case CEC_MSG_DEVICE_VENDOR_ID: case CEC_MSG_REPORT_POWER_STATUS: case CEC_MSG_SET_STREAM_PATH: case CEC_MSG_ROUTING_CHANGE: case CEC_MSG_REQUEST_ACTIVE_SOURCE: case CEC_MSG_SYSTEM_AUDIO_MODE_STATUS: return 0; case CEC_MSG_STANDBY: p->splitter->is_standby = true; cec_out_standby(p->splitter); return 0; case CEC_MSG_IMAGE_VIEW_ON: case CEC_MSG_TEXT_VIEW_ON: p->splitter->is_standby = false; cec_out_wakeup(p->splitter, msg->msg[1]); return 0; case CEC_MSG_ACTIVE_SOURCE: cec_out_active_source(p->splitter); return 0; case CEC_MSG_SET_SYSTEM_AUDIO_MODE: cec_out_passthrough(p->splitter, msg); return 0; case CEC_MSG_GIVE_DEVICE_POWER_STATUS: p->splitter->give_device_power_status_dest = cec_msg_initiator(msg); if (cec_out_give_device_power_status(p->splitter)) cec_feature_abort_reason(p->adap, msg, CEC_OP_ABORT_INCORRECT_MODE); return 0; case CEC_MSG_REQUEST_CURRENT_LATENCY: { u16 pa; p->splitter->request_current_latency_dest = cec_msg_initiator(msg); cec_ops_request_current_latency(msg, &pa); if (pa == p->adap->phys_addr && cec_out_request_current_latency(p->splitter)) cec_feature_abort_reason(p->adap, msg, CEC_OP_ABORT_INCORRECT_MODE); return 0; } default: return -ENOMSG; } return -ENOMSG; } void cec_splitter_nb_transmit_canceled_output(struct cec_splitter_port *p, const struct cec_msg *msg, struct cec_adapter *input_adap) { struct cec_splitter *splitter = p->splitter; u32 seq = msg->sequence | (1U << 31); /* * If this is the result of a failed non-blocking transmit, or it is * the result of the failed reply to a non-blocking transmit, then * check if the original transmit was to get the current power status * or latency and, if so, assume that the remove device is for one * reason or another unavailable and assume that it is in the same * power status as the splitter, or has no video latency. */ if ((cec_msg_recv_is_tx_result(msg) && !(msg->tx_status & CEC_TX_STATUS_OK)) || (cec_msg_recv_is_rx_result(msg) && !(msg->rx_status & CEC_RX_STATUS_OK))) { u8 tx_op = msg->msg[1]; if (msg->len < 2) return; if (cec_msg_recv_is_rx_result(msg) && (msg->rx_status & CEC_RX_STATUS_FEATURE_ABORT)) tx_op = msg->msg[2]; switch (tx_op) { case CEC_MSG_GIVE_DEVICE_POWER_STATUS: if (p->out_give_device_power_status_seq != seq) break; p->out_give_device_power_status_seq = 0; p->out_give_device_power_status_ts = ktime_set(0, 0); p->power_status = splitter->is_standby ? CEC_OP_POWER_STATUS_STANDBY : CEC_OP_POWER_STATUS_ON; cec_out_report_power_status(splitter, input_adap); break; case CEC_MSG_REQUEST_CURRENT_LATENCY: if (p->out_request_current_latency_seq != seq) break; p->video_latency = 1; p->out_request_current_latency_seq = 0; p->out_request_current_latency_ts = ktime_set(0, 0); cec_out_report_current_latency(splitter, input_adap); break; } return; } if (cec_msg_recv_is_tx_result(msg)) { if (p->out_request_current_latency_seq != seq) return; p->out_request_current_latency_ts = ns_to_ktime(msg->tx_ts); return; } } /* * CEC messages received on an HDMI output of the splitter * are processed here. */ int cec_splitter_received_output(struct cec_splitter_port *p, struct cec_msg *msg, struct cec_adapter *input_adap) { struct cec_adapter *adap = p->adap; struct cec_splitter *splitter = p->splitter; u32 seq = msg->sequence | (1U << 31); struct cec_msg reply = {}; u16 pa; if (!adap->is_configured || msg->len < 2) return -ENOMSG; switch (msg->msg[1]) { case CEC_MSG_REPORT_POWER_STATUS: { u8 pwr; cec_ops_report_power_status(msg, &pwr); if (pwr > CEC_OP_POWER_STATUS_TO_STANDBY) pwr = splitter->is_standby ? CEC_OP_POWER_STATUS_TO_STANDBY : CEC_OP_POWER_STATUS_TO_ON; p->power_status = pwr; if (p->out_give_device_power_status_seq == seq) { p->out_give_device_power_status_seq = 0; p->out_give_device_power_status_ts = ktime_set(0, 0); } cec_out_report_power_status(splitter, input_adap); return 0; } case CEC_MSG_REPORT_CURRENT_LATENCY: { u8 video_lat; u8 low_lat_mode; u8 audio_out_comp; u8 audio_out_delay; cec_ops_report_current_latency(msg, &pa, &video_lat, &low_lat_mode, &audio_out_comp, &audio_out_delay); if (!video_lat || video_lat >= 252) video_lat = 1; p->video_latency = video_lat; if (p->out_request_current_latency_seq == seq) { p->out_request_current_latency_seq = 0; p->out_request_current_latency_ts = ktime_set(0, 0); } cec_out_report_current_latency(splitter, input_adap); return 0; } case CEC_MSG_STANDBY: case CEC_MSG_ROUTING_CHANGE: case CEC_MSG_GIVE_SYSTEM_AUDIO_MODE_STATUS: return 0; case CEC_MSG_ACTIVE_SOURCE: cec_ops_active_source(msg, &pa); if (pa == 0) p->is_active_source = false; return 0; case CEC_MSG_REQUEST_ACTIVE_SOURCE: if (!p->is_active_source) return 0; cec_msg_set_reply_to(&reply, msg); cec_msg_active_source(&reply, adap->phys_addr); cec_transmit_msg(adap, &reply, false); return 0; case CEC_MSG_GIVE_DEVICE_POWER_STATUS: cec_msg_set_reply_to(&reply, msg); cec_msg_report_power_status(&reply, splitter->is_standby ? CEC_OP_POWER_STATUS_STANDBY : CEC_OP_POWER_STATUS_ON); cec_transmit_msg(adap, &reply, false); return 0; case CEC_MSG_SET_STREAM_PATH: cec_ops_set_stream_path(msg, &pa); if (pa == adap->phys_addr) { cec_msg_set_reply_to(&reply, msg); cec_msg_active_source(&reply, pa); cec_transmit_msg(adap, &reply, false); } return 0; default: return -ENOMSG; } return -ENOMSG; } /* * Called every second to check for timed out messages and whether there * still is a video sink connected or not. * * Returns true if sinks were lost. */ bool cec_splitter_poll(struct cec_splitter *splitter, struct cec_adapter *input_adap, bool debug) { ktime_t now = ktime_get(); u8 pwr = splitter->is_standby ? CEC_OP_POWER_STATUS_STANDBY : CEC_OP_POWER_STATUS_ON; unsigned int max_delay_ms = input_adap->xfer_timeout_ms + 2000; unsigned int i; bool res = false; for (i = 0; i < splitter->num_out_ports; i++) { struct cec_splitter_port *p = splitter->ports[i]; s64 pwr_delta, lat_delta; bool pwr_timeout, lat_timeout; if (!p) continue; pwr_delta = ktime_ms_delta(now, p->out_give_device_power_status_ts); pwr_timeout = p->out_give_device_power_status_seq && pwr_delta >= max_delay_ms; lat_delta = ktime_ms_delta(now, p->out_request_current_latency_ts); lat_timeout = p->out_request_current_latency_seq && lat_delta >= max_delay_ms; /* * If the HPD is low for more than 5 seconds, then assume no display * is connected. */ if (p->found_sink && ktime_to_ns(p->lost_sink_ts) && ktime_ms_delta(now, p->lost_sink_ts) > 5000) { if (debug) dev_info(splitter->dev, "port %u: HPD low for more than 5s, assume no sink is connected.\n", p->port); p->found_sink = false; p->lost_sink_ts = ktime_set(0, 0); res = true; } /* * If the power status request timed out, then set the port's * power status to that of the splitter, ensuring a consistent * power state. */ if (pwr_timeout) { mutex_lock(&p->adap->lock); if (debug) dev_info(splitter->dev, "port %u: give up on power status for seq %u\n", p->port, p->out_give_device_power_status_seq & ~(1 << 31)); p->power_status = pwr; p->out_give_device_power_status_seq = 0; p->out_give_device_power_status_ts = ktime_set(0, 0); mutex_unlock(&p->adap->lock); cec_out_report_power_status(splitter, input_adap); } /* * If the current latency request timed out, then set the port's * latency to 1. */ if (lat_timeout) { mutex_lock(&p->adap->lock); if (debug) dev_info(splitter->dev, "port %u: give up on latency for seq %u\n", p->port, p->out_request_current_latency_seq & ~(1 << 31)); p->video_latency = 1; p->out_request_current_latency_seq = 0; p->out_request_current_latency_ts = ktime_set(0, 0); mutex_unlock(&p->adap->lock); cec_out_report_current_latency(splitter, input_adap); } } return res; }