bluetooth le audio tutorial – AICS Control Point Procedures
Part 4 of 5 | All 5 Opcodes, Packet Formats & Error Flows
5 Opcodes
0x01 – 0x05
2–3 Bytes
per command
5 Error Codes
handled
The Audio Input Control Point — Your Command Interface
The Audio Input Control Point (AICP) characteristic is the only write target in AICS. When the client wants to change the gain, mute/unmute, or switch the gain mode, it writes a formatted byte sequence to this characteristic. The server parses the opcode and operands, validates the Change_Counter, executes the action (or returns an error), and — on success — increments the Change_Counter and sends a notification to all clients.
This part gives you the exact wire format for each of the five operations, with byte diagrams and BlueZ code for each one.
Common Structure of Every Control Point Write
Every write to the Audio Input Control Point starts with two mandatory bytes: the Opcode and the Change_Counter. Only the Set Gain Setting opcode adds a third byte (the new gain value). The reason the Change_Counter comes second in every command is to prevent the race conditions we described in Part 3.
Audio Input Control Point — Common Write Format
|
Byte 0
Opcode
0x01 = Set Gain Setting
0x02 = Unmute
0x03 = Mute
0x04 = Set Manual Gain Mode
0x05 = Set Automatic Gain Mode
|
Byte 1
Change_Counter
Must match the current
Change_Counter from the
last read of Audio Input State
Range: 0x00 – 0xFF
|
Byte 2 (optional)
Operand
Only present for opcode 0x01
(Set Gain Setting)
Gain_Setting: -128 to +127
Format: int8
|
All Five Opcodes — Quick Reference
| Opcode |
Procedure Name |
Total Bytes |
Prerequisite Gain_Mode |
On Success: Server Does |
| 0x01 |
Set Gain Setting |
3 |
Manual (2) or Manual Only (0) |
Updates Gain_Setting, increments CC, notifies |
| 0x02 |
Unmute |
2 |
Mute must be 0 or 1 (not Disabled=2) |
Sets Mute=0, increments CC, notifies |
| 0x03 |
Mute |
2 |
Mute must be 0 or 1 (not Disabled=2) |
Sets Mute=1, increments CC, notifies |
| 0x04 |
Set Manual Gain Mode |
2 |
Gain_Mode must be Automatic (3) |
Sets Gain_Mode=2, increments CC, notifies |
| 0x05 |
Set Automatic Gain Mode |
2 |
Gain_Mode must be Manual (2) |
Sets Gain_Mode=3, increments CC, notifies |
Opcode 0x01 — Set Gain Setting
This is the primary command. It tells the server to set the microphone/audio input gain to a specific step value. It is the only 3-byte command because it needs to include the desired gain alongside the opcode and Change_Counter.
Set Gain Setting — Packet Format (3 bytes)
| Byte 0 — Opcode |
Byte 1 — Change_Counter |
Byte 2 — Gain_Setting |
0x01
(Set Gain Setting) |
0x07
(current CC = 7) |
0x05
(desired gain = +5) |
Wire bytes: 01 07 05 — “Set Gain to step +5, my counter is 7”
Set Gain Setting — Server Decision Logic
| Client writes: [0x01][CC_from_client][gain_value] |
|
CHECK 1: CC match?
client_CC == server’s current Change_Counter?
YES → continue NO → return 0x80
|
CHECK 2: Range valid?
Min ≤ gain_value ≤ Max?
YES → continue NO → return 0x83
|
|
CHECK 3: Gain_Mode allows manual control?
Is Gain_Mode == Manual (2) or Manual Only (0)?
YES → Apply new gain, increment CC, send Notify NO (Auto mode) → Write succeeds but gain is NOT applied
|
|
/* BlueZ client: build and send Set Gain Setting packet */ static int aics_set_gain(int sock, uint16_t aicp_handle, uint8_t change_counter, int8_t new_gain, const struct gain_properties *props) { uint8_t cmd[3]; /* Clamp gain to valid range first */ if (new_gain < props->minimum) new_gain = props->minimum; if (new_gain > props->maximum) new_gain = props->maximum; cmd[0] = 0x01; /* Opcode: Set Gain Setting */ cmd[1] = change_counter; /* Current CC from last Read */ cmd[2] = (uint8_t)new_gain; /* Cast int8 to uint8 for wire */ printf(“Sending SetGain: opcode=0x%02X cc=0x%02X gain=%d\n”, cmd[0], cmd[1], new_gain); /* gatttool equivalent: * gatttool -b AA:BB:CC:DD:EE:FF \ * –char-write-req –handle=0xHHHH \ * –value=$(printf “%02X%02X%02X” 0x01 $CC $GAIN) */ /* In real BlueZ D-Bus code you would call: * g_dbus_proxy_method_call(aicp_proxy, “WriteValue”, * build_write_value_params(cmd, 3, false), * aics_write_callback, NULL, NULL); */ return 0; }
Opcode 0x02 — Unmute
The Unmute command is two bytes: opcode 0x02 and the current Change_Counter. If the current Mute state is already Not Muted, the server applies the write without error — it simply does nothing (no change in state means no CC increment either, and no notification).
Unmute — Packet Format (2 bytes)
| Byte 0 — Opcode |
Byte 1 — Change_Counter |
0x02
(Unmute) |
0x08
(current CC = 8) |
Wire bytes: 02 08
✅ Success Case
Mute=1 (Muted) → Server sets Mute=0 → CC increments → Notify sent
✔ No-op Case
Mute=0 already → Write accepted, no state change → CC unchanged → No notify
❌ Error: Mute Disabled
Mute=2 (Disabled) → Server returns ATT Error 0x82 → Nothing changes
❌ Error: Stale CC
CC mismatch → Server returns ATT Error 0x80 → Client must re-read state
Opcode 0x03 — Mute
The Mute command is identical in format to Unmute — two bytes, but with opcode 0x03. The same edge cases apply: if already muted, it is a no-op. If Mute is Disabled, error 0x82 is returned.
Mute — Packet Format (2 bytes)
| Byte 0 — Opcode |
Byte 1 — Change_Counter |
0x03
(Mute) |
0x08
(current CC = 8) |
Wire bytes: 03 08
Opcode 0x04 — Set Manual Gain Mode
This command switches the device from Automatic gain mode (AGC) to Manual gain mode. It only works when the current Gain_Mode is Automatic (3). If the device is Manual Only (0) or Automatic Only (1) — fixed modes — the server returns error 0x84.
Set Manual Gain Mode — Packet Format (2 bytes)
| Byte 0 — Opcode |
Byte 1 — Change_Counter |
0x04
(Set Manual Mode) |
0x09
(current CC = 9) |
Wire bytes: 04 09
Gain Mode Switching — What Is Allowed
|
Manual Only (0)
Fixed manual. No switching allowed.
|
⛔ |
Manual (2)
Currently manual. CAN switch to Auto.
|
⇆ |
Automatic (3)
Currently auto. CAN switch to Manual.
|
⛔ |
Auto Only (1)
Fixed auto. No switching allowed.
|
Opcode 0x05 → Error 0x84
Opcode 0x04 → No-op (already manual) |
Opcode 0x04 → switches 3→2
Opcode 0x05 → switches 2→3 |
Opcode 0x04 → Error 0x84
Opcode 0x05 → No-op (already auto) |
Opcode 0x05 — Set Automatic Gain Mode
This command is the mirror of opcode 0x04. It switches from Manual (2) to Automatic (3). Same two-byte format, same error conditions for fixed modes.
Set Automatic Gain Mode — Packet Format (2 bytes)
| Byte 0 — Opcode |
Byte 1 — Change_Counter |
0x05
(Set Auto Mode) |
0x0A
(current CC = 10) |
Wire bytes: 05 0A
Complete Error Handling Flowchart
Server-Side Decision Tree for Every Control Point Write
| Server receives ATT Write Request on Audio Input Control Point handle |
| Is link encrypted? |
→ NO |
ATT Error Response: 0x0F (Insufficient Encryption)
Connection must be encrypted before any AICS access |
|
| ↓ YES — link is encrypted |
| Is opcode in 0x01–0x05? |
→ NO |
ATT Error Response: 0x81 (Opcode Not Supported) |
|
| ↓ YES — valid opcode |
| Does client CC match server CC? |
→ NO |
ATT Error Response: 0x80 (Invalid Change Counter)
Client must re-read Audio Input State and retry |
|
| ↓ YES — CC matches |
Opcode-specific checks
(range, mute disabled, gain mode) |
→ FAIL |
0x82 (Mute Disabled) | 0x83 (Value Out of Range)
0x84 (Gain Mode Change Not Allowed) |
|
| ↓ PASS — all checks OK |
| Apply the change → Increment Change_Counter → Send ATT Notification to all subscribed clients → Return ATT Write Response (success) |
Complete BlueZ Client Command Dispatcher
/* File: aics_client_commands.c * Minimal AICS client command builder for BlueZ implementations. * Uses pure C — wire byte construction only. * In real BlueZ code, pass these byte arrays to g_dbus_proxy WriteValue. */ #include <stdint.h> #include <string.h> #include <stdio.h> /* AICS Opcodes */ #define AICS_OP_SET_GAIN 0x01 #define AICS_OP_UNMUTE 0x02 #define AICS_OP_MUTE 0x03 #define AICS_OP_SET_MANUAL_MODE 0x04 #define AICS_OP_SET_AUTO_MODE 0x05 /* Build Mute command into buf[2] */ static size_t aics_build_mute(uint8_t *buf, uint8_t cc) { buf[0] = AICS_OP_MUTE; buf[1] = cc; return 2; } /* Build Unmute command into buf[2] */ static size_t aics_build_unmute(uint8_t *buf, uint8_t cc) { buf[0] = AICS_OP_UNMUTE; buf[1] = cc; return 2; } /* Build Set Manual Gain Mode into buf[2] */ static size_t aics_build_set_manual_mode(uint8_t *buf, uint8_t cc) { buf[0] = AICS_OP_SET_MANUAL_MODE; buf[1] = cc; return 2; } /* Build Set Automatic Gain Mode into buf[2] */ static size_t aics_build_set_auto_mode(uint8_t *buf, uint8_t cc) { buf[0] = AICS_OP_SET_AUTO_MODE; buf[1] = cc; return 2; } /* Build Set Gain Setting into buf[3] */ static size_t aics_build_set_gain(uint8_t *buf, uint8_t cc, int8_t gain) { buf[0] = AICS_OP_SET_GAIN; buf[1] = cc; buf[2] = (uint8_t)gain; /* int8 cast to uint8 for wire */ return 3; } /* Handle ATT error response from server */ static void aics_handle_error(uint8_t error_code) { switch (error_code) { case 0x80: printf(“Error: Invalid Change Counter — re-read Audio Input State\n”); break; case 0x81: printf(“Error: Opcode Not Supported\n”); break; case 0x82: printf(“Error: Mute Disabled — hardware lock active\n”); break; case 0x83: printf(“Error: Value Out of Range — check Gain Setting Properties\n”); break; case 0x84: printf(“Error: Gain Mode Change Not Allowed — device has fixed mode\n”); break; default: printf(“Error: ATT error 0x%02X\n”, error_code); break; } } /* Example: full workflow to mute the device */ static void example_mute_workflow(void) { uint8_t cmd[3]; size_t cmd_len; /* Step 1: Read Audio Input State to get current Change_Counter */ /* Assume we read: [03][00][02][07] → CC = 0x07 */ uint8_t current_cc = 0x07; /* Step 2: Build the Mute command */ cmd_len = aics_build_mute(cmd, current_cc); printf(“Mute command (%zu bytes): “, cmd_len); for (size_t i = 0; i < cmd_len; i++) printf(“%02X “, cmd[i]); printf(“\n”); /* Output: Mute command (2 bytes): 03 07 */ /* Step 3: Write to Audio Input Control Point via BlueZ D-Bus */ /* g_dbus_proxy_method_call(aicp_proxy, “WriteValue”, …) */ /* Step 4: Wait for ATT Write Response or ATT Error Response */ /* On success: server notifies with new state [03][01][02][08] * (same gain, now muted, same mode, CC incremented to 8) */ } int main(void) { example_mute_workflow(); return 0; }
Testing All 5 Commands with gatttool
You can test AICS commands directly from the Linux terminal using gatttool without writing any code. First discover the AICP characteristic handle, then send your commands byte by byte.
#!/bin/bash # AICS Control Point testing with gatttool # Replace DEVICE_ADDR and HANDLE with actual values DEVICE=”AA:BB:CC:DD:EE:FF” AICP_HANDLE=”0x0025″ # Audio Input Control Point handle (discover with –primary) # Step 0: Discover handles (run this first) # gatttool -b $DEVICE –primary # gatttool -b $DEVICE –characteristics # Step 1: Read Audio Input State to get current Change_Counter # Assume handle 0x0022 is Audio Input State CC=$(gatttool -b $DEVICE –char-read –handle=0x0022 | \ awk ‘{print $NF}’) # Last byte = Change_Counter echo “Current state bytes: $CC” # For the examples below, assume current CC = 07 # —————————————————————— # Opcode 0x01 — Set Gain Setting to step +5 (CC=07) gatttool -b $DEVICE –char-write-req \ –handle=$AICP_HANDLE \ –value=010705 # ^^ Opcode=0x01 # ^^ CC=0x07 # ^^ Gain=0x05 (+5 as int8) # —————————————————————— # Opcode 0x02 — Unmute (CC=08 after previous success) gatttool -b $DEVICE –char-write-req \ –handle=$AICP_HANDLE \ –value=0208 # ^^ Opcode=0x02 (Unmute) # ^^ CC=0x08 # —————————————————————— # Opcode 0x03 — Mute (CC=09) gatttool -b $DEVICE –char-write-req \ –handle=$AICP_HANDLE \ –value=0309 # —————————————————————— # Opcode 0x04 — Set Manual Gain Mode (CC=0A) gatttool -b $DEVICE –char-write-req \ –handle=$AICP_HANDLE \ –value=040A # —————————————————————— # Opcode 0x05 — Set Automatic Gain Mode (CC=0B) gatttool -b $DEVICE –char-write-req \ –handle=$AICP_HANDLE \ –value=050B # Note: If you get “Characteristic Write failed” it may be: # 1. ATT error response (check with -v flag for verbose output) # 2. Connection not encrypted — pair the device first via bluetoothctl
Key Concepts in Part 4
Opcode 0x01 Set Gain Opcode 0x02 Unmute Opcode 0x03 Mute Opcode 0x04 Manual Mode Opcode 0x05 Auto Mode ATT Write Request ATT Write Response ATT Error Response No-op write gatttool
Final Part: BlueZ GATT Server Implementation
Part 5 puts everything together — build a working AICS GATT server in Python using BlueZ D-Bus APIs, complete with notifications and error handling.
→ Part 5: BlueZ Implementation