root/drivers/media/cec/usb/extron-da-hd-4k-plus/cec-splitter.c
// SPDX-License-Identifier: GPL-2.0-only

/*
 * Copyright 2021-2024 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
 */

#include <media/cec.h>

#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;
}