summaryrefslogblamecommitdiffstats
path: root/drivers/media/usb/rainshadow-cec/rainshadow-cec.c
blob: cecdcbcd400c5f6c1a6c1f841d7582b9bf0f83b4 (plain) (tree)



































































































                                                                            
                                                                        

                      
                                                                          

                      
                                                                               










                                                           


                                                          


                                                                       
                 
 




                                                                 


































































































































































                                                                                              
                                                       















                                                                      
                                                                              











                                                                   
                                                                          







                                                      
                                        

























                                                            
                                                        






















                                                            
/*
 * RainShadow Tech HDMI CEC driver
 *
 * Copyright 2016 Hans Verkuil <hverkuil@xs4all.nl
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version of 2 of the License, or (at your
 * option) any later version. See the file COPYING in the main directory of
 * this archive for more details.
 */

/*
 * Notes:
 *
 * The higher level protocols are currently disabled. This can be added
 * later, similar to how this is done for the Pulse Eight CEC driver.
 *
 * Documentation of the protocol is available here:
 *
 * http://rainshadowtech.com/doc/HDMICECtoUSBandRS232v2.0.pdf
 */

#include <linux/completion.h>
#include <linux/ctype.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/serio.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/time.h>
#include <linux/workqueue.h>

#include <media/cec.h>

MODULE_AUTHOR("Hans Verkuil <hverkuil@xs4all.nl>");
MODULE_DESCRIPTION("RainShadow Tech HDMI CEC driver");
MODULE_LICENSE("GPL");

#define DATA_SIZE 256

struct rain {
	struct device *dev;
	struct serio *serio;
	struct cec_adapter *adap;
	struct completion cmd_done;
	struct work_struct work;

	/* Low-level ringbuffer, collecting incoming characters */
	char buf[DATA_SIZE];
	unsigned int buf_rd_idx;
	unsigned int buf_wr_idx;
	unsigned int buf_len;
	spinlock_t buf_lock;

	/* command buffer */
	char cmd[DATA_SIZE];
	unsigned int cmd_idx;
	bool cmd_started;

	/* reply to a command, only used to store the firmware version */
	char cmd_reply[DATA_SIZE];

	struct mutex write_lock;
};

static void rain_process_msg(struct rain *rain)
{
	struct cec_msg msg = {};
	const char *cmd = rain->cmd + 3;
	int stat = -1;

	for (; *cmd; cmd++) {
		if (!isxdigit(*cmd))
			continue;
		if (isxdigit(cmd[0]) && isxdigit(cmd[1])) {
			if (msg.len == CEC_MAX_MSG_SIZE)
				break;
			if (hex2bin(msg.msg + msg.len, cmd, 1))
				continue;
			msg.len++;
			cmd++;
			continue;
		}
		if (!cmd[1])
			stat = hex_to_bin(cmd[0]);
		break;
	}

	if (rain->cmd[0] == 'R') {
		if (stat == 1 || stat == 2)
			cec_received_msg(rain->adap, &msg);
		return;
	}

	switch (stat) {
	case 1:
		cec_transmit_attempt_done(rain->adap, CEC_TX_STATUS_OK);
		break;
	case 2:
		cec_transmit_attempt_done(rain->adap, CEC_TX_STATUS_NACK);
		break;
	default:
		cec_transmit_attempt_done(rain->adap, CEC_TX_STATUS_LOW_DRIVE);
		break;
	}
}

static void rain_irq_work_handler(struct work_struct *work)
{
	struct rain *rain =
		container_of(work, struct rain, work);

	while (true) {
		unsigned long flags;
		char data;

		spin_lock_irqsave(&rain->buf_lock, flags);
		if (!rain->buf_len) {
			spin_unlock_irqrestore(&rain->buf_lock, flags);
			break;
		}

		data = rain->buf[rain->buf_rd_idx];
		rain->buf_len--;
		rain->buf_rd_idx = (rain->buf_rd_idx + 1) & 0xff;

		spin_unlock_irqrestore(&rain->buf_lock, flags);

		if (!rain->cmd_started && data != '?')
			continue;

		switch (data) {
		case '\r':
			rain->cmd[rain->cmd_idx] = '\0';
			dev_dbg(rain->dev, "received: %s\n", rain->cmd);
			if (!memcmp(rain->cmd, "REC", 3) ||
			    !memcmp(rain->cmd, "STA", 3)) {
				rain_process_msg(rain);
			} else {
				strcpy(rain->cmd_reply, rain->cmd);
				complete(&rain->cmd_done);
			}
			rain->cmd_idx = 0;
			rain->cmd_started = false;
			break;

		case '\n':
			rain->cmd_idx = 0;
			rain->cmd_started = false;
			break;

		case '?':
			rain->cmd_idx = 0;
			rain->cmd_started = true;
			break;

		default:
			if (rain->cmd_idx >= DATA_SIZE - 1) {
				dev_dbg(rain->dev,
					"throwing away %d bytes of garbage\n", rain->cmd_idx);
				rain->cmd_idx = 0;
			}
			rain->cmd[rain->cmd_idx++] = data;
			break;
		}
	}
}

static irqreturn_t rain_interrupt(struct serio *serio, unsigned char data,
				    unsigned int flags)
{
	struct rain *rain = serio_get_drvdata(serio);

	if (rain->buf_len == DATA_SIZE) {
		dev_warn_once(rain->dev, "buffer overflow\n");
		return IRQ_HANDLED;
	}
	spin_lock(&rain->buf_lock);
	rain->buf_len++;
	rain->buf[rain->buf_wr_idx] = data;
	rain->buf_wr_idx = (rain->buf_wr_idx + 1) & 0xff;
	spin_unlock(&rain->buf_lock);
	schedule_work(&rain->work);
	return IRQ_HANDLED;
}

static void rain_disconnect(struct serio *serio)
{
	struct rain *rain = serio_get_drvdata(serio);

	cancel_work_sync(&rain->work);
	cec_unregister_adapter(rain->adap);
	dev_info(&serio->dev, "disconnected\n");
	serio_close(serio);
	serio_set_drvdata(serio, NULL);
	kfree(rain);
}

static int rain_send(struct rain *rain, const char *command)
{
	int err = serio_write(rain->serio, '!');

	dev_dbg(rain->dev, "send: %s\n", command);
	while (!err && *command)
		err = serio_write(rain->serio, *command++);
	if (!err)
		err = serio_write(rain->serio, '~');

	return err;
}

static int rain_send_and_wait(struct rain *rain,
			      const char *cmd, const char *reply)
{
	int err;

	init_completion(&rain->cmd_done);

	mutex_lock(&rain->write_lock);
	err = rain_send(rain, cmd);
	if (err)
		goto err;

	if (!wait_for_completion_timeout(&rain->cmd_done, HZ)) {
		err = -ETIMEDOUT;
		goto err;
	}
	if (reply && strncmp(rain->cmd_reply, reply, strlen(reply))) {
		dev_dbg(rain->dev,
			 "transmit of '%s': received '%s' instead of '%s'\n",
			 cmd, rain->cmd_reply, reply);
		err = -EIO;
	}
err:
	mutex_unlock(&rain->write_lock);
	return err;
}

static int rain_setup(struct rain *rain, struct serio *serio,
			struct cec_log_addrs *log_addrs, u16 *pa)
{
	int err;

	err = rain_send_and_wait(rain, "R", "REV");
	if (err)
		return err;
	dev_info(rain->dev, "Firmware version %s\n", rain->cmd_reply + 4);

	err = rain_send_and_wait(rain, "Q 1", "QTY");
	if (err)
		return err;
	err = rain_send_and_wait(rain, "c0000", "CFG");
	if (err)
		return err;
	return rain_send_and_wait(rain, "A F 0000", "ADR");
}

static int rain_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
	return 0;
}

static int rain_cec_adap_log_addr(struct cec_adapter *adap, u8 log_addr)
{
	struct rain *rain = cec_get_drvdata(adap);
	u8 cmd[16];

	if (log_addr == CEC_LOG_ADDR_INVALID)
		log_addr = CEC_LOG_ADDR_UNREGISTERED;
	snprintf(cmd, sizeof(cmd), "A %x", log_addr);
	return rain_send_and_wait(rain, cmd, "ADR");
}

static int rain_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
				    u32 signal_free_time, struct cec_msg *msg)
{
	struct rain *rain = cec_get_drvdata(adap);
	char cmd[2 * CEC_MAX_MSG_SIZE + 16];
	unsigned int i;
	int err;

	if (msg->len == 1) {
		snprintf(cmd, sizeof(cmd), "x%x", cec_msg_destination(msg));
	} else {
		char hex[3];

		snprintf(cmd, sizeof(cmd), "x%x %02x ",
			 cec_msg_destination(msg), msg->msg[1]);
		for (i = 2; i < msg->len; i++) {
			snprintf(hex, sizeof(hex), "%02x", msg->msg[i]);
			strlcat(cmd, hex, sizeof(cmd));
		}
	}
	mutex_lock(&rain->write_lock);
	err = rain_send(rain, cmd);
	mutex_unlock(&rain->write_lock);
	return err;
}

static const struct cec_adap_ops rain_cec_adap_ops = {
	.adap_enable = rain_cec_adap_enable,
	.adap_log_addr = rain_cec_adap_log_addr,
	.adap_transmit = rain_cec_adap_transmit,
};

static int rain_connect(struct serio *serio, struct serio_driver *drv)
{
	u32 caps = CEC_CAP_DEFAULTS | CEC_CAP_PHYS_ADDR | CEC_CAP_MONITOR_ALL;
	struct rain *rain;
	int err = -ENOMEM;
	struct cec_log_addrs log_addrs = {};
	u16 pa = CEC_PHYS_ADDR_INVALID;

	rain = kzalloc(sizeof(*rain), GFP_KERNEL);

	if (!rain)
		return -ENOMEM;

	rain->serio = serio;
	rain->adap = cec_allocate_adapter(&rain_cec_adap_ops, rain,
					  dev_name(&serio->dev), caps, 1);
	err = PTR_ERR_OR_ZERO(rain->adap);
	if (err < 0)
		goto free_device;

	rain->dev = &serio->dev;
	serio_set_drvdata(serio, rain);
	INIT_WORK(&rain->work, rain_irq_work_handler);
	mutex_init(&rain->write_lock);
	spin_lock_init(&rain->buf_lock);

	err = serio_open(serio, drv);
	if (err)
		goto delete_adap;

	err = rain_setup(rain, serio, &log_addrs, &pa);
	if (err)
		goto close_serio;

	err = cec_register_adapter(rain->adap, &serio->dev);
	if (err < 0)
		goto close_serio;

	rain->dev = &rain->adap->devnode.dev;
	return 0;

close_serio:
	serio_close(serio);
delete_adap:
	cec_delete_adapter(rain->adap);
	serio_set_drvdata(serio, NULL);
free_device:
	kfree(rain);
	return err;
}

static const struct serio_device_id rain_serio_ids[] = {
	{
		.type	= SERIO_RS232,
		.proto	= SERIO_RAINSHADOW_CEC,
		.id	= SERIO_ANY,
		.extra	= SERIO_ANY,
	},
	{ 0 }
};

MODULE_DEVICE_TABLE(serio, rain_serio_ids);

static struct serio_driver rain_drv = {
	.driver		= {
		.name	= "rainshadow-cec",
	},
	.description	= "RainShadow Tech HDMI CEC driver",
	.id_table	= rain_serio_ids,
	.interrupt	= rain_interrupt,
	.connect	= rain_connect,
	.disconnect	= rain_disconnect,
};

module_serio_driver(rain_drv);