Bluetooth: Audio: Audio Input Control Service and client
This commit implements the secondary service Audio Input Control Service (AICS) server and client. Signed-off-by: Emil Gydesen <emil.gydesen@nordicsemi.no>
This commit is contained in:
parent
74a5ed7a23
commit
2016509756
9 changed files with 2485 additions and 2 deletions
451
include/bluetooth/audio/aics.h
Normal file
451
include/bluetooth/audio/aics.h
Normal file
|
@ -0,0 +1,451 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Nordic Semiconductor ASA
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#ifndef ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_AICS_H_
|
||||
#define ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_AICS_H_
|
||||
|
||||
/**
|
||||
* @brief Audio Input Control Service (AICS)
|
||||
*
|
||||
* @defgroup bt_gatt_aics Audio Input Control Service (AICS)
|
||||
*
|
||||
* @ingroup bluetooth
|
||||
* @{
|
||||
*
|
||||
* The Audio Input Control Service is a secondary service, and as such should not be used on its
|
||||
* own, but rather in the context of another (primary) service.
|
||||
*
|
||||
* This API implements both the server and client functionality.
|
||||
* Note that the API abstracts away the change counter in the audio input control state and will
|
||||
* automatically handle any changes to that. If out of date, the client implementation will
|
||||
* autonomously read the change counter value when executing a write request.
|
||||
*
|
||||
* [Experimental] Users should note that the APIs can change as a part of ongoing development.
|
||||
*/
|
||||
|
||||
#include <zephyr/types.h>
|
||||
#include <bluetooth/bluetooth.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Audio Input Control Service mute states */
|
||||
#define BT_AICS_STATE_UNMUTED 0x00
|
||||
#define BT_AICS_STATE_MUTED 0x01
|
||||
#define BT_AICS_STATE_MUTE_DISABLED 0x02
|
||||
|
||||
/** Audio Input Control Service input modes */
|
||||
#define BT_AICS_MODE_MANUAL_ONLY 0x00
|
||||
#define BT_AICS_MODE_AUTO_ONLY 0x01
|
||||
#define BT_AICS_MODE_MANUAL 0x02
|
||||
#define BT_AICS_MODE_AUTO 0x03
|
||||
|
||||
/** Audio Input Control Service input types */
|
||||
#define BT_AICS_INPUT_TYPE_UNSPECIFIED 0x00
|
||||
#define BT_AICS_INPUT_TYPE_BLUETOOTH 0x01
|
||||
#define BT_AICS_INPUT_TYPE_MICROPHONE 0x02
|
||||
#define BT_AICS_INPUT_TYPE_ANALOG 0x03
|
||||
#define BT_AICS_INPUT_TYPE_DIGITAL 0x04
|
||||
#define BT_AICS_INPUT_TYPE_RADIO 0x05
|
||||
#define BT_AICS_INPUT_TYPE_STREAMING 0x06
|
||||
|
||||
/** Audio Input Control Service Error codes */
|
||||
#define BT_AICS_ERR_INVALID_COUNTER 0x80
|
||||
#define BT_AICS_ERR_OP_NOT_SUPPORTED 0x81
|
||||
#define BT_AICS_ERR_MUTE_DISABLED 0x82
|
||||
#define BT_AICS_ERR_OUT_OF_RANGE 0x83
|
||||
#define BT_AICS_ERR_GAIN_MODE_NOT_ALLOWED 0x84
|
||||
|
||||
/** @brief Opaque Audio Input Control Service instance. */
|
||||
struct bt_aics;
|
||||
|
||||
/** @brief Structure for initializing a Audio Input Control Service instance. */
|
||||
struct bt_aics_register_param {
|
||||
/** Initial audio input gain (-128 to 127) */
|
||||
int8_t gain;
|
||||
|
||||
/** Initial audio input mute state */
|
||||
uint8_t mute;
|
||||
|
||||
/** Initial audio input mode */
|
||||
uint8_t gain_mode;
|
||||
|
||||
/** Initial audio input gain units (N * 0.1 dB) */
|
||||
uint8_t units;
|
||||
|
||||
/** Initial audio input minimum gain */
|
||||
int8_t min_gain;
|
||||
|
||||
/** Initial audio input maximum gain */
|
||||
int8_t max_gain;
|
||||
|
||||
/** Initial audio input type */
|
||||
uint8_t type;
|
||||
|
||||
/** Initial audio input status (active/inactive) */
|
||||
bool status;
|
||||
|
||||
/** Boolean to set whether the description is writable by clients */
|
||||
bool desc_writable;
|
||||
|
||||
/** Initial audio input description */
|
||||
char *description;
|
||||
|
||||
/** Pointer to the callback structure. */
|
||||
struct bt_aics_cb *cb;
|
||||
};
|
||||
|
||||
/** @brief Structure for discovering a Audio Input Control Service instance. */
|
||||
struct bt_aics_discover_param {
|
||||
/** @brief The start handle of the discovering.
|
||||
*
|
||||
* Typically the @p start_handle of a @ref bt_gatt_include.
|
||||
*/
|
||||
uint16_t start_handle;
|
||||
/** @brief The end handle of the discovering.
|
||||
*
|
||||
* Typically the @p end_handle of a @ref bt_gatt_include.
|
||||
*/
|
||||
uint16_t end_handle;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Get a free instance of Audio Input Control Service from the pool.
|
||||
*
|
||||
* @return Audio Input Control Service instance in case of success or NULL in case of error.
|
||||
*/
|
||||
struct bt_aics *bt_aics_free_instance_get(void);
|
||||
|
||||
/**
|
||||
* @brief Get the service declaration attribute.
|
||||
*
|
||||
* The first service attribute returned can be included in any other GATT service.
|
||||
*
|
||||
* @param aics Audio Input Control Service instance.
|
||||
*
|
||||
* @return Pointer to the attributes of the service.
|
||||
*/
|
||||
void *bt_aics_svc_decl_get(struct bt_aics *aics);
|
||||
|
||||
/**
|
||||
* @brief Initialize the Audio Input Control Service instance.
|
||||
*
|
||||
* @param aics Audio Input Control Service instance.
|
||||
* @param init Audio Input Control Service initialization structure.
|
||||
*
|
||||
* @return 0 if success, ERRNO on failure.
|
||||
*/
|
||||
int bt_aics_register(struct bt_aics *aics, struct bt_aics_register_param *init);
|
||||
|
||||
/**
|
||||
* @brief Callback function for writes.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL if local server write.
|
||||
* @param inst The instance pointer.
|
||||
* @param err Error value. 0 on success, GATT error on positive value
|
||||
* or ERRNO on negative value.
|
||||
*/
|
||||
typedef void (*bt_aics_write_cb)(struct bt_conn *conn, struct bt_aics *inst,
|
||||
int err);
|
||||
|
||||
/**
|
||||
* @brief Callback function for the input state.
|
||||
*
|
||||
* Called when the value is read,
|
||||
* or if the value is changed by either the server or client.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL if local server read.
|
||||
* @param inst The instance pointer.
|
||||
* @param err Error value. 0 on success, GATT error on positive value
|
||||
* or ERRNO on negative value.
|
||||
* For notifications, this will always be 0.
|
||||
* @param gain The gain setting value.
|
||||
* @param mute The mute value.
|
||||
* @param mode The mode value.
|
||||
*/
|
||||
typedef void (*bt_aics_state_cb)(struct bt_conn *conn, struct bt_aics *inst,
|
||||
int err, int8_t gain, uint8_t mute,
|
||||
uint8_t mode);
|
||||
|
||||
/**
|
||||
* @brief Callback function for the gain settings.
|
||||
*
|
||||
* Called when the value is read,
|
||||
* or if the value is changed by either the server or client.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL if local server read.
|
||||
* @param inst The instance pointer.
|
||||
* @param err Error value. 0 on success, GATT error on positive value
|
||||
* or ERRNO on negative value.
|
||||
* For notifications, this will always be 0.
|
||||
* @param units The value that reflect the size of a single increment or decrement of the
|
||||
* Gain Setting value in 0.1 decibel units.
|
||||
* @param minimum The minimum gain allowed for the gain setting.
|
||||
* @param maximum The maximum gain allowed for the gain setting.
|
||||
*/
|
||||
typedef void (*bt_aics_gain_setting_cb)(struct bt_conn *conn,
|
||||
struct bt_aics *inst, int err,
|
||||
uint8_t units, int8_t minimum,
|
||||
int8_t maximum);
|
||||
|
||||
/**
|
||||
* @brief Callback function for the input type.
|
||||
*
|
||||
* Called when the value is read, or if the value is changed by either the server or client.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL if local server read.
|
||||
* @param inst The instance pointer.
|
||||
* @param err Error value. 0 on success, GATT error on positive value
|
||||
* or ERRNO on negative value.
|
||||
* For notifications, this will always be 0.
|
||||
* @param type The input type.
|
||||
*/
|
||||
typedef void (*bt_aics_type_cb)(struct bt_conn *conn, struct bt_aics *inst,
|
||||
int err, uint8_t type);
|
||||
|
||||
/**
|
||||
* @brief Callback function for the input status.
|
||||
*
|
||||
* Called when the value is read, or if the value is changed by either the server or client.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL if local server read.
|
||||
* @param inst The instance pointer.
|
||||
* @param err Error value. 0 on success, GATT error on positive value
|
||||
* or ERRNO on negative value.
|
||||
* For notifications, this will always be 0.
|
||||
* @param active Whether the instance is active or inactive.
|
||||
*/
|
||||
typedef void (*bt_aics_status_cb)(struct bt_conn *conn, struct bt_aics *inst,
|
||||
int err, bool active);
|
||||
|
||||
/**
|
||||
* @brief Callback function for the description.
|
||||
*
|
||||
* Called when the value is read, or if the value is changed by either the server or client.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL if local server read.
|
||||
* @param inst The instance pointer.
|
||||
* @param err Error value. 0 on success, GATT error on positive value
|
||||
* or ERRNO on negative value.
|
||||
* For notifications, this will always be 0.
|
||||
* @param description The description as an UTF-8 encoded string (may have been clipped).
|
||||
*/
|
||||
typedef void (*bt_aics_description_cb)(struct bt_conn *conn,
|
||||
struct bt_aics *inst, int err,
|
||||
char *description);
|
||||
|
||||
/**
|
||||
* @brief Callback function for bt_aics_discover.
|
||||
*
|
||||
* This callback will usually be overwritten by the primary service that
|
||||
* includes the Audio Input Control Service client.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL if local server read.
|
||||
* @param inst The instance pointer.
|
||||
* @param err Error value. 0 on success, GATT error on positive value
|
||||
* or ERRNO on negative value.
|
||||
* For notifications, this will always be 0.
|
||||
*/
|
||||
typedef void (*bt_aics_discover_cb)(struct bt_conn *conn, struct bt_aics *inst,
|
||||
int err);
|
||||
|
||||
struct bt_aics_cb {
|
||||
bt_aics_state_cb state;
|
||||
bt_aics_gain_setting_cb gain_setting;
|
||||
bt_aics_type_cb type;
|
||||
bt_aics_status_cb status;
|
||||
bt_aics_description_cb description;
|
||||
|
||||
#if defined(CONFIG_BT_AICS_CLIENT)
|
||||
bt_aics_discover_cb discover;
|
||||
bt_aics_write_cb set_gain;
|
||||
bt_aics_write_cb unmute;
|
||||
bt_aics_write_cb mute;
|
||||
bt_aics_write_cb set_manual_mode;
|
||||
bt_aics_write_cb set_auto_mode;
|
||||
#endif /* CONFIG_BT_AICS_CLIENT */
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @brief Discover a Audio Input Control Service.
|
||||
*
|
||||
* Attempts to discover a Audio Input Control Service on a server given the
|
||||
* @p param.
|
||||
*
|
||||
* @param conn Connection to the peer with the Audio Input Control Service.
|
||||
* @param inst The instance pointer.
|
||||
* @param param Pointer to the parameters.
|
||||
*
|
||||
* @return 0 on success, ERRNO on fail.
|
||||
*/
|
||||
int bt_aics_discover(struct bt_conn *conn, struct bt_aics *inst,
|
||||
const struct bt_aics_discover_param *param);
|
||||
|
||||
/**
|
||||
* @brief Deactivates a Audio Input Control Service instance.
|
||||
*
|
||||
* Audio Input Control Services are activated by default, but this will allow
|
||||
* the server to deactivate an Audio Input Control Service.
|
||||
*
|
||||
* @param inst The instance pointer.
|
||||
*
|
||||
* @return 0 if success, ERRNO on failure.
|
||||
*/
|
||||
int bt_aics_deactivate(struct bt_aics *inst);
|
||||
|
||||
/**
|
||||
* @brief Activates a Audio Input Control Service instance.
|
||||
*
|
||||
* Audio Input Control Services are activated by default, but this will allow
|
||||
* the server reactivate a Audio Input Control Service instance after it has
|
||||
* been deactivated with @ref bt_aics_deactivate.
|
||||
*
|
||||
* @param inst The instance pointer.
|
||||
*
|
||||
* @return 0 if success, ERRNO on failure.
|
||||
*/
|
||||
int bt_aics_activate(struct bt_aics *inst);
|
||||
|
||||
/**
|
||||
* @brief Read the Audio Input Control Service input state.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL to read local server value.
|
||||
* @param inst The instance pointer.
|
||||
*
|
||||
* @return 0 on success, GATT error value on fail.
|
||||
*/
|
||||
int bt_aics_state_get(struct bt_conn *conn, struct bt_aics *inst);
|
||||
|
||||
/**
|
||||
* @brief Read the Audio Input Control Service gain settings.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL to read local server value.
|
||||
* @param inst The instance pointer.
|
||||
*
|
||||
* @return 0 on success, GATT error value on fail.
|
||||
*/
|
||||
int bt_aics_gain_setting_get(struct bt_conn *conn, struct bt_aics *inst);
|
||||
|
||||
/**
|
||||
* @brief Read the Audio Input Control Service input type.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL to read local server value.
|
||||
* @param inst The instance pointer.
|
||||
*
|
||||
* @return 0 on success, GATT error value on fail.
|
||||
*/
|
||||
int bt_aics_type_get(struct bt_conn *conn, struct bt_aics *inst);
|
||||
|
||||
/**
|
||||
* @brief Read the Audio Input Control Service input status.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL to read local server value.
|
||||
* @param inst The instance pointer.
|
||||
*
|
||||
* @return 0 on success, GATT error value on fail.
|
||||
*/
|
||||
int bt_aics_status_get(struct bt_conn *conn, struct bt_aics *inst);
|
||||
|
||||
/**
|
||||
* @brief Unmute the Audio Input Control Service input.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL to read local server value.
|
||||
* @param inst The instance pointer.
|
||||
*
|
||||
* @return 0 on success, GATT error value on fail.
|
||||
*/
|
||||
int bt_aics_unmute(struct bt_conn *conn, struct bt_aics *inst);
|
||||
|
||||
/**
|
||||
* @brief Mute the Audio Input Control Service input.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL to read local server value.
|
||||
* @param inst The instance pointer.
|
||||
*
|
||||
* @return 0 on success, GATT error value on fail.
|
||||
*/
|
||||
int bt_aics_mute(struct bt_conn *conn, struct bt_aics *inst);
|
||||
|
||||
/**
|
||||
* @brief Set input gain to manual.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL to set local server value.
|
||||
* @param inst The instance pointer.
|
||||
*
|
||||
* @return 0 on success, GATT error value on fail.
|
||||
*/
|
||||
int bt_aics_manual_gain_set(struct bt_conn *conn, struct bt_aics *inst);
|
||||
|
||||
/**
|
||||
* @brief Set the input gain to automatic.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL to set local server value.
|
||||
* @param inst The instance pointer.
|
||||
*
|
||||
* @return 0 on success, GATT error value on fail.
|
||||
*/
|
||||
int bt_aics_automatic_gain_set(struct bt_conn *conn, struct bt_aics *inst);
|
||||
|
||||
/**
|
||||
* @brief Set the input gain.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL to set local server value.
|
||||
* @param inst The instance pointer.
|
||||
* @param gain The gain to set (-128 to 127) in gain setting units
|
||||
* (see @ref bt_aics_gain_setting_cb).
|
||||
*
|
||||
* @return 0 on success, GATT error value on fail.
|
||||
*/
|
||||
int bt_aics_gain_set(struct bt_conn *conn, struct bt_aics *inst, int8_t gain);
|
||||
|
||||
/**
|
||||
* @brief Read the Audio Input Control Service description.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL to read local server value.
|
||||
* @param inst The instance pointer.
|
||||
*
|
||||
* @return 0 on success, GATT error value on fail.
|
||||
*/
|
||||
int bt_aics_description_get(struct bt_conn *conn, struct bt_aics *inst);
|
||||
|
||||
/**
|
||||
* @brief Set the Audio Input Control Service description.
|
||||
*
|
||||
* @param conn Connection to peer device, or NULL to set local server value.
|
||||
* @param inst The instance pointer.
|
||||
* @param description The description as an UTF-8 encoded string.
|
||||
*
|
||||
* @return 0 on success, GATT error value on fail.
|
||||
*/
|
||||
int bt_aics_description_set(struct bt_conn *conn, struct bt_aics *inst,
|
||||
const char *description);
|
||||
|
||||
/**
|
||||
* @brief Get a new Audio Input Control Service client instance.
|
||||
*
|
||||
* @return Pointer to the instance, or NULL if no free instances are left.
|
||||
*/
|
||||
struct bt_aics *bt_aics_client_free_instance_get(void);
|
||||
|
||||
/**
|
||||
* @brief Registers the callbacks for the Audio Input Control Service client.
|
||||
*
|
||||
* @param inst The instance pointer.
|
||||
* @param cb Pointer to the callback structure.
|
||||
*/
|
||||
void bt_aics_client_cb_register(struct bt_aics *inst, struct bt_aics_cb *cb);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
#endif /* ZEPHYR_INCLUDE_BLUETOOTH_SERVICES_AICS_H_ */
|
|
@ -410,6 +410,15 @@ struct bt_uuid_128 {
|
|||
*/
|
||||
#define BT_UUID_MESH_PROXY \
|
||||
BT_UUID_DECLARE_16(BT_UUID_MESH_PROXY_VAL)
|
||||
/** @def BT_UUID_AICS_VAL
|
||||
* @brief Audio Input Control Service value
|
||||
*/
|
||||
#define BT_UUID_AICS_VAL 0x1843
|
||||
/** @def BT_UUID_AICS
|
||||
* @brief Audio Input Control Service
|
||||
*/
|
||||
#define BT_UUID_AICS \
|
||||
BT_UUID_DECLARE_16(BT_UUID_AICS_VAL)
|
||||
/** @def BT_UUID_VOCS_VAL
|
||||
* @brief Volume Offset Control Service value
|
||||
*/
|
||||
|
@ -1313,6 +1322,60 @@ struct bt_uuid_128 {
|
|||
#define BT_UUID_GATT_SERVER_FEATURES \
|
||||
BT_UUID_DECLARE_16(BT_UUID_GATT_SERVER_FEATURES_VAL)
|
||||
|
||||
/** @def BT_UUID_AICS_STATE_VAL
|
||||
* @brief Audio Input Control Service State value
|
||||
*/
|
||||
#define BT_UUID_AICS_STATE_VAL 0x2B77
|
||||
/** @def BT_UUID_AICS_STATE
|
||||
* @brief Audio Input Control Service State
|
||||
*/
|
||||
#define BT_UUID_AICS_STATE \
|
||||
BT_UUID_DECLARE_16(BT_UUID_AICS_STATE_VAL)
|
||||
/** @def BT_UUID_AICS_GAIN_SETTINGS_VAL
|
||||
* @brief Audio Input Control Service Gain Settings Properties value
|
||||
*/
|
||||
#define BT_UUID_AICS_GAIN_SETTINGS_VAL 0x2B78
|
||||
/** @def BT_UUID_AICS_GAIN_SETTINGS
|
||||
* @brief Audio Input Control Service Gain Settings Properties
|
||||
*/
|
||||
#define BT_UUID_AICS_GAIN_SETTINGS \
|
||||
BT_UUID_DECLARE_16(BT_UUID_AICS_GAIN_SETTINGS_VAL)
|
||||
/** @def BT_UUID_AICS_INPUT_TYPE_VAL
|
||||
* @brief Audio Input Control Service Input Type value
|
||||
*/
|
||||
#define BT_UUID_AICS_INPUT_TYPE_VAL 0x2B79
|
||||
/** @def BT_UUID_AICS_INPUT_TYPE
|
||||
* @brief Audio Input Control Service Input Type
|
||||
*/
|
||||
#define BT_UUID_AICS_INPUT_TYPE \
|
||||
BT_UUID_DECLARE_16(BT_UUID_AICS_INPUT_TYPE_VAL)
|
||||
/** @def BT_UUID_AICS_INPUT_STATUS_VAL
|
||||
* @brief Audio Input Control Service Input Status value
|
||||
*/
|
||||
#define BT_UUID_AICS_INPUT_STATUS_VAL 0x2B7A
|
||||
/** @def BT_UUID_AICS_INPUT_STATUS
|
||||
* @brief Audio Input Control Service Input Status
|
||||
*/
|
||||
#define BT_UUID_AICS_INPUT_STATUS \
|
||||
BT_UUID_DECLARE_16(BT_UUID_AICS_INPUT_STATUS_VAL)
|
||||
/** @def BT_UUID_AICS_CONTROL_VAL
|
||||
* @brief Audio Input Control Service Control Point value
|
||||
*/
|
||||
#define BT_UUID_AICS_CONTROL_VAL 0x2B7B
|
||||
/** @def BT_UUID_AICS_CONTROL
|
||||
* @brief Audio Input Control Service Control Point
|
||||
*/
|
||||
#define BT_UUID_AICS_CONTROL \
|
||||
BT_UUID_DECLARE_16(BT_UUID_AICS_CONTROL_VAL)
|
||||
/** @def BT_UUID_AICS_DESCRIPTION_VAL
|
||||
* @brief Audio Input Control Service Input Description value
|
||||
*/
|
||||
#define BT_UUID_AICS_DESCRIPTION_VAL 0x2B7C
|
||||
/** @def BT_UUID_AICS_DESCRIPTION
|
||||
* @brief Audio Input Control Service Input Description
|
||||
*/
|
||||
#define BT_UUID_AICS_DESCRIPTION \
|
||||
BT_UUID_DECLARE_16(BT_UUID_AICS_DESCRIPTION_VAL)
|
||||
/** @def BT_UUID_VOCS_STATE_VAL
|
||||
* @brief Volume Offset State value
|
||||
*/
|
||||
|
@ -1349,7 +1412,6 @@ struct bt_uuid_128 {
|
|||
*/
|
||||
#define BT_UUID_VOCS_DESCRIPTION \
|
||||
BT_UUID_DECLARE_16(BT_UUID_VOCS_DESCRIPTION_VAL)
|
||||
|
||||
/*
|
||||
* Protocol UUIDs
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue