bluetooth le audio tutorial – AICS Control Point Procedures

 

 

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

Leave a Reply

Your email address will not be published. Required fields are marked *