What is MICP and Why Should You Care?
Picture this: you are on a video call, wearing wireless earbuds. The meeting app on your phone taps the mute button. Instantly, your headset microphone goes silent. No audio packets. No ring tone leak. Just silence — on command.
That tap-to-mute magic across a BLE link is exactly what the Microphone Control Profile (MICP) standardises. Published by the Bluetooth SIG’s Generic Audio Working Group in February 2021, MICP defines exactly how a controller device (phone, laptop, tablet) discovers and manipulates the mute state — and optionally the gain — of a microphone device (headset, earbuds, conference speaker, hearing aid).
Before MICP existed, vendors built proprietary solutions. A Sony headset and an Apple iPhone used different private commands. MICP fixes that with an open, standardised GATT-based protocol that any compliant device can speak.
This tutorial walks through the entire spec in plain English, with architecture diagrams, packet-level breakdowns, security rules, and real BlueZ code so you can implement both sides yourself.
Topics Covered
MICP is a GATT profile. That means it lives entirely on top of the Generic Attribute Protocol (GATT), which itself runs over ATT (Attribute Protocol) over BLE L2CAP. You do not need BR/EDR Classic Bluetooth to use MICP, though the spec does describe BR/EDR security requirements for dual-mode devices.
The profile depends on exactly one thing: GATT. Everything else — pairing, bonding, advertising — follows standard BLE GAP procedures.
Protocol stack showing where MICP sits:
| MICP — Microphone Control Profile |
| MICS (Microphone Control Service) | AICS (Audio Input Control Service) |
| GATT — Generic Attribute Profile |
| ATT — Attribute Protocol |
| L2CAP → LL (Link Layer) → PHY |
MICP itself has zero novel wire formats. It does not invent new PDUs. It simply defines which GATT services to instantiate, which characteristics to read/write, and under what conditions. This makes it relatively easy to implement once you understand GATT.
MICP defines exactly two roles. Every device in a MICP session plays one of them.
| Role | GATT Role | Real-World Example | What it Hosts |
|---|---|---|---|
| Microphone Device | GATT Server | Wireless headset, earbuds, conference mic, hearing aid | MICS service (mandatory) + AICS instances (optional) |
| Microphone Controller | GATT Client | Smartphone, laptop, tablet, video conferencing dongle | Nothing — it reads/writes the server’s attributes |
The key insight is simple: the headset exposes controls, and the phone operates those controls. The headset is like a panel of switches bolted to the wall. The phone is the hand that flips those switches.
Role relationship and data flow:
|
📱 Microphone Controller
GATT Client
Reads Mute State
Writes Mute / Gain
Subscribes to Notifications
|
Read Mute
Set Mute / Gain
Notifications
|
🎤 Microphone Device
GATT Server
MICS (Primary)
Mute Characteristic
AICS (Secondary, optional)
Audio Input State
Gain Setting Properties
Audio Input Control Point
|
Important rule: AICS is always a secondary service included inside MICS, not a standalone primary service. A device can have zero, one, or several AICS instances — one per physical microphone input is a common design. The phone discovers AICS via GATT’s “Find Included Services” sub-procedure.
3.1 — Microphone Control Service (MICS)
MICS is the top-level service. It has exactly one mandatory characteristic: the Mute characteristic.
| Characteristic | Requirement | Possible Values | Operations |
|---|---|---|---|
| Mute | M | 0x00 = Not Muted 0x01 = Muted 0x02 = Disabled |
Read, Write (Mute/Unmute), Notify |
The “Disabled” value (0x02) is interesting — it means the device does not allow muting at this moment (for example, an active emergency call). A Microphone Controller that tries to write 0x01 when the current value is 0x02 will receive an ATT error.
The Mute characteristic controls the entire device with a single bit. Want per-microphone control? That is what AICS is for.
3.2 — Audio Input Control Service (AICS)
AICS adds fine-grained per-input control. Each instance represents one audio input path inside the device. A dual-microphone headset might have two AICS instances — one for the boom mic and one for the ambient noise mic.
| Characteristic | Requirement (Controller) | What It Tells You |
|---|---|---|
| Audio Input State | C.1 | Current gain setting, mute state, gain mode, and a Change_Counter |
| Gain Setting Properties | C.1 | Unit step size, min gain, max gain (defines valid range for Set Gain) |
| Audio Input Type | O | Type of physical input (microphone, line-in, bluetooth, etc.) |
| Audio Input Status | O | Active or Inactive — is this input currently producing audio? |
| Audio Input Control Point | O | Write-only command register: Set Gain, Mute, Unmute, Set Gain Mode |
| Audio Input Description | O | Human-readable UTF-8 string label (e.g., “Boom Mic”, “Ambience”) |
C.1 = Mandatory if you implement the Audio Input Control Point. You cannot issue control commands without first reading the current state (specifically the Change_Counter). More on this in Chapter 5.
A dual-microphone headset (two mics mixed to one output) is the canonical example from the spec. Here is how the GATT attribute tree looks:
|
|
|
|
|
|||
|
separate input path
|
|||||||
|
|
|
|||||
The MICS service is the single primary service a controller discovers first. It acts as a container. The two AICS instances are referenced as included services inside MICS. Discovery of AICS always goes through MICS — you find MICS first, then call “Find Included Services” to get the AICS handles.
There are no concurrency restrictions. Multiple controllers can connect and interact with the same device simultaneously — the spec imposes no limits there.
When you implement the Microphone Controller side, you need certain GATT sub-procedures available. The spec breaks them into mandatory and conditional. Here is a clear breakdown:
| GATT Sub-Procedure | Requirement | When Needed |
|---|---|---|
| Discover All Primary Services | C.1 | At least one of C.1 group is mandatory |
| Discover Primary Services by UUID | C.1 | Faster — search for MICS UUID directly |
| Find Included Services | C.2 | Mandatory if you support AICS procedures |
| Discover All Characteristics of a Service | C.3 | At least one of C.3 group is mandatory |
| Discover Characteristics by UUID | C.3 | More targeted, less ATT traffic |
| Discover All Characteristic Descriptors | M | Always — to find the CCCD for notifications |
| Notifications (subscribe via CCCD) | M | Always — for Mute state change alerts |
| Read Characteristic Value | M | Always — to poll Mute and Audio Input State |
| Write Characteristic Value | M | Always — to mute, unmute, set gain |
| Read Characteristic Descriptors | M | Always |
| Write Characteristic Descriptors | M | Always — to write CCCD and enable notifications |
The CCCD (Client Characteristic Configuration Descriptor) is the BLE mechanism for turning on notifications. When a controller writes 0x0001 to the CCCD of the Mute characteristic, the device will automatically send ATT Notification packets whenever the mute state changes. This replaces polling.
There are three MICS sub-procedures. Only “Read Mute” is mandatory. The other two are optional but nearly always implemented in practice.
6.1 — Configure Mute Notifications (Optional but Recommended)
The controller writes 0x0001 to the CCCD of the Mute characteristic. After this, any time the device’s mute state changes (user presses the mute button on the headset, for instance), the device sends an unsolicited ATT Notification to the controller with the new value. This is far more efficient than polling.
Notification subscription sequence:
| Controller | Headset (Device) | |
| Write CCCD = 0x0001 |
ATT Write Request
|
|
|
ATT Write Response
|
Success (0x00) | |
| [User presses mute button on headset] | ||
|
ATT Notification
|
Mute = 0x01 (Muted) |
6.2 — Read Mute (Mandatory)
The controller issues a standard ATT Read Request on the Mute characteristic handle. The device responds with the current value: 0x00 (Not Muted), 0x01 (Muted), or 0x02 (Disabled). This is always mandatory — the controller must be able to read the initial state on connection.
6.3 — Set Mute (Optional)
To mute the entire device, the controller writes 0x01 to the Mute characteristic. To unmute, it writes 0x00. If the current value is 0x02 (Disabled), the write will be rejected with an application error.
This is a device-wide mute. It affects all microphone inputs simultaneously. For per-input control, use the AICS Audio Input Control Point described next.
AICS is more complex than MICS because it supports gain (volume) control in addition to muting. The most important design decision in AICS is the Change_Counter synchronisation mechanism. Understanding this is key to getting AICS right.
7.1 — The Change_Counter: Why It Exists
Imagine two phones connected to the same headset simultaneously. Phone A reads the gain as 5. Phone B also reads it as 5. Then Phone A sets the gain to 8. Meanwhile, Phone B — still thinking the gain is 5 — also tries to set it to 7. If Phone B’s command arrived after Phone A’s, it would silently overwrite a gain change that Phone B does not even know happened. The result is unpredictable.
The Change_Counter prevents this. Every time any field in Audio Input State changes, the device increments a one-byte counter. A controller must read this counter and include it in every control command. If the device’s counter has moved on since you read it, the command is rejected with error 0x80 (Invalid Change Counter). You must re-read the state and try again with the fresh counter value.
Change_Counter synchronisation flow (Set Gain example):
| Controller | Device (AICS) | |
| Read Audio Input State |
|
|
|
|
State = {gain:5, muted:0, mode:Manual, counter:3} | |
| Write Control Point: Opcode=SetGain, Counter=3, Gain=8 |
|
|
| If device counter still = 3: | ||
|
|
✓ Write Response: Success (counter now = 4) | |
| If device counter already moved to 4 (another client changed something): | ||
|
|
✗ ATT Error: 0x80 Invalid Change Counter — retry | |
7.2 — Audio Input Control Point Opcodes
Every write to the Audio Input Control Point follows a fixed format: Opcode + Change_Counter + (optional extra parameter). The table below shows each command:
| Opcode | Byte 1 | Byte 2 | Byte 3 | Effect |
|---|---|---|---|---|
| Set Gain Setting | 0x01 | Change_Counter | Gain_Setting | Sets gain to specified level (Manual mode only) |
| Unmute | 0x02 | Change_Counter | — | Sets this input’s Mute field to Not Muted |
| Mute | 0x03 | Change_Counter | — | Sets this input’s Mute field to Muted |
| Set Manual Gain Mode | 0x04 | Change_Counter | — | Switches gain control to Manual (not if Automatic Only or Manual Only) |
| Set Automatic Gain Mode | 0x05 | Change_Counter | — | Switches gain to Automatic AGC (not if Automatic Only or Manual Only) |
Gain Mode rules: Some microphones have hardware-fixed gain modes. If the Audio Input State reports Automatic Only or Manual Only, the controller must not attempt to change the gain mode — that opcode will be rejected. Set Gain Setting is also blocked when the gain mode is Automatic (the hardware is controlling gain itself).
7.3 — Gain Setting Properties
Before calling Set Gain Setting, the controller should read Gain Setting Properties. It returns three values: the unit step size (how much one unit represents in dB), the minimum gain setting value, and the maximum gain setting value. Your new Gain_Setting parameter must fall within [min, max]. Submitting a value outside this range will result in an ATT error.
BlueZ (the Linux Bluetooth stack) exposes a D-Bus GATT API that lets you register GATT services, characteristics, and descriptors from userspace. Below are practical code patterns for both the Device side (GATT server) and Controller side (GATT client).
8.1 — MICS GATT Server: Exposing the Mute Characteristic
This pattern uses the BlueZ GATT D-Bus API. The Mute characteristic needs Read, Write, and Notify properties, plus a CCCD (Client Characteristic Configuration Descriptor) for notifications.
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* MICP – Microphone Control Profile
* MICS GATT Server skeleton (BlueZ style)
* Based on BlueZ src/shared/gatt-db.c patterns
*/
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include "src/shared/gatt-db.h"
#include "src/shared/att.h"
/* Bluetooth SIG assigned UUIDs */
#define MICS_UUID 0x184D /* Microphone Control Service */
#define MUTE_CHAR_UUID 0x2BC3 /* Mute Characteristic */
#define CCCD_UUID 0x2902 /* Client Characteristic Configuration */
/* Mute state values as defined in MICS spec */
#define MUTE_NOT_MUTED 0x00
#define MUTE_MUTED 0x01
#define MUTE_DISABLED 0x02
struct mics_server {
struct gatt_db *db;
struct gatt_db_attribute *mics_service;
struct gatt_db_attribute *mute_characteristic;
struct gatt_db_attribute *mute_cccd;
uint8_t mute_value; /* current mute state */
uint16_t cccd_value; /* 0x0001 = notifications enabled */
};
/*
* Called when the Microphone Controller reads the Mute characteristic.
* We respond immediately with the current mute_value.
*/
static void mute_read_cb(struct gatt_db_attribute *attrib,
unsigned int id,
uint16_t offset,
uint8_t opcode,
struct bt_att *att,
void *user_data)
{
struct mics_server *server = user_data;
uint8_t val = server->mute_value;
gatt_db_attribute_read_result(attrib, id, 0, &val, sizeof(val));
}
/*
* Called when the Microphone Controller writes to Mute.
* Only 0x00 (unmute) and 0x01 (mute) are valid writes.
* 0x02 (Disabled) cannot be written by the client — it is set by the server.
* If mute_value == MUTE_DISABLED, reject the write.
*/
static void mute_write_cb(struct gatt_db_attribute *attrib,
unsigned int id,
uint16_t offset,
const uint8_t *value,
size_t len,
uint8_t opcode,
struct bt_att *att,
void *user_data)
{
struct mics_server *server = user_data;
if (len != 1 || (value[0] != MUTE_NOT_MUTED && value[0] != MUTE_MUTED)) {
/* BT_ATT_ERROR_APPLICATION: invalid value */
gatt_db_attribute_write_result(attrib, id, 0x80);
return;
}
if (server->mute_value == MUTE_DISABLED) {
/* Muting is currently disabled – reject */
gatt_db_attribute_write_result(attrib, id, 0x80);
return;
}
server->mute_value = value[0];
/* Notify all subscribed controllers */
if (server->cccd_value & 0x0001) {
gatt_db_attribute_notify(server->mute_characteristic,
&server->mute_value, 1, NULL);
}
gatt_db_attribute_write_result(attrib, id, 0);
}
/*
* Build and register the MICS primary service in the GATT database.
*/
void mics_server_init(struct mics_server *server, struct gatt_db *db)
{
bt_uuid_t uuid;
server->db = db;
server->mute_value = MUTE_NOT_MUTED;
server->cccd_value = 0x0000;
/* Register MICS as a primary service */
bt_uuid16_create(&uuid, MICS_UUID);
server->mics_service = gatt_db_add_service(db, &uuid, true, 8);
/* Add Mute characteristic: Read + Write + Notify */
bt_uuid16_create(&uuid, MUTE_CHAR_UUID);
server->mute_characteristic = gatt_db_service_add_characteristic(
server->mics_service,
&uuid,
BT_ATT_PERM_READ | BT_ATT_PERM_WRITE,
GATT_CHR_PROP_READ | GATT_CHR_PROP_WRITE | GATT_CHR_PROP_NOTIFY,
mute_read_cb,
mute_write_cb,
server
);
/* Add CCCD descriptor so controllers can subscribe to notifications */
bt_uuid16_create(&uuid, CCCD_UUID);
server->mute_cccd = gatt_db_service_add_descriptor(
server->mics_service,
&uuid,
BT_ATT_PERM_READ | BT_ATT_PERM_WRITE,
NULL, NULL, NULL
);
gatt_db_service_set_active(server->mics_service, true);
}
8.2 — AICS GATT Server: Exposing One Audio Input Instance
AICS is a secondary service. It is included inside the MICS service via an GATT Include Definition. Below shows how to add a single AICS instance and the Audio Input State + Control Point characteristics.
/* Audio Input State layout (5 bytes): */
/* [0] Gain_Setting [1] Mute [2] Gain_Mode [3] Change_Counter */
/* Gain Mode values */
#define GAIN_MODE_MANUAL_ONLY 0x00
#define GAIN_MODE_AUTOMATIC_ONLY 0x01
#define GAIN_MODE_MANUAL 0x02
#define GAIN_MODE_AUTOMATIC 0x03
#define AICS_UUID 0x1843
#define AUDIO_INPUT_STATE_UUID 0x2B77
#define GAIN_SETTING_PROP_UUID 0x2B78
#define AUDIO_INPUT_TYPE_UUID 0x2B79
#define AUDIO_INPUT_STATUS_UUID 0x2B7A
#define AUDIO_INPUT_CTRL_UUID 0x2B7B /* Audio Input Control Point */
#define AUDIO_INPUT_DESC_UUID 0x2B7C
/* AICS Control Point opcodes */
#define OPCODE_SET_GAIN 0x01
#define OPCODE_UNMUTE 0x02
#define OPCODE_MUTE 0x03
#define OPCODE_SET_MANUAL_MODE 0x04
#define OPCODE_SET_AUTO_MODE 0x05
/* Application error: counter mismatch */
#define ERR_INVALID_CHANGE_COUNTER 0x80
struct aics_instance {
struct gatt_db_attribute *service;
uint8_t gain_setting; /* current gain 0..max */
uint8_t mute; /* 0=not muted, 1=muted */
uint8_t gain_mode; /* see GAIN_MODE_* */
uint8_t change_counter; /* increments on any state change */
uint8_t gain_unit; /* 1 unit = gain_unit * 0.1 dB */
uint8_t gain_minimum;
uint8_t gain_maximum;
};
/*
* Control Point write handler.
* Validates Change_Counter then applies the requested operation.
*/
static void aics_ctrl_write_cb(struct gatt_db_attribute *attrib,
unsigned int id,
uint16_t offset,
const uint8_t *value,
size_t len,
uint8_t opcode_att,
struct bt_att *att,
void *user_data)
{
struct aics_instance *aics = user_data;
if (len < 2) {
gatt_db_attribute_write_result(attrib, id, BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN);
return;
}
uint8_t opcode = value[0];
uint8_t counter = value[1];
/* Change_Counter check — reject stale commands */
if (counter != aics->change_counter) {
gatt_db_attribute_write_result(attrib, id, ERR_INVALID_CHANGE_COUNTER);
return;
}
switch (opcode) {
case OPCODE_SET_GAIN:
if (len < 3) {
gatt_db_attribute_write_result(attrib, id,
BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN);
return;
}
if (aics->gain_mode == GAIN_MODE_AUTOMATIC ||
aics->gain_mode == GAIN_MODE_AUTOMATIC_ONLY) {
/* Cannot set gain in automatic mode */
gatt_db_attribute_write_result(attrib, id, 0x81);
return;
}
{
uint8_t new_gain = value[2];
if (new_gain < aics->gain_minimum || new_gain > aics->gain_maximum) {
gatt_db_attribute_write_result(attrib, id, 0x82); /* out of range */
return;
}
aics->gain_setting = new_gain;
}
break;
case OPCODE_UNMUTE:
aics->mute = 0x00;
break;
case OPCODE_MUTE:
aics->mute = 0x01;
break;
case OPCODE_SET_MANUAL_MODE:
if (aics->gain_mode == GAIN_MODE_MANUAL_ONLY ||
aics->gain_mode == GAIN_MODE_AUTOMATIC_ONLY) {
gatt_db_attribute_write_result(attrib, id, 0x83); /* not permitted */
return;
}
aics->gain_mode = GAIN_MODE_MANUAL;
break;
case OPCODE_SET_AUTO_MODE:
if (aics->gain_mode == GAIN_MODE_MANUAL_ONLY ||
aics->gain_mode == GAIN_MODE_AUTOMATIC_ONLY) {
gatt_db_attribute_write_result(attrib, id, 0x83);
return;
}
aics->gain_mode = GAIN_MODE_AUTOMATIC;
break;
default:
gatt_db_attribute_write_result(attrib, id, 0x80);
return;
}
/* Increment Change_Counter on every successful state mutation */
aics->change_counter++;
gatt_db_attribute_write_result(attrib, id, 0);
/* TODO: send ATT Notification on Audio Input State characteristic */
}
8.3 — Controller Side: Service and Characteristic Discovery
On the controller side, you connect, discover MICS, then walk through GATT discovery to find included AICS instances. The pattern below uses BlueZ’s bt_gatt_client API.
/*
* Microphone Controller – service discovery with bt_gatt_client
* Based on BlueZ tools/btgatt-client.c patterns
*/
#include "src/shared/gatt-client.h"
#define MICS_UUID_STR "0000184d-0000-1000-8000-00805f9b34fb"
#define MUTE_CHAR_STR "00002bc3-0000-1000-8000-00805f9b34fb"
struct micp_controller {
struct bt_gatt_client *client;
uint16_t mute_value_handle;
uint16_t mute_cccd_handle;
uint16_t aics_ctrl_handle; /* Audio Input Control Point handle */
};
/*
* Callback fired when GATT service discovery completes.
* Walk all discovered services looking for MICS.
*/
static void service_discovery_cb(bool success,
uint8_t att_ecode,
void *user_data)
{
struct micp_controller *ctrl = user_data;
if (!success) {
fprintf(stderr, "GATT discovery failed: 0x%02x\n", att_ecode);
return;
}
printf("GATT discovery complete – searching for MICS\n");
/*
* Iterate the GATT database snapshot and find MICS + its
* Mute characteristic. In practice you would use
* bt_gatt_client_get_services() iterator here.
*
* Pseudo-code for clarity:
*
* for each service:
* if service.uuid == MICS_UUID:
* for each characteristic in service:
* if char.uuid == MUTE_CHAR_UUID:
* ctrl->mute_value_handle = char.value_handle
* ctrl->mute_cccd_handle = char.ccc_handle
* // now find included AICS services
* for each included_service in service:
* if included_service.uuid == AICS_UUID:
* discover_aics_characteristics(included_service)
*/
}
/*
* Subscribe to Mute notifications by writing 0x0001 to the CCCD.
*/
static void subscribe_mute_notifications(struct micp_controller *ctrl)
{
uint8_t cccd_val[2] = { 0x01, 0x00 }; /* Little-endian 0x0001 */
bt_gatt_client_write_value(
ctrl->client,
ctrl->mute_cccd_handle,
cccd_val,
sizeof(cccd_val),
NULL, /* write_callback – optional */
NULL,
NULL
);
printf("Subscribed to Mute notifications (CCCD handle 0x%04x)\n",
ctrl->mute_cccd_handle);
}
/*
* Mute the entire microphone device.
*/
static void set_device_mute(struct micp_controller *ctrl, bool muted)
{
uint8_t val = muted ? 0x01 : 0x00;
bt_gatt_client_write_value(
ctrl->client,
ctrl->mute_value_handle,
&val,
sizeof(val),
NULL, NULL, NULL
);
printf("Wrote Mute = 0x%02x to handle 0x%04x\n",
val, ctrl->mute_value_handle);
}
/*
* Set gain on a specific AICS input.
* Must supply the current Change_Counter value from Audio Input State.
*/
static void set_input_gain(struct micp_controller *ctrl,
uint8_t change_counter,
uint8_t new_gain)
{
uint8_t payload[3];
payload[0] = 0x01; /* OPCODE_SET_GAIN */
payload[1] = change_counter; /* MUST match server's current counter */
payload[2] = new_gain;
bt_gatt_client_write_value(
ctrl->client,
ctrl->aics_ctrl_handle,
payload,
sizeof(payload),
NULL, NULL, NULL
);
printf("Set gain: counter=%u new_gain=%u\n", change_counter, new_gain);
}
MICP takes security seriously. You cannot implement MICP with an unencrypted link. Here are the rules:
| Requirement | Controller | Device |
|---|---|---|
| SM1 Level 1 (no security) | ❌ Excluded | ❌ Excluded |
| SM1 Level 2 (encrypted, unauthenticated) | Optional | C.2 – Min Required |
| SM1 Level 3 (encrypted, authenticated MITM) | Optional | C.2 |
| SM1 Level 4 (LE Secure Connections, MITM) | Optional | C.2 |
| 128-bit Key Entropy | C.1 (if L2 or L3) | C.1 (if L2 or L3) |
| Bondable mode support | ✅ Mandatory | ✅ Mandatory |
In practical terms: both sides must bond, and the link must be encrypted with at least a 128-bit key. The spec explicitly says Security Mode 1 Level 1 (plain unencrypted BLE) is excluded — you cannot connect and just start muting things without a pairing handshake.
The recommended key derivation path is LE Secure Connections. If the device is dual-mode (BR/EDR + LE), cross-transport key derivation from BR/EDR Secure Connections is also acceptable. Out-of-band (NFC tap, QR code scan) pairing is also valid.
Link Layer Privacy (resolvable private addresses) is recommended — this prevents passive tracking of the headset by scanning observers.
BlueZ: Requiring Encryption Before Characteristic Access
When registering GATT characteristics in BlueZ, pass the appropriate ATT permission flags to enforce encryption at the ATT layer:
/*
* When adding the Mute characteristic, combine ENCRYPT_READ/WRITE
* permissions to require an encrypted link before access is granted.
* BlueZ will return ATT_ECODE_AUTHENTICATION if the link is unencrypted.
*/
server->mute_characteristic = gatt_db_service_add_characteristic(
server->mics_service,
&uuid,
/* Permissions: require encrypted read + write */
BT_ATT_PERM_READ | BT_ATT_PERM_READ_ENCRYPT |
BT_ATT_PERM_WRITE | BT_ATT_PERM_WRITE_ENCRYPT,
GATT_CHR_PROP_READ | GATT_CHR_PROP_WRITE | GATT_CHR_PROP_NOTIFY,
mute_read_cb,
mute_write_cb,
server
);
/* BT_ATT_PERM_READ_ENCRYPT → error 0x0F if link not encrypted */
/* BT_ATT_PERM_WRITE_ENCRYPT → error 0x0F if link not encrypted */
10.1 — Advertising (Microphone Device / Peripheral)
The headset (Peripheral) must use extended advertising PDUs — not legacy advertising — when trying to connect to a new device. The advertising data should include:
| AD Data Type | Required? | Content |
|---|---|---|
| Flags | Should | LE General Discoverable Mode + BR/EDR Not Supported (or both supported) |
| Service UUID | Should | MICS UUID (0x184D) — allows the controller to filter by service UUID |
Including the MICS UUID in advertising data is important for the controller. It lets a phone skip devices that do not support MICS without connecting and running full service discovery.
10.2 — Connection Interval Recommendation
The spec recommends a BLE connection interval of 10 to 30 milliseconds when acting as a Microphone Controller. This balances responsiveness (mute commands feel instant) against power consumption. A 100ms connection interval would make muting noticeably laggy.
/*
* When requesting a connection to the headset, hint the preferred
* connection parameters. The peripheral may reject or negotiate.
*
* Using hcitool or btmgmt:
* btmgmt conn-info <bdaddr>
*
* Or programmatically via BlueZ MGMT API after connection:
*/
struct mgmt_cp_set_conn_params params = {
.addr = peer_bdaddr,
.min_interval = 8, /* 8 * 1.25ms = 10ms */
.max_interval = 24, /* 24 * 1.25ms = 30ms */
.latency = 0,
.timeout = 100 /* 100 * 10ms = 1s supervision timeout */
};
/* Send via MGMT_OP_SET_CONN_PARAMS to kernel */
10.3 — Link Loss Reconnection
When the BLE link drops (moved out of range, interference), both sides should attempt to reconnect. The device re-enters its connectable advertising mode. The controller attempts reconnection using any of the standard GAP connection procedures — Auto Connection Establishment (using an allow list with the bonded device address) is most power-efficient.
Let us trace a complete interaction from the moment you put on a MICP-capable headset to the moment the meeting app mutes your mic. Every numbered step corresponds to a real BLE operation.
| 📱 Phone (Microphone Controller) | 🎧 Headset (Microphone Device) | |
| ① Scanning for BLE devices | Advertising: Flags + MICS UUID (0x184D) | |
|
ADV_EXT_IND
|
||
| ② Sees MICS UUID → Connects + Pairs (SMP) |
CONNECT_IND + SMP
|
Accepts connection, begins LE Secure Connections pairing |
| ③ Discover All Primary Services |
ATT Read By Group Type Req
|
Returns MICS handle range (e.g., 0x0010–0x001F) |
| ④ Find Included Services (AICS) |
ATT Find Information Req
|
Returns AICS instance handles |
| ⑤ Discover Characteristics of MICS + AICS |
|
Returns Mute, Audio Input State, Gain Props, AICP handles |
| ⑥ Read Mute (handle 0x0011) |
|
→ 0x00 (Not Muted) |
| ⑦ Write CCCD = 0x0001 (enable notify) |
|
OK |
| ⑧ Meeting app taps Mute → Write Mute = 0x01 |
ATT Write Request
|
Mutes hardware mic, stores mute_value = 0x01 |
| ✅ Mute icon shown on screen |
ATT Write Response
|
Success |
| Item | Value / Note |
|---|---|
| MICS UUID | 0x184D |
| AICS UUID | 0x1843 |
| Mute Characteristic UUID | 0x2BC3 |
| Audio Input State UUID | 0x2B77 |
| Audio Input Control Point UUID | 0x2B7B |
| Mute values | 0x00 = Not Muted, 0x01 = Muted, 0x02 = Disabled |
| Invalid Change Counter error | ATT App Error 0x80 |
| Minimum security | SM1 Level 2 (encrypted), 128-bit key, bonded |
| Recommended connection interval | 10 – 30 ms |
| Bluetooth Core Spec requirement | Any version that includes GATT (≥ BT 4.0) |
| Advertising PDU type (Device) | Extended Advertising PDUs (not legacy) |
| CoD Major Service Class bit (BR/EDR) | Bit 14 must be set to 1 |
Every write to the Audio Input Control Point must include the Change_Counter you last read from Audio Input State. Sending without it (or sending a stale value) will get error 0x80. The fix: always read Audio Input State immediately before each control write, or track the counter via notifications.
If the Audio Input State reports Gain_Mode = Automatic or Automatic Only, calling Set Gain Setting will be rejected. You must switch to Manual mode first (if the device permits it), then set the gain.
AICS is a secondary service — it will NOT appear in a “Discover All Primary Services” response. You must first find MICS, then call the “Find Included Services” GATT sub-procedure on the MICS handle range to discover AICS instances.
Accessing any MICS or AICS characteristic over an unencrypted link will result in an ATT error (Insufficient Encryption, 0x0F). Always pair and bond before reading or writing. Never skip SMP.
If Mute = 0x02 (Disabled), the device has locked muting. Attempting to write 0x01 will be rejected. Your application should read the mute state first and present the mute button as greyed out if the value is Disabled.
On connection, subscribe to Mute notifications and Audio Input State notifications first, then do a single read to get the initial state. After that, let notifications keep you up to date. This is far more power-efficient than periodic polling.
Keep Building with Embedded Pathashala
You now have everything you need to implement MICP on both the headset (GATT server) and phone (GATT client) sides — roles, services, characteristics, security, and working BlueZ C code patterns.
Next up: dive deeper into the Audio Input Control Service (AICS) specification, or explore how MICP fits alongside the Volume Control Profile (VCP) and Call Control Profile (CCP) in LE Audio headset architectures.
