Skip to content

Instantly share code, notes, and snippets.

@ejohnso49
Created July 8, 2024 19:35
Show Gist options
  • Save ejohnso49/ac6559fee341f4a121b5b1b96e8109bf to your computer and use it in GitHub Desktop.
Save ejohnso49/ac6559fee341f4a121b5b1b96e8109bf to your computer and use it in GitHub Desktop.
Zephyr + MDS
#
# Copyright (c) 2022 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#
menuconfig BT_MDS
bool "Memfault Diagnostic service"
depends on !MEMFAULT_HTTP_ENABLE
select BT_NRF_SERVICES
help
Enable GATT Memfault Diagnostic BLE service.
if BT_MDS
config BT_MDS_MAX_URI_LENGTH
int "Maiximum URI length"
default 64
help
Maximum URI length where diagnostic data should be forwarded to.
choice BT_MDS_PERM
prompt "Characteristic attributes permition"
default BT_MDS_PERM_RW_ENCRYPT if BT_SMP
default BT_MDS_PERM_RW
help
Default permissions for Memfault Diagnostic characteristic attributes.
config BT_MDS_PERM_RW
bool "Read and write allowed"
config BT_MDS_PERM_RW_ENCRYPT
bool "Require encryption for access"
depends on BT_SMP
config BT_MDS_PERM_RW_AUTHEN
bool "Require encryption using authenticated link-key for access"
depends on BT_SMP
endchoice
config BT_MDS_PIPELINE_COUNT
int "Number of chunks pipelined in the Bluetooth stack"
default 2
range 1 BT_CONN_TX_MAX
help
Number of the Memfault chunks pipelined in the Bluetooth stack. Pipelining chunks in the
Bluetooth stack reduces latency related to delivering subsequent data to the stack.
config BT_MDS_DATA_POLL_INTERVAL
int "Data poll interval"
default 60000
help
Memfault data poll interval in milliseconds. If there is any memfault data it will
be send after each interval expiration.
module = BT_MDS
module-str = MDS
source "${ZEPHYR_BASE}/subsys/logging/Kconfig.template.log_config"
endif # BT_MDS
# TODO: Remove if already sourced in other Kconfig files
source "Kconfig.zephyr"
/*
* Copyright (c) 2022 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
/** @file
* @brief Memfault Diagnostic GATT Service (MDS)
*/
#include <string.h>
#include <zephyr/logging/log.h>
#include <zephyr/bluetooth/gatt.h>
// TODO: Modify path as necessary
#include "mds.h"
#include <zephyr/net/buf.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/sys/__assert.h>
#include <memfault/components.h>
LOG_MODULE_REGISTER(mds, CONFIG_BT_MDS_LOG_LEVEL);
#ifndef CONFIG_BT_MDS_PERM_RW
#define CONFIG_BT_MDS_PERM_RW 0
#endif
#ifndef CONFIG_BT_MDS_PERM_RW_ENCRYPT
#define CONFIG_BT_MDS_PERM_RW_ENCRYPT 0
#endif
#ifndef CONFIG_BT_MDS_PERM_RW_AUTHEN
#define CONFIG_BT_MDS_PERM_RW_AUTHEN 0
#endif
#define MDS_GATT_PERM_READ ( \
CONFIG_BT_MDS_PERM_RW_AUTHEN ? \
BT_GATT_PERM_READ_AUTHEN : (CONFIG_BT_MDS_PERM_RW_ENCRYPT ? \
BT_GATT_PERM_READ_ENCRYPT : BT_GATT_PERM_READ) \
)
#define MDS_GATT_PERM_WRITE ( \
CONFIG_BT_MDS_PERM_RW_AUTHEN ? \
BT_GATT_PERM_WRITE_AUTHEN : (CONFIG_BT_MDS_PERM_RW_ENCRYPT ? \
BT_GATT_PERM_WRITE_ENCRYPT : BT_GATT_PERM_WRITE) \
)
#define MDS_MAX_URI_LENGTH CONFIG_BT_MDS_MAX_URI_LENGTH
#define MDS_URI_BASE \
MEMFAULT_HTTP_APIS_DEFAULT_SCHEME "://" MEMFAULT_HTTP_CHUNKS_API_HOST "/api/v0/chunks/"
#define MDS_AUTH_KEY "Memfault-Project-Key:" CONFIG_MEMFAULT_NCS_PROJECT_KEY
#define DATA_POLL_INTERVAL CONFIG_BT_MDS_DATA_POLL_INTERVAL
/* Memfault chunk number maximum value. Chunk number should overlaps after reaching this value. */
#define MDS_CHUNK_NUMBER_MAX_VALUE BIT_MASK(5)
#define MAX_PIPELINE CONFIG_BT_MDS_PIPELINE_COUNT
#define STREAM_ENABLED BIT(0)
/* Application error code defined by the MDS.
* According to BLE Core v5.3 Vol 3, Part F 3.4.1.
*/
enum mds_att_error {
MDS_ATT_ERROR_CLIENT_ALREADY_SUBSCRIBED = 0x80,
MDS_ATT_ERROR_CLIENT_NOT_SUBSCRIBED = 0x81
};
enum data_export_mode {
DATA_EXPORT_MODE_STREAMING_DISABLE = 0x00,
DATA_EXPORT_MODE_STREAMING_ENABLE = 0x01
};
enum mds_read_char {
MDS_READ_CHAR_SUPPORTED_FEATURES,
MDS_READ_CHAR_DEVICE_IDENTIFIER,
MDS_READ_CHAR_DATA_URI,
MDS_READ_CHAR_AUTHORIZATION
};
struct mds {
const struct bt_mds_cb *cb;
struct bt_conn *conn;
atomic_t send_cnt;
atomic_t stream_state;
uint8_t chunk_number;
};
struct mds_data_export_nfy {
uint8_t chunk_number:5;
uint8_t rfu:3;
uint8_t data[];
};
struct mds_subscription {
bool subscribed;
const struct bt_conn *conn;
const struct bt_gatt_attr *attr;
};
static struct mds mds_instance = {
.send_cnt = ATOMIC_INIT(MAX_PIPELINE)
};
static void mds_work_handler(struct k_work *work);
K_WORK_DELAYABLE_DEFINE(mds_work, mds_work_handler);
/* System workqueue thread cannot be preempted to ensure proper service operations.
* Make sure here that system workqueue is a cooperative thread.
*/
BUILD_ASSERT(CONFIG_SYSTEM_WORKQUEUE_PRIORITY < 0);
static ssize_t supported_feature_read(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
static const uint8_t supported_features;
LOG_DBG("MDS supported feature read, handle: %u, conn: %p", bt_gatt_attr_get_handle(attr),
(void *)conn);
return bt_gatt_attr_read(conn, attr, buf, len, offset, &supported_features,
sizeof(supported_features));
}
static ssize_t device_identifier_read(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
sMemfaultDeviceInfo info;
size_t device_identifier_length;
LOG_DBG("MDS Device Identifier characteristic read, handle: %u, conn: %p",
bt_gatt_attr_get_handle(attr), (void *)conn);
memfault_platform_get_device_info(&info);
device_identifier_length = strlen(info.device_serial);
return bt_gatt_attr_read(conn, attr, buf, len, offset, info.device_serial,
device_identifier_length);
}
static ssize_t data_uri_read(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
sMemfaultDeviceInfo info;
char uri[MDS_MAX_URI_LENGTH];
size_t uri_base_length = strlen(MDS_URI_BASE);
size_t uri_sn_length;
size_t uri_length;
LOG_DBG("MDS Data URI characteristic read, handle: %u, conn: %p",
bt_gatt_attr_get_handle(attr), (void *)conn);
memfault_platform_get_device_info(&info);
uri_sn_length = strlen(info.device_serial);
uri_length = uri_base_length + uri_sn_length;
if (uri_length > sizeof(uri)) {
LOG_ERR("Too long URI");
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
}
memcpy(uri, MDS_URI_BASE, uri_base_length);
memcpy(&uri[uri_base_length], info.device_serial, uri_sn_length);
return bt_gatt_attr_read(conn, attr, buf, len, offset, uri,
uri_length);
}
static ssize_t authorization_read(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
static const char *auth_key = MDS_AUTH_KEY;
size_t auth_key_len = strlen(auth_key);
LOG_DBG("MDS Authorization characteristic read, handle: %u, conn: %p",
bt_gatt_attr_get_handle(attr), (void *)conn);
return bt_gatt_attr_read(conn, attr, buf, len, offset, auth_key, auth_key_len);
}
static ssize_t mds_read(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf,
uint16_t len, uint16_t offset)
{
ssize_t ret;
enum mds_read_char characteristic = (enum mds_read_char)attr->user_data;
if (mds_instance.cb->access_enable) {
if (!mds_instance.cb->access_enable(conn)) {
return BT_GATT_ERR(BT_ATT_ERR_READ_NOT_PERMITTED);
}
}
switch (characteristic) {
case MDS_READ_CHAR_SUPPORTED_FEATURES:
ret = supported_feature_read(conn, attr, buf, len, offset);
break;
case MDS_READ_CHAR_DEVICE_IDENTIFIER:
ret = device_identifier_read(conn, attr, buf, len, offset);
break;
case MDS_READ_CHAR_DATA_URI:
ret = data_uri_read(conn, attr, buf, len, offset);
break;
case MDS_READ_CHAR_AUTHORIZATION:
ret = authorization_read(conn, attr, buf, len, offset);
break;
default:
ret = BT_GATT_ERR(BT_ATT_ERR_ATTRIBUTE_NOT_FOUND);
break;
}
return ret;
}
static void stream_enable(struct bt_conn *conn)
{
/* Check if stream is already enabled. */
if (atomic_test_and_set_bit(&mds_instance.stream_state, STREAM_ENABLED)) {
return;
}
if (!mds_instance.conn) {
mds_instance.conn = conn;
}
k_work_schedule(&mds_work, K_NO_WAIT);
}
static void stream_disable(struct bt_conn *conn)
{
atomic_clear_bit(&mds_instance.stream_state, STREAM_ENABLED);
/* This context cannot be preempted by the system workqueue so we can cancel workqueue here
* without checking a return value.
*/
(void)k_work_cancel_delayable(&mds_work);
}
static bool stream_mode_handler(struct bt_conn *conn, enum data_export_mode stream_mode)
{
bool valid = true;
switch (stream_mode) {
case DATA_EXPORT_MODE_STREAMING_ENABLE:
stream_enable(conn);
break;
case DATA_EXPORT_MODE_STREAMING_DISABLE:
stream_disable(conn);
break;
default:
valid = false;
break;
}
return valid;
}
static ssize_t data_export_write(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
{
enum data_export_mode mode;
LOG_DBG("MDS Data Export characteristic write, handle %u, conn: %p",
bt_gatt_attr_get_handle(attr), (void *)conn);
if (!bt_gatt_is_subscribed(conn, attr, BT_GATT_CCC_NOTIFY)) {
LOG_DBG("MDS Data Export notification are disabled");
return BT_GATT_ERR(MDS_ATT_ERROR_CLIENT_NOT_SUBSCRIBED);
}
if (mds_instance.cb->access_enable) {
if (!mds_instance.cb->access_enable(conn)) {
return BT_GATT_ERR(BT_ATT_ERR_WRITE_NOT_PERMITTED);
}
}
if (offset != 0) {
LOG_WRN("MDS Data Export invalid write offset");
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
if (len != sizeof(uint8_t)) {
LOG_WRN("MDS Data Export invalid attribute length");
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
}
mode = (enum data_export_mode)(((uint8_t *)buf)[0]);
if (!stream_mode_handler(conn, mode)) {
LOG_WRN("MDS Data Export characteristic write invalid value");
return BT_GATT_ERR(BT_ATT_ERR_OUT_OF_RANGE);
}
return len;
}
static void data_export_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
LOG_DBG("MDS Data Export CCCD changed, handle: %u, value: 0x%04X",
bt_gatt_attr_get_handle(attr), value);
}
static void conn_subscription_check(struct bt_conn *conn, void *user_data)
{
struct mds_subscription *mds_subscription = user_data;
if (bt_gatt_is_subscribed(conn, mds_subscription->attr, BT_GATT_CCC_NOTIFY)) {
mds_subscription->conn = conn;
mds_subscription->subscribed = true;
}
}
static ssize_t data_export_ccc_write(struct bt_conn *conn,
const struct bt_gatt_attr *attr, uint16_t value)
{
struct mds_subscription mds_subscription = {
.attr = attr
};
if ((value != BT_GATT_CCC_NOTIFY) && (value != 0)) {
return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
}
/* Allow only one subscription to the Memfault Data Export characteristic. */
bt_conn_foreach(BT_CONN_TYPE_LE, conn_subscription_check, &mds_subscription);
if (mds_subscription.subscribed && (mds_subscription.conn != conn)) {
LOG_WRN("Memfault Data Export characteristic is already subscribed");
return BT_GATT_ERR(MDS_ATT_ERROR_CLIENT_ALREADY_SUBSCRIBED);
}
if (mds_instance.cb->access_enable) {
if (!mds_instance.cb->access_enable(conn)) {
return BT_GATT_ERR(BT_ATT_ERR_WRITE_NOT_PERMITTED);
}
}
if ((value == 0) && (conn == mds_instance.conn)) {
stream_disable(conn);
}
return sizeof(value);
}
static struct _bt_gatt_ccc mds_data_export_ccc = BT_GATT_CCC_INITIALIZER(data_export_ccc_changed,
data_export_ccc_write,
NULL);
BT_GATT_SERVICE_DEFINE(mds_svc,
BT_GATT_PRIMARY_SERVICE(BT_UUID_MEMFAULT_DIAG),
BT_GATT_CHARACTERISTIC(BT_UUID_MDS_SUPPORTED_FEATURES, BT_GATT_CHRC_READ,
MDS_GATT_PERM_READ, mds_read, NULL,
(void *)MDS_READ_CHAR_SUPPORTED_FEATURES),
BT_GATT_CHARACTERISTIC(BT_UUID_MDS_DEVICE_IDENTIFIER, BT_GATT_CHRC_READ,
MDS_GATT_PERM_READ, mds_read, NULL,
(void *)MDS_READ_CHAR_DEVICE_IDENTIFIER),
BT_GATT_CHARACTERISTIC(BT_UUID_MDS_DATA_URI, BT_GATT_CHRC_READ,
MDS_GATT_PERM_READ, mds_read, NULL, (void *)MDS_READ_CHAR_DATA_URI),
BT_GATT_CHARACTERISTIC(BT_UUID_MDS_AUTHORIZATION, BT_GATT_CHRC_READ,
MDS_GATT_PERM_READ, mds_read, NULL,
(void *)MDS_READ_CHAR_AUTHORIZATION),
BT_GATT_CHARACTERISTIC(BT_UUID_MDS_DATA_EXPORT, BT_GATT_CHRC_NOTIFY | BT_GATT_CHRC_WRITE,
MDS_GATT_PERM_WRITE, NULL, data_export_write,
NULL),
BT_GATT_CCC_MANAGED(&mds_data_export_ccc, (MDS_GATT_PERM_READ | MDS_GATT_PERM_WRITE))
);
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
if (conn == mds_instance.conn) {
stream_disable(conn);
/* Clean up connection */
mds_instance.conn = NULL;
mds_instance.chunk_number = 0;
atomic_set(&mds_instance.send_cnt, MAX_PIPELINE);
/* It is called from a workqueue context, so checking the error codes is no needed,
* here.
*/
k_work_cancel_delayable(&mds_work);
}
}
BT_CONN_CB_DEFINE(conn_callbacks) = {
.disconnected = disconnected,
};
static void mds_sent_cb(struct bt_conn *conn, void *user_data)
{
atomic_inc(&mds_instance.send_cnt);
k_work_reschedule(&mds_work, K_NO_WAIT);
}
static size_t chunk_data_length_get(struct bt_conn *conn)
{
static const size_t att_header_length = 0x03;
size_t length;
if (!conn) {
return 0;
}
length = bt_gatt_get_mtu(conn);
if (length < (att_header_length + sizeof(struct mds_data_export_nfy))) {
LOG_ERR("MTU value too low: %d or link is disconnected", length);
return 0;
}
/* According to BLE Core v5.3 Vol 3, Part F 3.4.7.1 maximum supported length of the
* notification is (ATT_MTU - 3).
*/
length -= att_header_length;
length -= sizeof(struct mds_data_export_nfy);
return length;
}
static int chunk_send(struct bt_conn *conn, struct net_buf_simple *buf)
{
struct bt_gatt_notify_params params = {0};
static struct bt_gatt_attr *attr;
__ASSERT(conn, "Invalid parameters");
__ASSERT(buf, "Invalid parameters");
if (!attr) {
attr = bt_gatt_find_by_uuid(mds_svc.attrs, mds_svc.attr_count,
BT_UUID_MDS_DATA_EXPORT);
}
__ASSERT_NO_MSG(attr);
params.attr = attr;
params.data = buf->data;
params.len = buf->len;
params.func = mds_sent_cb;
return bt_gatt_notify_cb(conn, &params);
}
static uint8_t chunk_number_update(uint8_t chunk_number)
{
return ++chunk_number & MDS_CHUNK_NUMBER_MAX_VALUE;
}
static int mds_data_send(struct bt_conn *conn)
{
int err;
bool data_available;
struct mds_data_export_nfy *data_export_nfy;
size_t chunk_max_size = chunk_data_length_get(mds_instance.conn);
size_t chunk_size = chunk_max_size;
NET_BUF_SIMPLE_DEFINE(buf, (sizeof(struct mds_data_export_nfy) + chunk_max_size));
data_export_nfy = net_buf_simple_add(&buf,
sizeof(struct mds_data_export_nfy) + chunk_max_size);
if (!data_export_nfy) {
LOG_ERR("Cannot allocate place for memfault chunk data");
return -ENOMEM;
}
data_available = memfault_packetizer_get_chunk(data_export_nfy->data, &chunk_size);
if (data_available) {
data_export_nfy->chunk_number = mds_instance.chunk_number;
data_export_nfy->rfu = 0;
/* If chunk size is smaller than maximum chunk size then shrink the buffer to actual
* packet size.
*/
(void)net_buf_simple_remove_mem(&buf, (chunk_max_size - chunk_size));
err = chunk_send(conn, &buf);
if (err) {
memfault_packetizer_abort();
LOG_WRN("Failed to send Memfault diagnostic chunk, err %d", err);
return err;
}
LOG_DBG("Memfault diagnostic data chunk %d successfully sent",
mds_instance.chunk_number);
mds_instance.chunk_number = chunk_number_update(mds_instance.chunk_number);
return chunk_size;
}
return 0;
}
static void mds_work_handler(struct k_work *work)
{
int err;
bool data_available;
if (!atomic_test_bit(&mds_instance.stream_state, STREAM_ENABLED)) {
return;
}
err = mds_data_send(mds_instance.conn);
if (err > 0) {
atomic_dec(&mds_instance.send_cnt);
}
data_available = memfault_packetizer_data_available();
if (data_available) {
if (atomic_get(&mds_instance.send_cnt)) {
k_work_reschedule(&mds_work, K_NO_WAIT);
}
return;
}
/* Reschedule the workqueue here to check if there is any new Memfault data. */
k_work_reschedule(&mds_work, K_MSEC(DATA_POLL_INTERVAL));
}
int bt_mds_cb_register(const struct bt_mds_cb *cb)
{
if (mds_instance.cb) {
return -EALREADY;
}
mds_instance.cb = cb;
return 0;
}
/*
* Copyright (c) 2022 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/
#ifndef MDS_H_
#define MDS_H_
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/uuid.h>
/**@file
* @defgroup bt_mds Memfault Diagnostic GATT service API
* @{
* @brief API for the Memfault Diagnostic GATT service (MDS).
*/
#ifdef __cplusplus
extern "C" {
#endif
/** @brief UUID of the Memfault Diagnostic Service. */
#define BT_UUID_MDS_VAL \
BT_UUID_128_ENCODE(0x54220000, 0xf6a5, 0x4007, 0xa371, 0x722f4ebd8436)
/** @brief UUID of the MDS Supported Features Characteristic. */
#define BT_UUID_MDS_SUPPORTED_FEATURES_VAL \
BT_UUID_128_ENCODE(0x54220001, 0xf6a5, 0x4007, 0xa371, 0x722f4ebd8436)
/** @brief UUID of the MDS Device Identifier Characteristic. */
#define BT_UUID_MDS_DEVICE_IDENTIFIER_VAL \
BT_UUID_128_ENCODE(0x54220002, 0xf6a5, 0x4007, 0xa371, 0x722f4ebd8436)
/** @brief UUID of the MDS Data URI Characteristic. */
#define BT_UUID_MDS_DATA_URI_VAL \
BT_UUID_128_ENCODE(0x54220003, 0xf6a5, 0x4007, 0xa371, 0x722f4ebd8436)
/** @brief UUID of the MDS Authorization Characteristic. */
#define BT_UUID_MDS_AUTHORIZATION_VAL \
BT_UUID_128_ENCODE(0x54220004, 0xf6a5, 0x4007, 0xa371, 0x722f4ebd8436)
/** @brief UUID of the MDS Data Export Characteristic. */
#define BT_UUID_MDS_DATA_EXPORT_VAL \
BT_UUID_128_ENCODE(0x54220005, 0xf6a5, 0x4007, 0xa371, 0x722f4ebd8436)
#define BT_UUID_MEMFAULT_DIAG BT_UUID_DECLARE_128(BT_UUID_MDS_VAL)
#define BT_UUID_MDS_SUPPORTED_FEATURES BT_UUID_DECLARE_128(BT_UUID_MDS_SUPPORTED_FEATURES_VAL)
#define BT_UUID_MDS_DEVICE_IDENTIFIER BT_UUID_DECLARE_128(BT_UUID_MDS_DEVICE_IDENTIFIER_VAL)
#define BT_UUID_MDS_DATA_URI BT_UUID_DECLARE_128(BT_UUID_MDS_DATA_URI_VAL)
#define BT_UUID_MDS_AUTHORIZATION BT_UUID_DECLARE_128(BT_UUID_MDS_AUTHORIZATION_VAL)
#define BT_UUID_MDS_DATA_EXPORT BT_UUID_DECLARE_128(BT_UUID_MDS_DATA_EXPORT_VAL)
/** @brief Memfault Diagnostic Service callback structure.
*/
struct bt_mds_cb {
/** @brief A callback for enabling Memfault access.
*
* If this callback is not implemented, any connected peer can access the Memfault
* characteristics and descriptors data, including write access to them, without
* authentication.
*
* The Memfault Diagnostic service allows only one client subscription.
*
* @param[in] conn Connection object.
*
* @retval True if peer associated with connection object has granted access to
* the Memfault data.
* False if access to the Memfault data is forbidden.
*/
bool (*access_enable)(struct bt_conn *conn);
};
/** @brief Register the Memfault Diagnostic service callback.
*
* This function should be called before enabling Bluetooth stack to ensure proper access grating
* to MDS characteristics data through @p access_enable callback.
*
* @param[in] cb Memfault callback structure. This parameter can be set to NULL but it will let the
* first connected client access the Memfault data without authentication.
*
* @retval 0 If the operation was successful.
* Otherwise, a negative error code is returned
*/
int bt_mds_cb_register(const struct bt_mds_cb *cb);
#ifdef __cplusplus
}
#endif
/**
*@}
*/
#endif /* MDS_H_ */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment