Commit 9cb2173e authored by Ezequiel García's avatar Ezequiel García Committed by Mauro Carvalho Chehab

[media] media: Add stk1160 new driver (easycap replacement)

This driver adds support for stk1160 usb bridge as used in some
video/audio usb capture devices.
It is a complete rewrite of staging/media/easycap driver and
it's meant as a replacement.

As stk1160 allows communication with an ac97 codec chip, this
driver allows to register a control-only sound card to allow the user
to access ac97 controls.

Two devices have been used for testing:
* 1-cvbs video and 1-audio ac97 input,
* 4-cvbs inputs
Both of these devices reports with the same id [05e1:0408],
so the driver tries to support a superset of the capabilities.

By using keep_buffers module parameter it's possible to prevent
the driver from releasing urb buffers when streaming is stopped.
The usage of this parameter can avoid memory fragmentation that may
cause the driver to stop working on low memory systems.
A similar mechanism is implemented in em28xx driver (see commit 86d38d).
Signed-off-by: default avatarEzequiel Garcia <elezegarcia@gmail.com>
Signed-off-by: default avatarMauro Carvalho Chehab <mchehab@redhat.com>
parent ffd491fd
......@@ -748,6 +748,8 @@ source "drivers/media/video/tm6000/Kconfig"
source "drivers/media/video/usbvision/Kconfig"
source "drivers/media/video/stk1160/Kconfig"
endif # V4L_USB_DRIVERS
#
......
......@@ -126,6 +126,7 @@ obj-$(CONFIG_VIDEO_HEXIUM_ORION) += hexium_orion.o
obj-$(CONFIG_VIDEO_HEXIUM_GEMINI) += hexium_gemini.o
obj-$(CONFIG_STA2X11_VIP) += sta2x11_vip.o
obj-$(CONFIG_VIDEO_TIMBERDALE) += timblogiw.o
obj-$(CONFIG_VIDEO_STK1160) += stk1160/
obj-$(CONFIG_VIDEOBUF_GEN) += videobuf-core.o
obj-$(CONFIG_VIDEOBUF_DMA_SG) += videobuf-dma-sg.o
......
config VIDEO_STK1160
tristate "STK1160 USB video capture support"
depends on VIDEO_DEV && I2C
select VIDEOBUF2_VMALLOC
select VIDEO_SAA711X
---help---
This is a video4linux driver for STK1160 based video capture devices.
To compile this driver as a module, choose M here: the
module will be called stk1160
config VIDEO_STK1160_AC97
bool "STK1160 AC97 codec support"
depends on VIDEO_STK1160 && SND
select SND_AC97_CODEC
---help---
Enables AC97 codec support for stk1160 driver.
.
obj-stk1160-ac97-$(CONFIG_VIDEO_STK1160_AC97) := stk1160-ac97.o
stk1160-y := stk1160-core.o \
stk1160-v4l.o \
stk1160-video.o \
stk1160-i2c.o \
$(obj-stk1160-ac97-y)
obj-$(CONFIG_VIDEO_STK1160) += stk1160.o
ccflags-y += -Idrivers/media/video
/*
* STK1160 driver
*
* Copyright (C) 2012 Ezequiel Garcia
* <elezegarcia--a.t--gmail.com>
*
* Based on Easycap driver by R.M. Thomas
* Copyright (C) 2010 R.M. Thomas
* <rmthomas--a.t--sciolus.org>
*
* 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 2 of the License, or
* (at your option) any later version.
*
* 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/module.h>
#include <sound/core.h>
#include <sound/initval.h>
#include <sound/ac97_codec.h>
#include "stk1160.h"
#include "stk1160-reg.h"
static struct snd_ac97 *stk1160_ac97;
static void stk1160_write_ac97(struct snd_ac97 *ac97, u16 reg, u16 value)
{
struct stk1160 *dev = ac97->private_data;
/* Set codec register address */
stk1160_write_reg(dev, STK1160_AC97_ADDR, reg);
/* Set codec command */
stk1160_write_reg(dev, STK1160_AC97_CMD, value & 0xff);
stk1160_write_reg(dev, STK1160_AC97_CMD + 1, (value & 0xff00) >> 8);
/*
* Set command write bit to initiate write operation.
* The bit will be cleared when transfer is done.
*/
stk1160_write_reg(dev, STK1160_AC97CTL_0, 0x8c);
}
static u16 stk1160_read_ac97(struct snd_ac97 *ac97, u16 reg)
{
struct stk1160 *dev = ac97->private_data;
u8 vall = 0;
u8 valh = 0;
/* Set codec register address */
stk1160_write_reg(dev, STK1160_AC97_ADDR, reg);
/*
* Set command read bit to initiate read operation.
* The bit will be cleared when transfer is done.
*/
stk1160_write_reg(dev, STK1160_AC97CTL_0, 0x8b);
/* Retrieve register value */
stk1160_read_reg(dev, STK1160_AC97_CMD, &vall);
stk1160_read_reg(dev, STK1160_AC97_CMD + 1, &valh);
return (valh << 8) | vall;
}
static void stk1160_reset_ac97(struct snd_ac97 *ac97)
{
struct stk1160 *dev = ac97->private_data;
/* Two-step reset AC97 interface and hardware codec */
stk1160_write_reg(dev, STK1160_AC97CTL_0, 0x94);
stk1160_write_reg(dev, STK1160_AC97CTL_0, 0x88);
/* Set 16-bit audio data and choose L&R channel*/
stk1160_write_reg(dev, STK1160_AC97CTL_1 + 2, 0x01);
}
static struct snd_ac97_bus_ops stk1160_ac97_ops = {
.read = stk1160_read_ac97,
.write = stk1160_write_ac97,
.reset = stk1160_reset_ac97,
};
int stk1160_ac97_register(struct stk1160 *dev)
{
struct snd_card *card = NULL;
struct snd_ac97_bus *ac97_bus;
struct snd_ac97_template ac97_template;
int rc;
/*
* Just want a card to access ac96 controls,
* the actual capture interface will be handled by snd-usb-audio
*/
rc = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
THIS_MODULE, 0, &card);
if (rc < 0)
return rc;
snd_card_set_dev(card, dev->dev);
/* TODO: I'm not sure where should I get these names :-( */
snprintf(card->shortname, sizeof(card->shortname),
"stk1160-mixer");
snprintf(card->longname, sizeof(card->longname),
"stk1160 ac97 codec mixer control");
strncpy(card->driver, dev->dev->driver->name, sizeof(card->driver));
rc = snd_ac97_bus(card, 0, &stk1160_ac97_ops, NULL, &ac97_bus);
if (rc)
goto err;
/* We must set private_data before calling snd_ac97_mixer */
memset(&ac97_template, 0, sizeof(ac97_template));
ac97_template.private_data = dev;
ac97_template.scaps = AC97_SCAP_SKIP_MODEM;
rc = snd_ac97_mixer(ac97_bus, &ac97_template, &stk1160_ac97);
if (rc)
goto err;
dev->snd_card = card;
rc = snd_card_register(card);
if (rc)
goto err;
return 0;
err:
dev->snd_card = NULL;
if (card)
snd_card_free(card);
return rc;
}
int stk1160_ac97_unregister(struct stk1160 *dev)
{
struct snd_card *card = dev->snd_card;
/*
* We need to check usb_device,
* because ac97 release attempts to communicate with codec
*/
if (card && dev->udev)
snd_card_free(card);
return 0;
}
This diff is collapsed.
/*
* STK1160 driver
*
* Copyright (C) 2012 Ezequiel Garcia
* <elezegarcia--a.t--gmail.com>
*
* Based on Easycap driver by R.M. Thomas
* Copyright (C) 2010 R.M. Thomas
* <rmthomas--a.t--sciolus.org>
*
* 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 2 of the License, or
* (at your option) any later version.
*
* 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/module.h>
#include <linux/usb.h>
#include <linux/i2c.h>
#include "stk1160.h"
#include "stk1160-reg.h"
static unsigned int i2c_debug;
module_param(i2c_debug, int, 0644);
MODULE_PARM_DESC(i2c_debug, "enable debug messages [i2c]");
#define dprintk_i2c(fmt, args...) \
do { \
if (i2c_debug) \
printk(KERN_DEBUG fmt, ##args); \
} while (0)
static int stk1160_i2c_busy_wait(struct stk1160 *dev, u8 wait_bit_mask)
{
unsigned long end;
u8 flag;
/* Wait until read/write finish bit is set */
end = jiffies + msecs_to_jiffies(STK1160_I2C_TIMEOUT);
while (time_is_after_jiffies(end)) {
stk1160_read_reg(dev, STK1160_SICTL+1, &flag);
/* read/write done? */
if (flag & wait_bit_mask)
goto done;
usleep_range(10 * USEC_PER_MSEC, 20 * USEC_PER_MSEC);
}
return -ETIMEDOUT;
done:
return 0;
}
static int stk1160_i2c_write_reg(struct stk1160 *dev, u8 addr,
u8 reg, u8 value)
{
int rc;
/* Set serial device address */
rc = stk1160_write_reg(dev, STK1160_SICTL_SDA, addr);
if (rc < 0)
return rc;
/* Set i2c device register sub-address */
rc = stk1160_write_reg(dev, STK1160_SBUSW_WA, reg);
if (rc < 0)
return rc;
/* Set i2c device register value */
rc = stk1160_write_reg(dev, STK1160_SBUSW_WD, value);
if (rc < 0)
return rc;
/* Start write now */
rc = stk1160_write_reg(dev, STK1160_SICTL, 0x01);
if (rc < 0)
return rc;
rc = stk1160_i2c_busy_wait(dev, 0x04);
if (rc < 0)
return rc;
return 0;
}
static int stk1160_i2c_read_reg(struct stk1160 *dev, u8 addr,
u8 reg, u8 *value)
{
int rc;
/* Set serial device address */
rc = stk1160_write_reg(dev, STK1160_SICTL_SDA, addr);
if (rc < 0)
return rc;
/* Set i2c device register sub-address */
rc = stk1160_write_reg(dev, STK1160_SBUSR_RA, reg);
if (rc < 0)
return rc;
/* Start read now */
rc = stk1160_write_reg(dev, STK1160_SICTL, 0x20);
if (rc < 0)
return rc;
rc = stk1160_i2c_busy_wait(dev, 0x01);
if (rc < 0)
return rc;
stk1160_read_reg(dev, STK1160_SBUSR_RD, value);
if (rc < 0)
return rc;
return 0;
}
/*
* stk1160_i2c_check_for_device()
* check if there is a i2c_device at the supplied address
*/
static int stk1160_i2c_check_for_device(struct stk1160 *dev,
unsigned char addr)
{
int rc;
/* Set serial device address */
rc = stk1160_write_reg(dev, STK1160_SICTL_SDA, addr);
if (rc < 0)
return rc;
/* Set device sub-address, we'll chip version reg */
rc = stk1160_write_reg(dev, STK1160_SBUSR_RA, 0x00);
if (rc < 0)
return rc;
/* Start read now */
rc = stk1160_write_reg(dev, STK1160_SICTL, 0x20);
if (rc < 0)
return rc;
rc = stk1160_i2c_busy_wait(dev, 0x01);
if (rc < 0)
return -ENODEV;
return 0;
}
/*
* stk1160_i2c_xfer()
* the main i2c transfer function
*/
static int stk1160_i2c_xfer(struct i2c_adapter *i2c_adap,
struct i2c_msg msgs[], int num)
{
struct stk1160 *dev = i2c_adap->algo_data;
int addr, rc, i;
for (i = 0; i < num; i++) {
addr = msgs[i].addr << 1;
dprintk_i2c("%s: addr=%x", __func__, addr);
if (!msgs[i].len) {
/* no len: check only for device presence */
rc = stk1160_i2c_check_for_device(dev, addr);
if (rc < 0) {
dprintk_i2c(" no device\n");
return rc;
}
} else if (msgs[i].flags & I2C_M_RD) {
/* read request without preceding register selection */
dprintk_i2c(" subaddr not selected");
rc = -EOPNOTSUPP;
goto err;
} else if (i + 1 < num && msgs[i].len <= 2 &&
(msgs[i + 1].flags & I2C_M_RD) &&
msgs[i].addr == msgs[i + 1].addr) {
if (msgs[i].len != 1 || msgs[i + 1].len != 1) {
dprintk_i2c(" len not supported");
rc = -EOPNOTSUPP;
goto err;
}
dprintk_i2c(" subaddr=%x", msgs[i].buf[0]);
rc = stk1160_i2c_read_reg(dev, addr, msgs[i].buf[0],
msgs[i + 1].buf);
dprintk_i2c(" read=%x", *msgs[i + 1].buf);
/* consumed two msgs, so we skip one of them */
i++;
} else {
if (msgs[i].len != 2) {
dprintk_i2c(" len not supported");
rc = -EOPNOTSUPP;
goto err;
}
dprintk_i2c(" subaddr=%x write=%x",
msgs[i].buf[0], msgs[i].buf[1]);
rc = stk1160_i2c_write_reg(dev, addr, msgs[i].buf[0],
msgs[i].buf[1]);
}
if (rc < 0)
goto err;
dprintk_i2c(" OK\n");
}
return num;
err:
dprintk_i2c(" ERROR: %d\n", rc);
return num;
}
/*
* functionality(), what da heck is this?
*/
static u32 functionality(struct i2c_adapter *adap)
{
return I2C_FUNC_SMBUS_EMUL;
}
static struct i2c_algorithm algo = {
.master_xfer = stk1160_i2c_xfer,
.functionality = functionality,
};
static struct i2c_adapter adap_template = {
.owner = THIS_MODULE,
.name = "stk1160",
.algo = &algo,
};
static struct i2c_client client_template = {
.name = "stk1160 internal",
};
/*
* stk1160_i2c_register()
* register i2c bus
*/
int stk1160_i2c_register(struct stk1160 *dev)
{
int rc;
dev->i2c_adap = adap_template;
dev->i2c_adap.dev.parent = dev->dev;
strcpy(dev->i2c_adap.name, "stk1160");
dev->i2c_adap.algo_data = dev;
i2c_set_adapdata(&dev->i2c_adap, &dev->v4l2_dev);
rc = i2c_add_adapter(&dev->i2c_adap);
if (rc < 0) {
stk1160_err("cannot add i2c adapter (%d)\n", rc);
return rc;
}
dev->i2c_client = client_template;
dev->i2c_client.adapter = &dev->i2c_adap;
/* Set i2c clock divider device address */
stk1160_write_reg(dev, STK1160_SICTL_CD, 0x0f);
/* ??? */
stk1160_write_reg(dev, STK1160_ASIC + 3, 0x00);
return 0;
}
/*
* stk1160_i2c_unregister()
* unregister i2c_bus
*/
int stk1160_i2c_unregister(struct stk1160 *dev)
{
i2c_del_adapter(&dev->i2c_adap);
return 0;
}
/*
* STK1160 driver
*
* Copyright (C) 2012 Ezequiel Garcia
* <elezegarcia--a.t--gmail.com>
*
* Based on Easycap driver by R.M. Thomas
* Copyright (C) 2010 R.M. Thomas
* <rmthomas--a.t--sciolus.org>
*
* 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 2 of the License, or
* (at your option) any later version.
*
* 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.
*
*/
/* GPIO Control */
#define STK1160_GCTRL 0x000
/* Remote Wakup Control */
#define STK1160_RMCTL 0x00c
/*
* Decoder Control Register:
* This byte controls capture start/stop
* with bit #7 (0x?? OR 0x80 to activate).
*/
#define STK1160_DCTRL 0x100
/* Capture Frame Start Position */
#define STK116_CFSPO 0x110
#define STK116_CFSPO_STX_L 0x110
#define STK116_CFSPO_STX_H 0x111
#define STK116_CFSPO_STY_L 0x112
#define STK116_CFSPO_STY_H 0x113
/* Capture Frame End Position */
#define STK116_CFEPO 0x114
#define STK116_CFEPO_ENX_L 0x114
#define STK116_CFEPO_ENX_H 0x115
#define STK116_CFEPO_ENY_L 0x116
#define STK116_CFEPO_ENY_H 0x117
/* Serial Interface Control */
#define STK1160_SICTL 0x200
#define STK1160_SICTL_CD 0x202
#define STK1160_SICTL_SDA 0x203
/* Serial Bus Write */
#define STK1160_SBUSW 0x204
#define STK1160_SBUSW_WA 0x204
#define STK1160_SBUSW_WD 0x205
/* Serial Bus Read */
#define STK1160_SBUSR 0x208
#define STK1160_SBUSR_RA 0x208
#define STK1160_SBUSR_RD 0x209
/* Alternate Serial Inteface Control */
#define STK1160_ASIC 0x2fc
/* PLL Select Options */
#define STK1160_PLLSO 0x018
/* PLL Frequency Divider */
#define STK1160_PLLFD 0x01c
/* Timing Generator */
#define STK1160_TIGEN 0x300
/* Timing Control Parameter */
#define STK1160_TICTL 0x350
/* AC97 Audio Control */
#define STK1160_AC97CTL_0 0x500
#define STK1160_AC97CTL_1 0x504
/* Use [0:6] bits of register 0x504 to set codec command address */
#define STK1160_AC97_ADDR 0x504
/* Use [16:31] bits of register 0x500 to set codec command data */
#define STK1160_AC97_CMD 0x502
/* Audio I2S Interface */
#define STK1160_I2SCTL 0x50c
/* EEPROM Interface */
#define STK1160_EEPROM_SZ 0x5f0
This diff is collapsed.
This diff is collapsed.
/*
* STK1160 driver
*
* Copyright (C) 2012 Ezequiel Garcia
* <elezegarcia--a.t--gmail.com>
*
* Based on Easycap driver by R.M. Thomas
* Copyright (C) 2010 R.M. Thomas
* <rmthomas--a.t--sciolus.org>
*
* 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 2 of the License, or
* (at your option) any later version.
*
* 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/i2c.h>
#include <sound/core.h>
#include <sound/ac97_codec.h>
#include <media/videobuf2-core.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ctrls.h>
#define STK1160_VERSION "0.9.5"
#define STK1160_VERSION_NUM 0x000905
/* TODO: Decide on number of packets for each buffer */
#define STK1160_NUM_PACKETS 64
/* Number of buffers for isoc transfers */
#define STK1160_NUM_BUFS 16 /* TODO */
/* TODO: This endpoint address should be retrieved */
#define STK1160_EP_VIDEO 0x82
#define STK1160_EP_AUDIO 0x81
/* Max and min video buffers */
#define STK1160_MIN_VIDEO_BUFFERS 8
#define STK1160_MAX_VIDEO_BUFFERS 32
#define STK1160_MIN_PKT_SIZE 3072
#define STK1160_MAX_INPUT 3
#define STK1160_I2C_TIMEOUT 100
/* TODO: Print helpers
* I could use dev_xxx, pr_xxx, v4l2_xxx or printk.
* However, there isn't a solid consensus on which
* new drivers should use.
*
*/
#define DEBUG
#ifdef DEBUG
#define stk1160_dbg(fmt, args...) \
printk(KERN_DEBUG "stk1160: " fmt, ## args)
#else
#define stk1160_dbg(fmt, args...)
#endif
#define stk1160_info(fmt, args...) \
pr_info("stk1160: " fmt, ## args)
#define stk1160_warn(fmt, args...) \
pr_warn("stk1160: " fmt, ## args)
#define stk1160_err(fmt, args...) \
pr_err("stk1160: " fmt, ## args)
/* Buffer for one video frame */
struct stk1160_buffer {
/* common v4l buffer stuff -- must be first */
struct vb2_buffer vb;
struct list_head list;
void *mem;
unsigned int length; /* buffer length */
unsigned int bytesused; /* bytes written */
int odd; /* current oddity */
/*
* Since we interlace two fields per frame,
* this is different from bytesused.
*/
unsigned int pos; /* current pos inside buffer */
};
struct stk1160_isoc_ctl {
/* max packet size of isoc transaction */
int max_pkt_size;
/* number of allocated urbs */
int num_bufs;
/* urb for isoc transfers */
struct urb **urb;
/* transfer buffers for isoc transfer */
char **transfer_buffer;
/* current buffer */
struct stk1160_buffer *buf;
};
struct stk1160_fmt {
char *name;
u32 fourcc; /* v4l2 format id */
int depth;
};
struct stk1160 {
struct v4l2_device v4l2_dev;
struct video_device vdev;
struct v4l2_ctrl_handler ctrl_handler;
struct device *dev;
struct usb_device *udev;
/* saa7115 subdev */
struct v4l2_subdev *sd_saa7115;
/* isoc control struct */
struct list_head avail_bufs;
/* video capture */
struct vb2_queue vb_vidq;
/* max packet size of isoc transaction */
int max_pkt_size;
/* array of wMaxPacketSize */
unsigned int *alt_max_pkt_size;
/* alternate */
int alt;
/* Number of alternative settings */
int num_alt;
struct stk1160_isoc_ctl isoc_ctl;
char urb_buf[255]; /* urb control msg buffer */
/* frame properties */
int width; /* current frame width */
int height; /* current frame height */
unsigned int ctl_input; /* selected input */
v4l2_std_id norm; /* current norm */
struct stk1160_fmt *fmt; /* selected format */
unsigned int field_count; /* not sure ??? */
enum v4l2_field field; /* also not sure :/ */
/* i2c i/o */
struct i2c_adapter i2c_adap;
struct i2c_client i2c_client;
struct mutex v4l_lock;
struct mutex vb_queue_lock;
spinlock_t buf_lock;
struct file *fh_owner; /* filehandle ownership */
/* EXPERIMENTAL */
struct snd_card *snd_card;
};
struct regval {
u16 reg;
u16 val;
};
/* Provided by stk1160-v4l.c */
int stk1160_vb2_setup(struct stk1160 *dev);
int stk1160_video_register(struct stk1160 *dev);
void stk1160_video_unregister(struct stk1160 *dev);
void stk1160_clear_queue(struct stk1160 *dev);
/* Provided by stk1160-video.c */
int stk1160_alloc_isoc(struct stk1160 *dev);
void stk1160_free_isoc(struct stk1160 *dev);
void stk1160_cancel_isoc(struct stk1160 *dev);
void stk1160_uninit_isoc(struct stk1160 *dev);
/* Provided by stk1160-i2c.c */
int stk1160_i2c_register(struct stk1160 *dev);
int stk1160_i2c_unregister(struct stk1160 *dev);
/* Provided by stk1160-core.c */
int stk1160_read_reg(struct stk1160 *dev, u16 reg, u8 *value);
int stk1160_write_reg(struct stk1160 *dev, u16 reg, u16 value);
int stk1160_write_regs_req(struct stk1160 *dev, u8 req, u16 reg,
char *buf, int len);
int stk1160_read_reg_req_len(struct stk1160 *dev, u8 req, u16 reg,
char *buf, int len);
void stk1160_select_input(struct stk1160 *dev);
/* Provided by stk1160-ac97.c */
#ifdef CONFIG_VIDEO_STK1160_AC97
int stk1160_ac97_register(struct stk1160 *dev);
int stk1160_ac97_unregister(struct stk1160 *dev);
#else
static inline int stk1160_ac97_register(struct stk1160 *dev) { return 0; }
static inline int stk1160_ac97_unregister(struct stk1160 *dev) { return 0; }
#endif
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment