Bugfixes and improvements: rtmetrics are now 16-bit clean. Added experimental feature that proactively probes neighbors that have not yet been tried out, in order to get an initial ETX for them.

This commit is contained in:
adamdunkels 2010-10-03 20:08:44 +00:00
parent d0579c8a81
commit 605392522f
2 changed files with 154 additions and 70 deletions

View File

@ -33,7 +33,7 @@
* *
* This file is part of the Contiki operating system. * This file is part of the Contiki operating system.
* *
* $Id: collect.c,v 1.54 2010/09/28 06:53:02 adamdunkels Exp $ * $Id: collect.c,v 1.55 2010/10/03 20:08:44 adamdunkels Exp $
*/ */
/** /**
@ -127,9 +127,9 @@ struct ack_msg {
#define MAX_MAC_REXMITS 2 #define MAX_MAC_REXMITS 2
#define MAX_ACK_MAC_REXMITS 3 #define MAX_ACK_MAC_REXMITS 3
#define REXMIT_TIME CLOCK_SECOND * 1 #define REXMIT_TIME CLOCK_SECOND * 1
#define FORWARD_PACKET_LIFETIME (6 * (REXMIT_TIME) << 3) #define FORWARD_PACKET_LIFETIME (60 * (REXMIT_TIME) << 3)
#define MAX_SENDING_QUEUE 16 #define MAX_SENDING_QUEUE 16
#define KEEPALIVE_REXMITS 4 #define KEEPALIVE_REXMITS 8
MEMB(send_queue_memb, struct packetqueue_item, MAX_SENDING_QUEUE); MEMB(send_queue_memb, struct packetqueue_item, MAX_SENDING_QUEUE);
@ -144,14 +144,22 @@ MEMB(send_queue_memb, struct packetqueue_item, MAX_SENDING_QUEUE);
rtmetric. This is used to determine when a new parent should be rtmetric. This is used to determine when a new parent should be
chosen over an old parent and when to begin more rapidly advertise chosen over an old parent and when to begin more rapidly advertise
a new rtmetric. */ a new rtmetric. */
#define SIGNIFICANT_RTMETRIC_CHANGE (COLLECT_LINK_ESTIMATE_UNIT + \ #define SIGNIFICANT_RTMETRIC_PARENT_CHANGE (COLLECT_LINK_ESTIMATE_UNIT + \
COLLECT_LINK_ESTIMATE_UNIT / 2) COLLECT_LINK_ESTIMATE_UNIT / 2)
/* This defines the maximum hops that a packet can take before it is /* This defines the maximum hops that a packet can take before it is
dropped. */ dropped. */
#define MAX_HOPLIM 15 #define MAX_HOPLIM 15
/* Experimental mechanism: when there are no packets in the send
queue, the system periodically sends a dummy packet to potential
parents, i.e., neighbors with a lower rtmetric than we have but for
which we do not yet have a link quality estimate. */
#define WITH_PROACTIVE_MAINTENANCE 0
#define PROACTIVE_MAINTENANCE_INTERVAL CLOCK_SECOND * 60 * 4
#define PROACTIVE_MAINTENANCE_REXMITS 8
/* COLLECT_CONF_ANNOUNCEMENTS defines if the Collect implementation /* COLLECT_CONF_ANNOUNCEMENTS defines if the Collect implementation
should use Contiki's announcement primitive to announce its routes should use Contiki's announcement primitive to announce its routes
or if it should use periodic broadcasts. */ or if it should use periodic broadcasts. */
@ -215,11 +223,11 @@ static void set_keepalive_timer(struct collect_conn *c);
* parent. * parent.
* *
*/ */
static uint8_t static uint16_t
rtmetric_compute(struct collect_conn *tc) rtmetric_compute(struct collect_conn *tc)
{ {
struct collect_neighbor *n; struct collect_neighbor *n;
uint8_t rtmetric = RTMETRIC_MAX; uint16_t rtmetric = RTMETRIC_MAX;
/* This function computes the current rtmetric for this node. It /* This function computes the current rtmetric for this node. It
uses the rtmetric of the parent node in the tree and adds the uses the rtmetric of the parent node in the tree and adds the
@ -291,7 +299,8 @@ update_parent(struct collect_conn *tc)
#if DRAW_TREE #if DRAW_TREE
printf("#L %d 0\n", tc->parent.u8[0]); printf("#L %d 0\n", tc->parent.u8[0]);
#endif /* DRAW_TREE */ #endif /* DRAW_TREE */
if(collect_neighbor_rtmetric_link_estimate(best) + SIGNIFICANT_RTMETRIC_CHANGE < if(collect_neighbor_rtmetric_link_estimate(best) +
SIGNIFICANT_RTMETRIC_PARENT_CHANGE <
collect_neighbor_rtmetric_link_estimate(current)) { collect_neighbor_rtmetric_link_estimate(current)) {
/* We switch parent. */ /* We switch parent. */
PRINTF("update_parent: new parent %d.%d (%d) old parent %d.%d (%d)\n", PRINTF("update_parent: new parent %d.%d (%d) old parent %d.%d (%d)\n",
@ -348,7 +357,7 @@ update_rtmetric(struct collect_conn *tc)
/* We should only update the rtmetric if we are not the sink. */ /* We should only update the rtmetric if we are not the sink. */
if(tc->rtmetric != RTMETRIC_SINK) { if(tc->rtmetric != RTMETRIC_SINK) {
uint8_t old_rtmetric, new_rtmetric; uint16_t old_rtmetric, new_rtmetric;
/* We remember the current (old) rtmetric for later. */ /* We remember the current (old) rtmetric for later. */
old_rtmetric = tc->rtmetric; old_rtmetric = tc->rtmetric;
@ -385,10 +394,10 @@ update_rtmetric(struct collect_conn *tc)
/* If we now have a significantly better or worse rtmetric than /* If we now have a significantly better or worse rtmetric than
we had before, what we need to make sure that our neighbors we had before, what we need to make sure that our neighbors
find out about this quickly. */ find out about this quickly. */
if(new_rtmetric < old_rtmetric - SIGNIFICANT_RTMETRIC_CHANGE || if(new_rtmetric < old_rtmetric - SIGNIFICANT_RTMETRIC_PARENT_CHANGE ||
new_rtmetric > old_rtmetric + SIGNIFICANT_RTMETRIC_CHANGE) { new_rtmetric > old_rtmetric + SIGNIFICANT_RTMETRIC_PARENT_CHANGE) {
PRINTF("update_rtmetric: new_rtmetric %d + %d < old_rtmetric %d\n", PRINTF("update_rtmetric: new_rtmetric %d + %d < old_rtmetric %d\n",
new_rtmetric, SIGNIFICANT_RTMETRIC_CHANGE, old_rtmetric); new_rtmetric, SIGNIFICANT_RTMETRIC_PARENT_CHANGE, old_rtmetric);
bump_advertisement(tc); bump_advertisement(tc);
} }
} }
@ -409,6 +418,36 @@ update_rtmetric(struct collect_conn *tc)
#endif /* DRAW_TREE */ #endif /* DRAW_TREE */
} }
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
static int
enqueue_dummy_packet(struct collect_conn *c, int rexmits)
{
struct collect_neighbor *n;
packetbuf_clear();
packetbuf_set_attr(PACKETBUF_ATTR_EPACKET_ID, c->eseqno - 1);
packetbuf_set_attr(PACKETBUF_ATTR_PACKET_ID, c->seqno - 1);
packetbuf_set_addr(PACKETBUF_ADDR_ESENDER, &rimeaddr_node_addr);
packetbuf_set_attr(PACKETBUF_ATTR_HOPS, 1);
packetbuf_set_attr(PACKETBUF_ATTR_TTL, 1);
packetbuf_set_attr(PACKETBUF_ATTR_MAX_REXMIT, rexmits);
PRINTF("%d.%d: enqueueing dummy packet %d, max_rexmits %d\n",
rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1],
packetbuf_attr(PACKETBUF_ATTR_EPACKET_ID),
packetbuf_attr(PACKETBUF_ATTR_MAX_REXMIT));
/* Allocate space for the header. */
packetbuf_hdralloc(sizeof(struct data_msg_hdr));
n = collect_neighbor_list_find(&c->neighbor_list, &c->parent);
if(n != NULL) {
return packetqueue_enqueue_packetbuf(&c->send_queue,
FORWARD_PACKET_LIFETIME,
c);
}
return 0;
}
/*---------------------------------------------------------------------------*/
/** /**
* This function is called when a queued packet should be sent * This function is called when a queued packet should be sent
* out. The function takes the first packet on the output queue, adds * out. The function takes the first packet on the output queue, adds
@ -425,20 +464,52 @@ send_queued_packet(struct collect_conn *c)
struct data_msg_hdr hdr; struct data_msg_hdr hdr;
int max_mac_rexmits; int max_mac_rexmits;
/* Grab the first packet on the send queue. */ /* If we are currently sending a packet, we do not attempt to send
i = packetqueue_first(&c->send_queue); another one. */
if(i == NULL) { if(c->sending) {
PRINTF("%d.%d: nothing on queue\n", PRINTF("%d.%d: queue, c is sending\n",
rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1]); rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1]);
/* No packet on the queue, so there is nothing for us to send. */
return; return;
} }
if(c->sending) {
/* If we are currently sending a packet, we wait until the /* Grab the first packet on the send queue. */
packet is forwarded and try again then. */ i = packetqueue_first(&c->send_queue);
PRINTF("%d.%d: queue, c is sending\n", if(i == NULL) {
rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1]); PRINTF("%d.%d: nothing on queue\n",
rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1]);
/* No packet on the queue, so there is nothing for us to send. */
/* XXX experimental feature: go through the list of neighbors to
find a potential parent for which we do not have a link
estimate and send a dummy packet to it. This allows us to
quickly gauge the link quality of neighbors that we do not
currently use as parents. */
if(WITH_PROACTIVE_MAINTENANCE && timer_expired(&c->proactive_maintenence_timer)) {
struct collect_neighbor *n;
timer_set(&c->proactive_maintenence_timer,
PROACTIVE_MAINTENANCE_INTERVAL);
for(n = list_head(collect_neighbor_list(&c->neighbor_list));
n != NULL; n = list_item_next(n)) {
if(n->rtmetric + COLLECT_LINK_ESTIMATE_UNIT < c->rtmetric &&
collect_link_estimate_num_estimates(&n->le) == 0) {
rimeaddr_t current_parent;
printf("proactive maintenance: found neighbor with no link estimate, %d.%d\n",
n->addr.u8[0], n->addr.u8[1]);
rimeaddr_copy(&current_parent, &c->parent);
rimeaddr_copy(&c->parent, &n->addr);
if(enqueue_dummy_packet(c, PROACTIVE_MAINTENANCE_REXMITS)) {
send_queued_packet(c);
}
rimeaddr_copy(&c->parent, &current_parent);
break;
}
}
}
return; return;
} }
@ -543,11 +614,32 @@ retransmit_current_packet(struct collect_conn *c)
/* Get hold of the queuebuf. */ /* Get hold of the queuebuf. */
q = packetqueue_queuebuf(i); q = packetqueue_queuebuf(i);
if(q != NULL) { if(q != NULL) {
update_rtmetric(c);
/* Place the queued packet into the packetbuf. */ /* Place the queued packet into the packetbuf. */
queuebuf_to_packetbuf(q); queuebuf_to_packetbuf(q);
/* Pick the neighbor to which to send the packet. We use the /* Pick the neighbor to which to send the packet. If we have found
parent in the n->parent. */ a better parent while we were transmitting this packet, we
chose that neighbor instead. If so, we need to attribute the
transmissions we made for the parent to that neighbor. */
if(!rimeaddr_cmp(&c->current_parent, &c->parent)) {
/* struct collect_neighbor *current_neighbor;
current_neighbor = collect_neighbor_list_find(&c->neighbor_list,
&c->current_parent);
if(current_neighbor != NULL) {
collect_neighbor_tx(current_neighbor, c->max_rexmits);
}*/
printf("parent change from %d.%d to %d.%d after %d tx\n",
c->current_parent.u8[0], c->current_parent.u8[1],
c->parent.u8[0], c->parent.u8[1],
c->transmissions);
rimeaddr_copy(&c->current_parent, &c->parent);
c->transmissions = 0;
}
n = collect_neighbor_list_find(&c->neighbor_list, &c->current_parent); n = collect_neighbor_list_find(&c->neighbor_list, &c->current_parent);
if(n != NULL) { if(n != NULL) {
@ -651,6 +743,8 @@ handle_ack(struct collect_conn *tc)
if(!(msg->flags & ACK_FLAGS_DROPPED)) { if(!(msg->flags & ACK_FLAGS_DROPPED)) {
send_next_packet(tc); send_next_packet(tc);
} else {
PRINTF("ack drop flag\n");
} }
if(msg->flags & ACK_FLAGS_RTMETRIC_NEEDS_UPDATE) { if(msg->flags & ACK_FLAGS_RTMETRIC_NEEDS_UPDATE) {
bump_advertisement(tc); bump_advertisement(tc);
@ -666,8 +760,7 @@ send_ack(struct collect_conn *tc, const rimeaddr_t *to, int flags)
{ {
struct ack_msg *ack; struct ack_msg *ack;
uint16_t packet_seqno = packetbuf_attr(PACKETBUF_ATTR_PACKET_ID); uint16_t packet_seqno = packetbuf_attr(PACKETBUF_ATTR_PACKET_ID);
uint16_t packet_eseqno = packetbuf_attr(PACKETBUF_ATTR_EPACKET_ID);
packetbuf_clear(); packetbuf_clear();
packetbuf_set_datalen(sizeof(struct ack_msg)); packetbuf_set_datalen(sizeof(struct ack_msg));
ack = packetbuf_dataptr(); ack = packetbuf_dataptr();
@ -685,9 +778,8 @@ send_ack(struct collect_conn *tc, const rimeaddr_t *to, int flags)
PRINTF("%d.%d: collect: Sending ACK to %d.%d for %d (epacket_id %d)\n", PRINTF("%d.%d: collect: Sending ACK to %d.%d for %d (epacket_id %d)\n",
rimeaddr_node_addr.u8[0],rimeaddr_node_addr.u8[1], rimeaddr_node_addr.u8[0],rimeaddr_node_addr.u8[1],
to->u8[0], to->u8[0], to->u8[1], packet_seqno,
to->u8[1], packetbuf_attr(PACKETBUF_ATTR_EPACKET_ID));
packet_seqno, packet_eseqno);
RIMESTATS_ADD(acktx); RIMESTATS_ADD(acktx);
stats.acksent++; stats.acksent++;
@ -836,7 +928,7 @@ node_packet_received(struct unicast_conn *c, const rimeaddr_t *from)
} else { } else {
send_ack(tc, &ack_to, send_ack(tc, &ack_to,
ackflags | ACK_FLAGS_DROPPED | ACK_FLAGS_CONGESTED); ackflags | ACK_FLAGS_DROPPED | ACK_FLAGS_CONGESTED);
PRINTF("%d.%d: packet dropped: no queue buffer available\n", printf("%d.%d: packet dropped: no queue buffer available\n",
rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1]); rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1]);
stats.qdrop++; stats.qdrop++;
} }
@ -874,7 +966,7 @@ node_packet_sent(struct unicast_conn *c, int status, int transmissions)
/* For data packets, we record the number of transmissions */ /* For data packets, we record the number of transmissions */
if(packetbuf_attr(PACKETBUF_ATTR_PACKET_TYPE) == if(packetbuf_attr(PACKETBUF_ATTR_PACKET_TYPE) ==
PACKETBUF_ATTR_PACKET_TYPE_DATA && transmissions > 0) { PACKETBUF_ATTR_PACKET_TYPE_DATA) {
tc->transmissions += transmissions; tc->transmissions += transmissions;
@ -891,7 +983,7 @@ node_packet_sent(struct unicast_conn *c, int status, int transmissions)
rexmit_time_scaling = 3; rexmit_time_scaling = 3;
} }
time = REXMIT_TIME << rexmit_time_scaling; time = REXMIT_TIME << rexmit_time_scaling;
time = time / 2 + random_rand() % (time / 2); time = time / 2 + (random_rand() % (time / 2));
PRINTF("retransmission time %lu scaling %d\n", time, rexmit_time_scaling); PRINTF("retransmission time %lu scaling %d\n", time, rexmit_time_scaling);
ctimer_set(&tc->retransmission_timer, time, ctimer_set(&tc->retransmission_timer, time,
retransmit_callback, tc); retransmit_callback, tc);
@ -902,8 +994,9 @@ static void
timedout(struct collect_conn *tc) timedout(struct collect_conn *tc)
{ {
struct collect_neighbor *n; struct collect_neighbor *n;
PRINTF("%d.%d: timedout after %d retransmissions (max retransmissions %d): packet dropped\n", printf("%d.%d: timedout after %d retransmissions to %d.%d (max retransmissions %d): packet dropped\n",
rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1], tc->transmissions, rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1], tc->transmissions,
tc->current_parent.u8[0], tc->current_parent.u8[1],
tc->max_rexmits); tc->max_rexmits);
tc->sending = 0; tc->sending = 0;
@ -917,6 +1010,13 @@ timedout(struct collect_conn *tc)
set_keepalive_timer(tc); set_keepalive_timer(tc);
} }
/*---------------------------------------------------------------------------*/ /*---------------------------------------------------------------------------*/
/**
* This function is called from a ctimer that is setup when a packet
* is sent. The purpose of this function is to either retransmit the
* current packet, or timeout the packet. The descision is made
* depending on how many times the packet has been transmitted. The
* ctimer is set up in the function node_packet_sent().
*/
static void static void
retransmit_callback(void *ptr) retransmit_callback(void *ptr)
{ {
@ -981,10 +1081,14 @@ received_announcement(struct announcement *a, const rimeaddr_t *from,
n = collect_neighbor_list_find(&tc->neighbor_list, from); n = collect_neighbor_list_find(&tc->neighbor_list, from);
if(n == NULL) { if(n == NULL) {
collect_neighbor_list_add(&tc->neighbor_list, from, value); /* Only add neighbors that have an rtmetric that is lower than
PRINTF("%d.%d: new neighbor %d.%d, etx %d\n", ours. */
rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1], if(value < tc->rtmetric) {
from->u8[0], from->u8[1], value); collect_neighbor_list_add(&tc->neighbor_list, from, value);
PRINTF("%d.%d: new neighbor %d.%d, rtmetric %d\n",
rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1],
from->u8[0], from->u8[1], value);
}
if(value == RTMETRIC_MAX) { if(value == RTMETRIC_MAX) {
bump_advertisement(tc); bump_advertisement(tc);
} }
@ -1062,37 +1166,15 @@ static void
send_keepalive(void *ptr) send_keepalive(void *ptr)
{ {
struct collect_conn *c = ptr; struct collect_conn *c = ptr;
struct collect_neighbor *n;
set_keepalive_timer(c); set_keepalive_timer(c);
/* Send keepalive message only if there are no pending transmissions. */ /* Send keepalive message only if there are no pending transmissions. */
if(packetqueue_len(&c->send_queue) == 0) { if(c->sending == 0 && packetqueue_len(&c->send_queue) == 0) {
packetbuf_clear(); if(enqueue_dummy_packet(c, KEEPALIVE_REXMITS)) {
packetbuf_set_addr(PACKETBUF_ADDR_ESENDER, &rimeaddr_node_addr); PRINTF("%d.%d: sending keepalive\n",
packetbuf_set_attr(PACKETBUF_ATTR_HOPS, 1); rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1]);
packetbuf_set_attr(PACKETBUF_ATTR_TTL, 1); send_queued_packet(c);
packetbuf_set_attr(PACKETBUF_ATTR_MAX_REXMIT, KEEPALIVE_REXMITS);
PRINTF("%d.%d: saending keepalive packet %d, max_rexmits %d\n",
rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1],
packetbuf_attr(PACKETBUF_ATTR_EPACKET_ID),
packetbuf_attr(PACKETBUF_ATTR_MAX_REXMIT));
/* Allocate space for the header. */
packetbuf_hdralloc(sizeof(struct data_msg_hdr));
n = collect_neighbor_list_find(&c->neighbor_list, &c->parent);
if(n != NULL) {
PRINTF("%d.%d: sending keepalive to %d.%d\n",
rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1],
n->addr.u8[0], n->addr.u8[1]);
if(packetqueue_enqueue_packetbuf(&c->send_queue,
FORWARD_PACKET_LIFETIME,
c)) {
send_queued_packet(c);
}
} }
} }
} }
@ -1204,7 +1286,7 @@ collect_send(struct collect_conn *tc, int rexmits)
send_queued_packet(tc); send_queued_packet(tc);
return 1; return 1;
} else { } else {
PRINTF("%d.%d: drop originated packet: no queuebuf\n", printf("%d.%d: drop originated packet: no queuebuf\n",
rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1]); rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1]);
} }
@ -1228,7 +1310,7 @@ collect_send(struct collect_conn *tc, int rexmits)
tc)) { tc)) {
return 1; return 1;
} else { } else {
PRINTF("%d.%d: drop originated packet: no queuebuf\n", printf("%d.%d: drop originated packet: no queuebuf\n",
rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1]); rimeaddr_node_addr.u8[0], rimeaddr_node_addr.u8[1]);
} }
} }

View File

@ -47,7 +47,7 @@
* *
* This file is part of the Contiki operating system. * This file is part of the Contiki operating system.
* *
* $Id: collect.h,v 1.21 2010/09/22 22:08:08 adamdunkels Exp $ * $Id: collect.h,v 1.22 2010/10/03 20:08:44 adamdunkels Exp $
*/ */
/** /**
@ -75,7 +75,7 @@
{ PACKETBUF_ATTR_PACKET_ID, PACKETBUF_ATTR_BIT * COLLECT_PACKET_ID_BITS }, \ { PACKETBUF_ATTR_PACKET_ID, PACKETBUF_ATTR_BIT * COLLECT_PACKET_ID_BITS }, \
{ PACKETBUF_ATTR_TTL, PACKETBUF_ATTR_BIT * 4 }, \ { PACKETBUF_ATTR_TTL, PACKETBUF_ATTR_BIT * 4 }, \
{ PACKETBUF_ATTR_HOPS, PACKETBUF_ATTR_BIT * 4 }, \ { PACKETBUF_ATTR_HOPS, PACKETBUF_ATTR_BIT * 4 }, \
{ PACKETBUF_ATTR_MAX_REXMIT, PACKETBUF_ATTR_BIT * 4 }, \ { PACKETBUF_ATTR_MAX_REXMIT, PACKETBUF_ATTR_BIT * 5 }, \
{ PACKETBUF_ATTR_PACKET_TYPE, PACKETBUF_ATTR_BIT }, \ { PACKETBUF_ATTR_PACKET_TYPE, PACKETBUF_ATTR_BIT }, \
UNICAST_ATTRIBUTES UNICAST_ATTRIBUTES
@ -101,6 +101,8 @@ struct collect_conn {
struct ctimer keepalive_timer; struct ctimer keepalive_timer;
clock_time_t keepalive_period; clock_time_t keepalive_period;
struct timer proactive_maintenence_timer;
rimeaddr_t parent, current_parent; rimeaddr_t parent, current_parent;
uint16_t rtmetric; uint16_t rtmetric;
uint8_t seqno; uint8_t seqno;
@ -129,7 +131,7 @@ void collect_set_keepalive(struct collect_conn *c, clock_time_t period);
void collect_print_stats(void); void collect_print_stats(void);
#define COLLECT_MAX_DEPTH 255 #define COLLECT_MAX_DEPTH ((1 << 12) - 1)
#endif /* __COLLECT_H__ */ #endif /* __COLLECT_H__ */
/** @} */ /** @} */