mirror of
https://github.com/oliverschmidt/contiki.git
synced 2024-08-15 00:29:02 +00:00
613 lines
18 KiB
C
613 lines
18 KiB
C
/*
|
|
* Copyright (c) 2005, Swedish Institute of Computer Science
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. Neither the name of the Institute nor the names of its contributors
|
|
* may be used to endorse or promote products derived from this software
|
|
* without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*
|
|
* This file is part of the Contiki operating system.
|
|
*
|
|
* @(#)$Id: dtc.c,v 1.1 2006/06/18 07:49:34 adamdunkels Exp $
|
|
*/
|
|
/**
|
|
* \defgroup dtc Distributed TCP caching
|
|
*
|
|
* Distributed TCP Caching (DTC) is a mechanism that helps to improve
|
|
* the energy efficiency of TCP in wireless sensor networks.
|
|
*
|
|
* @{
|
|
*/
|
|
|
|
/**
|
|
* \file
|
|
* Implementation of the distributed TCP caching mechanism for uIP.
|
|
* \author Adam Dunkels <adam@sics.se>
|
|
*
|
|
*/
|
|
|
|
|
|
#include "contiki.h"
|
|
#include "dtc-service.h"
|
|
#include "uip-fw.h"
|
|
|
|
#include "dtc.h"
|
|
#include "dtc-conf.h"
|
|
#include "uip.h"
|
|
#include "uip-fw.h"
|
|
#include "uip_arch.h"
|
|
|
|
#include "list.h"
|
|
#include "contiki.h"
|
|
|
|
/*------------------------------------------------------------------------------*/
|
|
/**
|
|
* \internal
|
|
* The TCP and IP headers.
|
|
*/
|
|
struct tcpip_hdr {
|
|
/* IP header. */
|
|
u8_t vhl,
|
|
tos;
|
|
u16_t len;
|
|
|
|
u8_t ipid[2],
|
|
ipoffset[2],
|
|
ttl,
|
|
proto;
|
|
u16_t ipchksum;
|
|
u32_t srcipaddr,
|
|
destipaddr;
|
|
|
|
/* TCP header. */
|
|
u16_t srcport,
|
|
destport;
|
|
u32_t seqno, ackno;
|
|
u8_t tcpoffset,
|
|
flags;
|
|
u16_t wnd;
|
|
u16_t tcpchksum;
|
|
u8_t urgp[2];
|
|
u8_t data[4];
|
|
};
|
|
|
|
/**
|
|
* \internal
|
|
* Structure for keeping the state of one TCP end-point.
|
|
*/
|
|
struct dtc_tcpend {
|
|
u32_t ipaddr; /**< The IP address of the end-point. */
|
|
u32_t seqno; /**< The next expected TCP sequence number from the
|
|
end-point. */
|
|
u32_t rttseq; /**< The sequence number for which an RTT
|
|
measurement is currently being made, or 0 if no
|
|
measurement is being made. */
|
|
u16_t port; /**< The TCP port number of the end-point. */
|
|
u16_t window; /**< The receiver window for the end-point. */
|
|
int sa; /**< The smoothed avergage for the RTT
|
|
estimations. */
|
|
int sv; /**< The smoothed variance for the RTT
|
|
estimations. */
|
|
u8_t rto; /**< The RTO generated from the RTT estimations. */
|
|
u8_t tmr; /**< Timer. */
|
|
u8_t nrtx; /**< Number of retransmissions performed by the
|
|
distributed TCP snoop module. */
|
|
};
|
|
|
|
/**
|
|
* \internal
|
|
* The structure holding the state of a TCP connection.
|
|
*/
|
|
struct dtc_conn {
|
|
struct dtc_conn *next;
|
|
list_t segments;
|
|
struct dtc_tcpend e1; /**< First TCP end-point. */
|
|
struct dtc_tcpend e2; /**< Second TCP end-point. */
|
|
u8_t tmr; /**< Timer. */
|
|
};
|
|
|
|
/**
|
|
* \internal
|
|
* The structure holding a cached TCP segment.
|
|
*/
|
|
struct dtc_segment {
|
|
struct dtc_segment *next;
|
|
struct tcpip_hdr hdr; /**< The TCP and IP headers of the
|
|
packet. */
|
|
struct dtc_conn *conn; /**< Pointer to the connection that the
|
|
segment belongs to. */
|
|
u16_t len; /**< The length of the segment,
|
|
measured in TCP bytes. */
|
|
u8_t data[DTC_CONF_MAX_SEGSIZE];
|
|
/**< The data contained in the
|
|
segment. */
|
|
u8_t tmr; /**< Timer - starts at 0 and counts
|
|
upward every time
|
|
dtc_periodic() is invoked. */
|
|
};
|
|
|
|
/**
|
|
* \internal
|
|
* The list of cached TCP segments.
|
|
*/
|
|
MEMB(segments, sizeof(struct dtc_segment), DTC_CONF_NUM_SEGMENTS);
|
|
/*static struct dtc_segment segments[DTC_CONF_NUM_SEGMENTS];*/
|
|
|
|
/**
|
|
* \internal
|
|
* The list of TCP connections for which the distributed TCP snoop
|
|
* mechanism is active.
|
|
*/
|
|
MEMB(conns, sizeof(struct dtc_conn), DTC_CONF_NUM_CONNECTIONS);
|
|
/*static struct dtc_conn conns[DTC_CONF_NUM_CONNECTIONS];*/
|
|
|
|
LIST(connlist);
|
|
|
|
#define TCP_SEQ_LT(a,b) ((s32_t)((a)-(b)) < 0)
|
|
#define TCP_SEQ_LEQ(a,b) ((s32_t)((a)-(b)) <= 0)
|
|
#define TCP_SEQ_GT(a,b) ((s32_t)((a)-(b)) > 0)
|
|
#define TCP_SEQ_GEQ(a,b) ((s32_t)((a)-(b)) >= 0)
|
|
#define TCP_FIN 0x01
|
|
#define TCP_SYN 0x02
|
|
#define TCP_RST 0x04
|
|
#define TCP_PSH 0x08
|
|
#define TCP_ACK 0x10
|
|
#define TCP_URG 0x20
|
|
#define TCP_CTL 0x3f
|
|
|
|
/**
|
|
* \internal
|
|
* The length of the standard IP header, without IP options.
|
|
*/
|
|
#define IP_HLEN 20
|
|
|
|
/**
|
|
* \internal
|
|
* The start value of the retransmission time-out.
|
|
*/
|
|
#define RTO 30
|
|
|
|
static u32_t htonl(u32_t n);
|
|
|
|
|
|
static u8_t input(u8_t *packetptr, u16_t len);
|
|
static u8_t output(void) {return UIP_FW_OK;}
|
|
|
|
SERVICE(dtc_service, dtc_service, { input, output });
|
|
|
|
PROCESS(dtc_process, "Distributed TCP Caching");
|
|
|
|
/*------------------------------------------------------------------------------*/
|
|
/**
|
|
* Initialize the distributed snoop module.
|
|
*
|
|
* This function initializes the distributed snoop module and must be
|
|
* called before any of the other functions are used.
|
|
*/
|
|
/*------------------------------------------------------------------------------*/
|
|
static void
|
|
init_dtc(void)
|
|
{
|
|
memb_init(&conns);
|
|
memb_init(&segments);
|
|
|
|
list_init(connlist);
|
|
/* list_init(segmentlist);*/
|
|
}
|
|
/*------------------------------------------------------------------------------*/
|
|
/**
|
|
* \internal
|
|
* Save a segment in the segment cache.
|
|
*
|
|
* \param hdr Pointer to the header of the segment.
|
|
* \param len The length of the segment.
|
|
*
|
|
* \return A pointer to the cached segment, or NULL if the cache was full.
|
|
*
|
|
*/
|
|
/*------------------------------------------------------------------------------*/
|
|
static struct dtc_segment *
|
|
cache_segment(struct dtc_conn *conn,
|
|
struct tcpip_hdr *hdr, u16_t len)
|
|
{
|
|
struct dtc_segment *s;
|
|
|
|
/* Check if there are any unused slots in the segment cache. */
|
|
s = (struct dtc_segment *)memb_alloc(&segments);
|
|
|
|
if(s == NULL) {
|
|
/* Here we might try to make room in the segment cache, but for
|
|
now we simply return. */
|
|
return NULL;
|
|
}
|
|
|
|
/* Do some sanity checks on the segment to be cached. */
|
|
if(len < 40 ||
|
|
len - 40 > DTC_CONF_MAX_SEGSIZE) {
|
|
return NULL;
|
|
}
|
|
|
|
/* Calculate the length of the segment in TCP bytes. We do this by
|
|
taking the length of the IP packet, and subtract the size of the
|
|
IP and TCP headers. Since we only allow IP packets without
|
|
options, we know that the IP header is 20 bytes large, and we use
|
|
the header offset field of the TCP header to calculate the length
|
|
of the TCP header + options.
|
|
|
|
Finally, if the segment has the SYN or FIN flags set, we add one
|
|
to the segment length. */
|
|
|
|
s->len = htons(hdr->len) - IP_HLEN - ((hdr->tcpoffset >> 4) << 2);
|
|
if((hdr->flags & (TCP_SYN | TCP_FIN)) != 0) {
|
|
++s->len;
|
|
}
|
|
|
|
|
|
/* If there is no TCP data in the segment, there is no need to cache
|
|
it. */
|
|
if(s->len == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
s->tmr = 0;
|
|
|
|
/* Copy the TCP/IP headers. */
|
|
memcpy(&s->hdr, hdr, 40);
|
|
|
|
/* Copy the segment data. */
|
|
memcpy(&s->data, &hdr->data[0], len - 40);
|
|
|
|
/* Add segment to list of segments on the connection's list. */
|
|
list_add(conn->segments, s);
|
|
|
|
return s;
|
|
}
|
|
/*------------------------------------------------------------------------------*/
|
|
/**
|
|
* \internal
|
|
* Process an incoming TCP segment for a connection.
|
|
*/
|
|
/*------------------------------------------------------------------------------*/
|
|
static u8_t
|
|
process_segment(struct dtc_conn *conn,
|
|
struct dtc_tcpend *src,
|
|
struct dtc_tcpend *dst,
|
|
u8_t *packetptr, u16_t len)
|
|
{
|
|
struct tcpip_hdr *hdr;
|
|
struct dtc_segment *s;
|
|
int m;
|
|
|
|
hdr = (struct tcpip_hdr *)packetptr;
|
|
|
|
/* Check if the incoming segment acknowledges one in the segment
|
|
list of the connection. */
|
|
for(s = list_head(conn->segments); s != NULL; s = s->next) {
|
|
|
|
printf("process_segment: for this conn, ack %d, ack %lu, seq %lu\n",
|
|
(hdr->flags & TCP_ACK), htonl(hdr->ackno),
|
|
htonl(s->hdr.seqno) + s->len);
|
|
|
|
|
|
/* Check if the incoming segment acknowledges this one. If so,
|
|
we should drop the cached segment and possibly do RTT
|
|
measurements. */
|
|
|
|
if((s->hdr.srcipaddr == hdr->destipaddr) &&
|
|
(hdr->flags & TCP_ACK) != 0 &&
|
|
TCP_SEQ_GEQ(htonl(hdr->ackno), htonl(s->hdr.seqno) + s->len)) {
|
|
|
|
/* Do RTT estimation, unless we have done retransmissions. */
|
|
if(src->rttseq != 0 &&
|
|
TCP_SEQ_GEQ(htonl(hdr->ackno), src->rttseq) &&
|
|
src->nrtx == 0) {
|
|
|
|
printf("Performing RTT estimation for seqno %lu (timer %d)\n",
|
|
src->rttseq, src->tmr);
|
|
|
|
m = src->tmr;
|
|
/* This is taken directly from VJs original code in his paper */
|
|
m = m - (src->sa >> 3);
|
|
src->sa += m;
|
|
if(m < 0) {
|
|
m = -m;
|
|
}
|
|
m = m - (src->sv >> 2);
|
|
src->sv += m;
|
|
src->rto = (src->sa >> 3) + src->sv;
|
|
src->rttseq = 0;
|
|
printf("Performing RTO stuff, new rto %d\n", src->rto);
|
|
}
|
|
src->tmr = 0;
|
|
|
|
printf("process_segment: bropping acked segment.\n");
|
|
/* s->state = STATE_FREE;*/
|
|
list_remove(conn->segments, s);
|
|
memb_free(&segments, s);
|
|
|
|
/* Next, check if the incoming packet has the same sequence
|
|
number as one of the segments in the cache. If so, it is
|
|
either a retransmission or a duplicated packet. In any
|
|
case, the RTT estimation would be thwarted if we made
|
|
estimations, so we cancel any running RTT measurements. */
|
|
} else if(s->hdr.seqno == hdr->seqno) {
|
|
if(s->hdr.srcipaddr == hdr->srcipaddr) {
|
|
src->rttseq = 0;
|
|
} else {
|
|
dst->rttseq = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
s = cache_segment(conn, hdr, len);
|
|
|
|
if(s != NULL) {
|
|
s->conn = conn;
|
|
if(dst->rttseq == 0) {
|
|
dst->rttseq = htonl(hdr->seqno);
|
|
dst->tmr = 0;
|
|
printf("Starting RTT est for %lu\n", dst->rttseq);
|
|
}
|
|
printf("Process_segment: segment cached\n");
|
|
} else {
|
|
printf("Process_segment: segment not cached \n");
|
|
}
|
|
|
|
return uip_fw_forward();
|
|
}
|
|
/*------------------------------------------------------------------------------*/
|
|
/**
|
|
* Process an incoming packet by running it through the distributed
|
|
* TCP snooping mechanism, and possibly by forwarding it.
|
|
*
|
|
* This function processes an incoming packet, and should be called
|
|
* from the network device driver for every packet that arrives. The
|
|
* function might call uip_fw_output() to send out packets.
|
|
*
|
|
* \param packetptr A pointer to the first byte of the packet header
|
|
* of the incoming packet. The packet must follow contiguously the
|
|
* header in memory.
|
|
*
|
|
* \param len The length of the packet, including the packet header.
|
|
*
|
|
* \retval UIP_FW_OK If the packet should be further processed by the
|
|
* caller.
|
|
*
|
|
* \retval UIP_FW_FORWARDED If the packet was forwarded and therefor
|
|
* should not be further processed.
|
|
*/
|
|
/*------------------------------------------------------------------------------*/
|
|
static u8_t
|
|
input(u8_t *packetptr, u16_t len)
|
|
{
|
|
struct tcpip_hdr *hdr;
|
|
struct dtc_conn *c;
|
|
|
|
hdr = (struct tcpip_hdr *)packetptr;
|
|
|
|
/* XXX: when testing, only run dtc for connections on port 12347. */
|
|
if(hdr->proto != UIP_PROTO_TCP ||
|
|
(hdr->destport != HTONS(12347) && hdr->srcport != HTONS(12347))) {
|
|
return uip_fw_forward();
|
|
}
|
|
|
|
|
|
printf("dtc_input (%d, %d): vhl 0x%x proto %d len %d ipchk 0x%x\n",
|
|
node_x(), node_y(),
|
|
hdr->vhl, hdr->proto, HTONS(hdr->len),
|
|
uip_ipchksum());
|
|
|
|
/* First, do some initial checking on the packet to make sure that
|
|
it is an undamaged TCP packet. Note that the checks are placed in
|
|
an order with the most computationally expensive at the end. */
|
|
|
|
if(hdr->vhl == 0x45 && /* IP version and header length must be normal. */
|
|
|
|
/* It must be a TCP packet. */
|
|
hdr->proto == UIP_PROTO_TCP &&
|
|
|
|
/* We do not allow IP fragments. */
|
|
hdr->ipoffset[1] == 0 &&
|
|
(hdr->ipoffset[0] & 0x3f) == 0 &&
|
|
|
|
/* We do not process segments destined for ourselves. */
|
|
hdr->destipaddr != *(u32_t *)uip_hostaddr &&
|
|
|
|
/* The packet length must not exceed the size of the segment
|
|
cache. */
|
|
(DTC_CONF_MAX_SEGSIZE + 40) >= HTONS(hdr->len) &&
|
|
|
|
/* The IP checksum must be OK. */
|
|
uip_ipchksum() == 0xffff &&
|
|
|
|
/* The TCP checksum must be OK. */
|
|
uip_tcpchksum() == 0xffff) {
|
|
|
|
/* Check if this segment belongs to a connection for which DTC
|
|
currently is active. Also keep track of the first unused
|
|
connection, in case we need to allocated it further down. */
|
|
for(c = list_head(connlist); c != NULL; c = c->next) {
|
|
if(c->e1.ipaddr == hdr->destipaddr &&
|
|
c->e1.port == htons(hdr->destport) &&
|
|
c->e2.ipaddr == hdr->srcipaddr &&
|
|
c->e2.port == htons(hdr->srcport)) {
|
|
printf("dtc_input %d, %d: process segment 1\n",
|
|
node_x(), node_y());
|
|
return process_segment(c, &c->e2, &c->e1,
|
|
packetptr, len);
|
|
} else if(c->e2.ipaddr == hdr->destipaddr &&
|
|
c->e2.port == htons(hdr->destport) &&
|
|
c->e1.ipaddr == hdr->srcipaddr &&
|
|
c->e1.port == htons(hdr->srcport)) {
|
|
printf("dtc_input %d, %d: process segment 2\n", node_x(), node_y());
|
|
return process_segment(c, &c->e1, &c->e2,
|
|
packetptr, len);
|
|
|
|
/* } else {
|
|
printf("dtc_input %d, %d: not processing segment\n",
|
|
node_x(), node_y());
|
|
return UIP_FW_LOCAL;*/
|
|
}
|
|
}
|
|
|
|
|
|
/* The incoming segment did not belong to any active connections,
|
|
so we allocate a new connection if there is a free one that we
|
|
can use. */
|
|
printf("dtc_input %d, %d: creating new connection\n", node_x(), node_y());
|
|
|
|
c = (struct dtc_conn *)memb_alloc(&conns);
|
|
list_add(connlist, c);
|
|
/* Fill in the fields from the incoming header. */
|
|
c->e1.ipaddr = hdr->srcipaddr;
|
|
c->e1.seqno = htonl(hdr->seqno);
|
|
c->e1.rttseq = 0;
|
|
c->e1.port = htons(hdr->srcport);
|
|
c->e1.window = htons(hdr->wnd);
|
|
c->e1.sa = 16;
|
|
c->e1.sv = 0;
|
|
c->e1.rto = RTO;
|
|
c->e1.tmr = 0;
|
|
c->e1.nrtx = 0;
|
|
|
|
c->e2.ipaddr = hdr->destipaddr;
|
|
c->e2.seqno = htonl(hdr->ackno);
|
|
c->e2.rttseq = 0;
|
|
c->e2.port = htons(hdr->destport);
|
|
c->e2.window = 0;
|
|
c->e2.sa = 16;
|
|
c->e2.sv = 0;
|
|
c->e2.rto = RTO;
|
|
c->e2.tmr = 0;
|
|
c->e2.nrtx = 0;
|
|
|
|
return process_segment(c, &c->e1, &c->e2, packetptr, len);
|
|
}
|
|
return uip_fw_forward();
|
|
}
|
|
/*------------------------------------------------------------------------------*/
|
|
/**
|
|
* \internal
|
|
* Perform periodic processing for a specific connection end-point.
|
|
*
|
|
* The periodic processing does two things: update timers and
|
|
* retransmit segments, where the timers drive the
|
|
* retransmissions. This must be done on a per-TCP-endpoint basis,
|
|
* rather than on a per-TCP-connection basis, because we maintain an
|
|
* RTT estimate for each endpoint. The reason for not having a single
|
|
* RTT for each TCP connection is that this node may be located on a
|
|
* path with a very long delay to one end and a very short delay to
|
|
* the other.
|
|
*/
|
|
/*------------------------------------------------------------------------------*/
|
|
static void
|
|
periodic_endpoint(struct dtc_tcpend *c)
|
|
{
|
|
++c->tmr;
|
|
/* If the timer has reached its threshold value, we should
|
|
retransmit the segment with the lowest sequence number, if
|
|
cached. */
|
|
if(c->tmr == c->rto) {
|
|
printf("periodic_endpoint: should do retransmission.\n");
|
|
c->tmr = 0;
|
|
}
|
|
}
|
|
/*------------------------------------------------------------------------------*/
|
|
/**
|
|
* Perform periodic processing.
|
|
*
|
|
* This function does the periodic processing in the distributed snoop
|
|
* module. It should be called as often as the uIP periodic function
|
|
* is called, typically twice a second.
|
|
*
|
|
*/
|
|
/*------------------------------------------------------------------------------*/
|
|
static void
|
|
periodic(void)
|
|
{
|
|
struct dtc_segment *s;
|
|
struct dtc_conn *c;
|
|
|
|
/* for(i = 0; i < DTC_CONF_NUM_CONNECTIONS; ++i) {
|
|
c = &conns[i];
|
|
if(c->state == STATE_USED) {
|
|
periodic_endpoint(&c->e1);
|
|
periodic_endpoint(&c->e2);
|
|
}
|
|
}
|
|
|
|
for(i = 0; i < DTC_CONF_NUM_SEGMENTS; ++i) {
|
|
s = &segments[i];
|
|
if(s->state == STATE_USED) {
|
|
++s->tmr;
|
|
}
|
|
}*/
|
|
}
|
|
/*------------------------------------------------------------------------------*/
|
|
/**
|
|
* \internal
|
|
* Convert a 32-bit unsigned value from host to network byte order.
|
|
*
|
|
* \param n The 32-bit value to be converted.
|
|
*
|
|
* \return The converted 32-bit value.
|
|
*
|
|
*/
|
|
/*------------------------------------------------------------------------------*/
|
|
static u32_t
|
|
htonl(u32_t n)
|
|
{
|
|
#if BYTE_ORDER == BIG_ENDIAN
|
|
return n;
|
|
#else /* BYTE_ORDER == BIG_ENDIAN */
|
|
return (((u32_t)n & 0xff) << 24) |
|
|
(((u32_t)n & 0xff00) << 8) |
|
|
(((u32_t)n & 0xff0000) >> 8) |
|
|
(((u32_t)n & 0xff000000) >> 24);
|
|
#endif /* BYTE_ORDER == BIG_ENDIAN */
|
|
|
|
}
|
|
/*------------------------------------------------------------------------------*/
|
|
PROCESS_THREAD(dtc_process, ev, data)
|
|
{
|
|
static struct etimer et;
|
|
|
|
PROCESS_BEGIN();
|
|
|
|
init_dtc();
|
|
|
|
SERVICE_REGISTER(dtc_service);
|
|
|
|
while(1) {
|
|
etimer_set(&et, CLOCK_SECOND / 2);
|
|
PROCESS_YIELD_UNTIL(etimer_expired(&et));
|
|
|
|
periodic();
|
|
|
|
}
|
|
|
|
PROCESS_END();
|
|
}
|
|
/*---------------------------------------------------------------------------*/
|
|
/** @} */
|