mirror of
https://github.com/RIOT-OS/RIOT.git
synced 2025-12-26 06:53:52 +01:00
Merge pull request #14448 from benpicco/l2-peerstats-rebased
net/netstats: L1/L2 per neighbor statistics
This commit is contained in:
commit
5fba2c8387
@ -2,7 +2,7 @@ MODULE = nrfmin
|
||||
|
||||
SRC = nrfmin.c
|
||||
|
||||
ifneq (,$(filter gnrc_netdev_default,$(USEMODULE)))
|
||||
ifneq (,$(filter gnrc_netif,$(USEMODULE)))
|
||||
SRC += nrfmin_gnrc.c
|
||||
endif
|
||||
|
||||
|
||||
@ -91,6 +91,11 @@ PSEUDOMODULES += netdev_layer
|
||||
PSEUDOMODULES += netdev_register
|
||||
PSEUDOMODULES += netstats
|
||||
PSEUDOMODULES += netstats_l2
|
||||
PSEUDOMODULES += netstats_neighbor_etx
|
||||
PSEUDOMODULES += netstats_neighbor_count
|
||||
PSEUDOMODULES += netstats_neighbor_rssi
|
||||
PSEUDOMODULES += netstats_neighbor_lqi
|
||||
PSEUDOMODULES += netstats_neighbor_tx_time
|
||||
PSEUDOMODULES += netstats_ipv6
|
||||
PSEUDOMODULES += netstats_rpl
|
||||
PSEUDOMODULES += nimble
|
||||
|
||||
@ -107,6 +107,9 @@ endif
|
||||
ifneq (,$(filter netopt,$(USEMODULE)))
|
||||
DIRS += net/crosslayer/netopt
|
||||
endif
|
||||
ifneq (,$(filter netstats_neighbor,$(USEMODULE)))
|
||||
DIRS += net/netstats
|
||||
endif
|
||||
ifneq (,$(filter sema,$(USEMODULE)))
|
||||
DIRS += sema
|
||||
endif
|
||||
|
||||
@ -704,6 +704,11 @@ ifneq (,$(filter netstats_%, $(USEMODULE)))
|
||||
USEMODULE += netstats
|
||||
endif
|
||||
|
||||
ifneq (,$(filter netstats_neighbor_%, $(USEMODULE)))
|
||||
USEMODULE += netstats_neighbor
|
||||
USEMODULE += xtimer
|
||||
endif
|
||||
|
||||
ifneq (,$(filter gnrc_lwmac,$(USEMODULE)))
|
||||
USEMODULE += gnrc_netif
|
||||
USEMODULE += gnrc_nettype_lwmac
|
||||
|
||||
@ -191,6 +191,26 @@ char *l2util_addr_to_str(const uint8_t *addr, size_t addr_len, char *out);
|
||||
*/
|
||||
size_t l2util_addr_from_str(const char *str, uint8_t *out);
|
||||
|
||||
/**
|
||||
* @brief Checks if two l2 addresses are equal.
|
||||
*
|
||||
* @param[in] addr_a First hardware address.
|
||||
* @param[in] addr_a_len Length of first hardware address.
|
||||
* @param[in] addr_b Second hardware address.
|
||||
* @param[in] addr_b_len Length of second hardware address.
|
||||
*
|
||||
* @return true if the addresses match, false if not.
|
||||
*/
|
||||
static inline bool l2util_addr_equal(const uint8_t *addr_a, uint8_t addr_a_len,
|
||||
const uint8_t *addr_b, uint8_t addr_b_len)
|
||||
{
|
||||
if (addr_a_len != addr_b_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return memcmp(addr_a, addr_b, addr_a_len) == 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -38,6 +38,11 @@
|
||||
#include "list.h"
|
||||
#include "net/netopt.h"
|
||||
|
||||
#ifdef MODULE_NETSTATS_NEIGHBOR
|
||||
#include "cib.h"
|
||||
#include "net/netstats.h"
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@ -61,7 +66,10 @@ extern "C" {
|
||||
* @note All network interfaces should inherit from this structure.
|
||||
*/
|
||||
typedef struct {
|
||||
list_node_t node; /**< Pointer to the next interface */
|
||||
list_node_t node; /**< Pointer to the next interface */
|
||||
#ifdef MODULE_NETSTATS_NEIGHBOR
|
||||
netstats_nb_table_t neighbors; /**< Structure containing all L2 neighbors */
|
||||
#endif
|
||||
} netif_t;
|
||||
|
||||
/**
|
||||
|
||||
@ -19,6 +19,8 @@
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include "net/l2util.h"
|
||||
#include "mutex.h"
|
||||
|
||||
#ifndef NET_NETSTATS_H
|
||||
#define NET_NETSTATS_H
|
||||
@ -27,6 +29,20 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief The max number of entries in the peer stats table
|
||||
*/
|
||||
#ifndef NETSTATS_NB_SIZE
|
||||
#define NETSTATS_NB_SIZE (8)
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief The CIB size for tx correlation
|
||||
*/
|
||||
#ifndef NETSTATS_NB_QUEUE_SIZE
|
||||
#define NETSTATS_NB_QUEUE_SIZE (4)
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @name @ref net_netstats module names
|
||||
* @{
|
||||
@ -53,6 +69,64 @@ typedef struct {
|
||||
uint32_t rx_bytes; /**< received bytes */
|
||||
} netstats_t;
|
||||
|
||||
/**
|
||||
* @brief Stats per peer struct
|
||||
*/
|
||||
typedef struct {
|
||||
#if IS_USED(MODULE_NETSTATS_NEIGHBOR_TX_TIME) || DOXYGEN
|
||||
uint32_t time_tx_avg; /**< Average frame TX time in µs */
|
||||
#endif
|
||||
#if IS_USED(MODULE_NETSTATS_NEIGHBOR_ETX) || DOXYGEN
|
||||
uint16_t etx; /**< ETX of this peer */
|
||||
#endif
|
||||
#if IS_USED(MODULE_NETSTATS_NEIGHBOR_COUNT) || DOXYGEN
|
||||
uint16_t tx_count; /**< Number of sent frames to this peer */
|
||||
uint16_t tx_fail; /**< Number of sent frames that did not get ACKed */
|
||||
uint16_t rx_count; /**< Number of received frames */
|
||||
#endif
|
||||
uint16_t last_updated; /**< seconds timestamp of last update */
|
||||
uint16_t last_halved; /**< seconds timestamp of last halving */
|
||||
uint8_t l2_addr[L2UTIL_ADDR_MAX_LEN]; /**< Link layer address of the neighbor */
|
||||
uint8_t l2_addr_len; /**< Length of netstats_nb::l2_addr */
|
||||
uint8_t freshness; /**< Freshness counter */
|
||||
#if IS_USED(MODULE_NETSTATS_NEIGHBOR_RSSI) || DOXYGEN
|
||||
uint8_t rssi; /**< Average RSSI of received frames in abs([dBm]) */
|
||||
#endif
|
||||
#if IS_USED(MODULE_NETSTATS_NEIGHBOR_LQI) || DOXYGEN
|
||||
uint8_t lqi; /**< Average LQI of received frames */
|
||||
#endif
|
||||
} netstats_nb_t;
|
||||
|
||||
/**
|
||||
* @brief L2 Peer Info struct
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* @brief CIB for the tx correlation
|
||||
*/
|
||||
cib_t stats_idx;
|
||||
|
||||
/**
|
||||
* @brief send/callback mac association array
|
||||
*/
|
||||
netstats_nb_t *stats_queue[NETSTATS_NB_QUEUE_SIZE];
|
||||
|
||||
/**
|
||||
* @brief TX timestamp of stats_queue entries
|
||||
*/
|
||||
uint32_t stats_queue_time_tx[NETSTATS_NB_QUEUE_SIZE];
|
||||
|
||||
/**
|
||||
* @brief Per neighbor statistics array
|
||||
*/
|
||||
netstats_nb_t pstats[NETSTATS_NB_SIZE];
|
||||
|
||||
/**
|
||||
* @brief Neighbor Table access lock
|
||||
*/
|
||||
mutex_t lock;
|
||||
} netstats_nb_table_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
194
sys/include/net/netstats/neighbor.h
Normal file
194
sys/include/net/netstats/neighbor.h
Normal file
@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Koen Zandberg <koen@bergzand.net>
|
||||
*
|
||||
* This file is subject to the terms and conditions of the GNU Lesser General
|
||||
* Public License v2.1. See the file LICENSE in the top level directory for
|
||||
* more details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ingroup net_netstats
|
||||
* @brief Records statistics about link layer neighbors
|
||||
* @{
|
||||
*
|
||||
* @file
|
||||
* @brief Neighbor stats definitions
|
||||
*
|
||||
* @author Koen Zandberg <koen@bergzand.net>
|
||||
*/
|
||||
#ifndef NET_NETSTATS_NEIGHBOR_H
|
||||
#define NET_NETSTATS_NEIGHBOR_H
|
||||
|
||||
#include <string.h>
|
||||
#include "net/netif.h"
|
||||
#include "xtimer.h"
|
||||
#include "timex.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Result of the transmission
|
||||
* @{
|
||||
*/
|
||||
typedef enum {
|
||||
NETSTATS_NB_BUSY, /**< Failed due to medium busy */
|
||||
NETSTATS_NB_NOACK, /**< Failed due to no ack received */
|
||||
NETSTATS_NB_SUCCESS, /**< Successful transmission */
|
||||
} netstats_nb_result_t;
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @name @ref EWMA parameters
|
||||
* @{
|
||||
*/
|
||||
/**
|
||||
* @brief Multiplication factor of the EWMA
|
||||
*/
|
||||
#define NETSTATS_NB_EWMA_SCALE 100
|
||||
|
||||
/**
|
||||
* @brief Alpha factor of the EWMA
|
||||
*/
|
||||
#define NETSTATS_NB_EWMA_ALPHA 15
|
||||
|
||||
/**
|
||||
* @brief Alpha factor of the EWMA when stats are not fresh
|
||||
*/
|
||||
#define NETSTATS_NB_EWMA_ALPHA_RAMP 30
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @name @ref ETX parameters
|
||||
* @{
|
||||
*/
|
||||
/**
|
||||
* @brief ETX penalty for not receiving any ACK
|
||||
*/
|
||||
#define NETSTATS_NB_ETX_NOACK_PENALTY 6
|
||||
/**
|
||||
* @brief ETX fixed point divisor (rfc 6551)
|
||||
*/
|
||||
#define NETSTATS_NB_ETX_DIVISOR 128
|
||||
/**
|
||||
* @brief Initial ETX, assume a mediocre link
|
||||
*/
|
||||
#define NETSTATS_NB_ETX_INIT 2
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @name @ref Freshness parameters
|
||||
* @{
|
||||
*/
|
||||
/**
|
||||
* @brief seconds after the freshness counter is halved
|
||||
*/
|
||||
#define NETSTATS_NB_FRESHNESS_HALF 600
|
||||
/**
|
||||
* @brief freshness count needed before considering the statistics fresh
|
||||
*/
|
||||
#define NETSTATS_NB_FRESHNESS_TARGET 4
|
||||
/**
|
||||
* @brief Maximum freshness
|
||||
*/
|
||||
#define NETSTATS_NB_FRESHNESS_MAX 16
|
||||
/**
|
||||
* @brief seconds after statistics have expired
|
||||
*/
|
||||
#define NETSTATS_NB_FRESHNESS_EXPIRATION 1200
|
||||
/** @} */
|
||||
/**
|
||||
* @name @ref Timeout Parameters
|
||||
* @{
|
||||
*/
|
||||
/**
|
||||
* @brief milliseconds without TX done notification after which
|
||||
* a TX event is discarded
|
||||
*/
|
||||
#define NETSTATS_NB_TX_TIMEOUT_MS 100
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @brief Initialize the neighbor stats
|
||||
*
|
||||
* @param[in] netif network interface descriptor
|
||||
*
|
||||
*/
|
||||
void netstats_nb_init(netif_t *netif);
|
||||
|
||||
/**
|
||||
* @brief Find a neighbor stat by the mac address.
|
||||
*
|
||||
* @param[in] netif network interface descriptor
|
||||
* @param[in] l2_addr pointer to the L2 address
|
||||
* @param[in] len length of the L2 address
|
||||
* @param[out] out destination for the matching neighbor entry
|
||||
*
|
||||
* @return true if a matching peer was found, false otherwise
|
||||
*/
|
||||
bool netstats_nb_get(netif_t *netif, const uint8_t *l2_addr, uint8_t len, netstats_nb_t *out);
|
||||
|
||||
/**
|
||||
* @brief Store this neighbor as next in the transmission queue.
|
||||
*
|
||||
* Set @p len to zero if a nop record is needed, for example if the
|
||||
* transmission has a multicast address as a destination.
|
||||
*
|
||||
* @param[in] netif network interface descriptor
|
||||
* @param[in] l2_addr pointer to the L2 address
|
||||
* @param[in] len length of the L2 address
|
||||
*
|
||||
*/
|
||||
void netstats_nb_record(netif_t *netif, const uint8_t *l2_addr, uint8_t len);
|
||||
|
||||
/**
|
||||
* @brief Update the next recorded neighbor with the provided numbers
|
||||
*
|
||||
* This only increments the statistics if the length of the l2-address of the retrieved record
|
||||
* is non-zero. See also @ref netstats_nb_record. The numbers indicate the number of transmissions
|
||||
* the radio had to perform before a successful transmission was performed. For example: in the case
|
||||
* of a single send operation needing 3 tries before an ACK was received, there are 2 failed
|
||||
* transmissions and 1 successful transmission.
|
||||
*
|
||||
* @param[in] netif network interface descriptor
|
||||
* @param[in] result Result of the transmission
|
||||
* @param[in] transmissions Number of times the packet was sent over the air
|
||||
*
|
||||
* @return pointer to the record
|
||||
*/
|
||||
netstats_nb_t *netstats_nb_update_tx(netif_t *netif, netstats_nb_result_t result,
|
||||
uint8_t transmissions);
|
||||
|
||||
/**
|
||||
* @brief Record rx stats for the l2_addr
|
||||
*
|
||||
* @param[in] netif network interface descriptor
|
||||
* @param[in] l2_addr pointer to the L2 address
|
||||
* @param[in] l2_addr_len length of the L2 address
|
||||
* @param[in] rssi RSSI of the received transmission in abs([dBm])
|
||||
* @param[in] lqi Link Quality Indication provided by the radio
|
||||
*
|
||||
* @return pointer to the updated record
|
||||
*/
|
||||
netstats_nb_t *netstats_nb_update_rx(netif_t *netif, const uint8_t *l2_addr,
|
||||
uint8_t l2_addr_len, uint8_t rssi, uint8_t lqi);
|
||||
|
||||
/**
|
||||
* @brief Check if a record is fresh
|
||||
*
|
||||
* Freshness half time is checked and updated before verifying freshness.
|
||||
*
|
||||
* @param[in] netif network interface the statistic belongs to
|
||||
* @param[in] stats pointer to the statistic
|
||||
*/
|
||||
bool netstats_nb_isfresh(netif_t *netif, netstats_nb_t *stats);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NET_NETSTATS_NEIGHBOR_H */
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@ -35,9 +35,8 @@
|
||||
#if IS_USED(MODULE_GNRC_SIXLOWPAN_FRAG_SFR)
|
||||
#include "net/gnrc/sixlowpan/frag/sfr.h"
|
||||
#endif /* IS_USED(MODULE_GNRC_SIXLOWPAN_FRAG_SFR) */
|
||||
#if IS_USED(MODULE_NETSTATS)
|
||||
#include "net/netstats.h"
|
||||
#endif /* IS_USED(MODULE_NETSTATS) */
|
||||
#include "net/netstats/neighbor.h"
|
||||
#include "fmt.h"
|
||||
#include "log.h"
|
||||
#include "sched.h"
|
||||
@ -78,6 +77,11 @@ int gnrc_netif_create(gnrc_netif_t *netif, char *stack, int stacksize,
|
||||
netif_register((netif_t*) netif);
|
||||
assert(netif->dev == NULL);
|
||||
netif->dev = netdev;
|
||||
|
||||
#ifdef MODULE_NETSTATS_NEIGHBOR
|
||||
netstats_nb_init(&netif->netif);
|
||||
#endif
|
||||
|
||||
res = thread_create(stack, stacksize, priority, THREAD_CREATE_STACKTEST,
|
||||
_gnrc_netif_thread, (void *)netif, name);
|
||||
(void)res;
|
||||
@ -1432,6 +1436,27 @@ static inline void _event_post(gnrc_netif_t *netif)
|
||||
#endif
|
||||
}
|
||||
|
||||
static void _process_receive_stats(gnrc_netif_t *netdev, gnrc_pktsnip_t *pkt)
|
||||
{
|
||||
if (!IS_USED(MODULE_NETSTATS_NEIGHBOR)) {
|
||||
return;
|
||||
}
|
||||
|
||||
gnrc_netif_hdr_t *hdr;
|
||||
const uint8_t *src = NULL;
|
||||
gnrc_pktsnip_t *netif = gnrc_pktsnip_search_type(pkt, GNRC_NETTYPE_NETIF);
|
||||
|
||||
if (netif == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t src_len;
|
||||
hdr = netif->data;
|
||||
src = gnrc_netif_hdr_get_src_addr(hdr);
|
||||
src_len = hdr->src_l2addr_len;
|
||||
netstats_nb_update_rx(&netdev->netif, src, src_len, hdr->rssi, hdr->lqi);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Retrieve the netif event queue if enabled
|
||||
*
|
||||
@ -1535,6 +1560,23 @@ static void _send(gnrc_netif_t *netif, gnrc_pktsnip_t *pkt, bool push_back)
|
||||
* layer implementations in case `gnrc_netif_pktq` is included */
|
||||
gnrc_pktbuf_hold(pkt, 1);
|
||||
#endif /* IS_USED(MODULE_GNRC_NETIF_PKTQ) */
|
||||
|
||||
/* Record send in neighbor statistics if destination is unicast */
|
||||
if (IS_USED(MODULE_NETSTATS_NEIGHBOR)) {
|
||||
gnrc_netif_hdr_t *netif_hdr = pkt->data;
|
||||
if (netif_hdr->flags &
|
||||
(GNRC_NETIF_HDR_FLAGS_BROADCAST | GNRC_NETIF_HDR_FLAGS_MULTICAST)) {
|
||||
DEBUG("l2 stats: Destination is multicast or unicast, NULL recorded\n");
|
||||
netstats_nb_record(&netif->netif, NULL, 0);
|
||||
} else {
|
||||
DEBUG("l2 stats: recording transmission\n");
|
||||
netstats_nb_record(&netif->netif,
|
||||
gnrc_netif_hdr_get_dst_addr(netif_hdr),
|
||||
netif_hdr->dst_l2addr_len);
|
||||
}
|
||||
}
|
||||
|
||||
/* Split off the TX sync snip */
|
||||
gnrc_pktsnip_t *tx_sync = IS_USED(MODULE_GNRC_TX_SYNC)
|
||||
? gnrc_tx_sync_split(pkt) : NULL;
|
||||
res = netif->ops->send(netif, pkt);
|
||||
@ -1542,6 +1584,21 @@ static void _send(gnrc_netif_t *netif, gnrc_pktsnip_t *pkt, bool push_back)
|
||||
uint32_t err = (res < 0) ? -res : GNRC_NETERR_SUCCESS;
|
||||
gnrc_pktbuf_release_error(tx_sync, err);
|
||||
}
|
||||
|
||||
/* no frame was transmitted */
|
||||
if (res < 0) {
|
||||
DEBUG("gnrc_netif: error sending packet %p (code: %i)\n",
|
||||
(void *)pkt, res);
|
||||
|
||||
if (IS_USED(MODULE_NETSTATS_NEIGHBOR)) {
|
||||
netstats_nb_update_tx(&netif->netif, NETSTATS_NB_BUSY, 0);
|
||||
}
|
||||
}
|
||||
#ifdef MODULE_NETSTATS_L2
|
||||
else {
|
||||
netif->stats.tx_bytes += res;
|
||||
}
|
||||
#endif
|
||||
#if IS_USED(MODULE_GNRC_NETIF_PKTQ)
|
||||
if (res == -EBUSY) {
|
||||
int put_res;
|
||||
@ -1576,15 +1633,6 @@ static void _send(gnrc_netif_t *netif, gnrc_pktsnip_t *pkt, bool push_back)
|
||||
gnrc_pktbuf_release(pkt);
|
||||
}
|
||||
#endif /* IS_USED(MODULE_GNRC_NETIF_PKTQ) */
|
||||
if (res < 0) {
|
||||
DEBUG("gnrc_netif: error sending packet %p (code: %i)\n",
|
||||
(void *)pkt, res);
|
||||
}
|
||||
#ifdef MODULE_NETSTATS_L2
|
||||
else {
|
||||
netif->stats.tx_bytes += res;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void *_gnrc_netif_thread(void *args)
|
||||
@ -1757,6 +1805,7 @@ static void _event_cb(netdev_t *dev, netdev_event_t event)
|
||||
* Further packets will be sent on later TX_COMPLETE */
|
||||
_send_queued_pkt(netif);
|
||||
if (pkt) {
|
||||
_process_receive_stats(netif, pkt);
|
||||
_pass_on_packet(pkt);
|
||||
}
|
||||
break;
|
||||
@ -1773,11 +1822,29 @@ static void _event_cb(netdev_t *dev, netdev_event_t event)
|
||||
* so no acquire necessary */
|
||||
netif->stats.tx_success++;
|
||||
#endif /* IS_USED(MODULE_NETSTATS_L2) */
|
||||
if (IS_USED(MODULE_NETSTATS_NEIGHBOR)) {
|
||||
int8_t retries = -1;
|
||||
dev->driver->get(dev, NETOPT_TX_RETRIES_NEEDED, &retries, sizeof(retries));
|
||||
netstats_nb_update_tx(&netif->netif, NETSTATS_NB_SUCCESS, retries + 1);
|
||||
}
|
||||
break;
|
||||
#endif /* IS_USED(MODULE_NETSTATS_L2) || IS_USED(MODULE_GNRC_NETIF_PKTQ) */
|
||||
#if IS_USED(MODULE_NETSTATS_L2) || IS_USED(MODULE_GNRC_NETIF_PKTQ)
|
||||
#if IS_USED(MODULE_NETSTATS_L2) || IS_USED(MODULE_GNRC_NETIF_PKTQ) || \
|
||||
IS_USED(MODULE_NETSTATS_NEIGHBOR)
|
||||
case NETDEV_EVENT_TX_MEDIUM_BUSY:
|
||||
case NETDEV_EVENT_TX_NOACK:
|
||||
/* update neighbor statistics */
|
||||
if (IS_USED(MODULE_NETSTATS_NEIGHBOR)) {
|
||||
int8_t retries = -1;
|
||||
netstats_nb_result_t result;
|
||||
if (event == NETDEV_EVENT_TX_NOACK) {
|
||||
result = NETSTATS_NB_NOACK;
|
||||
dev->driver->get(dev, NETOPT_TX_RETRIES_NEEDED, &retries, sizeof(retries));
|
||||
} else {
|
||||
result = NETSTATS_NB_BUSY;
|
||||
}
|
||||
netstats_nb_update_tx(&netif->netif, result, retries + 1);
|
||||
}
|
||||
/* send packet previously queued within netif due to the lower
|
||||
* layer being busy.
|
||||
* Further packets will be sent on later TX_COMPLETE or
|
||||
|
||||
3
sys/net/netstats/Makefile
Normal file
3
sys/net/netstats/Makefile
Normal file
@ -0,0 +1,3 @@
|
||||
MODULE = netstats_neighbor
|
||||
|
||||
include $(RIOTBASE)/Makefile.base
|
||||
400
sys/net/netstats/netstats_neighbor.c
Normal file
400
sys/net/netstats/netstats_neighbor.c
Normal file
@ -0,0 +1,400 @@
|
||||
/*
|
||||
* Copyright (C) Koen Zandberg <koen@bergzand.net>
|
||||
*
|
||||
* This file is subject to the terms and conditions of the GNU Lesser
|
||||
* General Public License v2.1. See the file LICENSE in the top level
|
||||
* directory for more details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @{
|
||||
* @ingroup net
|
||||
* @file
|
||||
* @brief Neighbor level stats for netdev
|
||||
*
|
||||
* @author Koen Zandberg <koen@bergzand.net>
|
||||
* @author Benjamin Valentin <benpicco@beuth-hochschule.de>
|
||||
* @}
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
#include "net/l2util.h"
|
||||
#include "net/netdev.h"
|
||||
#include "net/netstats/neighbor.h"
|
||||
|
||||
#define ENABLE_DEBUG 0
|
||||
#include "debug.h"
|
||||
|
||||
static inline void _lock(netif_t *dev)
|
||||
{
|
||||
mutex_lock(&dev->neighbors.lock);
|
||||
}
|
||||
|
||||
static inline void _unlock(netif_t *dev)
|
||||
{
|
||||
mutex_unlock(&dev->neighbors.lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Compare the freshness of two records
|
||||
*
|
||||
* @param[in] a pointer to the first record
|
||||
* @param[in] b pointer to the second record
|
||||
* @param[in] now current timestamp in seconds
|
||||
*
|
||||
* @return pointer to the least fresh record
|
||||
*/
|
||||
static inline netstats_nb_t *netstats_nb_comp(const netstats_nb_t *a,
|
||||
const netstats_nb_t *b,
|
||||
uint16_t now)
|
||||
{
|
||||
return (netstats_nb_t *)(((now - a->last_updated) > now - b->last_updated) ? a : b);
|
||||
}
|
||||
|
||||
static void half_freshness(netstats_nb_t *stats, uint16_t now_sec)
|
||||
{
|
||||
uint8_t diff = (now_sec - stats->last_halved) / NETSTATS_NB_FRESHNESS_HALF;
|
||||
stats->freshness >>= diff;
|
||||
|
||||
if (diff) {
|
||||
/* Set to the last time point where this should have been halved */
|
||||
stats->last_halved = now_sec - diff;
|
||||
}
|
||||
}
|
||||
|
||||
static void incr_freshness(netstats_nb_t *stats)
|
||||
{
|
||||
uint16_t now = xtimer_now_usec() / US_PER_SEC;;
|
||||
|
||||
/* First halve the freshness if applicable */
|
||||
half_freshness(stats, now);
|
||||
|
||||
/* Increment the freshness capped at FRESHNESS_MAX */
|
||||
if (stats->freshness < NETSTATS_NB_FRESHNESS_MAX) {
|
||||
stats->freshness++;
|
||||
}
|
||||
|
||||
stats->last_updated = now;
|
||||
}
|
||||
|
||||
static bool isfresh(netstats_nb_t *stats)
|
||||
{
|
||||
uint16_t now = xtimer_now_usec() / US_PER_SEC;
|
||||
|
||||
/* Half freshness if applicable to update to current freshness */
|
||||
half_freshness(stats, now);
|
||||
|
||||
return (stats->freshness >= NETSTATS_NB_FRESHNESS_TARGET) &&
|
||||
(now - stats->last_updated < NETSTATS_NB_FRESHNESS_EXPIRATION);
|
||||
}
|
||||
|
||||
bool netstats_nb_isfresh(netif_t *dev, netstats_nb_t *stats)
|
||||
{
|
||||
bool ret;
|
||||
|
||||
_lock(dev);
|
||||
ret = isfresh(stats);
|
||||
_unlock(dev);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void netstats_nb_init(netif_t *dev)
|
||||
{
|
||||
mutex_init(&dev->neighbors.lock);
|
||||
|
||||
_lock(dev);
|
||||
memset(dev->neighbors.pstats, 0, sizeof(netstats_nb_t) * NETSTATS_NB_SIZE);
|
||||
cib_init(&dev->neighbors.stats_idx, NETSTATS_NB_QUEUE_SIZE);
|
||||
_unlock(dev);
|
||||
}
|
||||
|
||||
static void netstats_nb_create(netstats_nb_t *entry, const uint8_t *l2_addr, uint8_t l2_len)
|
||||
{
|
||||
memset(entry, 0, sizeof(netstats_nb_t));
|
||||
memcpy(entry->l2_addr, l2_addr, l2_len);
|
||||
entry->l2_addr_len = l2_len;
|
||||
|
||||
#ifdef MODULE_NETSTATS_NEIGHBOR_ETX
|
||||
entry->etx = NETSTATS_NB_ETX_INIT * NETSTATS_NB_ETX_DIVISOR;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool netstats_nb_get(netif_t *dev, const uint8_t *l2_addr, uint8_t len, netstats_nb_t *out)
|
||||
{
|
||||
_lock(dev);
|
||||
|
||||
netstats_nb_t *stats = dev->neighbors.pstats;
|
||||
bool found = false;
|
||||
|
||||
for (int i = 0; i < NETSTATS_NB_SIZE; i++) {
|
||||
|
||||
/* Check if this is the matching entry */
|
||||
if (l2util_addr_equal(stats[i].l2_addr, stats[i].l2_addr_len, l2_addr, len)) {
|
||||
*out = stats[i];
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_unlock(dev);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* find the oldest inactive entry to replace. Empty entries are infinity old */
|
||||
static netstats_nb_t *netstats_nb_get_or_create(netif_t *dev, const uint8_t *l2_addr, uint8_t len)
|
||||
{
|
||||
netstats_nb_t *old_entry = NULL;
|
||||
netstats_nb_t *stats = dev->neighbors.pstats;
|
||||
uint16_t now = xtimer_now_usec() / US_PER_SEC;
|
||||
|
||||
for (int i = 0; i < NETSTATS_NB_SIZE; i++) {
|
||||
|
||||
/* Check if this is the matching entry */
|
||||
if (l2util_addr_equal(stats[i].l2_addr, stats[i].l2_addr_len, l2_addr, len)) {
|
||||
return &stats[i];
|
||||
}
|
||||
|
||||
/* Entry is oldest if it is empty */
|
||||
if (stats[i].l2_addr_len == 0) {
|
||||
old_entry = &stats[i];
|
||||
}
|
||||
/* Check if the entry is expired */
|
||||
else if (!isfresh(&stats[i])) {
|
||||
/* Entry is oldest if it is expired */
|
||||
if (old_entry == NULL) {
|
||||
old_entry = &stats[i];
|
||||
}
|
||||
/* don't replace old entry if there are still empty ones */
|
||||
else if (old_entry->l2_addr_len > 0) {
|
||||
/* Check if current entry is older than current oldest entry */
|
||||
old_entry = netstats_nb_comp(old_entry, &stats[i], now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* if there is no matching entry,
|
||||
* create a new entry if we have an expired one */
|
||||
if (old_entry) {
|
||||
netstats_nb_create(old_entry, l2_addr, len);
|
||||
}
|
||||
|
||||
return old_entry;
|
||||
}
|
||||
|
||||
void netstats_nb_record(netif_t *dev, const uint8_t *l2_addr, uint8_t len)
|
||||
{
|
||||
_lock(dev);
|
||||
|
||||
int idx = cib_put(&dev->neighbors.stats_idx);
|
||||
|
||||
if (idx < 0) {
|
||||
DEBUG("%s: put buffer empty\n", __func__);
|
||||
goto out;
|
||||
}
|
||||
|
||||
DEBUG("put %d\n", idx);
|
||||
|
||||
if (len == 0) {
|
||||
/* Fill queue with a NOP */
|
||||
dev->neighbors.stats_queue[idx] = NULL;
|
||||
} else {
|
||||
dev->neighbors.stats_queue[idx] = netstats_nb_get_or_create(dev, l2_addr, len);
|
||||
dev->neighbors.stats_queue_time_tx[idx] = xtimer_now_usec();
|
||||
}
|
||||
|
||||
out:
|
||||
_unlock(dev);
|
||||
}
|
||||
|
||||
/* Get the first available neighbor in the transmission queue
|
||||
* and increment pointer. */
|
||||
static netstats_nb_t *netstats_nb_get_recorded(netif_t *dev, uint32_t *time_tx)
|
||||
{
|
||||
netstats_nb_t *res;
|
||||
int idx = cib_get(&dev->neighbors.stats_idx);
|
||||
|
||||
if (idx < 0) {
|
||||
DEBUG("%s: can't get record\n", __func__);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DEBUG("get %d (%d left)\n", idx, cib_avail(&dev->neighbors.stats_idx));
|
||||
|
||||
res = dev->neighbors.stats_queue[idx];
|
||||
dev->neighbors.stats_queue[idx] = NULL;
|
||||
|
||||
*time_tx = dev->neighbors.stats_queue_time_tx[idx];
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
__attribute__((unused))
|
||||
static uint32_t _ewma(bool fresh, uint32_t old_val, uint32_t new_val)
|
||||
{
|
||||
uint8_t ewma_alpha;
|
||||
|
||||
if (old_val == 0) {
|
||||
return new_val;
|
||||
}
|
||||
|
||||
/* If the stats are not fresh, use a larger alpha to average aggressive */
|
||||
if (fresh) {
|
||||
ewma_alpha = NETSTATS_NB_EWMA_ALPHA;
|
||||
} else {
|
||||
ewma_alpha = NETSTATS_NB_EWMA_ALPHA_RAMP;
|
||||
}
|
||||
|
||||
/* Exponential weighted moving average */
|
||||
return (old_val * (NETSTATS_NB_EWMA_SCALE - ewma_alpha)
|
||||
+ new_val * ewma_alpha) / NETSTATS_NB_EWMA_SCALE;
|
||||
}
|
||||
|
||||
static void netstats_nb_update_etx(netstats_nb_t *stats, netstats_nb_result_t result,
|
||||
uint8_t transmissions, bool fresh)
|
||||
{
|
||||
/* don't do anything if driver does not report ETX */
|
||||
if (transmissions == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result != NETSTATS_NB_SUCCESS) {
|
||||
transmissions = NETSTATS_NB_ETX_NOACK_PENALTY;
|
||||
}
|
||||
|
||||
#ifdef MODULE_NETSTATS_NEIGHBOR_ETX
|
||||
stats->etx = _ewma(fresh, stats->etx, transmissions * NETSTATS_NB_ETX_DIVISOR);
|
||||
#else
|
||||
(void)stats;
|
||||
(void)result;
|
||||
(void)transmissions;
|
||||
(void)fresh;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void netstats_nb_update_time(netstats_nb_t *stats, netstats_nb_result_t result,
|
||||
uint32_t duration, bool fresh)
|
||||
{
|
||||
/* TX time already got a penalty due to retransmissions */
|
||||
if (result != NETSTATS_NB_SUCCESS) {
|
||||
duration *= 2;
|
||||
}
|
||||
|
||||
#if MODULE_NETSTATS_NEIGHBOR_TX_TIME
|
||||
stats->time_tx_avg = _ewma(fresh, stats->time_tx_avg, duration);
|
||||
#else
|
||||
(void)stats;
|
||||
(void)result;
|
||||
(void)duration;
|
||||
(void)fresh;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void netstats_nb_update_rssi(netstats_nb_t *stats, uint8_t rssi, bool fresh)
|
||||
{
|
||||
#ifdef MODULE_NETSTATS_NEIGHBOR_RSSI
|
||||
stats->rssi = _ewma(fresh, stats->rssi, rssi);
|
||||
#else
|
||||
(void)stats;
|
||||
(void)rssi;
|
||||
(void)fresh;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void netstats_nb_update_lqi(netstats_nb_t *stats, uint8_t lqi, bool fresh)
|
||||
{
|
||||
#ifdef MODULE_NETSTATS_NEIGHBOR_LQI
|
||||
stats->lqi = _ewma(fresh, stats->lqi, lqi);
|
||||
#else
|
||||
(void)stats;
|
||||
(void)lqi;
|
||||
(void)fresh;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void netstats_nb_incr_count_tx(netstats_nb_t *stats, netstats_nb_result_t result)
|
||||
{
|
||||
#ifdef MODULE_NETSTATS_NEIGHBOR_COUNT
|
||||
stats->tx_count++;
|
||||
|
||||
/* gracefully handle overflow */
|
||||
if (stats->tx_count == 0) {
|
||||
stats->tx_count = ~stats->tx_count;
|
||||
stats->tx_count = stats->tx_count >> 4;
|
||||
stats->tx_fail = stats->tx_fail >> 4;
|
||||
}
|
||||
|
||||
if (result != NETSTATS_NB_SUCCESS) {
|
||||
stats->tx_fail++;
|
||||
}
|
||||
#else
|
||||
(void)stats;
|
||||
(void)result;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void netstats_nb_incr_count_rx(netstats_nb_t *stats)
|
||||
{
|
||||
#ifdef MODULE_NETSTATS_NEIGHBOR_COUNT
|
||||
stats->rx_count++;
|
||||
#else
|
||||
(void)stats;
|
||||
#endif
|
||||
}
|
||||
|
||||
netstats_nb_t *netstats_nb_update_tx(netif_t *dev, netstats_nb_result_t result,
|
||||
uint8_t transmissions)
|
||||
{
|
||||
uint32_t now = xtimer_now_usec();
|
||||
netstats_nb_t *stats;
|
||||
uint32_t time_tx = 0;
|
||||
|
||||
_lock(dev);
|
||||
|
||||
/* Buggy drivers don't always generate TX done events.
|
||||
* Discard old events to prevent the tx start <-> tx done correlation
|
||||
* from getting out of sync. */
|
||||
do {
|
||||
stats = netstats_nb_get_recorded(dev, &time_tx);
|
||||
} while (cib_avail(&dev->neighbors.stats_idx)
|
||||
&& ((now - time_tx) > NETSTATS_NB_TX_TIMEOUT_MS * US_PER_MS));
|
||||
|
||||
/* Nothing to do for multicast or if packet was not sent */
|
||||
if (result == NETSTATS_NB_BUSY || stats == NULL) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
bool fresh = isfresh(stats);
|
||||
|
||||
netstats_nb_update_time(stats, result, now - time_tx, fresh);
|
||||
netstats_nb_update_etx(stats, result, transmissions, fresh);
|
||||
netstats_nb_incr_count_tx(stats, result);
|
||||
|
||||
incr_freshness(stats);
|
||||
|
||||
out:
|
||||
_unlock(dev);
|
||||
return stats;
|
||||
}
|
||||
|
||||
netstats_nb_t *netstats_nb_update_rx(netif_t *dev, const uint8_t *l2_addr,
|
||||
uint8_t l2_addr_len, uint8_t rssi, uint8_t lqi)
|
||||
{
|
||||
_lock(dev);
|
||||
|
||||
netstats_nb_t *stats = netstats_nb_get_or_create(dev, l2_addr, l2_addr_len);
|
||||
|
||||
if (stats != NULL) {
|
||||
bool fresh = isfresh(stats);
|
||||
|
||||
netstats_nb_update_rssi(stats, rssi, fresh);
|
||||
netstats_nb_update_lqi(stats, lqi, fresh);
|
||||
netstats_nb_incr_count_rx(stats);
|
||||
|
||||
incr_freshness(stats);
|
||||
}
|
||||
|
||||
_unlock(dev);
|
||||
return stats;
|
||||
}
|
||||
@ -35,6 +35,9 @@ endif
|
||||
ifneq (,$(filter gnrc_netif,$(USEMODULE)))
|
||||
SRC += sc_gnrc_netif.c
|
||||
endif
|
||||
ifneq (,$(filter netstats_neighbor,$(USEMODULE)))
|
||||
SRC += sc_netstats_nb.c
|
||||
endif
|
||||
ifneq (,$(filter fib,$(USEMODULE)))
|
||||
SRC += sc_fib.c
|
||||
endif
|
||||
|
||||
102
sys/shell/commands/sc_netstats_nb.c
Normal file
102
sys/shell/commands/sc_netstats_nb.c
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) Koen Zandberg <koen@bergzand.net>
|
||||
*
|
||||
* This file is subject to the terms and conditions of the GNU Lesser
|
||||
* General Public License v2.1. See the file LICENSE in the top level
|
||||
* directory for more details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ingroup sys_shell_commands
|
||||
* @{
|
||||
*
|
||||
* @file
|
||||
* @brief Shell commands for displaying neighbor statistics
|
||||
*
|
||||
* @author Koen Zandberg <koen@bergzand.net>
|
||||
* @author Benjamin Valentin <benpicco@beuth-hochschule.de>
|
||||
*
|
||||
* @}
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include "net/gnrc/netif.h"
|
||||
#include "net/netstats.h"
|
||||
#include "net/netstats/neighbor.h"
|
||||
|
||||
static void _print_neighbors(netif_t *dev)
|
||||
{
|
||||
netstats_nb_t *stats = &dev->neighbors.pstats[0];
|
||||
unsigned header_len = 0;
|
||||
char l2addr_str[3 * L2UTIL_ADDR_MAX_LEN];
|
||||
puts("Neighbor link layer stats:");
|
||||
header_len += printf("L2 address fresh");
|
||||
if (IS_USED(MODULE_NETSTATS_NEIGHBOR_ETX)) {
|
||||
header_len += printf(" etx");
|
||||
}
|
||||
if (IS_USED(MODULE_NETSTATS_NEIGHBOR_COUNT)) {
|
||||
header_len += printf(" sent received");
|
||||
}
|
||||
if (IS_USED(MODULE_NETSTATS_NEIGHBOR_RSSI)) {
|
||||
header_len += printf(" rssi ");
|
||||
}
|
||||
if (IS_USED(MODULE_NETSTATS_NEIGHBOR_LQI)) {
|
||||
header_len += printf(" lqi");
|
||||
}
|
||||
if (IS_USED(MODULE_NETSTATS_NEIGHBOR_TX_TIME)) {
|
||||
header_len += printf(" avg tx time");
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
while (header_len--) {
|
||||
printf("-");
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
for (unsigned i = 0; i < NETSTATS_NB_SIZE; ++i) {
|
||||
netstats_nb_t *entry = &stats[i];
|
||||
|
||||
if (entry->l2_addr_len == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
printf("%-24s ",
|
||||
gnrc_netif_addr_to_str(entry->l2_addr, entry->l2_addr_len, l2addr_str));
|
||||
if (netstats_nb_isfresh(dev, entry)) {
|
||||
printf("%5u", (unsigned)entry->freshness);
|
||||
} else {
|
||||
printf("STALE");
|
||||
}
|
||||
|
||||
#if IS_USED(MODULE_NETSTATS_NEIGHBOR_ETX)
|
||||
printf(" %3u%%", (100 * entry->etx) / NETSTATS_NB_ETX_DIVISOR);
|
||||
#endif
|
||||
#if IS_USED(MODULE_NETSTATS_NEIGHBOR_COUNT)
|
||||
printf(" %4"PRIu16" %8"PRIu16, entry->tx_count, entry->rx_count);
|
||||
#endif
|
||||
#if IS_USED(MODULE_NETSTATS_NEIGHBOR_RSSI)
|
||||
printf(" %4i dBm", (int8_t) entry->rssi);
|
||||
#endif
|
||||
#if IS_USED(MODULE_NETSTATS_NEIGHBOR_LQI)
|
||||
printf(" %u", entry->lqi);
|
||||
#endif
|
||||
#if IS_USED(MODULE_NETSTATS_NEIGHBOR_TX_TIME)
|
||||
printf(" %7"PRIu32" µs", entry->time_tx_avg);
|
||||
#endif
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
int _netstats_nb(int argc, char **argv)
|
||||
{
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
gnrc_netif_t *netif = NULL;
|
||||
while ((netif = gnrc_netif_iter(netif))) {
|
||||
_print_neighbors(&netif->netif);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@ -92,6 +92,10 @@ extern int _random_get(int argc, char **argv);
|
||||
extern int _gnrc_ipv6_nib(int argc, char **argv);
|
||||
#endif
|
||||
|
||||
#ifdef MODULE_NETSTATS_NEIGHBOR
|
||||
extern int _netstats_nb(int argc, char **argv);
|
||||
#endif
|
||||
|
||||
#ifdef MODULE_GNRC_NETIF
|
||||
extern int _gnrc_netif_config(int argc, char **argv);
|
||||
#ifdef MODULE_GNRC_TXTSND
|
||||
@ -245,6 +249,9 @@ const shell_command_t _shell_command_list[] = {
|
||||
#ifdef MODULE_GNRC_IPV6_NIB
|
||||
{"nib", "Configure neighbor information base", _gnrc_ipv6_nib},
|
||||
#endif
|
||||
#ifdef MODULE_NETSTATS_NEIGHBOR
|
||||
{"neigh", "Show neighbor statistics", _netstats_nb},
|
||||
#endif
|
||||
#ifdef MODULE_GNRC_NETIF
|
||||
{"ifconfig", "Configure network interfaces", _gnrc_netif_config},
|
||||
#ifdef MODULE_GNRC_TXTSND
|
||||
|
||||
9
tests/netstats_neighbor/Makefile
Normal file
9
tests/netstats_neighbor/Makefile
Normal file
@ -0,0 +1,9 @@
|
||||
USEMODULE = netdev_default
|
||||
|
||||
USEMODULE += netstats_neighbor_etx
|
||||
USEMODULE += netstats_neighbor_count
|
||||
USEMODULE += netstats_neighbor_rssi
|
||||
USEMODULE += netstats_neighbor_lqi
|
||||
USEMODULE += netstats_neighbor_tx_time
|
||||
|
||||
include ../driver_netdev_common/Makefile.netdev.mk
|
||||
23
tests/netstats_neighbor/Makefile.ci
Normal file
23
tests/netstats_neighbor/Makefile.ci
Normal file
@ -0,0 +1,23 @@
|
||||
BOARD_INSUFFICIENT_MEMORY := \
|
||||
arduino-duemilanove \
|
||||
arduino-leonardo \
|
||||
arduino-mega2560 \
|
||||
arduino-nano \
|
||||
arduino-uno \
|
||||
atmega328p \
|
||||
bluepill-stm32f030c8 \
|
||||
i-nucleo-lrwan1 \
|
||||
nucleo-f030r8 \
|
||||
nucleo-f031k6 \
|
||||
nucleo-f042k6 \
|
||||
nucleo-l011k4 \
|
||||
nucleo-l031k6 \
|
||||
nucleo-l053r8 \
|
||||
samd10-xmini \
|
||||
slstk3400a \
|
||||
stk3200 \
|
||||
stm32f030f4-demo \
|
||||
stm32f0discovery \
|
||||
stm32l0538-disco \
|
||||
waspmote-pro \
|
||||
#
|
||||
1
tests/netstats_neighbor/main.c
Symbolic link
1
tests/netstats_neighbor/main.c
Symbolic link
@ -0,0 +1 @@
|
||||
../driver_netdev_common/main.c
|
||||
Loading…
x
Reference in New Issue
Block a user