BLE GATT Service β How Bluetooth Controls Your Mic Mute Button
What is MICS?
Ever pressed the mute button on your wireless headset and wondered β how does your phone know the mic is muted? The answer (in modern BLE audio devices) is the Microphone Control Service (MICS).
MICS is a BLE GATT service defined by the Bluetooth SIG. It gives any connected BLE client the ability to:
- π Read the current microphone mute state
- βοΈ Write a new mute state (mute or unmute)
- π Subscribe to notifications when the state changes
Think of it as a smart, encrypted mute button exposed over Bluetooth β any trusted connected device can see and control it in real time.
π Key Concepts in This Post
The three mute states map perfectly to a hotel “Do Not Disturb” sign:
| State | Value | Analogy | Who Can Change It? |
|---|---|---|---|
| β Not Muted | 0x00 | DND sign is off β guests can knock | Client or Server |
| π Muted | 0x01 | DND sign is on β no entry | Client or Server |
| π Disabled | 0x02 | Door is bolted shut from inside β nobody external can change it | Server ONLY |
The Disabled state represents a physical hardware privacy switch (like a laptop privacy shutter). The device locks the mic mute at hardware level β no BLE command can override it.
π Service Topologies β Three Ways to Use MICS
MICS can work alone or alongside the Audio Input Control Service (AICS). MICS handles global device-wide mute. AICS handles per-microphone gain (volume) and per-input mute. There are three standard configurations:
No AICS included. Just a global mute switch. Best for headsets or speakers that only need ON/OFF mute with no individual gain control.
Microphone In
Microphone
Control Service
(MICS)
Microphone Out
One AICS is included inside MICS. MICS = device-wide mute. AICS = individual input gain (volume) and mute for that one microphone. Together they give you fine-grained control.
Mic In
Audio Input
Control
(AICS)
Microphone
Control
(MICS)
Mic Out
Multiple AICS instances, one per microphone. Each mic gets its own independent gain and mute. MICS still provides the single device-wide master mute. Perfect for TWS earbuds (left + right mic) or conference room speakerphones.
MICS
Global Mute
Audio Out
π§ The Mute Characteristic β The Only Characteristic in MICS
MICS has exactly one characteristic: the Mute characteristic. It’s a single byte that tells the current mute state of the device’s audio.
| Property | Value | What It Means |
|---|---|---|
| Characteristic Name | Mute | The only characteristic in MICS |
| UUID | 0x2BC3 | Assigned by Bluetooth SIG |
| Requirement | Mandatory (M) | Every MICS server must have it |
| Properties | Read, Write, Notify | Can read state, write new state, subscribe for changes |
| Security | π Encryption Required | BLE link must be encrypted (bonded) first |
| Data Size | 1 byte (uint8) | Transmitted little-endian (LSO first) |
| Hex Value | State | Meaning | Client Can Write? |
|---|---|---|---|
| 0x00 | β Not Muted | Microphone is active, audio is flowing normally | β Yes (to mute) |
| 0x01 | π Muted | Microphone is silenced β no audio output | β Yes (to unmute) |
| 0x02 | π Disabled | Mute is hardware-locked (e.g., physical privacy switch). Mic is muted and stays muted. | β No β server only |
| 0x03β0xFF | β RFU | Reserved for future use β not valid today | β No β ATT error returned |
The mute state transitions follow strict rules. The most important: only the device itself (server) can enter or exit the Disabled state.
NOT MUTED
0x00
0x010x00MUTED
0x01
0x02Disabled
DISABLED
0x02
- Client can freely toggle between Not Muted β Muted
- Only the server (device hardware) can set or clear the Disabled state
- If client tries to write anything when state is Disabled β ATT error
0x80 (Mute Disabled) - If client tries to write value 0x02 or any RFU value β ATT error
0x13 (Value Not Allowed)
When a client does something invalid, the server returns an ATT error response. MICS defines one custom application-level error:
| Error Name | Error Code | Trigger Condition |
|---|---|---|
| Mute Disabled | 0x80 | Client writes any value while current state is Disabled |
| Value Not Allowed | 0x13 | Client writes 0x02 (Disabled) or any RFU value (0x03β0xFF) β standard ATT error |
Both errors tell the client: “you can’t do that.” The difference is the reason β one is about the current locked state, the other is about writing an invalid value.
To implement MICS, your BLE server and client code must support these four GATT operations:
| GATT Sub-Procedure | Required? | Purpose in MICS |
|---|---|---|
| Write Characteristic Values | Mandatory | Client sends mute/unmute command |
| Notifications | Mandatory | Server alerts clients when mute state changes |
| Read Characteristic Descriptors | Mandatory | Client reads the CCCD to check notification status |
| Write Characteristic Descriptors | Mandatory | Client writes CCCD to enable/disable notifications |
Once the client writes 0x0001 to the CCCD (Client Characteristic Configuration Descriptor), it subscribes to mute change notifications. Every time the mute state changes β whether by the client, server, or a hardware switch β all subscribed clients get notified.
| π± BLE Client (Phone / Laptop) | Action | π§ BLE Server (Headset / Mic Device) |
|---|---|---|
Writes CCCD = 0x0001 |
βββ | Saves notification preference for this client |
| Reads Mute characteristic | βββ | Returns current value: 0x00 (Not Muted) |
Writes Mute = 0x01 (Mute!) |
βββ | Updates Mute value to 0x01, triggers notifications |
β Receives Notification: Mute = 0x01 |
βββ | Sends notification to all subscribed clients |
Writes Mute = 0x00 when Disabled β β ATT Error 0x80 |
β β | Returns ATT Error Response: Mute Disabled (0x80) |
π» BlueZ Implementation β C Code Examples
Start by defining the UUIDs and mute state constants. In BlueZ, GATT services are registered through D-Bus using the GATT Manager API.
/* ===== mics.h β MICS Constants ===== */
/* MICS Service UUID (Bluetooth SIG Assigned Numbers) */
#define MICS_UUID "0000184d-0000-1000-8000-00805f9b34fb"
/* Mute Characteristic UUID */
#define MUTE_CHAR_UUID "00002bc3-0000-1000-8000-00805f9b34fb"
/* Mute characteristic values */
#define MUTE_NOT_MUTED 0x00 /* Mic active, audio flowing */
#define MUTE_MUTED 0x01 /* Mic silenced by software */
#define MUTE_DISABLED 0x02 /* Mute locked by hardware */
/* ATT Application Error Code β defined by MICS spec */
#define ATT_ERR_MUTE_DISABLED 0x80
/* Standard ATT error (BT Core Spec) */
#define ATT_ERR_VALUE_NOT_ALLOWED 0x13
/* MICS service state structure */
struct mics_service {
uint8_t mute_state; /* Current mute value */
bool notify_enabled; /* Client subscribed? */
};
/* Initialize with default Not Muted state */
static struct mics_service *mics_init(void)
{
struct mics_service *svc = g_new0(struct mics_service, 1);
svc->mute_state = MUTE_NOT_MUTED;
svc->notify_enabled = false;
return svc;
}
After pairing with a BLE device that exposes MICS, use BlueZ’s interactive tool to explore the service. The UUID 0x184D identifies MICS; the Mute characteristic UUID is 0x2BC3.
# Step 1: Power on and scan
bluetoothctl power on
bluetoothctl scan on
# Step 2: Connect to your MICS device
bluetoothctl connect AA:BB:CC:DD:EE:FF
# Step 3: List all GATT attributes (services + characteristics)
bluetoothctl list-attributes AA:BB:CC:DD:EE:FF
# Look for output like:
# Service /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service000c
# 0000184d-0000-1000-8000-00805f9b34fb (MICS)
# Characteristic /org/.../service000c/char000d
# 00002bc3-0000-1000-8000-00805f9b34fb (Mute)
# Flags: read write notify
# Step 4: Select the Mute characteristic
bluetoothctl select-attribute /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service000c/char000d
# Step 5: Read the current mute state
bluetoothctl read-attribute
# Example responses:
# Attribute Value (1): 00 -> Not Muted
# Attribute Value (1): 01 -> Muted
# Attribute Value (1): 02 -> Disabled (hardware lock)
To programmatically mute or unmute over BLE in C, call WriteValue on the Mute characteristic proxy. The link must be encrypted (bonded) or the ATT operation will be rejected.
#include <glib.h>
#include <gio/gio.h>
/*
* mics_write_mute() β send mute or unmute command to MICS server
*
* @char_proxy : D-Bus proxy for the Mute characteristic
* @mute_val : MUTE_NOT_MUTED (0x00) or MUTE_MUTED (0x01)
*
* Returns 0 on success, negative errno on failure.
*/
static int mics_write_mute(GDBusProxy *char_proxy, uint8_t mute_val)
{
GVariant *params;
GVariantBuilder opts_builder;
GError *error = NULL;
/* Validate: client can only write 0x00 or 0x01 */
if (mute_val != MUTE_NOT_MUTED && mute_val != MUTE_MUTED) {
g_printerr("[MICS] Invalid value 0x%02x β only 0x00/0x01 allowed\n",
mute_val);
return -EINVAL;
}
/* Build WriteValue call: (ay, a{sv}) */
g_variant_builder_init(&opts_builder, G_VARIANT_TYPE_VARDICT);
params = g_variant_new(
"(ay@a{sv})",
g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE,
&mute_val, 1, sizeof(uint8_t)),
g_variant_builder_end(&opts_builder));
g_dbus_proxy_call_sync(char_proxy, "WriteValue", params,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
if (error) {
/*
* If the server is in Disabled state it returns
* application error 0x80 (Mute Disabled).
*/
g_printerr("[MICS] WriteValue failed: %s\n", error->message);
g_error_free(error);
return -EIO;
}
g_print("[MICS] Mute set to: %s\n",
mute_val == MUTE_MUTED ? "MUTED π" : "NOT MUTED β
");
return 0;
}
Subscribe to the Mute characteristic via StartNotify. Connect a GObject signal to handle incoming notifications β the callback fires whenever the mute state changes on the server side.
/*
* mics_notify_cb() β called whenever Mute characteristic value changes
* Triggered by: BLE notification from MICS server
*/
static void mics_notify_cb(GDBusProxy *proxy,
GVariant *changed_props,
GStrv invalidated,
gpointer user_data)
{
GVariant *val;
const uint8_t *data;
gsize len;
val = g_variant_lookup_value(changed_props,
"Value",
G_VARIANT_TYPE_BYTESTRING);
if (!val)
return;
data = g_variant_get_fixed_array(val, &len, sizeof(uint8_t));
if (len < 1) {
g_variant_unref(val);
return;
}
/* Decode and print the new mute state */
switch (data[0]) {
case MUTE_NOT_MUTED:
g_print("[MICS Notify] π€ Microphone: NOT MUTED (0x00)\n");
break;
case MUTE_MUTED:
g_print("[MICS Notify] π Microphone: MUTED (0x01)\n");
break;
case MUTE_DISABLED:
g_print("[MICS Notify] π Mute control: DISABLED by hardware (0x02)\n");
break;
default:
g_printerr("[MICS Notify] β οΈ Unknown value: 0x%02x\n", data[0]);
}
g_variant_unref(val);
}
/* Enable notifications for the Mute characteristic */
static int mics_enable_notify(GDBusProxy *char_proxy)
{
GError *error = NULL;
/* Wire up the PropertiesChanged signal BEFORE calling StartNotify */
g_signal_connect(char_proxy, "g-properties-changed",
G_CALLBACK(mics_notify_cb), NULL);
/* Tell BlueZ to subscribe (writes CCCD = 0x0001 under the hood) */
g_dbus_proxy_call_sync(char_proxy, "StartNotify", NULL,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
if (error) {
g_printerr("[MICS] StartNotify failed: %s\n", error->message);
g_error_free(error);
return -EIO;
}
g_print("[MICS] β
Subscribed to Mute notifications\n");
return 0;
}
On the peripheral (server) side, when a remote client sends a Write request to the Mute characteristic, you must validate it and return the correct ATT error when needed. This is the critical guard logic the spec mandates.
/*
* mics_server_write_handler()
* Called by BlueZ GATT server when client writes Mute characteristic.
*
* Returns NULL on success, or a D-Bus error message on failure.
*/
static DBusMessage *mics_server_write_handler(DBusConnection *conn,
DBusMessage *msg,
void *user_data)
{
struct mics_service *svc = user_data;
DBusMessageIter iter, array_iter;
uint8_t new_val = 0;
/* Parse the incoming byte value */
dbus_message_iter_init(msg, &iter);
if (dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_ARRAY) {
dbus_message_iter_recurse(&iter, &array_iter);
dbus_message_iter_get_basic(&array_iter, &new_val);
}
/*
* Rule 1 (MICS spec Β§3.1.1):
* Client must NOT write Disabled (0x02) or any RFU value.
* Return standard ATT error: Value Not Allowed (0x13)
*/
if (new_val >= MUTE_DISABLED) {
g_print("[MICS Server] β Rejected write 0x%02x β Value Not Allowed\n",
new_val);
return g_dbus_create_error(msg,
"org.bluez.Error.NotPermitted",
"ATT Error 0x13: Value Not Allowed");
}
/*
* Rule 2 (MICS spec Β§3.1.1):
* If current state is Disabled, reject ALL client writes.
* Return application error: Mute Disabled (0x80)
*/
if (svc->mute_state == MUTE_DISABLED) {
g_print("[MICS Server] π Rejected write β Mute is hardware-disabled\n");
return g_dbus_create_error(msg,
"org.bluez.Error.NotPermitted",
"ATT App Error 0x80: Mute Disabled");
}
/* β
Valid write β apply new mute state */
svc->mute_state = new_val;
g_print("[MICS Server] β
Mute state updated to: 0x%02x (%s)\n",
new_val, new_val == MUTE_MUTED ? "MUTED" : "NOT MUTED");
/* Notify all subscribed clients */
mics_send_notification(conn, svc, new_val);
return dbus_message_new_method_return(msg);
}
/*
* mics_hardware_disable()
* Called locally when a physical privacy switch is engaged.
* ONLY the server can call this β clients cannot trigger it.
*/
static void mics_hardware_disable(struct mics_service *svc,
DBusConnection *conn)
{
svc->mute_state = MUTE_DISABLED;
g_print("[MICS Server] π Hardware privacy switch engaged β Disabled\n");
/* Notify all connected clients about the state change */
mics_send_notification(conn, svc, MUTE_DISABLED);
}
Although MICS is a GATT/BLE service, it can also run over Bluetooth Classic (BR/EDR). When it does, the device must register an SDP record so Classic Bluetooth clients can discover it.
| SDP Record Item | Value | Status |
|---|---|---|
| Service Class UUID | Β«Microphone Control ServiceΒ» | Mandatory |
| Protocol: L2CAP β ATT | PSM = ATT | Mandatory |
| Enhanced ATT (EATT) | PSM = EATT | Conditional (C.1) |
| BrowseGroupList | PublicBrowseRoot | Mandatory |
C.1 β EATT condition: The Additional Protocol Descriptor List (with PSM = EATT) is mandatory if Enhanced Attribute Protocol is supported, and excluded otherwise.
Enhanced ATT (EATT) uses L2CAP’s Credit-Based Flow Control mode for sending ATT PDUs. Unlike the basic ATT bearer, EATT guarantees ordered, reliable delivery of notifications β making it ideal for time-sensitive mute state changes in audio applications.
| Aspect | Detail |
|---|---|
| MICS Service UUID | 0x184D |
| Mute Char UUID | 0x2BC3 |
| Max Instances on a Device | 1 (only one MICS per device) |
| Byte Order | Little Endian β LSO (Least Significant Octet) first |
| Bluetooth Core Spec | v4.0 or later (any version with GATT) |
| Spec Version | v1.0 β Adopted by Bluetooth SIG (February 2021) |
| Prepared By | Generic Audio Working Group |
π Keep Exploring BLE Audio Services!
MICS is part of the Bluetooth LE Audio profile family. The next logical step is AICS (Audio Input Control Service) β which adds per-input gain control and individual mute per microphone. Head back to EmbeddedPathashala for more free Bluetooth deep-dives!
