summaryrefslogtreecommitdiff
path: root/linux/drivers/media/video/cx88/cx88-dvb.c
diff options
context:
space:
mode:
authorGerd Knorr <devnull@localhost>2004-08-25 14:47:53 +0000
committerGerd Knorr <devnull@localhost>2004-08-25 14:47:53 +0000
commit6550ed72a57b714bfb1a718538d1fb5c542aa470 (patch)
tree7e7eb707f80203ecdb680cfd8b3d7ac4f2d3af11 /linux/drivers/media/video/cx88/cx88-dvb.c
parentf680c1f3f262eae1802d080db3eccee4f9da3f43 (diff)
downloadmediapointer-dvb-s2-6550ed72a57b714bfb1a718538d1fb5c542aa470.tar.gz
mediapointer-dvb-s2-6550ed72a57b714bfb1a718538d1fb5c542aa470.tar.bz2
- cx88: plenty of mpeg work. blackbird might even work with
some luck. dvb needs more work.
Diffstat (limited to 'linux/drivers/media/video/cx88/cx88-dvb.c')
-rw-r--r--linux/drivers/media/video/cx88/cx88-dvb.c521
1 files changed, 264 insertions, 257 deletions
diff --git a/linux/drivers/media/video/cx88/cx88-dvb.c b/linux/drivers/media/video/cx88/cx88-dvb.c
index 08cc56d27..398fbbd16 100644
--- a/linux/drivers/media/video/cx88/cx88-dvb.c
+++ b/linux/drivers/media/video/cx88/cx88-dvb.c
@@ -23,342 +23,349 @@
#include <linux/init.h>
#include <linux/device.h>
#include <linux/fs.h>
+#include <linux/kthread.h>
+#include <linux/file.h>
#include "cx88.h"
MODULE_DESCRIPTION("driver for cx2388x based DVB cards");
MODULE_AUTHOR("Chris Pascoe <c.pascoe@itee.uq.edu.au>");
+MODULE_AUTHOR("Gerd Knorr <kraxel@bytesex.org> [SuSE Labs]");
MODULE_LICENSE("GPL");
-/* ------------------------------------------------------------------ */
+static unsigned int debug = 0;
+MODULE_PARM(debug,"i");
+MODULE_PARM_DESC(debug,"enable debug messages [dvb]");
-#if 0
-static void cx88_dvb_tasklet(unsigned long data)
-{
- struct cx8800_dvb *dvb = (struct cx8800_dvb *)data;
- struct cx8800_dev *dev = dvb->dev;
- struct cx88_dmaqueue *q = &dvb->tsq;
- struct cx88_buffer *buf;
- unsigned long flags;
+#define dprintk(level,fmt, arg...) if (debug >= level) \
+ printk(KERN_DEBUG "%s/2-dvb: " fmt, dev->core->name , ## arg)
- /* Process any pending buffers */
- spin_lock_irqsave(&dev->slock, flags);
- while (! list_empty(&q->queued)) {
- /* get items */
- buf = list_entry(q->queued.next, struct cx88_buffer, vb.queue);
- spin_unlock_irqrestore(&dev->slock, flags);
-
- /* Hand the received frames to the software filter */
- (dvb->ts_includes_fec ? dvb_dmx_swfilter_204 : dvb_dmx_swfilter)
- (&dvb->demux, (char *)buf->vb.dma.vmalloc,
- buf->vb.field_count * buf->bpl);
-
- /* Reactivate the buffer */
- spin_lock_irqsave(&dev->slock, flags);
- list_del(&buf->vb.queue);
- activate_ts_buffer(dvb, buf, 1);
- }
- spin_unlock_irqrestore(&dev->slock, flags);
-}
+/* ------------------------------------------------------------------ */
-static void cx8800_wakeup_dvb_tasklet(struct cx8800_dvb *dvb,
- unsigned int maxframe, int timeout_case)
+static int dvb_buf_setup(struct file *file, unsigned int *count, unsigned int *size)
{
- struct cx88_dmaqueue *q = &dvb->tsq;
- struct cx88_buffer *buf;
- int processed_buffers = 0;
-
- /* Called with dev->slock held */
-
- dprintk(2, "cx8800_wakeup_dvb_tasklet called, maxframe: %d, "
- "timeout_case: %d\n", maxframe, timeout_case);
-
- while (! list_empty(&q->active)) {
- buf = list_entry(q->active.next, struct cx88_buffer, vb.queue);
-
- /* Calculate how many frames that are waiting for processing */
- maxframe &= 0xffff;
- buf->count &= 0xffff;
- buf->vb.field_count = (maxframe - buf->count) & 0xffff;
-
- dprintk(2, "[%p/%d] wakeup reg=%d buf=%d, field_count=%d, "
- "processed=%d\n", buf, buf->vb.i, maxframe, buf->count,
- buf->vb.field_count, processed_buffers);
-
- /* Reject any incomplete buffer if we aren't handling a timeout,
- * as data transfer into it is still occurring. */
- if (buf->vb.field_count < buf->vb.height && !timeout_case) {
- if (! processed_buffers) {
- dprintk(2, "incomplete buffer: mf: %d, count: "
- "%d, fc: %d height: %d\n", maxframe,
- buf->count, buf->vb.field_count,
- buf->vb.height);
- return; /* Don't re-timeout... */
- }
- break;
- }
+ struct cx8802_dev *dev = file->private_data;
- /* We've got no frames to process, so just reschedule
- * the timeout. */
- if (buf->vb.field_count == 0) {
- dprintk(2, "no frames... %d %d\n", maxframe,
- buf->count);
- break;
- }
-
- /* We can't possibly have more frames than we have size for
- * in the buffer, so trim back how many we think we have. */
- if (buf->vb.field_count > buf->vb.height) {
- if (buf->vb.field_count > buf->vb.height * 2 &&
- ++too_many_frames_in_one_interrupt == 10) {
- printk(KERN_WARNING "%s: you should consider"
- "increasing ts_frames_per_irq (%d "
- "frame/irq overload)\n", dvb->dev->name,
- (buf->vb.field_count - buf->vb.height));
- }
- buf->vb.field_count = buf->vb.height;
- }
-
- /* Mark the buffer as done. */
- buf->vb.state = timeout_case ? STATE_ERROR : STATE_DONE;
-
- /* Move the buffer from the active queue to the
- * queued-for-processing queue */
- list_move_tail(&buf->vb.queue, &q->queued);
-
- /* Schedule the tasklet to process them */
- tasklet_schedule(&dvb->dvb_tasklet);
-
- /* Don't queue more than one buffer if we timed out. */
- if (timeout_case)
- break;
+ dev->ts_packet_size = 188 * 4;
+ dev->ts_packet_count = 32;
- /* Record that we processed a buffer. */
- processed_buffers++;
- }
+ *size = dev->ts_packet_size * dev->ts_packet_count;
+ *count = 32;
+ return 0;
+}
- if (list_empty(&q->active)) {
- del_timer(&q->timeout);
- } else {
- mod_timer(&q->timeout, jiffies + TS_BUFFER_TIMEOUT);
- }
+static int dvb_buf_prepare(struct file *file, struct videobuf_buffer *vb,
+ enum v4l2_field field)
+{
+ struct cx8802_dev *dev = file->private_data;
+ return cx8802_buf_prepare(dev, (struct cx88_buffer*)vb);
}
-/* ---------------------------------------------------------------------------- */
+static void dvb_buf_queue(struct file *file, struct videobuf_buffer *vb)
+{
+ struct cx8802_dev *dev = file->private_data;
+ cx8802_buf_queue(dev, (struct cx88_buffer*)vb);
+}
-static int master_xfer(struct dvb_i2c_bus *i2c, const struct i2c_msg msgs[],
- int num)
+static void dvb_buf_release(struct file *file, struct videobuf_buffer *vb)
{
- struct cx8800_dvb *dvb = (struct cx8800_dvb *)i2c->data;
- struct cx8800_dev *dev = dvb->dev;
- int retval;
+ struct cx8802_dev *dev = file->private_data;
+ cx88_free_buffer(dev->pci, (struct cx88_buffer*)vb);
+}
- if (down_interruptible(&dvb->dvb_i2c_lock))
- return -ERESTARTSYS;
+struct videobuf_queue_ops dvb_qops = {
+ .buf_setup = dvb_buf_setup,
+ .buf_prepare = dvb_buf_prepare,
+ .buf_queue = dvb_buf_queue,
+ .buf_release = dvb_buf_release,
+};
- retval = i2c_transfer(&dev->i2c_adap, (struct i2c_msg *)msgs, num);
+static int dvb_thread(void *data)
+{
+ struct cx8802_dev *dev = data;
+ struct videobuf_buffer *buf;
+ struct file *file;
+ unsigned long flags;
+ int err;
+
+ dprintk(1,"cx88: dvb thread started\n");
+ file = get_empty_filp();
+ file->private_data = dev;
+ videobuf_read_start(file, &dev->dvbq);
+
+ for (;;) {
+ if (kthread_should_stop())
+ break;
- up(&dvb->dvb_i2c_lock);
+ /* fetch next buffer */
+ buf = list_entry(dev->dvbq.stream.next,
+ struct videobuf_buffer, stream);
+ list_del(&buf->stream);
+ err = videobuf_waiton(buf,0,0);
+
+ /* feed buffer data to demux */
+ if (buf->state == STATE_DONE)
+ dvb_dmx_swfilter(&dev->demux, buf->dma.vmalloc,
+ buf->size);
+
+ /* requeue buffer */
+ list_add_tail(&buf->stream,&dev->dvbq.stream);
+ spin_lock_irqsave(dev->dvbq.irqlock,flags);
+ dev->dvbq.ops->buf_queue(file,buf);
+ spin_unlock_irqrestore(dev->dvbq.irqlock,flags);
+ }
- return retval;
+ videobuf_read_stop(file, &dev->dvbq);
+ put_filp(file);
+ dprintk(1,"cx88: dvb thread stopped\n");
+ return 0;
}
-static int dvb_cx8800_start_feed(struct dvb_demux_feed *feed)
+/* ---------------------------------------------------------------------------- */
+
+static int dvb_start_feed(struct dvb_demux_feed *feed)
{
struct dvb_demux *demux = feed->demux;
- struct cx8800_dvb *dvb = demux->priv;
+ struct cx8802_dev *dev = demux->priv;
+ int rc = 0;
dprintk(2, "dvb_cx8800_start_feed\n");
if (!demux->dmx.frontend)
return -EINVAL;
+ if (dev->dvb_thread)
+ return -EINVAL;
- return start_ts_capture(dvb);
+ dev->dvb_thread = kthread_run(dvb_thread, dev, "%s dvb", dev->core->name);
+ if (IS_ERR(dev->dvb_thread)) {
+ rc = PTR_ERR(dev->dvb_thread);
+ dev->dvb_thread = NULL;
+ }
+ return rc;
}
-static int dvb_cx8800_stop_feed(struct dvb_demux_feed *feed)
+static int dvb_stop_feed(struct dvb_demux_feed *feed)
{
struct dvb_demux *demux = feed->demux;
- struct cx8800_dvb *dvb = demux->priv;
+ struct cx8802_dev *dev = demux->priv;
+ int err;
dprintk(2, "dvb_cx8800_stop_feed\n");
- if (!demux->dmx.frontend)
+ if (NULL == dev->dvb_thread)
return -EINVAL;
-
- return stop_ts_capture(dvb);
+ err = kthread_stop(dev->dvb_thread);
+ dev->dvb_thread = NULL;
+ return err;
}
-static void cx8800_ts_release_dev(struct cx8800_dev *dev)
+static void dvb_unregister(struct cx8802_dev *dev)
{
- struct cx8800_dvb *dvb = dev->dvb;
-
- /* Force us to stop any feed */
- spin_lock(&dvb->feedlock);
- if (dvb->ts_feeding != 0) {
- spin_unlock(&dvb->feedlock);
- printk("Error: releasing a card while feed in progress\n");
- stop_ts_capture(dvb);
- } else
- spin_unlock(&dvb->feedlock);
-
-
- /* release everything we registered */
- dvb_net_release(&dvb->dvbnet);
- dvb->demux.dmx.remove_frontend(&dvb->demux.dmx, &dvb->fe_mem);
- dvb->demux.dmx.remove_frontend(&dvb->demux.dmx, &dvb->fe_hw);
- dvb_dmxdev_release(&dvb->dmxdev);
- dvb_dmx_release(&dvb->demux);
- dvb_unregister_i2c_bus(master_xfer, dvb->dvb_adapter, 0);
- dvb_unregister_adapter(dvb->dvb_adapter);
- btcx_riscmem_free(dev->pci, &dvb->tsq.stopper);
-
- /* Free ourselves */
- kfree(dvb);
- dev->dvb = NULL;
- return;
-}
+#if 1 /* really needed? */
+ if (NULL != dev->dvb_thread) {
+ kthread_stop(dev->dvb_thread);
+ BUG();
+ }
+#endif
-static void cx8800_ts_resume_dev(struct cx8800_dev *dev)
-{
- /* Called with dev->slock held */
- cx8800_restart_ts_queue(dev->dvb);
+ dvb_net_release(&dev->dvbnet);
+ dev->demux.dmx.remove_frontend(&dev->demux.dmx, &dev->fe_mem);
+ dev->demux.dmx.remove_frontend(&dev->demux.dmx, &dev->fe_hw);
+ dvb_dmxdev_release(&dev->dmxdev);
+ dvb_dmx_release(&dev->demux);
+ dvb_unregister_adapter(dev->dvb_adapter);
+ return;
}
-static int cx8800_ts_attach_dev(struct cx8800_dev *dev)
+static int dvb_register(struct cx8802_dev *dev)
{
- struct cx8800_dvb *dvb;
int result;
+#if 0 /* hmm, this is board specific I guess? move to cx88-cards.c? */
/* Take DVB hardware out of reset */
cx_set(MO_GP0_IO, cx88_boards[dev->board].ts.gpio0);
cx_clear(MO_GP0_IO, cx88_boards[dev->board].ts.gpio0 & 0xff);
- udelay(500);
+ msleep(1);
cx_set(MO_GP0_IO, cx88_boards[dev->board].ts.gpio0);
+#endif
- /* Allocate a DVB structure */
- dvb = kmalloc(sizeof(struct cx8800_dvb), GFP_KERNEL);
- if (dvb == NULL) {
- result = -ENOMEM;
- goto fail;
- }
- memset(dvb, 0, sizeof(struct cx8800_dvb));
-
- /* Record our device and details */
- dvb->dev = dev;
- dvb->irq_handler = cx8800_ts_irq;
- dvb->release_dev = cx8800_ts_release_dev;
- dvb->resume_dev = cx8800_ts_resume_dev;
-
- /* init TS dma queues */
- INIT_LIST_HEAD(&dvb->tsq.active);
- INIT_LIST_HEAD(&dvb->tsq.queued);
- dvb->tsq.timeout.function = cx8800_ts_timeout;
- dvb->tsq.timeout.data = (unsigned long) dvb;
- init_timer(&dvb->tsq.timeout);
- cx88_risc_stopper(dev->pci, &dvb->tsq.stopper,
- MO_TS_DMACNTRL, 0x11, 0x00);
-
- /* init TS feed spinlock */
- dvb->feedlock = SPIN_LOCK_UNLOCKED;
-
- /* Clear any pending interrupts */
- cx8800_stop_ts_dma(dvb);
-
- if ((result =
- dvb_register_adapter(&dvb->dvb_adapter, dev->name,
- THIS_MODULE)) < 0) {
- printk(KERN_WARNING
- "%s: dvb_register_adapter failed (errno = %d)\n",
- dev->name, result);
- goto fail;
+ result = dvb_register_adapter(&dev->dvb_adapter, dev->core->name,
+ THIS_MODULE);
+ if (result < 0) {
+ printk(KERN_WARNING "%s: dvb_register_adapter failed (errno = %d)\n",
+ dev->core->name, result);
+ goto fail1;
}
- init_MUTEX(&dvb->dvb_i2c_lock);
-
- if (! dvb_register_i2c_bus(master_xfer, dvb, dvb->dvb_adapter, 0)) {
- printk(KERN_WARNING "%s: dvb_register_i2c_bus failed\n",
- dev->name);
- result = -EFAULT;
+ dev->demux.dmx.capabilities =
+ DMX_TS_FILTERING | DMX_SECTION_FILTERING |
+ DMX_MEMORY_BASED_FILTERING;
+ dev->demux.priv = dev;
+ dev->demux.filternum = 256;
+ dev->demux.feednum = 256;
+ dev->demux.start_feed = dvb_start_feed;
+ dev->demux.stop_feed = dvb_stop_feed;
+ result = dvb_dmx_init(&dev->demux);
+ if (result < 0) {
+ printk(KERN_WARNING "%s: dvb_dmx_init failed (errno = %d)\n",
+ dev->core->name, result);
goto fail2;
}
- memset(&dvb->demux, 0, sizeof(struct dvb_demux));
-
- dvb->demux.dmx.capabilities =
- DMX_TS_FILTERING | DMX_SECTION_FILTERING |
- DMX_MEMORY_BASED_FILTERING;
-
- dvb->demux.priv = dvb;
- dvb->demux.filternum = 256;
- dvb->demux.feednum = 256;
- dvb->demux.start_feed = dvb_cx8800_start_feed;
- dvb->demux.stop_feed = dvb_cx8800_stop_feed;
- dvb->demux.write_to_decoder = NULL;
-
- if ((result = dvb_dmx_init(&dvb->demux)) < 0) {
- printk(KERN_WARNING "%s: dvb_dmx_init failed (errno = %d)\n",
- dev->name, result);
+ dev->dmxdev.filternum = 256;
+ dev->dmxdev.demux = &dev->demux.dmx;
+ dev->dmxdev.capabilities = 0;
+ result = dvb_dmxdev_init(&dev->dmxdev, dev->dvb_adapter);
+ if (result < 0) {
+ printk(KERN_WARNING "%s: dvb_dmxdev_init failed (errno = %d)\n",
+ dev->core->name, result);
goto fail3;
}
- dvb->dmxdev.filternum = 256;
- dvb->dmxdev.demux = &dvb->demux.dmx;
- dvb->dmxdev.capabilities = 0;
-
- if ((result = dvb_dmxdev_init(&dvb->dmxdev, dvb->dvb_adapter)) < 0) {
- printk(KERN_WARNING "%s: dvb_dmxdev_init failed (errno = %d)\n",
- dev->name, result);
+ dev->fe_hw.source = DMX_FRONTEND_0;
+ result = dev->demux.dmx.add_frontend(&dev->demux.dmx, &dev->fe_hw);
+ if (result < 0) {
+ printk(KERN_WARNING "%s: add_frontend failed (DMX_FRONTEND_0, errno = %d)\n",
+ dev->core->name, result);
goto fail4;
}
- dvb->fe_hw.source = DMX_FRONTEND_0;
-
- if ((result =
- dvb->demux.dmx.add_frontend(&dvb->demux.dmx, &dvb->fe_hw)) < 0) {
- printk(KERN_WARNING "%s: dvb_dmx_init failed (errno = %d)\n",
- dev->name, result);
+ dev->fe_mem.source = DMX_MEMORY_FE;
+ result = dev->demux.dmx.add_frontend(&dev->demux.dmx, &dev->fe_mem);
+ if (result < 0) {
+ printk(KERN_WARNING "%s: add_frontent failed (DMX_MEMORY_FE, errno = %d)\n",
+ dev->core->name, result);
goto fail5;
}
- dvb->fe_mem.source = DMX_MEMORY_FE;
-
- if ((result =
- dvb->demux.dmx.add_frontend(&dvb->demux.dmx, &dvb->fe_mem)) < 0) {
- printk(KERN_WARNING "%s: dvb_dmx_init failed (errno = %d)\n",
- dev->name, result);
+ result = dev->demux.dmx.connect_frontend(&dev->demux.dmx, &dev->fe_hw);
+ if (result < 0) {
+ printk(KERN_WARNING "%s: connect_frontent failed (errno = %d)\n",
+ dev->core->name, result);
goto fail6;
}
- if ((result =
- dvb->demux.dmx.connect_frontend(&dvb->demux.dmx,
- &dvb->fe_hw)) < 0) {
- printk(KERN_WARNING "%s: dvb_dmx_init failed (errno = %d)\n",
- dev->name, result);
- goto fail7;
- }
-
- dvb_net_init(dvb->dvb_adapter, &dvb->dvbnet, &dvb->demux.dmx);
-
- dev->dvb = dvb;
- printk(KERN_INFO "DVB attached to dev %p, dvb: %p\n", dev, dvb);
-
+ dvb_net_init(dev->dvb_adapter, &dev->dvbnet, &dev->demux.dmx);
return 0;
-fail7:
- dvb->demux.dmx.remove_frontend(&dvb->demux.dmx, &dvb->fe_mem);
fail6:
- dvb->demux.dmx.remove_frontend(&dvb->demux.dmx, &dvb->fe_hw);
+ dev->demux.dmx.remove_frontend(&dev->demux.dmx, &dev->fe_mem);
fail5:
- dvb_dmxdev_release(&dvb->dmxdev);
+ dev->demux.dmx.remove_frontend(&dev->demux.dmx, &dev->fe_hw);
fail4:
- dvb_dmx_release(&dvb->demux);
+ dvb_dmxdev_release(&dev->dmxdev);
fail3:
- dvb_unregister_i2c_bus(master_xfer, dvb->dvb_adapter, 0);
+ dvb_dmx_release(&dev->demux);
fail2:
- dvb_unregister_adapter(dvb->dvb_adapter);
-fail:
+ dvb_unregister_adapter(dev->dvb_adapter);
+fail1:
return result;
}
+/* ----------------------------------------------------------- */
+
+static int __devinit dvb_probe(struct pci_dev *pci_dev,
+ const struct pci_device_id *pci_id)
+{
+ struct cx8802_dev *dev;
+ struct cx88_core *core;
+ int err;
+
+ /* general setup */
+ core = cx88_core_get(pci_dev);
+ if (NULL == core)
+ return -EINVAL;
+
+ err = -ENODEV;
+ if (!cx88_boards[core->board].dvb)
+ goto fail_core;
+
+ err = -ENOMEM;
+ dev = kmalloc(sizeof(*dev),GFP_KERNEL);
+ if (NULL == dev)
+ goto fail_core;
+ memset(dev,0,sizeof(*dev));
+ dev->pci = pci_dev;
+ dev->core = core;
+
+ err = cx8802_init_common(dev);
+ if (0 != err)
+ goto fail_free;
+
+ /* dvb stuff */
+ printk("%s/2: cx23416 based dvb card\n", core->name);
+ videobuf_queue_init(&dev->dvbq, &dvb_qops,
+ dev->pci, &dev->slock,
+ V4L2_BUF_TYPE_VIDEO_CAPTURE,
+ V4L2_FIELD_TOP,
+ sizeof(struct cx88_buffer));
+ init_MUTEX(&dev->dvbq.lock);
+ dvb_register(dev);
+
+ return 0;
+
+ fail_free:
+ kfree(dev);
+ fail_core:
+ cx88_core_put(core,pci_dev);
+ return err;
+}
+
+static void __devexit dvb_remove(struct pci_dev *pci_dev)
+{
+ struct cx8802_dev *dev = pci_get_drvdata(pci_dev);
+
+ /* dvb */
+ dvb_unregister(dev);
+
+ /* common */
+ cx8802_fini_common(dev);
+ cx88_core_put(dev->core,dev->pci);
+ kfree(dev);
+}
+
+static struct pci_device_id cx8802_pci_tbl[] = {
+ {
+ .vendor = 0x14f1,
+ .device = 0x8802,
+ .subvendor = PCI_ANY_ID,
+ .subdevice = PCI_ANY_ID,
+ },{
+ /* --- end of list --- */
+ }
+};
+MODULE_DEVICE_TABLE(pci, cx8802_pci_tbl);
+
+static struct pci_driver dvb_pci_driver = {
+ .name = "cx8802",
+ .id_table = cx8802_pci_tbl,
+ .probe = dvb_probe,
+ .remove = dvb_remove,
+};
+
+static int dvb_init(void)
+{
+ printk(KERN_INFO "cx2388x dvb driver version %d.%d.%d loaded\n",
+ (CX88_VERSION_CODE >> 16) & 0xff,
+ (CX88_VERSION_CODE >> 8) & 0xff,
+ CX88_VERSION_CODE & 0xff);
+#ifdef SNAPSHOT
+ printk(KERN_INFO "cx2388x: snapshot date %04d-%02d-%02d\n",
+ SNAPSHOT/10000, (SNAPSHOT/100)%100, SNAPSHOT%100);
#endif
+ return pci_module_init(&dvb_pci_driver);
+}
+
+static void dvb_fini(void)
+{
+ pci_unregister_driver(&dvb_pci_driver);
+}
+
+module_init(dvb_init);
+module_exit(dvb_fini);
+
+/*
+ * Local variables:
+ * c-basic-offset: 8
+ * End:
+ */