/*
ChibiOS - Copyright (C) 2006..2018 Giovanni Di Sirio
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file hal_buffers.c
* @brief I/O Buffers code.
*
* @addtogroup HAL_BUFFERS
* @details Buffers Queues are used when there is the need to exchange
* fixed-length data buffers between ISRs and threads.
* On the ISR side data can be exchanged only using buffers,
* on the thread side data can be exchanged both using buffers and/or
* using an emulation of regular byte queues.
* There are several kind of buffers queues:
* - Input queue, unidirectional queue where the writer is the
* ISR side and the reader is the thread side.
* - Output queue, unidirectional queue where the writer is the
* ISR side and the reader is the thread side.
* - Full duplex queue, bidirectional queue. Full duplex queues
* are implemented by pairing an input queue and an output queue
* together.
* .
* @{
*/
#include
#include "hal.h"
/*===========================================================================*/
/* Driver local definitions. */
/*===========================================================================*/
/*===========================================================================*/
/* Driver exported variables. */
/*===========================================================================*/
/*===========================================================================*/
/* Driver local variables and types. */
/*===========================================================================*/
/*===========================================================================*/
/* Driver local functions. */
/*===========================================================================*/
/*===========================================================================*/
/* Driver exported functions. */
/*===========================================================================*/
/**
* @brief Initializes an input buffers queue object.
*
* @param[out] ibqp pointer to the @p input_buffers_queue_t object
* @param[in] suspended initial state of the queue
* @param[in] bp pointer to a memory area allocated for buffers
* @param[in] size buffers size
* @param[in] n number of buffers
* @param[in] infy callback called when a buffer is returned to the queue
* @param[in] link application defined pointer
*
* @init
*/
void ibqObjectInit(input_buffers_queue_t *ibqp, bool suspended, uint8_t *bp,
size_t size, size_t n, bqnotify_t infy, void *link) {
osalDbgCheck((ibqp != NULL) && (bp != NULL) && (size >= 2U));
osalThreadQueueObjectInit(&ibqp->waiting);
ibqp->suspended = suspended;
ibqp->bcounter = 0;
ibqp->brdptr = bp;
ibqp->bwrptr = bp;
ibqp->btop = bp + ((size + sizeof (size_t)) * n);
ibqp->bsize = size + sizeof (size_t);
ibqp->bn = n;
ibqp->buffers = bp;
ibqp->ptr = NULL;
ibqp->top = NULL;
ibqp->notify = infy;
ibqp->link = link;
}
/**
* @brief Resets an input buffers queue.
* @details All the data in the input buffers queue is erased and lost, any
* waiting thread is resumed with status @p MSG_RESET.
* @note A reset operation can be used by a low level driver in order to
* obtain immediate attention from the high level layers.
*
* @param[in] ibqp pointer to the @p input_buffers_queue_t object
*
* @iclass
*/
void ibqResetI(input_buffers_queue_t *ibqp) {
osalDbgCheckClassI();
ibqp->bcounter = 0;
ibqp->brdptr = ibqp->buffers;
ibqp->bwrptr = ibqp->buffers;
ibqp->ptr = NULL;
ibqp->top = NULL;
osalThreadDequeueAllI(&ibqp->waiting, MSG_RESET);
}
/**
* @brief Gets the next empty buffer from the queue.
* @note The function always returns the same buffer if called repeatedly.
*
* @param[in] ibqp pointer to the @p input_buffers_queue_t object
* @return A pointer to the next buffer to be filled.
* @retval NULL if the queue is full.
*
* @iclass
*/
uint8_t *ibqGetEmptyBufferI(input_buffers_queue_t *ibqp) {
osalDbgCheckClassI();
if (ibqIsFullI(ibqp)) {
return NULL;
}
return ibqp->bwrptr + sizeof (size_t);
}
/**
* @brief Posts a new filled buffer to the queue.
*
* @param[in] ibqp pointer to the @p input_buffers_queue_t object
* @param[in] size used size of the buffer, cannot be zero
*
* @iclass
*/
void ibqPostFullBufferI(input_buffers_queue_t *ibqp, size_t size) {
osalDbgCheckClassI();
osalDbgCheck((size > 0U) && (size <= (ibqp->bsize - sizeof (size_t))));
osalDbgAssert(!ibqIsFullI(ibqp), "buffers queue full");
/* Writing size field in the buffer.*/
*((size_t *)ibqp->bwrptr) = size;
/* Posting the buffer in the queue.*/
ibqp->bcounter++;
ibqp->bwrptr += ibqp->bsize;
if (ibqp->bwrptr >= ibqp->btop) {
ibqp->bwrptr = ibqp->buffers;
}
/* Waking up one waiting thread, if any.*/
osalThreadDequeueNextI(&ibqp->waiting, MSG_OK);
}
/**
* @brief Gets the next filled buffer from the queue.
* @note The function always acquires the same buffer if called repeatedly.
* @post After calling the function the fields @p ptr and @p top are set
* at beginning and end of the buffer data or @p NULL if the queue
* is empty.
*
* @param[in] ibqp pointer to the @p input_buffers_queue_t object
* @param[in] timeout the number of ticks before the operation timeouts,
* the following special values are allowed:
* - @a TIME_IMMEDIATE immediate timeout.
* - @a TIME_INFINITE no timeout.
* .
* @return The operation status.
* @retval MSG_OK if a buffer has been acquired.
* @retval MSG_TIMEOUT if the specified time expired.
* @retval MSG_RESET if the queue has been reset or has been put in
* suspended state.
*
* @api
*/
msg_t ibqGetFullBufferTimeout(input_buffers_queue_t *ibqp,
sysinterval_t timeout) {
msg_t msg;
osalSysLock();
msg = ibqGetFullBufferTimeoutS(ibqp, timeout);
osalSysUnlock();
return msg;
}
/**
* @brief Gets the next filled buffer from the queue.
* @note The function always acquires the same buffer if called repeatedly.
* @post After calling the function the fields @p ptr and @p top are set
* at beginning and end of the buffer data or @p NULL if the queue
* is empty.
*
* @param[in] ibqp pointer to the @p input_buffers_queue_t object
* @param[in] timeout the number of ticks before the operation timeouts,
* the following special values are allowed:
* - @a TIME_IMMEDIATE immediate timeout.
* - @a TIME_INFINITE no timeout.
* .
* @return The operation status.
* @retval MSG_OK if a buffer has been acquired.
* @retval MSG_TIMEOUT if the specified time expired.
* @retval MSG_RESET if the queue has been reset or has been put in
* suspended state.
*
* @sclass
*/
msg_t ibqGetFullBufferTimeoutS(input_buffers_queue_t *ibqp,
sysinterval_t timeout) {
osalDbgCheckClassS();
while (ibqIsEmptyI(ibqp)) {
if (ibqp->suspended) {
return MSG_RESET;
}
msg_t msg = osalThreadEnqueueTimeoutS(&ibqp->waiting, timeout);
if (msg < MSG_OK) {
return msg;
}
}
osalDbgAssert(!ibqIsEmptyI(ibqp), "still empty");
/* Setting up the "current" buffer and its boundary.*/
ibqp->ptr = ibqp->brdptr + sizeof (size_t);
ibqp->top = ibqp->ptr + *((size_t *)ibqp->brdptr);
return MSG_OK;
}
/**
* @brief Releases the buffer back in the queue.
* @note The object callback is called after releasing the buffer.
*
* @param[in] ibqp pointer to the @p input_buffers_queue_t object
*
* @api
*/
void ibqReleaseEmptyBuffer(input_buffers_queue_t *ibqp) {
osalSysLock();
ibqReleaseEmptyBufferS(ibqp);
osalSysUnlock();
}
/**
* @brief Releases the buffer back in the queue.
* @note The object callback is called after releasing the buffer.
*
* @param[in] ibqp pointer to the @p input_buffers_queue_t object
*
* @sclass
*/
void ibqReleaseEmptyBufferS(input_buffers_queue_t *ibqp) {
osalDbgCheckClassS();
osalDbgAssert(!ibqIsEmptyI(ibqp), "buffers queue empty");
/* Freeing a buffer slot in the queue.*/
ibqp->bcounter--;
ibqp->brdptr += ibqp->bsize;
if (ibqp->brdptr >= ibqp->btop) {
ibqp->brdptr = ibqp->buffers;
}
/* No "current" buffer.*/
ibqp->ptr = NULL;
/* Notifying the buffer release.*/
if (ibqp->notify != NULL) {
ibqp->notify(ibqp);
}
}
/**
* @brief Input queue read with timeout.
* @details This function reads a byte value from an input queue. If
* the queue is empty then the calling thread is suspended until a
* new buffer arrives in the queue or a timeout occurs.
*
* @param[in] ibqp pointer to the @p input_buffers_queue_t object
* @param[in] timeout the number of ticks before the operation timeouts,
* the following special values are allowed:
* - @a TIME_IMMEDIATE immediate timeout.
* - @a TIME_INFINITE no timeout.
* .
* @return A byte value from the queue.
* @retval MSG_TIMEOUT if the specified time expired.
* @retval MSG_RESET if the queue has been reset or has been put in
* suspended state.
*
* @api
*/
msg_t ibqGetTimeout(input_buffers_queue_t *ibqp, sysinterval_t timeout) {
msg_t msg;
osalSysLock();
/* This condition indicates that a new buffer must be acquired.*/
if (ibqp->ptr == NULL) {
msg = ibqGetFullBufferTimeoutS(ibqp, timeout);
if (msg != MSG_OK) {
osalSysUnlock();
return msg;
}
}
/* Next byte from the buffer.*/
msg = (msg_t)*ibqp->ptr;
ibqp->ptr++;
/* If the current buffer has been fully read then it is returned as
empty in the queue.*/
if (ibqp->ptr >= ibqp->top) {
ibqReleaseEmptyBufferS(ibqp);
}
osalSysUnlock();
return msg;
}
/**
* @brief Input queue read with timeout.
* @details The function reads data from an input queue into a buffer.
* The operation completes when the specified amount of data has been
* transferred or after the specified timeout or if the queue has
* been reset.
*
* @param[in] ibqp pointer to the @p input_buffers_queue_t object
* @param[out] bp pointer to the data buffer
* @param[in] n the maximum amount of data to be transferred, the
* value 0 is reserved
* @param[in] timeout the number of ticks before the operation timeouts,
* the following special values are allowed:
* - @a TIME_IMMEDIATE immediate timeout.
* - @a TIME_INFINITE no timeout.
* .
* @return The number of bytes effectively transferred.
* @retval 0 if a timeout occurred.
*
* @api
*/
size_t ibqReadTimeout(input_buffers_queue_t *ibqp, uint8_t *bp,
size_t n, sysinterval_t timeout) {
size_t r = 0;
osalDbgCheck(n > 0U);
osalSysLock();
while (true) {
size_t size;
/* This condition indicates that a new buffer must be acquired.*/
if (ibqp->ptr == NULL) {
msg_t msg;
/* Getting a data buffer using the specified timeout.*/
msg = ibqGetFullBufferTimeoutS(ibqp, timeout);
/* Anything except MSG_OK interrupts the operation.*/
if (msg != MSG_OK) {
osalSysUnlock();
return r;
}
}
/* Size of the data chunk present in the current buffer.*/
size = (size_t)ibqp->top - (size_t)ibqp->ptr;
if (size > (n - r)) {
size = n - r;
}
/* Smaller chunks in order to not make the critical zone too long,
this impacts throughput however.*/
if (size > 64U) {
/* Giving the compiler a chance to optimize for a fixed size move.*/
memcpy(bp, ibqp->ptr, 64U);
bp += 64U;
ibqp->ptr += 64U;
r += 64U;
}
else {
memcpy(bp, ibqp->ptr, size);
bp += size;
ibqp->ptr += size;
r += size;
}
/* Has the current data buffer been finished? if so then release it.*/
if (ibqp->ptr >= ibqp->top) {
ibqReleaseEmptyBufferS(ibqp);
}
/* Giving a preemption chance.*/
osalSysUnlock();
if (r >= n) {
return r;
}
osalSysLock();
}
}
/**
* @brief Initializes an output buffers queue object.
*
* @param[out] obqp pointer to the @p output_buffers_queue_t object
* @param[in] suspended initial state of the queue
* @param[in] bp pointer to a memory area allocated for buffers
* @param[in] size buffers size
* @param[in] n number of buffers
* @param[in] onfy callback called when a buffer is posted in the queue
* @param[in] link application defined pointer
*
* @init
*/
void obqObjectInit(output_buffers_queue_t *obqp, bool suspended, uint8_t *bp,
size_t size, size_t n, bqnotify_t onfy, void *link) {
osalDbgCheck((obqp != NULL) && (bp != NULL) && (size >= 2U));
osalThreadQueueObjectInit(&obqp->waiting);
obqp->suspended = suspended;
obqp->bcounter = n;
obqp->brdptr = bp;
obqp->bwrptr = bp;
obqp->btop = bp + ((size + sizeof (size_t)) * n);
obqp->bsize = size + sizeof (size_t);
obqp->bn = n;
obqp->buffers = bp;
obqp->ptr = NULL;
obqp->top = NULL;
obqp->notify = onfy;
obqp->link = link;
}
/**
* @brief Resets an output buffers queue.
* @details All the data in the output buffers queue is erased and lost, any
* waiting thread is resumed with status @p MSG_RESET.
* @note A reset operation can be used by a low level driver in order to
* obtain immediate attention from the high level layers.
*
* @param[in] obqp pointer to the @p output_buffers_queue_t object
*
* @iclass
*/
void obqResetI(output_buffers_queue_t *obqp) {
osalDbgCheckClassI();
obqp->bcounter = bqSizeX(obqp);
obqp->brdptr = obqp->buffers;
obqp->bwrptr = obqp->buffers;
obqp->ptr = NULL;
obqp->top = NULL;
osalThreadDequeueAllI(&obqp->waiting, MSG_RESET);
}
/**
* @brief Gets the next filled buffer from the queue.
* @note The function always returns the same buffer if called repeatedly.
*
* @param[in] obqp pointer to the @p output_buffers_queue_t object
* @param[out] sizep pointer to the filled buffer size
* @return A pointer to the filled buffer.
* @retval NULL if the queue is empty.
*
* @iclass
*/
uint8_t *obqGetFullBufferI(output_buffers_queue_t *obqp,
size_t *sizep) {
osalDbgCheckClassI();
if (obqIsEmptyI(obqp)) {
return NULL;
}
/* Buffer size.*/
*sizep = *((size_t *)obqp->brdptr);
return obqp->brdptr + sizeof (size_t);
}
/**
* @brief Releases the next filled buffer back in the queue.
*
* @param[in] obqp pointer to the @p output_buffers_queue_t object
*
* @iclass
*/
void obqReleaseEmptyBufferI(output_buffers_queue_t *obqp) {
osalDbgCheckClassI();
osalDbgAssert(!obqIsEmptyI(obqp), "buffers queue empty");
/* Freeing a buffer slot in the queue.*/
obqp->bcounter++;
obqp->brdptr += obqp->bsize;
if (obqp->brdptr >= obqp->btop) {
obqp->brdptr = obqp->buffers;
}
/* Waking up one waiting thread, if any.*/
osalThreadDequeueNextI(&obqp->waiting, MSG_OK);
}
/**
* @brief Gets the next empty buffer from the queue.
* @note The function always acquires the same buffer if called repeatedly.
* @post After calling the function the fields @p ptr and @p top are set
* at beginning and end of the buffer data or @p NULL if the queue
* is empty.
*
* @param[in] obqp pointer to the @p output_buffers_queue_t object
* @param[in] timeout the number of ticks before the operation timeouts,
* the following special values are allowed:
* - @a TIME_IMMEDIATE immediate timeout.
* - @a TIME_INFINITE no timeout.
* .
* @return The operation status.
* @retval MSG_OK if a buffer has been acquired.
* @retval MSG_TIMEOUT if the specified time expired.
* @retval MSG_RESET if the queue has been reset or has been put in
* suspended state.
*
* @api
*/
msg_t obqGetEmptyBufferTimeout(output_buffers_queue_t *obqp,
sysinterval_t timeout) {
msg_t msg;
osalSysLock();
msg = obqGetEmptyBufferTimeoutS(obqp, timeout);
osalSysUnlock();
return msg;
}
/**
* @brief Gets the next empty buffer from the queue.
* @note The function always acquires the same buffer if called repeatedly.
* @post After calling the function the fields @p ptr and @p top are set
* at beginning and end of the buffer data or @p NULL if the queue
* is empty.
*
* @param[in] obqp pointer to the @p output_buffers_queue_t object
* @param[in] timeout the number of ticks before the operation timeouts,
* the following special values are allowed:
* - @a TIME_IMMEDIATE immediate timeout.
* - @a TIME_INFINITE no timeout.
* .
* @return The operation status.
* @retval MSG_OK if a buffer has been acquired.
* @retval MSG_TIMEOUT if the specified time expired.
* @retval MSG_RESET if the queue has been reset or has been put in
* suspended state.
*
* @sclass
*/
msg_t obqGetEmptyBufferTimeoutS(output_buffers_queue_t *obqp,
sysinterval_t timeout) {
osalDbgCheckClassS();
while (obqIsFullI(obqp)) {
if (obqp->suspended) {
return MSG_RESET;
}
msg_t msg = osalThreadEnqueueTimeoutS(&obqp->waiting, timeout);
if (msg < MSG_OK) {
return msg;
}
}
osalDbgAssert(!obqIsFullI(obqp), "still full");
/* Setting up the "current" buffer and its boundary.*/
obqp->ptr = obqp->bwrptr + sizeof (size_t);
obqp->top = obqp->bwrptr + obqp->bsize;
return MSG_OK;
}
/**
* @brief Posts a new filled buffer to the queue.
* @note The object callback is called after releasing the buffer.
*
* @param[in] obqp pointer to the @p output_buffers_queue_t object
* @param[in] size used size of the buffer, cannot be zero
*
* @api
*/
void obqPostFullBuffer(output_buffers_queue_t *obqp, size_t size) {
osalSysLock();
obqPostFullBufferS(obqp, size);
osalSysUnlock();
}
/**
* @brief Posts a new filled buffer to the queue.
* @note The object callback is called after releasing the buffer.
*
* @param[in] obqp pointer to the @p output_buffers_queue_t object
* @param[in] size used size of the buffer, cannot be zero
*
* @sclass
*/
void obqPostFullBufferS(output_buffers_queue_t *obqp, size_t size) {
osalDbgCheckClassS();
osalDbgCheck((size > 0U) && (size <= (obqp->bsize - sizeof (size_t))));
osalDbgAssert(!obqIsFullI(obqp), "buffers queue full");
/* Writing size field in the buffer.*/
*((size_t *)obqp->bwrptr) = size;
/* Posting the buffer in the queue.*/
obqp->bcounter--;
obqp->bwrptr += obqp->bsize;
if (obqp->bwrptr >= obqp->btop) {
obqp->bwrptr = obqp->buffers;
}
/* No "current" buffer.*/
obqp->ptr = NULL;
/* Notifying the buffer release.*/
if (obqp->notify != NULL) {
obqp->notify(obqp);
}
}
/**
* @brief Output queue write with timeout.
* @details This function writes a byte value to an output queue. If
* the queue is full then the calling thread is suspended until a
* new buffer is freed in the queue or a timeout occurs.
*
* @param[in] obqp pointer to the @p output_buffers_queue_t object
* @param[in] b byte value to be transferred
* @param[in] timeout the number of ticks before the operation timeouts,
* the following special values are allowed:
* - @a TIME_IMMEDIATE immediate timeout.
* - @a TIME_INFINITE no timeout.
* .
* @return A byte value from the queue.
* @retval MSG_TIMEOUT if the specified time expired.
* @retval MSG_RESET if the queue has been reset or has been put in
* suspended state.
*
* @api
*/
msg_t obqPutTimeout(output_buffers_queue_t *obqp, uint8_t b,
sysinterval_t timeout) {
msg_t msg;
osalSysLock();
/* This condition indicates that a new buffer must be acquired.*/
if (obqp->ptr == NULL) {
msg = obqGetEmptyBufferTimeoutS(obqp, timeout);
if (msg != MSG_OK) {
osalSysUnlock();
return msg;
}
}
/* Writing the byte to the buffer.*/
*obqp->ptr = b;
obqp->ptr++;
/* If the current buffer has been fully written then it is posted as
full in the queue.*/
if (obqp->ptr >= obqp->top) {
obqPostFullBufferS(obqp, obqp->bsize - sizeof (size_t));
}
osalSysUnlock();
return MSG_OK;
}
/**
* @brief Output queue write with timeout.
* @details The function writes data from a buffer to an output queue. The
* operation completes when the specified amount of data has been
* transferred or after the specified timeout or if the queue has
* been reset.
*
* @param[in] obqp pointer to the @p output_buffers_queue_t object
* @param[in] bp pointer to the data buffer
* @param[in] n the maximum amount of data to be transferred, the
* value 0 is reserved
* @param[in] timeout the number of ticks before the operation timeouts,
* the following special values are allowed:
* - @a TIME_IMMEDIATE immediate timeout.
* - @a TIME_INFINITE no timeout.
* .
* @return The number of bytes effectively transferred.
* @retval 0 if a timeout occurred.
*
* @api
*/
size_t obqWriteTimeout(output_buffers_queue_t *obqp, const uint8_t *bp,
size_t n, sysinterval_t timeout) {
size_t w = 0;
osalDbgCheck(n > 0U);
osalSysLock();
while (true) {
size_t size;
/* This condition indicates that a new buffer must be acquired.*/
if (obqp->ptr == NULL) {
msg_t msg;
/* Getting an empty buffer using the specified timeout.*/
msg = obqGetEmptyBufferTimeoutS(obqp, timeout);
/* Anything except MSG_OK interrupts the operation.*/
if (msg != MSG_OK) {
osalSysUnlock();
return w;
}
}
/* Size of the space available in the current buffer.*/
size = (size_t)obqp->top - (size_t)obqp->ptr;
if (size > (n - w)) {
size = n - w;
}
/* Smaller chunks in order to not make the critical zone too long,
this impacts throughput however.*/
if (size > 64U) {
/* Giving the compiler a chance to optimize for a fixed size move.*/
memcpy(obqp->ptr, bp, 64U);
bp += 64U;
obqp->ptr += 64U;
w += 64U;
}
else {
memcpy(obqp->ptr, bp, size);
bp += size;
obqp->ptr += size;
w += size;
}
/* Has the current data buffer been finished? if so then release it.*/
if (obqp->ptr >= obqp->top) {
obqPostFullBufferS(obqp, obqp->bsize - sizeof (size_t));
}
/* Giving a preemption chance.*/
osalSysUnlock();
if (w >= n) {
return w;
}
osalSysLock();
}
}
/**
* @brief Flushes the current, partially filled, buffer to the queue.
* @note The notification callback is not invoked because the function
* is meant to be called from ISR context. An operation status is
* returned instead.
*
* @param[in] obqp pointer to the @p output_buffers_queue_t object
* @return The operation status.
* @retval false if no new filled buffer has been posted to the queue.
* @retval true if a new filled buffer has been posted to the queue.
*
* @iclass
*/
bool obqTryFlushI(output_buffers_queue_t *obqp) {
osalDbgCheckClassI();
/* If queue is empty and there is a buffer partially filled and
it is not being written.*/
if (obqIsEmptyI(obqp) && (obqp->ptr != NULL)) {
size_t size = (size_t)obqp->ptr - ((size_t)obqp->bwrptr + sizeof (size_t));
if (size > 0U) {
/* Writing size field in the buffer.*/
*((size_t *)obqp->bwrptr) = size;
/* Posting the buffer in the queue.*/
obqp->bcounter--;
obqp->bwrptr += obqp->bsize;
if (obqp->bwrptr >= obqp->btop) {
obqp->bwrptr = obqp->buffers;
}
/* No "current" buffer.*/
obqp->ptr = NULL;
return true;
}
}
return false;
}
/**
* @brief Flushes the current, partially filled, buffer to the queue.
*
* @param[in] obqp pointer to the @p output_buffers_queue_t object
*
* @api
*/
void obqFlush(output_buffers_queue_t *obqp) {
osalSysLock();
/* If there is a buffer partially filled and not being written.*/
if (obqp->ptr != NULL) {
size_t size = ((size_t)obqp->ptr - (size_t)obqp->bwrptr) - sizeof (size_t);
if (size > 0U) {
obqPostFullBufferS(obqp, size);
}
}
osalSysUnlock();
}
/** @} */