blob: c78b45354c96c65b4ee161970998e072742918e6 [file] [log] [blame]
/* drivers/tty/smux_loopback.c
*
* Copyright (c) 2012, The Linux Foundation. All rights reserved.
*
* This software is licensed under the terms of the GNU General Public
* License version 2, as published by the Free Software Foundation, and
* may be copied, distributed, and modified under those terms.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*/
#include <linux/types.h>
#include <linux/err.h>
#include <linux/workqueue.h>
#include <linux/kfifo.h>
#include <linux/slab.h>
#include <linux/smux.h>
#include "smux_private.h"
#define SMUX_LOOP_FIFO_SIZE 128
static void smux_loopback_rx_worker(struct work_struct *work);
static struct workqueue_struct *smux_loopback_wq;
static DECLARE_WORK(smux_loopback_work, smux_loopback_rx_worker);
static struct kfifo smux_loop_pkt_fifo;
static DEFINE_SPINLOCK(hw_fn_lock);
/**
* Initialize loopback framework (called by n_smux.c).
*/
int smux_loopback_init(void)
{
int ret = 0;
spin_lock_init(&hw_fn_lock);
smux_loopback_wq = create_singlethread_workqueue("smux_loopback_wq");
if (IS_ERR(smux_loopback_wq)) {
pr_err("%s: failed to create workqueue\n", __func__);
return -ENOMEM;
}
ret |= kfifo_alloc(&smux_loop_pkt_fifo,
SMUX_LOOP_FIFO_SIZE * sizeof(struct smux_pkt_t *),
GFP_KERNEL);
return ret;
}
/**
* Simulate a write to the TTY hardware by duplicating
* the TX packet and putting it into the RX queue.
*
* @pkt Packet to write
*
* @returns 0 on success
*/
int smux_tx_loopback(struct smux_pkt_t *pkt_ptr)
{
struct smux_pkt_t *send_pkt;
unsigned long flags;
int i;
int ret;
/* duplicate packet */
send_pkt = smux_alloc_pkt();
send_pkt->hdr = pkt_ptr->hdr;
if (pkt_ptr->hdr.payload_len) {
ret = smux_alloc_pkt_payload(send_pkt);
if (ret) {
ret = -ENOMEM;
goto out;
}
memcpy(send_pkt->payload, pkt_ptr->payload,
pkt_ptr->hdr.payload_len);
}
/* queue duplicate as pseudo-RX data */
spin_lock_irqsave(&hw_fn_lock, flags);
i = kfifo_avail(&smux_loop_pkt_fifo);
if (i < sizeof(struct smux_pkt_t *)) {
pr_err("%s: no space in fifo\n", __func__);
ret = -ENOMEM;
goto unlock;
}
i = kfifo_in(&smux_loop_pkt_fifo,
&send_pkt,
sizeof(struct smux_pkt_t *));
if (i < 0) {
pr_err("%s: fifo error\n", __func__);
ret = -ENOMEM;
goto unlock;
}
queue_work(smux_loopback_wq, &smux_loopback_work);
ret = 0;
unlock:
spin_unlock_irqrestore(&hw_fn_lock, flags);
out:
return ret;
}
/**
* Receive loopback byte processor.
*
* @pkt Incoming packet
*/
static void smux_loopback_rx_byte(struct smux_pkt_t *pkt)
{
static int simulated_retry_cnt;
const char ack = SMUX_WAKEUP_ACK;
switch (pkt->hdr.flags) {
case SMUX_WAKEUP_REQ:
/* reply with ACK after appropriate delays */
++simulated_retry_cnt;
if (simulated_retry_cnt >= smux_simulate_wakeup_delay) {
pr_err("%s: completed %d of %d\n",
__func__, simulated_retry_cnt,
smux_simulate_wakeup_delay);
pr_err("%s: simulated wakeup\n", __func__);
simulated_retry_cnt = 0;
smux_rx_state_machine(&ack, 1, 0);
} else {
/* force retry */
pr_err("%s: dropping wakeup request %d of %d\n",
__func__, simulated_retry_cnt,
smux_simulate_wakeup_delay);
}
break;
case SMUX_WAKEUP_ACK:
/* this shouldn't happen since we don't send requests */
pr_err("%s: wakeup ACK unexpected\n", __func__);
break;
default:
/* invalid character */
pr_err("%s: invalid character 0x%x\n",
__func__, (unsigned)pkt->hdr.flags);
break;
}
}
/**
* Simulated remote hardware used for local loopback testing.
*
* @work Not used
*/
static void smux_loopback_rx_worker(struct work_struct *work)
{
struct smux_pkt_t *pkt;
struct smux_pkt_t reply_pkt;
char *data;
int len;
int lcid;
int i;
unsigned long flags;
data = kzalloc(SMUX_MAX_PKT_SIZE, GFP_ATOMIC);
spin_lock_irqsave(&hw_fn_lock, flags);
while (kfifo_len(&smux_loop_pkt_fifo) >= sizeof(struct smux_pkt_t *)) {
i = kfifo_out(&smux_loop_pkt_fifo, &pkt,
sizeof(struct smux_pkt_t *));
spin_unlock_irqrestore(&hw_fn_lock, flags);
if (pkt->hdr.magic != SMUX_MAGIC) {
pr_err("%s: invalid magic %x\n", __func__,
pkt->hdr.magic);
return;
}
lcid = pkt->hdr.lcid;
switch (pkt->hdr.cmd) {
case SMUX_CMD_OPEN_LCH:
if (smux_assert_lch_id(lcid)) {
pr_err("%s: invalid channel id %d\n",
__func__, lcid);
break;
}
if (pkt->hdr.flags & SMUX_CMD_OPEN_ACK)
break;
/* Reply with Open ACK */
smux_init_pkt(&reply_pkt);
reply_pkt.hdr.lcid = lcid;
reply_pkt.hdr.cmd = SMUX_CMD_OPEN_LCH;
reply_pkt.hdr.flags = SMUX_CMD_OPEN_ACK
| SMUX_CMD_OPEN_POWER_COLLAPSE;
reply_pkt.hdr.payload_len = 0;
reply_pkt.hdr.pad_len = 0;
smux_serialize(&reply_pkt, data, &len);
smux_rx_state_machine(data, len, 0);
/* Send Remote Open */
smux_init_pkt(&reply_pkt);
reply_pkt.hdr.lcid = lcid;
reply_pkt.hdr.cmd = SMUX_CMD_OPEN_LCH;
reply_pkt.hdr.flags = SMUX_CMD_OPEN_POWER_COLLAPSE;
reply_pkt.hdr.payload_len = 0;
reply_pkt.hdr.pad_len = 0;
smux_serialize(&reply_pkt, data, &len);
smux_rx_state_machine(data, len, 0);
break;
case SMUX_CMD_CLOSE_LCH:
if (smux_assert_lch_id(lcid)) {
pr_err("%s: invalid channel id %d\n",
__func__, lcid);
break;
}
if (pkt->hdr.flags == SMUX_CMD_CLOSE_ACK)
break;
/* Reply with Close ACK */
smux_init_pkt(&reply_pkt);
reply_pkt.hdr.lcid = lcid;
reply_pkt.hdr.cmd = SMUX_CMD_CLOSE_LCH;
reply_pkt.hdr.flags = SMUX_CMD_CLOSE_ACK;
reply_pkt.hdr.payload_len = 0;
reply_pkt.hdr.pad_len = 0;
smux_serialize(&reply_pkt, data, &len);
smux_rx_state_machine(data, len, 0);
/* Send Remote Close */
smux_init_pkt(&reply_pkt);
reply_pkt.hdr.lcid = lcid;
reply_pkt.hdr.cmd = SMUX_CMD_CLOSE_LCH;
reply_pkt.hdr.flags = 0;
reply_pkt.hdr.payload_len = 0;
reply_pkt.hdr.pad_len = 0;
smux_serialize(&reply_pkt, data, &len);
smux_rx_state_machine(data, len, 0);
break;
case SMUX_CMD_DATA:
if (smux_assert_lch_id(lcid)) {
pr_err("%s: invalid channel id %d\n",
__func__, lcid);
break;
}
/* Echo back received data */
smux_init_pkt(&reply_pkt);
reply_pkt.hdr.lcid = lcid;
reply_pkt.hdr.cmd = SMUX_CMD_DATA;
reply_pkt.hdr.flags = 0;
reply_pkt.hdr.payload_len = pkt->hdr.payload_len;
reply_pkt.payload = pkt->payload;
reply_pkt.hdr.pad_len = pkt->hdr.pad_len;
smux_serialize(&reply_pkt, data, &len);
smux_rx_state_machine(data, len, 0);
break;
case SMUX_CMD_STATUS:
if (smux_assert_lch_id(lcid)) {
pr_err("%s: invalid channel id %d\n",
__func__, lcid);
break;
}
/* Echo back received status */
smux_init_pkt(&reply_pkt);
reply_pkt.hdr.lcid = lcid;
reply_pkt.hdr.cmd = SMUX_CMD_STATUS;
reply_pkt.hdr.flags = pkt->hdr.flags;
reply_pkt.hdr.payload_len = 0;
reply_pkt.payload = NULL;
reply_pkt.hdr.pad_len = pkt->hdr.pad_len;
smux_serialize(&reply_pkt, data, &len);
smux_rx_state_machine(data, len, 0);
break;
case SMUX_CMD_PWR_CTL:
/* reply with ack */
smux_init_pkt(&reply_pkt);
reply_pkt.hdr.lcid = SMUX_BROADCAST_LCID;
reply_pkt.hdr.cmd = SMUX_CMD_PWR_CTL;
reply_pkt.hdr.flags = SMUX_CMD_PWR_CTL_ACK;
reply_pkt.hdr.payload_len = 0;
reply_pkt.payload = NULL;
reply_pkt.hdr.pad_len = pkt->hdr.pad_len;
smux_serialize(&reply_pkt, data, &len);
smux_rx_state_machine(data, len, 0);
break;
case SMUX_CMD_BYTE:
smux_loopback_rx_byte(pkt);
break;
default:
pr_err("%s: unknown command %d\n",
__func__, pkt->hdr.cmd);
break;
};
smux_free_pkt(pkt);
spin_lock_irqsave(&hw_fn_lock, flags);
}
spin_unlock_irqrestore(&hw_fn_lock, flags);
kfree(data);
}