Commit 5b3f03f0 authored by Huang Shijie's avatar Huang Shijie Committed by Mauro Carvalho Chehab

V4L/DVB: Add driver for Telegent tlg2300

pd-common.h contains the common data structures, while
vendorcmds.h contains the vendor commands for firmware.

[mchehab@redhat.com: Folded the 10 patches with the driver]
Signed-off-by: default avatarHuang Shijie <shijie8@gmail.com>
Signed-off-by: default avatarMauro Carvalho Chehab <mchehab@redhat.com>
parent 433763fa
tlg2300 release notes
====================
This is a v4l2/dvb device driver for the tlg2300 chip.
current status
==============
video
- support mmap and read().(no overlay)
audio
- The driver will register a ALSA card for the audio input.
vbi
- Works for almost TV norms.
dvb-t
- works for DVB-T
FM
- Works for radio.
---------------------------------------------------------------------------
TESTED APPLICATIONS:
-VLC1.0.4 test the video and dvb. The GUI is friendly to use.
-Mplayer test the video.
-Mplayer test the FM. The mplayer should be compiled with --enable-radio and
--enable-radio-capture.
The command runs as this(The alsa audio registers to card 1):
#mplayer radio://103.7/capture/ -radio adevice=hw=1,0:arate=48000 \
-rawaudio rate=48000:channels=2
---------------------------------------------------------------------------
KNOWN PROBLEMS:
country code
- The firmware of the chip needs the country code to determine
the stardards of video and audio when it runs for analog TV or radio.
The DVB-T does not need the country code.
So you must set the country-code correctly. The V4L2 does not have
the interface,the driver has to provide a parameter `country_code'.
You could set the coutry code in two ways, take USA as example
(The USA's country code is 1):
[1] add the following line in /etc/modprobe.conf before you insert the
card into USB hub's port :
poseidon country_code=1
[2] You can also modify the parameter at runtime (before you run the
application such as VLC)
#echo 1 > /sys/module/poseidon/parameter/country_code
The known country codes show below:
country code : country
93 "Afghanistan"
355 "Albania"
213 "Algeria"
684 "American Samoa"
376 "Andorra"
244 "Angola"
54 "Argentina"
374 "Armenia"
61 "Australia"
43 "Austria"
994 "Azerbaijan"
973 "Bahrain"
880 "Bangladesh"
375 "Belarus"
32 "Belgium"
501 "Belize"
229 "Benin"
591 "Bolivia"
387 "Bosnia and Herzegovina"
267 "Botswana"
55 "Brazil"
673 "Brunei Darussalam"
359 "Bulgalia"
226 "Burkina Faso"
257 "Burundi"
237 "Cameroon"
1 "Canada"
236 "Central African Republic"
235 "Chad"
56 "Chile"
86 "China"
57 "Colombia"
242 "Congo"
243 "Congo, Dem. Rep. of "
506 "Costa Rica"
385 "Croatia"
53 "Cuba or Guantanamo Bay"
357 "Cyprus"
420 "Czech Republic"
45 "Denmark"
246 "Diego Garcia"
253 "Djibouti"
593 "Ecuador"
20 "Egypt"
503 "El Salvador"
240 "Equatorial Guinea"
372 "Estonia"
251 "Ethiopia"
358 "Finland"
33 "France"
594 "French Guiana"
689 "French Polynesia"
241 "Gabonese Republic"
220 "Gambia"
995 "Georgia"
49 "Germany"
233 "Ghana"
350 "Gibraltar"
30 "Greece"
299 "Greenland"
671 "Guam"
502 "Guatemala"
592 "Guyana"
509 "Haiti"
504 "Honduras"
852 "Hong Kong SAR, China"
36 "Hungary"
354 "Iceland"
91 "India"
98 "Iran"
964 "Iraq"
353 "Ireland"
972 "Israel"
39 "Italy or Vatican City"
225 "Ivory Coast"
81 "Japan"
962 "Jordan"
7 "Kazakhstan or Kyrgyzstan"
254 "Kenya"
686 "Kiribati"
965 "Kuwait"
856 "Laos"
371 "Latvia"
961 "Lebanon"
266 "Lesotho"
231 "Liberia"
218 "Libya"
41 "Liechtenstein or Switzerland"
370 "Lithuania"
352 "Luxembourg"
853 "Macau SAR, China"
261 "Madagascar"
60 "Malaysia"
960 "Maldives"
223 "Mali Republic"
356 "Malta"
692 "Marshall Islands"
596 "Martinique"
222 "Mauritania"
230 "Mauritus"
52 "Mexico"
691 "Micronesia"
373 "Moldova"
377 "Monaco"
976 "Mongolia"
212 "Morocco"
258 "Mozambique"
95 "Myanmar"
264 "Namibia"
674 "Nauru"
31 "Netherlands"
687 "New Caledonia"
64 "New Zealand"
505 "Nicaragua"
227 "Niger"
234 "Nigeria"
850 "North Korea"
47 "Norway"
968 "Oman"
92 "Pakistan"
680 "Palau"
507 "Panama"
675 "Papua New Guinea"
595 "Paraguay"
51 "Peru"
63 "Philippines"
48 "Poland"
351 "Portugal"
974 "Qatar"
262 "Reunion Island"
40 "Romania"
7 "Russia"
378 "San Marino"
239 "Sao Tome and Principe"
966 "Saudi Arabia"
221 "Senegal"
248 "Seychelles Republic"
232 "Sierra Leone"
65 "Singapore"
421 "Slovak Republic"
386 "Slovenia"
27 "South Africa"
82 "South Korea "
34 "Spain"
94 "Sri Lanka"
508 "St. Pierre and Miquelon"
249 "Sudan"
597 "Suriname"
268 "Swaziland"
46 "Sweden"
963 "Syria"
886 "Taiwan Region"
255 "Tanzania"
66 "Thailand"
228 "Togolese Republic"
216 "Tunisia"
90 "Turkey"
993 "Turkmenistan"
256 "Uganda"
380 "Ukraine"
971 "United Arab Emirates"
44 "United Kingdom"
1 "United States of America"
598 "Uruguay"
58 "Venezuela"
84 "Vietnam"
967 "Yemen"
260 "Zambia"
255 "Zanzibar"
263 "Zimbabwe"
...@@ -4676,6 +4676,14 @@ F: drivers/media/common/saa7146* ...@@ -4676,6 +4676,14 @@ F: drivers/media/common/saa7146*
F: drivers/media/video/*7146* F: drivers/media/video/*7146*
F: include/media/*7146* F: include/media/*7146*
TLG2300 VIDEO4LINUX-2 DRIVER
M Huang Shijie <shijie8@gmail.com>
M Kang Yong <kangyong@telegent.com>
M Zhang Xiaobing <xbzhang@telegent.com>
S: Supported
F: drivers/media/video/tlg2300
SC1200 WDT DRIVER SC1200 WDT DRIVER
M: Zwane Mwaikambo <zwane@arm.linux.org.uk> M: Zwane Mwaikambo <zwane@arm.linux.org.uk>
S: Maintained S: Maintained
......
...@@ -949,6 +949,8 @@ source "drivers/media/video/hdpvr/Kconfig" ...@@ -949,6 +949,8 @@ source "drivers/media/video/hdpvr/Kconfig"
source "drivers/media/video/em28xx/Kconfig" source "drivers/media/video/em28xx/Kconfig"
source "drivers/media/video/tlg2300/Kconfig"
source "drivers/media/video/cx231xx/Kconfig" source "drivers/media/video/cx231xx/Kconfig"
source "drivers/media/video/usbvision/Kconfig" source "drivers/media/video/usbvision/Kconfig"
......
...@@ -99,6 +99,7 @@ obj-$(CONFIG_VIDEO_MEYE) += meye.o ...@@ -99,6 +99,7 @@ obj-$(CONFIG_VIDEO_MEYE) += meye.o
obj-$(CONFIG_VIDEO_SAA7134) += saa7134/ obj-$(CONFIG_VIDEO_SAA7134) += saa7134/
obj-$(CONFIG_VIDEO_CX88) += cx88/ obj-$(CONFIG_VIDEO_CX88) += cx88/
obj-$(CONFIG_VIDEO_EM28XX) += em28xx/ obj-$(CONFIG_VIDEO_EM28XX) += em28xx/
obj-$(CONFIG_VIDEO_TLG2300) += tlg2300/
obj-$(CONFIG_VIDEO_CX231XX) += cx231xx/ obj-$(CONFIG_VIDEO_CX231XX) += cx231xx/
obj-$(CONFIG_VIDEO_USBVISION) += usbvision/ obj-$(CONFIG_VIDEO_USBVISION) += usbvision/
obj-$(CONFIG_VIDEO_PVRUSB2) += pvrusb2/ obj-$(CONFIG_VIDEO_PVRUSB2) += pvrusb2/
......
config VIDEO_TLG2300
tristate "Telegent TLG2300 USB video capture support"
depends on VIDEO_DEV && I2C && INPUT && SND && DVB_CORE
select VIDEO_TUNER
select VIDEO_TVEEPROM
select VIDEO_IR
select VIDEOBUF_VMALLOC
select SND_PCM
select VIDEOBUF_DVB
---help---
This is a video4linux driver for Telegent tlg2300 based TV cards.
The driver supports V4L2, DVB-T and radio.
To compile this driver as a module, choose M here: the
module will be called poseidon
poseidon-objs := pd-video.o pd-alsa.o pd-dvb.o pd-radio.o pd-main.o
obj-$(CONFIG_VIDEO_TLG2300) += poseidon.o
EXTRA_CFLAGS += -Idrivers/media/video
EXTRA_CFLAGS += -Idrivers/media/common/tuners
EXTRA_CFLAGS += -Idrivers/media/dvb/dvb-core
EXTRA_CFLAGS += -Idrivers/media/dvb/frontends
#include <linux/kernel.h>
#include <linux/usb.h>
#include <linux/init.h>
#include <linux/sound.h>
#include <linux/spinlock.h>
#include <linux/soundcard.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/proc_fs.h>
#include <linux/module.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>
#include <sound/info.h>
#include <sound/initval.h>
#include <sound/control.h>
#include <media/v4l2-common.h>
#include "pd-common.h"
#include "vendorcmds.h"
static void complete_handler_audio(struct urb *urb);
#define AUDIO_EP (0x83)
#define AUDIO_BUF_SIZE (512)
#define PERIOD_SIZE (1024 * 8)
#define PERIOD_MIN (4)
#define PERIOD_MAX PERIOD_MIN
static struct snd_pcm_hardware snd_pd_hw_capture = {
.info = SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_MMAP_VALID,
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_48000,
.rate_min = 48000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = PERIOD_SIZE * PERIOD_MIN,
.period_bytes_min = PERIOD_SIZE,
.period_bytes_max = PERIOD_SIZE,
.periods_min = PERIOD_MIN,
.periods_max = PERIOD_MAX,
/*
.buffer_bytes_max = 62720 * 8,
.period_bytes_min = 64,
.period_bytes_max = 12544,
.periods_min = 2,
.periods_max = 98
*/
};
static int snd_pd_capture_open(struct snd_pcm_substream *substream)
{
struct poseidon *p = snd_pcm_substream_chip(substream);
struct poseidon_audio *pa = &p->audio;
struct snd_pcm_runtime *runtime = substream->runtime;
if (!p)
return -ENODEV;
pa->users++;
pa->card_close = 0;
pa->capture_pcm_substream = substream;
runtime->private_data = p;
runtime->hw = snd_pd_hw_capture;
snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
usb_autopm_get_interface(p->interface);
kref_get(&p->kref);
return 0;
}
static int snd_pd_pcm_close(struct snd_pcm_substream *substream)
{
struct poseidon *p = snd_pcm_substream_chip(substream);
struct poseidon_audio *pa = &p->audio;
pa->users--;
pa->card_close = 1;
usb_autopm_put_interface(p->interface);
kref_put(&p->kref, poseidon_delete);
return 0;
}
static int snd_pd_hw_capture_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *hw_params)
{
struct snd_pcm_runtime *runtime = substream->runtime;
unsigned int size;
size = params_buffer_bytes(hw_params);
if (runtime->dma_area) {
if (runtime->dma_bytes > size)
return 0;
vfree(runtime->dma_area);
}
runtime->dma_area = vmalloc(size);
if (!runtime->dma_area)
return -ENOMEM;
else
runtime->dma_bytes = size;
return 0;
}
static int audio_buf_free(struct poseidon *p)
{
struct poseidon_audio *pa = &p->audio;
int i;
for (i = 0; i < AUDIO_BUFS; i++)
if (pa->urb_array[i])
usb_kill_urb(pa->urb_array[i]);
free_all_urb_generic(pa->urb_array, AUDIO_BUFS);
logpm();
return 0;
}
static int snd_pd_hw_capture_free(struct snd_pcm_substream *substream)
{
struct poseidon *p = snd_pcm_substream_chip(substream);
logpm();
audio_buf_free(p);
return 0;
}
static int snd_pd_prepare(struct snd_pcm_substream *substream)
{
return 0;
}
#define AUDIO_TRAILER_SIZE (16)
static inline void handle_audio_data(struct urb *urb, int *period_elapsed)
{
struct poseidon_audio *pa = urb->context;
struct snd_pcm_runtime *runtime = pa->capture_pcm_substream->runtime;
int stride = runtime->frame_bits >> 3;
int len = urb->actual_length / stride;
unsigned char *cp = urb->transfer_buffer;
unsigned int oldptr = pa->rcv_position;
if (urb->actual_length == AUDIO_BUF_SIZE - 4)
len -= (AUDIO_TRAILER_SIZE / stride);
/* do the copy */
if (oldptr + len >= runtime->buffer_size) {
unsigned int cnt = runtime->buffer_size - oldptr;
memcpy(runtime->dma_area + oldptr * stride, cp, cnt * stride);
memcpy(runtime->dma_area, (cp + cnt * stride),
(len * stride - cnt * stride));
} else
memcpy(runtime->dma_area + oldptr * stride, cp, len * stride);
/* update the statas */
snd_pcm_stream_lock(pa->capture_pcm_substream);
pa->rcv_position += len;
if (pa->rcv_position >= runtime->buffer_size)
pa->rcv_position -= runtime->buffer_size;
pa->copied_position += (len);
if (pa->copied_position >= runtime->period_size) {
pa->copied_position -= runtime->period_size;
*period_elapsed = 1;
}
snd_pcm_stream_unlock(pa->capture_pcm_substream);
}
static void complete_handler_audio(struct urb *urb)
{
struct poseidon_audio *pa = urb->context;
struct snd_pcm_substream *substream = pa->capture_pcm_substream;
int period_elapsed = 0;
int ret;
if (1 == pa->card_close || pa->capture_stream != STREAM_ON)
return;
if (urb->status != 0) {
/*if (urb->status == -ESHUTDOWN)*/
return;
}
if (substream) {
if (urb->actual_length) {
handle_audio_data(urb, &period_elapsed);
if (period_elapsed)
snd_pcm_period_elapsed(substream);
}
}
ret = usb_submit_urb(urb, GFP_ATOMIC);
if (ret < 0)
log("audio urb failed (errcod = %i)", ret);
return;
}
static int fire_audio_urb(struct poseidon *p)
{
int i, ret = 0;
struct poseidon_audio *pa = &p->audio;
alloc_bulk_urbs_generic(pa->urb_array, AUDIO_BUFS,
p->udev, AUDIO_EP,
AUDIO_BUF_SIZE, GFP_ATOMIC,
complete_handler_audio, pa);
for (i = 0; i < AUDIO_BUFS; i++) {
ret = usb_submit_urb(pa->urb_array[i], GFP_KERNEL);
if (ret)
log("urb err : %d", ret);
}
log();
return ret;
}
static int snd_pd_capture_trigger(struct snd_pcm_substream *substream, int cmd)
{
struct poseidon *p = snd_pcm_substream_chip(substream);
struct poseidon_audio *pa = &p->audio;
if (debug_mode)
log("cmd %d, audio stat : %d\n", cmd, pa->capture_stream);
switch (cmd) {
case SNDRV_PCM_TRIGGER_RESUME:
case SNDRV_PCM_TRIGGER_START:
if (pa->capture_stream == STREAM_ON)
return 0;
pa->rcv_position = pa->copied_position = 0;
pa->capture_stream = STREAM_ON;
if (in_hibernation(p))
return 0;
fire_audio_urb(p);
return 0;
case SNDRV_PCM_TRIGGER_SUSPEND:
pa->capture_stream = STREAM_SUSPEND;
return 0;
case SNDRV_PCM_TRIGGER_STOP:
pa->capture_stream = STREAM_OFF;
return 0;
default:
return -EINVAL;
}
}
static snd_pcm_uframes_t
snd_pd_capture_pointer(struct snd_pcm_substream *substream)
{
struct poseidon *p = snd_pcm_substream_chip(substream);
struct poseidon_audio *pa = &p->audio;
return pa->rcv_position;
}
static struct page *snd_pcm_pd_get_page(struct snd_pcm_substream *subs,
unsigned long offset)
{
void *pageptr = subs->runtime->dma_area + offset;
return vmalloc_to_page(pageptr);
}
static struct snd_pcm_ops pcm_capture_ops = {
.open = snd_pd_capture_open,
.close = snd_pd_pcm_close,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = snd_pd_hw_capture_params,
.hw_free = snd_pd_hw_capture_free,
.prepare = snd_pd_prepare,
.trigger = snd_pd_capture_trigger,
.pointer = snd_pd_capture_pointer,
.page = snd_pcm_pd_get_page,
};
#ifdef CONFIG_PM
int pm_alsa_suspend(struct poseidon *p)
{
logpm(p);
audio_buf_free(p);
return 0;
}
int pm_alsa_resume(struct poseidon *p)
{
logpm(p);
fire_audio_urb(p);
return 0;
}
#endif
int poseidon_audio_init(struct poseidon *p)
{
struct poseidon_audio *pa = &p->audio;
struct snd_card *card;
struct snd_pcm *pcm;
int ret;
ret = snd_card_create(-1, "Telegent", THIS_MODULE, 0, &card);
if (ret != 0)
return ret;
ret = snd_pcm_new(card, "poseidon audio", 0, 0, 1, &pcm);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &pcm_capture_ops);
pcm->info_flags = 0;
pcm->private_data = p;
strcpy(pcm->name, "poseidon audio capture");
strcpy(card->driver, "ALSA driver");
strcpy(card->shortname, "poseidon Audio");
strcpy(card->longname, "poseidon ALSA Audio");
if (snd_card_register(card)) {
snd_card_free(card);
return -ENOMEM;
}
pa->card = card;
return 0;
}
int poseidon_audio_free(struct poseidon *p)
{
struct poseidon_audio *pa = &p->audio;
if (pa->card)
snd_card_free(pa->card);
return 0;
}
#ifndef PD_COMMON_H
#define PD_COMMON_H
#include <linux/version.h>
#include <linux/fs.h>
#include <linux/wait.h>
#include <linux/list.h>
#include <linux/videodev2.h>
#include <linux/semaphore.h>
#include <linux/usb.h>
#include <linux/poll.h>
#include <media/videobuf-vmalloc.h>
#include <media/v4l2-device.h>
#include "dvb_frontend.h"
#include "dvbdev.h"
#include "dvb_demux.h"
#include "dmxdev.h"
#define SBUF_NUM 8
#define MAX_BUFFER_NUM 6
#define PK_PER_URB 32
#define ISO_PKT_SIZE 3072
#define POSEIDON_STATE_NONE (0x0000)
#define POSEIDON_STATE_ANALOG (0x0001)
#define POSEIDON_STATE_FM (0x0002)
#define POSEIDON_STATE_DVBT (0x0004)
#define POSEIDON_STATE_VBI (0x0008)
#define POSEIDON_STATE_DISCONNECT (0x0080)
#define PM_SUSPEND_DELAY 3
#define V4L_PAL_VBI_LINES 18
#define V4L_NTSC_VBI_LINES 12
#define V4L_PAL_VBI_FRAMESIZE (V4L_PAL_VBI_LINES * 1440 * 2)
#define V4L_NTSC_VBI_FRAMESIZE (V4L_NTSC_VBI_LINES * 1440 * 2)
#define TUNER_FREQ_MIN (45000000)
#define TUNER_FREQ_MAX (862000000)
struct vbi_data {
struct video_device *v_dev;
struct video_data *video;
struct front_face *front;
unsigned int copied;
unsigned int vbi_size; /* the whole size of two fields */
int users;
};
/*
* This is the running context of the video, it is useful for
* resume()
*/
struct running_context {
u32 freq; /* VIDIOC_S_FREQUENCY */
int audio_idx; /* VIDIOC_S_TUNER */
v4l2_std_id tvnormid; /* VIDIOC_S_STD */
int sig_index; /* VIDIOC_S_INPUT */
struct v4l2_pix_format pix; /* VIDIOC_S_FMT */
};
struct video_data {
/* v4l2 video device */
struct video_device *v_dev;
/* the working context */
struct running_context context;
/* for data copy */
int field_count;
char *dst;
int lines_copied;
int prev_left;
int lines_per_field;
int lines_size;
/* for communication */
u8 endpoint_addr;
struct urb *urb_array[SBUF_NUM];
struct vbi_data *vbi;
struct poseidon *pd;
struct front_face *front;
int is_streaming;
int users;
/* for bubble handler */
struct work_struct bubble_work;
};
enum pcm_stream_state {
STREAM_OFF,
STREAM_ON,
STREAM_SUSPEND,
};
#define AUDIO_BUFS (3)
#define CAPTURE_STREAM_EN 1
struct poseidon_audio {
struct urb *urb_array[AUDIO_BUFS];
unsigned int copied_position;
struct snd_pcm_substream *capture_pcm_substream;
unsigned int rcv_position;
struct snd_card *card;
int card_close;
int users;
int pm_state;
enum pcm_stream_state capture_stream;
};
struct radio_data {
__u32 fm_freq;
int users;
unsigned int is_radio_streaming;
struct video_device *fm_dev;
};
#define DVB_SBUF_NUM 4
#define DVB_URB_BUF_SIZE 0x2000
struct pd_dvb_adapter {
struct dvb_adapter dvb_adap;
struct dvb_frontend dvb_fe;
struct dmxdev dmxdev;
struct dvb_demux demux;
atomic_t users;
atomic_t active_feed;
/* data transfer */
s32 is_streaming;
struct urb *urb_array[DVB_SBUF_NUM];
struct poseidon *pd_device;
u8 ep_addr;
u8 reserved[3];
/* data for power resume*/
struct dvb_frontend_parameters fe_param;
/* for channel scanning */
int prev_freq;
int bandwidth;
unsigned long last_jiffies;
};
struct front_face {
/* use this field to distinguish VIDEO and VBI */
enum v4l2_buf_type type;
/* for host */
struct videobuf_queue q;
/* the bridge for host and device */
struct videobuf_buffer *curr_frame;
/* for device */
spinlock_t queue_lock;
struct list_head active;
struct poseidon *pd;
};
struct poseidon {
struct list_head device_list;
struct mutex lock;
struct kref kref;
/* for V4L2 */
struct v4l2_device v4l2_dev;
/* hardware info */
struct usb_device *udev;
struct usb_interface *interface;
int cur_transfer_mode;
struct video_data video_data; /* video */
struct vbi_data vbi_data; /* vbi */
struct poseidon_audio audio; /* audio (alsa) */
struct radio_data radio_data; /* FM */
struct pd_dvb_adapter dvb_data; /* DVB */
u32 state;
int country_code;
struct file *file_for_stream; /* the active stream*/
#ifdef CONFIG_PM
int (*pm_suspend)(struct poseidon *);
int (*pm_resume)(struct poseidon *);
pm_message_t msg;
struct work_struct pm_work;
u8 portnum;
#endif
};
struct poseidon_format {
char *name;
int fourcc; /* video4linux 2 */
int depth; /* bit/pixel */
int flags;
};
struct poseidon_tvnorm {
v4l2_std_id v4l2_id;
char name[12];
u32 tlg_tvnorm;
};
/* video */
int pd_video_init(struct poseidon *);
void pd_video_exit(struct poseidon *);
int stop_all_video_stream(struct poseidon *);
/* alsa audio */
int poseidon_audio_init(struct poseidon *);
int poseidon_audio_free(struct poseidon *);
#ifdef CONFIG_PM
int pm_alsa_suspend(struct poseidon *);
int pm_alsa_resume(struct poseidon *);
#endif
/* dvb */
int pd_dvb_usb_device_init(struct poseidon *);
void pd_dvb_usb_device_exit(struct poseidon *);
void pd_dvb_usb_device_cleanup(struct poseidon *);
int pd_dvb_get_adapter_num(struct pd_dvb_adapter *);
void dvb_stop_streaming(struct pd_dvb_adapter *);
/* FM */
int poseidon_fm_init(struct poseidon *);
int poseidon_fm_exit(struct poseidon *);
struct video_device *vdev_init(struct poseidon *, struct video_device *);
/* vendor command ops */
int send_set_req(struct poseidon*, u8, s32, s32*);
int send_get_req(struct poseidon*, u8, s32, void*, s32*, s32);
s32 set_tuner_mode(struct poseidon*, unsigned char);
enum tlg__analog_audio_standard get_audio_std(s32, s32);
/* bulk urb alloc/free */
int alloc_bulk_urbs_generic(struct urb **urb_array, int num,
struct usb_device *udev, u8 ep_addr,
int buf_size, gfp_t gfp_flags,
usb_complete_t complete_fn, void *context);
void free_all_urb_generic(struct urb **urb_array, int num);
/* misc */
void poseidon_delete(struct kref *kref);
void destroy_video_device(struct video_device **v_dev);
extern int country_code;
extern int debug_mode;
void set_debug_mode(struct video_device *vfd, int debug_mode);
#define in_hibernation(pd) (pd->msg.event == PM_EVENT_FREEZE)
#define get_pm_count(p) (atomic_read(&(p)->interface->pm_usage_cnt))
#define log(a, ...) printk(KERN_DEBUG "\t[ %s : %.3d ] "a"\n", \
__func__, __LINE__, ## __VA_ARGS__)
/* for power management */
#define logpm(pd) do {\
if (debug_mode & 0x10)\
log();\
} while (0)
#define logs(f) do { \
if ((debug_mode & 0x4) && \
(f)->type == V4L2_BUF_TYPE_VBI_CAPTURE) \
log("type : VBI");\
\
if ((debug_mode & 0x8) && \
(f)->type == V4L2_BUF_TYPE_VIDEO_CAPTURE) \
log("type : VIDEO");\
} while (0)
#endif
This diff is collapsed.
This diff is collapsed.
#include <linux/init.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/bitmap.h>
#include <linux/usb.h>
#include <linux/i2c.h>
#include <media/v4l2-dev.h>
#include <linux/version.h>
#include <linux/mm.h>
#include <linux/mutex.h>
#include <media/v4l2-ioctl.h>
#include <linux/sched.h>
#include "pd-common.h"
#include "vendorcmds.h"
static int set_frequency(struct poseidon *p, __u32 frequency);
static int poseidon_fm_close(struct file *filp);
static int poseidon_fm_open(struct file *filp);
#define TUNER_FREQ_MIN_FM 76000000
#define TUNER_FREQ_MAX_FM 108000000
static int poseidon_check_mode_radio(struct poseidon *p)
{
int ret, radiomode;
u32 status;
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(HZ/2);
ret = usb_set_interface(p->udev, 0, BULK_ALTERNATE_IFACE);
if (ret < 0)
goto out;
ret = set_tuner_mode(p, TLG_MODE_FM_RADIO);
if (ret != 0)
goto out;
ret = send_set_req(p, SGNL_SRC_SEL, TLG_SIG_SRC_ANTENNA, &status);
radiomode = get_audio_std(TLG_MODE_FM_RADIO, p->country_code);
ret = send_set_req(p, TUNER_AUD_ANA_STD, radiomode, &status);
ret |= send_set_req(p, TUNER_AUD_MODE,
TLG_TUNE_TVAUDIO_MODE_STEREO, &status);
ret |= send_set_req(p, AUDIO_SAMPLE_RATE_SEL,
ATV_AUDIO_RATE_48K, &status);
ret |= send_set_req(p, TUNE_FREQ_SELECT, TUNER_FREQ_MIN_FM, &status);
out:
return ret;
}
#ifdef CONFIG_PM
static int pm_fm_suspend(struct poseidon *p)
{
logpm(p);
pm_alsa_suspend(p);
usb_set_interface(p->udev, 0, 0);
msleep(300);
return 0;
}
static int pm_fm_resume(struct poseidon *p)
{
logpm(p);
poseidon_check_mode_radio(p);
set_frequency(p, p->radio_data.fm_freq);
pm_alsa_resume(p);
return 0;
}
#endif
static int poseidon_fm_open(struct file *filp)
{
struct video_device *vfd = video_devdata(filp);
struct poseidon *p = video_get_drvdata(vfd);
int ret = 0;
if (!p)
return -1;
mutex_lock(&p->lock);
if (p->state & POSEIDON_STATE_DISCONNECT) {
ret = -ENODEV;
goto out;
}
if (p->state && !(p->state & POSEIDON_STATE_FM)) {
ret = -EBUSY;
goto out;
}
usb_autopm_get_interface(p->interface);
if (0 == p->state) {
p->country_code = country_code;
set_debug_mode(vfd, debug_mode);
ret = poseidon_check_mode_radio(p);
if (ret < 0) {
usb_autopm_put_interface(p->interface);
goto out;
}
p->state |= POSEIDON_STATE_FM;
}
p->radio_data.users++;
kref_get(&p->kref);
filp->private_data = p;
out:
mutex_unlock(&p->lock);
return ret;
}
static int poseidon_fm_close(struct file *filp)
{
struct poseidon *p = filp->private_data;
struct radio_data *fm = &p->radio_data;
uint32_t status;
mutex_lock(&p->lock);
fm->users--;
if (0 == fm->users)
p->state &= ~POSEIDON_STATE_FM;
if (fm->is_radio_streaming && filp == p->file_for_stream) {
fm->is_radio_streaming = 0;
send_set_req(p, PLAY_SERVICE, TLG_TUNE_PLAY_SVC_STOP, &status);
}
usb_autopm_put_interface(p->interface);
mutex_unlock(&p->lock);
kref_put(&p->kref, poseidon_delete);
filp->private_data = NULL;
return 0;
}
static int vidioc_querycap(struct file *file, void *priv,
struct v4l2_capability *v)
{
struct poseidon *p = file->private_data;
strlcpy(v->driver, "tele-radio", sizeof(v->driver));
strlcpy(v->card, "Telegent Poseidon", sizeof(v->card));
usb_make_path(p->udev, v->bus_info, sizeof(v->bus_info));
v->version = KERNEL_VERSION(0, 0, 1);
v->capabilities = V4L2_CAP_TUNER | V4L2_CAP_RADIO;
return 0;
}
static const struct v4l2_file_operations poseidon_fm_fops = {
.owner = THIS_MODULE,
.open = poseidon_fm_open,
.release = poseidon_fm_close,
.ioctl = video_ioctl2,
};
int tlg_fm_vidioc_g_tuner(struct file *file, void *priv, struct v4l2_tuner *vt)
{
struct tuner_fm_sig_stat_s fm_stat = {};
int ret, status, count = 5;
struct poseidon *p = file->private_data;
if (vt->index != 0)
return -EINVAL;
vt->type = V4L2_TUNER_RADIO;
vt->capability = V4L2_TUNER_CAP_STEREO;
vt->rangelow = TUNER_FREQ_MIN_FM / 62500;
vt->rangehigh = TUNER_FREQ_MAX_FM / 62500;
vt->rxsubchans = V4L2_TUNER_SUB_STEREO;
vt->audmode = V4L2_TUNER_MODE_STEREO;
vt->signal = 0;
vt->afc = 0;
mutex_lock(&p->lock);
ret = send_get_req(p, TUNER_STATUS, TLG_MODE_FM_RADIO,
&fm_stat, &status, sizeof(fm_stat));
while (fm_stat.sig_lock_busy && count-- && !ret) {
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(HZ);
ret = send_get_req(p, TUNER_STATUS, TLG_MODE_FM_RADIO,
&fm_stat, &status, sizeof(fm_stat));
}
mutex_unlock(&p->lock);
if (ret || status) {
vt->signal = 0;
} else if ((fm_stat.sig_present || fm_stat.sig_locked)
&& fm_stat.sig_strength == 0) {
vt->signal = 0xffff;
} else
vt->signal = (fm_stat.sig_strength * 255 / 10) << 8;
return 0;
}
int fm_get_freq(struct file *file, void *priv, struct v4l2_frequency *argp)
{
struct poseidon *p = file->private_data;
argp->frequency = p->radio_data.fm_freq;
return 0;
}
static int set_frequency(struct poseidon *p, __u32 frequency)
{
__u32 freq ;
int ret, status, radiomode;
mutex_lock(&p->lock);
radiomode = get_audio_std(TLG_MODE_FM_RADIO, p->country_code);
/*NTSC 8,PAL 2 */
ret = send_set_req(p, TUNER_AUD_ANA_STD, radiomode, &status);
freq = (frequency * 125) * 500 / 1000;/* kHZ */
if (freq < TUNER_FREQ_MIN_FM/1000 || freq > TUNER_FREQ_MAX_FM/1000) {
ret = -EINVAL;
goto error;
}
ret = send_set_req(p, TUNE_FREQ_SELECT, freq, &status);
if (ret < 0)
goto error ;
ret = send_set_req(p, TAKE_REQUEST, 0, &status);
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(HZ/4);
if (!p->radio_data.is_radio_streaming) {
ret = send_set_req(p, TAKE_REQUEST, 0, &status);
ret = send_set_req(p, PLAY_SERVICE,
TLG_TUNE_PLAY_SVC_START, &status);
p->radio_data.is_radio_streaming = 1;
}
p->radio_data.fm_freq = frequency;
error:
mutex_unlock(&p->lock);
return ret;
}
int fm_set_freq(struct file *file, void *priv, struct v4l2_frequency *argp)
{
struct poseidon *p = file->private_data;
p->file_for_stream = file;
#ifdef CONFIG_PM
p->pm_suspend = pm_fm_suspend;
p->pm_resume = pm_fm_resume;
#endif
return set_frequency(p, argp->frequency);
}
int tlg_fm_vidioc_g_ctrl(struct file *file, void *priv,
struct v4l2_control *arg)
{
return 0;
}
int tlg_fm_vidioc_exts_ctrl(struct file *file, void *fh,
struct v4l2_ext_controls *a)
{
return 0;
}
int tlg_fm_vidioc_s_ctrl(struct file *file, void *priv,
struct v4l2_control *arg)
{
return 0;
}
int tlg_fm_vidioc_queryctrl(struct file *file, void *priv,
struct v4l2_queryctrl *arg)
{
arg->minimum = 0;
arg->maximum = 65535;
return 0;
}
static int vidioc_s_tuner(struct file *file, void *priv, struct v4l2_tuner *vt)
{
return vt->index > 0 ? -EINVAL : 0;
}
static int vidioc_s_audio(struct file *file, void *priv, struct v4l2_audio *va)
{
return (va->index != 0) ? -EINVAL : 0;
}
static int vidioc_g_audio(struct file *file, void *priv, struct v4l2_audio *a)
{
a->index = 0;
a->mode = 0;
a->capability = V4L2_AUDCAP_STEREO;
strcpy(a->name, "Radio");
return 0;
}
static int vidioc_s_input(struct file *filp, void *priv, u32 i)
{
return (i != 0) ? -EINVAL : 0;
}
static int vidioc_g_input(struct file *filp, void *priv, u32 *i)
{
return (*i != 0) ? -EINVAL : 0;
}
static const struct v4l2_ioctl_ops poseidon_fm_ioctl_ops = {
.vidioc_querycap = vidioc_querycap,
.vidioc_g_audio = vidioc_g_audio,
.vidioc_s_audio = vidioc_s_audio,
.vidioc_g_input = vidioc_g_input,
.vidioc_s_input = vidioc_s_input,
.vidioc_queryctrl = tlg_fm_vidioc_queryctrl,
.vidioc_g_ctrl = tlg_fm_vidioc_g_ctrl,
.vidioc_s_ctrl = tlg_fm_vidioc_s_ctrl,
.vidioc_s_ext_ctrls = tlg_fm_vidioc_exts_ctrl,
.vidioc_s_tuner = vidioc_s_tuner,
.vidioc_g_tuner = tlg_fm_vidioc_g_tuner,
.vidioc_g_frequency = fm_get_freq,
.vidioc_s_frequency = fm_set_freq,
};
static struct video_device poseidon_fm_template = {
.name = "Telegent-Radio",
.fops = &poseidon_fm_fops,
.minor = -1,
.release = video_device_release,
.ioctl_ops = &poseidon_fm_ioctl_ops,
};
int poseidon_fm_init(struct poseidon *p)
{
struct video_device *fm_dev;
fm_dev = vdev_init(p, &poseidon_fm_template);
if (fm_dev == NULL)
return -1;
if (video_register_device(fm_dev, VFL_TYPE_RADIO, -1) < 0) {
video_device_release(fm_dev);
return -1;
}
p->radio_data.fm_dev = fm_dev;
return 0;
}
int poseidon_fm_exit(struct poseidon *p)
{
destroy_video_device(&p->radio_data.fm_dev);
return 0;
}
This diff is collapsed.
#ifndef VENDOR_CMD_H_
#define VENDOR_CMD_H_
#define BULK_ALTERNATE_IFACE (2)
#define ISO_3K_BULK_ALTERNATE_IFACE (1)
#define REQ_SET_CMD (0X00)
#define REQ_GET_CMD (0X80)
enum tlg__analog_audio_standard {
TLG_TUNE_ASTD_NONE = 0x00000000,
TLG_TUNE_ASTD_A2 = 0x00000001,
TLG_TUNE_ASTD_NICAM = 0x00000002,
TLG_TUNE_ASTD_EIAJ = 0x00000004,
TLG_TUNE_ASTD_BTSC = 0x00000008,
TLG_TUNE_ASTD_FM_US = 0x00000010,
TLG_TUNE_ASTD_FM_EUR = 0x00000020,
TLG_TUNE_ASTD_ALL = 0x0000003f
};
/*
* identifiers for Custom Parameter messages.
* @typedef cmd_custom_param_id_t
*/
enum cmd_custom_param_id {
CUST_PARM_ID_NONE = 0x00,
CUST_PARM_ID_BRIGHTNESS_CTRL = 0x01,
CUST_PARM_ID_CONTRAST_CTRL = 0x02,
CUST_PARM_ID_HUE_CTRL = 0x03,
CUST_PARM_ID_SATURATION_CTRL = 0x04,
CUST_PARM_ID_AUDIO_SNR_THRESHOLD = 0x10,
CUST_PARM_ID_AUDIO_AGC_THRESHOLD = 0x11,
CUST_PARM_ID_MAX
};
struct tuner_custom_parameter_s {
uint16_t param_id; /* Parameter identifier */
uint16_t param_value; /* Parameter value */
};
struct tuner_ber_rate_s {
uint32_t ber_rate; /* BER sample rate in seconds */
};
struct tuner_atv_sig_stat_s {
uint32_t sig_present;
uint32_t sig_locked;
uint32_t sig_lock_busy;
uint32_t sig_strength; /* milliDb */
uint32_t tv_audio_chan; /* mono/stereo/sap*/
uint32_t mvision_stat; /* macrovision status */
};
struct tuner_dtv_sig_stat_s {
uint32_t sig_present; /* Boolean*/
uint32_t sig_locked; /* Boolean */
uint32_t sig_lock_busy; /* Boolean (Can this time-out?) */
uint32_t sig_strength; /* milliDb*/
};
struct tuner_fm_sig_stat_s {
uint32_t sig_present; /* Boolean*/
uint32_t sig_locked; /* Boolean */
uint32_t sig_lock_busy; /* Boolean */
uint32_t sig_stereo_mono;/* TBD*/
uint32_t sig_strength; /* milliDb*/
};
enum _tag_tlg_tune_srv_cmd {
TLG_TUNE_PLAY_SVC_START = 1,
TLG_TUNE_PLAY_SVC_STOP
};
enum _tag_tune_atv_audio_mode_caps {
TLG_TUNE_TVAUDIO_MODE_MONO = 0x00000001,
TLG_TUNE_TVAUDIO_MODE_STEREO = 0x00000002,
TLG_TUNE_TVAUDIO_MODE_LANG_A = 0x00000010,/* Primary language*/
TLG_TUNE_TVAUDIO_MODE_LANG_B = 0x00000020,/* 2nd avail language*/
TLG_TUNE_TVAUDIO_MODE_LANG_C = 0x00000040
};
enum _tag_tuner_atv_audio_rates {
ATV_AUDIO_RATE_NONE = 0x00,/* Audio not supported*/
ATV_AUDIO_RATE_32K = 0x01,/* Audio rate = 32 KHz*/
ATV_AUDIO_RATE_48K = 0x02, /* Audio rate = 48 KHz*/
ATV_AUDIO_RATE_31_25K = 0x04 /* Audio rate = 31.25KHz */
};
enum _tag_tune_atv_vid_res_caps {
TLG_TUNE_VID_RES_NONE = 0x00000000,
TLG_TUNE_VID_RES_720 = 0x00000001,
TLG_TUNE_VID_RES_704 = 0x00000002,
TLG_TUNE_VID_RES_360 = 0x00000004
};
enum _tag_tuner_analog_video_format {
TLG_TUNER_VID_FORMAT_YUV = 0x00000001,
TLG_TUNER_VID_FORMAT_YCRCB = 0x00000002,
TLG_TUNER_VID_FORMAT_RGB_565 = 0x00000004,
};
enum tlg_ext_audio_support {
TLG_EXT_AUDIO_NONE = 0x00,/* No external audio input supported */
TLG_EXT_AUDIO_LR = 0x01/* LR external audio inputs supported*/
};
enum {
TLG_MODE_NONE = 0x00, /* No Mode specified*/
TLG_MODE_ANALOG_TV = 0x01, /* Analog Television mode*/
TLG_MODE_ANALOG_TV_UNCOMP = 0x01, /* Analog Television mode*/
TLG_MODE_ANALOG_TV_COMP = 0x02, /* Analog TV mode (compressed)*/
TLG_MODE_FM_RADIO = 0x04, /* FM Radio mode*/
TLG_MODE_DVB_T = 0x08, /* Digital TV (DVB-T)*/
};
enum tlg_signal_sources_t {
TLG_SIG_SRC_NONE = 0x00,/* Signal source not specified */
TLG_SIG_SRC_ANTENNA = 0x01,/* Signal src is: Antenna */
TLG_SIG_SRC_CABLE = 0x02,/* Signal src is: Coax Cable*/
TLG_SIG_SRC_SVIDEO = 0x04,/* Signal src is: S_VIDEO */
TLG_SIG_SRC_COMPOSITE = 0x08 /* Signal src is: Composite Video */
};
enum tuner_analog_video_standard {
TLG_TUNE_VSTD_NONE = 0x00000000,
TLG_TUNE_VSTD_NTSC_M = 0x00000001,
TLG_TUNE_VSTD_NTSC_M_J = 0x00000002,/* Japan */
TLG_TUNE_VSTD_PAL_B = 0x00000010,
TLG_TUNE_VSTD_PAL_D = 0x00000020,
TLG_TUNE_VSTD_PAL_G = 0x00000040,
TLG_TUNE_VSTD_PAL_H = 0x00000080,
TLG_TUNE_VSTD_PAL_I = 0x00000100,
TLG_TUNE_VSTD_PAL_M = 0x00000200,
TLG_TUNE_VSTD_PAL_N = 0x00000400,
TLG_TUNE_VSTD_SECAM_B = 0x00001000,
TLG_TUNE_VSTD_SECAM_D = 0x00002000,
TLG_TUNE_VSTD_SECAM_G = 0x00004000,
TLG_TUNE_VSTD_SECAM_H = 0x00008000,
TLG_TUNE_VSTD_SECAM_K = 0x00010000,
TLG_TUNE_VSTD_SECAM_K1 = 0x00020000,
TLG_TUNE_VSTD_SECAM_L = 0x00040000,
TLG_TUNE_VSTD_SECAM_L1 = 0x00080000,
TLG_TUNE_VSTD_PAL_N_COMBO = 0x00100000
};
enum tlg_mode_caps {
TLG_MODE_CAPS_NONE = 0x00, /* No Mode specified */
TLG_MODE_CAPS_ANALOG_TV_UNCOMP = 0x01, /* Analog TV mode */
TLG_MODE_CAPS_ANALOG_TV_COMP = 0x02, /* Analog TV (compressed)*/
TLG_MODE_CAPS_FM_RADIO = 0x04, /* FM Radio mode */
TLG_MODE_CAPS_DVB_T = 0x08, /* Digital TV (DVB-T) */
};
enum poseidon_vendor_cmds {
LAST_CMD_STAT = 0x00,
GET_CHIP_ID = 0x01,
GET_FW_ID = 0x02,
PRODUCT_CAPS = 0x03,
TUNE_MODE_CAP_ATV = 0x10,
TUNE_MODE_CAP_ATVCOMP = 0X10,
TUNE_MODE_CAP_DVBT = 0x10,
TUNE_MODE_CAP_FM = 0x10,
TUNE_MODE_SELECT = 0x11,
TUNE_FREQ_SELECT = 0x12,
SGNL_SRC_SEL = 0x13,
VIDEO_STD_SEL = 0x14,
VIDEO_STREAM_FMT_SEL = 0x15,
VIDEO_ROSOLU_AVAIL = 0x16,
VIDEO_ROSOLU_SEL = 0x17,
VIDEO_CONT_PROTECT = 0x20,
VCR_TIMING_MODSEL = 0x21,
EXT_AUDIO_CAP = 0x22,
EXT_AUDIO_SEL = 0x23,
TEST_PATTERN_SEL = 0x24,
VBI_DATA_SEL = 0x25,
AUDIO_SAMPLE_RATE_CAP = 0x28,
AUDIO_SAMPLE_RATE_SEL = 0x29,
TUNER_AUD_MODE = 0x2a,
TUNER_AUD_MODE_AVAIL = 0x2b,
TUNER_AUD_ANA_STD = 0x2c,
TUNER_CUSTOM_PARAMETER = 0x2f,
DVBT_TUNE_MODE_SEL = 0x30,
DVBT_BANDW_CAP = 0x31,
DVBT_BANDW_SEL = 0x32,
DVBT_GUARD_INTERV_CAP = 0x33,
DVBT_GUARD_INTERV_SEL = 0x34,
DVBT_MODULATION_CAP = 0x35,
DVBT_MODULATION_SEL = 0x36,
DVBT_INNER_FEC_RATE_CAP = 0x37,
DVBT_INNER_FEC_RATE_SEL = 0x38,
DVBT_TRANS_MODE_CAP = 0x39,
DVBT_TRANS_MODE_SEL = 0x3a,
DVBT_SEARCH_RANG = 0x3c,
TUNER_SETUP_ANALOG = 0x40,
TUNER_SETUP_DIGITAL = 0x41,
TUNER_SETUP_FM_RADIO = 0x42,
TAKE_REQUEST = 0x43, /* Take effect of the command */
PLAY_SERVICE = 0x44, /* Play start or Play stop */
TUNER_STATUS = 0x45,
TUNE_PROP_DVBT = 0x46,
ERR_RATE_STATS = 0x47,
TUNER_BER_RATE = 0x48,
SCAN_CAPS = 0x50,
SCAN_SETUP = 0x51,
SCAN_SERVICE = 0x52,
SCAN_STATS = 0x53,
PID_SET = 0x58,
PID_UNSET = 0x59,
PID_LIST = 0x5a,
IRD_CAP = 0x60,
IRD_MODE_SEL = 0x61,
IRD_SETUP = 0x62,
PTM_MODE_CAP = 0x70,
PTM_MODE_SEL = 0x71,
PTM_SERVICE = 0x72,
TUNER_REG_SCRIPT = 0x73,
CMD_CHIP_RST = 0x74,
};
enum tlg_bw {
TLG_BW_5 = 5,
TLG_BW_6 = 6,
TLG_BW_7 = 7,
TLG_BW_8 = 8,
TLG_BW_12 = 12,
TLG_BW_15 = 15
};
struct cmd_firmware_vers_s {
uint8_t fw_rev_major;
uint8_t fw_rev_minor;
uint16_t fw_patch;
};
#endif /* VENDOR_CMD_H_ */
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