575 lines
16 KiB
C
575 lines
16 KiB
C
|
|
//#include <libsimtrace/usb_buf.h>
|
|
|
|
|
|
/****************************************************************************
|
|
* buffered endpoint
|
|
****************************************************************************/
|
|
|
|
/* USB buffer library
|
|
*
|
|
* 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.
|
|
*/
|
|
//#pragma once
|
|
|
|
#include "tusb.h"
|
|
#include "device/usbd_pvt.h"
|
|
#include "trace.h"
|
|
#include <errno.h>
|
|
#include <osmocom/core/linuxlist.h>
|
|
#include <osmocom/core/msgb.h>
|
|
|
|
/* buffered USB endpoint (with queue of msgb) */
|
|
struct usb_buffered_ep {
|
|
/* endpoint address */
|
|
uint8_t ep;
|
|
/* currently any transfer in progress? */
|
|
struct msgb * msg_in_progress;
|
|
/* Tx queue (IN) / Rx queue (OUT) */
|
|
struct llist_head queue;
|
|
/* current length of queue */
|
|
unsigned int queue_len;
|
|
};
|
|
|
|
#define USB_MAX_QLEN 3
|
|
#define USB_ALLOC_SIZE 280
|
|
|
|
void bep_init(struct usb_buffered_ep *bep, uint8_t ep)
|
|
{
|
|
bep->ep = ep;
|
|
bep->msg_in_progress = NULL;
|
|
INIT_LLIST_HEAD(&bep->queue);
|
|
bep->queue_len = 0;
|
|
}
|
|
|
|
struct msgb *bep_msgb_alloc(struct usb_buffered_ep *bep)
|
|
{
|
|
struct msgb *msg = msgb_alloc(USB_ALLOC_SIZE, "USB");
|
|
if (!msg)
|
|
return NULL;
|
|
msg->dst = bep;
|
|
return msg;
|
|
}
|
|
|
|
void bep_msgb_free(struct msgb *msg)
|
|
{
|
|
msgb_free(msg);
|
|
}
|
|
|
|
#define LOGBEP(bep, fmt, args ...) \
|
|
printf("%s(%02x): " fmt, __func__, (bep)->ep, ## args)
|
|
|
|
|
|
/* IN/IRQ EP: dequeue next pending message and send it to host */
|
|
int bep_refill_to_host(struct usb_buffered_ep *bep)
|
|
{
|
|
unsigned long x;
|
|
const uint8_t rhport = 0;
|
|
|
|
if (usbd_edpt_busy(rhport, bep->ep)) {
|
|
LOGBEP(bep, "skipping: edpt_busy\r\n");
|
|
return 0;
|
|
}
|
|
|
|
local_irq_save(x);
|
|
if (bep->msg_in_progress) {
|
|
local_irq_restore(x);
|
|
LOGBEP(bep, "skipping: msg_in_progress\r\n");
|
|
return 0;
|
|
}
|
|
|
|
if (llist_empty(&bep->queue)) {
|
|
local_irq_restore(x);
|
|
LOGBEP(bep, "skipping: queue empty\r\n");
|
|
return 0;
|
|
}
|
|
|
|
bep->msg_in_progress = msgb_dequeue_count(&bep->queue, &bep->queue_len);
|
|
local_irq_restore(x);
|
|
|
|
TU_VERIFY(usbd_edpt_xfer(rhport, bep->ep, msgb_data(bep->msg_in_progress),
|
|
msgb_length(bep->msg_in_progress)));
|
|
|
|
LOGBEP(bep, "success (msg=%p, data=%p/%u)!\r\n", bep->msg_in_progress, msgb_data(bep->msg_in_progress), msgb_length(bep->msg_in_progress));
|
|
return 1;
|
|
}
|
|
|
|
/* assume the last in-progress msgb has completed + return it */
|
|
struct msgb *bep_get_completed(struct usb_buffered_ep *bep)
|
|
{
|
|
unsigned long x;
|
|
struct msgb *msg;
|
|
|
|
local_irq_save(x);
|
|
msg = bep->msg_in_progress;
|
|
bep->msg_in_progress = NULL;
|
|
local_irq_restore(x);
|
|
LOGBEP(bep, "(msg=%p)\r\n", msg);
|
|
|
|
return msg;
|
|
}
|
|
|
|
/* assume the last in-progress msgb has completed + free it */
|
|
void bep_complete_in_progress(struct usb_buffered_ep *bep)
|
|
{
|
|
struct msgb *msg = bep_get_completed(bep);
|
|
LOGBEP(bep, "(msg=%p)\r\n", msg);
|
|
bep_msgb_free(msg);
|
|
}
|
|
|
|
/* enqueue a USB buffer for transmission to host */
|
|
int bep_enqueue_msgb(struct msgb *msg)
|
|
{
|
|
struct usb_buffered_ep *ep = msg->dst;
|
|
|
|
if (!msg->dst) {
|
|
TRACE_ERROR("%s: msg without dst\r\n", __func__);
|
|
bep_msgb_free(msg);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* no need for irqsafe operation, as the usb_tx_queue is
|
|
* processed only by the main loop context */
|
|
|
|
if (ep->queue_len >= USB_MAX_QLEN) {
|
|
struct msgb *evict;
|
|
/* free the first pending buffer in the queue */
|
|
TRACE_INFO("EP%02x: dropping first queue element (qlen=%u)\r\n",
|
|
ep->ep, ep->queue_len);
|
|
evict = msgb_dequeue_count(&ep->queue, &ep->queue_len);
|
|
OSMO_ASSERT(evict);
|
|
bep_msgb_free(evict);
|
|
}
|
|
|
|
LOGBEP(ep, "(msg=%p)\r\n", msg);
|
|
msgb_enqueue_count(&ep->queue, msg, &ep->queue_len);
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "tusb.h"
|
|
#include "device/usbd_pvt.h"
|
|
|
|
/* endpoint indexes so we can use them as look-up into arrays */
|
|
enum tusb_cardem_epidx {
|
|
EPIDX_OUT = 0,
|
|
EPIDX_IN = 1,
|
|
EPIDX_IRQ = 2,
|
|
_NUM_EPIDX
|
|
};
|
|
|
|
/* per-interface/instance structure */
|
|
struct tusb_cardem_inst {
|
|
/* the interface to which this driver has been bound */
|
|
int itf_num;
|
|
struct usb_buffered_ep bep[_NUM_EPIDX];
|
|
};
|
|
|
|
/* must have at least as many elements as there could be cardem interfaces */
|
|
#define MAX_IF 1
|
|
static struct tusb_cardem_inst gtci[MAX_IF];
|
|
|
|
|
|
/****************************************************************************
|
|
* endpoint_nr -> cardem_inst + ep_idx map (used for dispatch tusb -> cardem)
|
|
****************************************************************************/
|
|
|
|
/* must have at least as many elements as there could be USB endpoint numbers */
|
|
#define MAX_EP 16
|
|
struct tusb_ep_map {
|
|
struct tusb_cardem_inst *tci;
|
|
uint8_t ep_idx;
|
|
};
|
|
/* same endpoint number can be used for IN and OUT, so we need two arrays */
|
|
static struct tusb_ep_map g_tem_in[MAX_EP];
|
|
static struct tusb_ep_map g_tem_out[MAX_EP];
|
|
|
|
/* resolve the tusb_ep_map entry for a given endpoint address */
|
|
static struct tusb_ep_map *tem_for_epaddr(uint8_t epaddr)
|
|
{
|
|
struct tusb_ep_map *map;
|
|
|
|
/* determine map based on input/output direction */
|
|
if (epaddr & 0x80)
|
|
map = g_tem_in;
|
|
else
|
|
map = g_tem_out;
|
|
|
|
/* resolve tusb_ep_map by indexing the array */
|
|
uint8_t epnr = tu_edpt_number(epaddr);
|
|
OSMO_ASSERT(epnr < MAX_EP);
|
|
|
|
return &map[epnr];
|
|
}
|
|
|
|
/* return the usb_buffered_ep (+ optionally endpoint idx) for a given endpoint address */
|
|
static struct usb_buffered_ep *bep_for_epaddr(uint8_t epaddr, uint8_t *epidx)
|
|
{
|
|
struct tusb_ep_map *tem = tem_for_epaddr(epaddr);
|
|
|
|
if (epidx)
|
|
*epidx = tem->ep_idx;
|
|
|
|
return &tem->tci->bep[tem->ep_idx];
|
|
}
|
|
|
|
|
|
|
|
/****************************************************************************
|
|
* actual driver code
|
|
****************************************************************************/
|
|
|
|
/* called during tud_init() time. */
|
|
static void cardemd_init(void)
|
|
{
|
|
printf("%s\r\n", __func__);
|
|
for (unsigned int i = 0; i < ARRAY_SIZE(gtci); i++) {
|
|
struct tusb_cardem_inst *tci = >ci[i];
|
|
for (unsigned int j = 0; j < ARRAY_SIZE(tci->bep); j++) {
|
|
/* endpoint numbers are written in cardemd_open(), so we pass 0 here */
|
|
bep_init(&tci->bep[j], 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* called during usdbd_reset(); happens during bus reset or USB unplug */
|
|
static void cardemd_reset(uint8_t __unused rhport)
|
|
{
|
|
printf("%s\r\n", __func__);
|
|
for (unsigned int i = 0; i < ARRAY_SIZE(gtci); i++) {
|
|
gtci[i].itf_num = -1;
|
|
/* TODO: ? */
|
|
}
|
|
|
|
for (unsigned int i = 0; i < ARRAY_SIZE(g_tem_in); i++)
|
|
g_tem_in[i].tci = NULL;
|
|
|
|
for (unsigned int i = 0; i < ARRAY_SIZE(g_tem_out); i++)
|
|
g_tem_out[i].tci = NULL;
|
|
|
|
/* FIXME free all the buffered msgb */
|
|
}
|
|
|
|
/* called during process_set_config() / process_control_request() */
|
|
static uint16_t cardemd_open(uint8_t rhport, tusb_desc_interface_t const *itf_desc, uint16_t max_len)
|
|
{
|
|
struct tusb_cardem_inst *tci = >ci[0]; // FIXME: multiple instances
|
|
|
|
printf("%s\r\n", __func__);
|
|
|
|
TU_VERIFY(TUSB_CLASS_VENDOR_SPECIFIC == itf_desc->bInterfaceClass &&
|
|
2 == itf_desc->bInterfaceSubClass &&
|
|
0 == itf_desc->bInterfaceProtocol, 0);
|
|
|
|
uint16_t drv_len = sizeof(tusb_desc_interface_t);
|
|
TU_VERIFY(max_len >= drv_len, 0);
|
|
|
|
tci->itf_num = itf_desc->bInterfaceNumber;
|
|
|
|
uint8_t const *p_desc = tu_desc_next(itf_desc);
|
|
uint8_t const * desc_end = p_desc + max_len;
|
|
if (itf_desc->bNumEndpoints) {
|
|
/* skip non-endpoint descriptors */
|
|
while ((TUSB_DESC_ENDPOINT != tu_desc_type(p_desc)) && (p_desc < desc_end))
|
|
p_desc = tu_desc_next(p_desc);
|
|
|
|
/* iterate over endpoints */
|
|
for (int i = 0; i < itf_desc->bNumEndpoints; i++) {
|
|
tusb_desc_endpoint_t const * desc_ep = (tusb_desc_endpoint_t const *) p_desc;
|
|
TU_ASSERT(TUSB_DESC_ENDPOINT == desc_ep->bDescriptorType);
|
|
/* open each endpoint */
|
|
printf("opening EP 0x%02x\r\n", desc_ep->bEndpointAddress);
|
|
TU_ASSERT(usbd_edpt_open(rhport, desc_ep));
|
|
|
|
uint8_t ep_nr = tu_edpt_number(desc_ep->bEndpointAddress);
|
|
|
|
if (tu_edpt_dir(desc_ep->bEndpointAddress) == TUSB_DIR_IN) {
|
|
g_tem_in[ep_nr].tci = tci;
|
|
if (TUSB_XFER_INTERRUPT == desc_ep->bmAttributes.xfer) {
|
|
g_tem_in[ep_nr].ep_idx = EPIDX_IRQ;
|
|
tci->bep[EPIDX_IRQ].ep = desc_ep->bEndpointAddress;
|
|
} else {
|
|
g_tem_in[ep_nr].ep_idx = EPIDX_IN;
|
|
tci->bep[EPIDX_IN].ep = desc_ep->bEndpointAddress;
|
|
}
|
|
/* FIXME: tud_vendor_n_write_flush() for each IN EP */
|
|
} else {
|
|
g_tem_out[ep_nr].tci = tci;
|
|
g_tem_out[ep_nr].ep_idx = EPIDX_OUT;
|
|
tci->bep[EPIDX_OUT].ep = desc_ep->bEndpointAddress;
|
|
|
|
/* enqueue a buffer for receiving data from OUT ep */
|
|
struct msgb *msg = bep_msgb_alloc(&tci->bep[EPIDX_OUT]);
|
|
if (msg)
|
|
bep_enqueue_msgb(msg);
|
|
}
|
|
|
|
p_desc = tu_desc_next(p_desc);
|
|
drv_len += sizeof(tusb_desc_endpoint_t);
|
|
}
|
|
}
|
|
|
|
return drv_len;
|
|
}
|
|
|
|
/* inbound control request from host */
|
|
static bool cardemd_control_xfer_cb(uint8_t __unused rhport, uint8_t __unused stage, tusb_control_request_t const *request)
|
|
{
|
|
/* we have no class specific control requests */
|
|
return false;
|
|
}
|
|
|
|
#include <libsimtrace/simtrace_prot.h>
|
|
static const struct simtrace_board_info g_board_info = {
|
|
.hardware = {
|
|
.manufacturer = "sysmocom",
|
|
.model = "rp2040-cardem-breakout",
|
|
.version = "0.1",
|
|
},
|
|
.software = {
|
|
.provider = "sysmocom",
|
|
.name = "rp2040-cardem-composite",
|
|
.version = "FIXME",
|
|
.buildhost = "FIXME",
|
|
.crc = 0, // FIXME
|
|
},
|
|
.speed = {
|
|
.max_baud_rate = 9600,
|
|
},
|
|
.cap_generic_bytes = 0, // FIXME
|
|
};
|
|
|
|
/* push (prepend) the generic simtrace_msg_hdr in front of msg and enqueue it for tx to USB host */
|
|
static int stp_push_hdr_and_send(struct msgb *msg, uint8_t msg_class, uint8_t msg_type, uint8_t seq_nr, uint8_t slot_nr)
|
|
{
|
|
struct simtrace_msg_hdr *sth = (struct simtrace_msg_hdr *) msgb_push(msg, sizeof(*sth));
|
|
|
|
sth->msg_class = msg_class;
|
|
sth->msg_type = msg_type;
|
|
sth->seq_nr = seq_nr;
|
|
sth->slot_nr = slot_nr;
|
|
sth->msg_len = msgb_length(msg);
|
|
|
|
return bep_enqueue_msgb(msg);
|
|
}
|
|
|
|
static void stp_tx_error(struct usb_buffered_ep *bep, uint8_t seq_nr, uint8_t slot_nr, uint8_t severity,
|
|
uint8_t subsystem, uint16_t code, const char *errmsg)
|
|
{
|
|
struct msgb *msg = bep_msgb_alloc(bep);
|
|
struct cardemu_usb_msg_error *err;
|
|
|
|
LOGBEP(bep, "(slot_nr=%u, severity=%u, subsys=%u, code=0x%04x, msg=%s)\r\n",
|
|
slot_nr, severity, subsystem, code, errmsg);
|
|
|
|
if (!msg)
|
|
return;
|
|
|
|
err = (struct cardemu_usb_msg_error *) msgb_push(msg, sizeof(*err));
|
|
err->severity = severity;
|
|
err->subsystem = subsystem;
|
|
err->code = code;
|
|
err->msg_len = strlen(errmsg);
|
|
if (err->msg_len) {
|
|
uint8_t *out = msgb_put(msg, err->msg_len);
|
|
memcpy(out, errmsg, err->msg_len);
|
|
}
|
|
|
|
stp_push_hdr_and_send(msg, SIMTRACE_MSGC_GENERIC, SIMTRACE_CMD_DO_ERROR, seq_nr, slot_nr);
|
|
|
|
}
|
|
|
|
/* handle SIMTRACE_MSGC_GENERIC received from USB host */
|
|
static int my_stp_generic_handler(const struct simtrace_msg_hdr *smh, struct usb_buffered_ep *bep)
|
|
{
|
|
struct simtrace_board_info *bdinfo;
|
|
struct msgb *msg;
|
|
|
|
switch (smh->msg_type) {
|
|
case SIMTRACE_CMD_BD_BOARD_INFO:
|
|
msg = bep_msgb_alloc(bep);
|
|
if (!msg)
|
|
break;
|
|
bdinfo = (struct simtrace_board_info *) msgb_put(msg, sizeof(*bdinfo));
|
|
memcpy(bdinfo, &g_board_info, sizeof(g_board_info));
|
|
stp_push_hdr_and_send(msg, SIMTRACE_MSGC_GENERIC, SIMTRACE_CMD_BD_BOARD_INFO,
|
|
smh->seq_nr, smh->slot_nr);
|
|
break;
|
|
default:
|
|
stp_tx_error(bep, smh->seq_nr, smh->slot_nr, 1, 1, 0x0003, "unknown message type");
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void my_ep_out_handler(struct msgb *msg, struct usb_buffered_ep *bep)
|
|
{
|
|
struct simtrace_msg_hdr *smh;
|
|
|
|
if (msgb_length(msg) < sizeof(*smh)) {
|
|
stp_tx_error(bep, 0, 0 /*slot_nr*/, 1, 1, 0x0001, "short message header");
|
|
goto out;
|
|
}
|
|
|
|
smh = (struct simtrace_msg_hdr *) msg->data;
|
|
|
|
if (msgb_length(msg) < smh->msg_len) {
|
|
stp_tx_error(bep, smh->seq_nr, smh->slot_nr, 1, 1, 0x0002, "short message");
|
|
goto out;
|
|
}
|
|
|
|
LOGBEP(bep, "(slot_nr=%u, class=%u, msg_type=%u, seq_nr=%u, len=%u)\r\n", __func__,
|
|
smh->slot_nr, smh->msg_class, smh->msg_type, smh->seq_nr, smh->msg_len);
|
|
|
|
switch (smh->msg_class) {
|
|
case SIMTRACE_MSGC_GENERIC:
|
|
my_stp_generic_handler(smh, bep);
|
|
break;
|
|
case SIMTRACE_MSGC_CARDEM:
|
|
case SIMTRACE_MSGC_MODEM:
|
|
case SIMTRACE_MSGC_SNIFF:
|
|
default:
|
|
/* FIXME: send error in return */
|
|
goto out;
|
|
}
|
|
|
|
out:
|
|
msgb_free(msg);
|
|
}
|
|
|
|
/* USB transfer has completed. */
|
|
static bool cardemd_xfer_cb(uint8_t __unused rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes)
|
|
{
|
|
struct msgb *msg;
|
|
uint8_t epidx;
|
|
printf("%s(ep=0x%02x, result=%u, bytes=%u\r\n", __func__, ep_addr, result, xferred_bytes);
|
|
|
|
struct usb_buffered_ep *bep = bep_for_epaddr(ep_addr, &epidx);
|
|
if (!bep)
|
|
return false;
|
|
|
|
switch (epidx) {
|
|
case EPIDX_OUT:
|
|
/* 1) resolve last submitted buffer + dispatch it to handler */
|
|
msg = bep_get_completed(bep);
|
|
/* dispatch to handler; transfers ownership */
|
|
my_ep_out_handler(msg, bep);
|
|
/* 2) allocate a new buffer */
|
|
msg = bep_msgb_alloc(bep);
|
|
if (!msg)
|
|
break;
|
|
bep_enqueue_msgb(msg);
|
|
/* 3) submit that new buffer */
|
|
bep_refill_to_host(bep);
|
|
break;
|
|
case EPIDX_IN:
|
|
/* resolve last submitted buffer + release it back to allocator/pool */
|
|
bep_complete_in_progress(bep);
|
|
/* submit next pending buffer, if any */
|
|
bep_refill_to_host(bep);
|
|
break;
|
|
case EPIDX_IRQ:
|
|
/* resolve last submitted buffer + release it back to allocator/pool */
|
|
bep_complete_in_progress(bep);
|
|
/* submit next pending buffer, if any */
|
|
bep_refill_to_host(bep);
|
|
break;
|
|
default:
|
|
OSMO_ASSERT(0);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static usbd_class_driver_t const _cardemd_driver = {
|
|
#if CFG_TUSB_DEBUG >= 2
|
|
.name = "cardem",
|
|
#endif
|
|
.init = cardemd_init,
|
|
.reset = cardemd_reset,
|
|
.open = cardemd_open,
|
|
.control_xfer_cb = cardemd_control_xfer_cb,
|
|
.xfer_cb = cardemd_xfer_cb,
|
|
.sof = NULL
|
|
};
|
|
|
|
/* Implement callback to add our custom driver. Called during tud_init() */
|
|
usbd_class_driver_t const *usbd_app_driver_get_cb(uint8_t *driver_count)
|
|
{
|
|
*driver_count = 1;
|
|
return &_cardemd_driver;
|
|
}
|
|
|
|
/* notes:
|
|
* - endpoint busy condition can be checked via usbd_edpt_busy()
|
|
* - IN/IRQ transfers to the host are initiated with usbd_edpt_xfer()
|
|
* - transfer completion signaled via xfer_cb()
|
|
* - OUT transfers to the host are initiated via usbd_edpt_xfer()
|
|
* - transfer completion signaled via xfer_cb()
|
|
* - memory used is allocated 'out of band'
|
|
*
|
|
* - usbd_edpt_claim() / usbd_edpt_release() called around usbd_edpt_xfer()
|
|
* - claim blocks the endpoint from other users
|
|
* - if usbd_edpt_xfer() is successful, *DO NOT* release it
|
|
* - tusb itself releases it before calling xfer_cb()
|
|
*/
|
|
|
|
/* = do we have to have a write-queue of buffers to the host?
|
|
*
|
|
* IN EP:
|
|
* - traffic-carrying messages triggered by ISO7816
|
|
* - RX_DATA: waits for answer from host; only one in flight
|
|
* - PTS: waits fro answer from host; only one in flight
|
|
*
|
|
* - responses to host requests:
|
|
* - STATS
|
|
* - STATUS
|
|
* - CONFIG
|
|
* either of those might happen in response to a host request, in parallel
|
|
* to ongoing DATA/PTS traffic. Only one of them at a time, as answers are immediate
|
|
*
|
|
* => write queue is needed, but usually will have only one entry backlog (beyond the
|
|
* current IN transfer)
|
|
*
|
|
* IRQ IN EP:
|
|
* - we probably want to have a queue of depth of at least 1 (beyond ongoing IN xfer)
|
|
* - queue depth should be limited to avoid noisy GPIO changes from causing OOM
|
|
*
|
|
* = execution context of call-backs (process? IRQ?)
|
|
*
|
|
* It seems that it's up to the application to regularly/repeatedly call tud_task()
|
|
* and we'll be doing that from normal process context.
|
|
*
|
|
*/
|
|
|
|
/* General High Level Flow
|
|
*
|
|
* = xfer_cb() for USB IN EP:
|
|
*
|
|
* - process any generic requests in-line (no alloc+memcpy)
|
|
* - process any CARDEM STATS/STATUS/CONFIG requests in-line (no malloc+memcpy)
|
|
* - process SET_ATR in-line (no malloc+memcpy)
|
|
* - allocate+memcpy for TX_DATA, put into queue
|
|
*
|
|
* All responses are dynamically allocated + enqueued!
|
|
*
|
|
* = xfer_cb() for USB OUT EP:
|
|
*
|
|
* - resolve msgb from endpoint / pointer (completion should happen in-order for each EP!)
|
|
* - release buffer back to pool
|
|
*
|
|
*/
|