LE Transport Parameters, GATT Services, Advertising & BlueZ Server Code
HA Role Overview
The Hearing Aid (HA) role is the most complex role in HAP — it is the peripheral GATT server side of the entire ecosystem. A hearing aid implementing this role must correctly handle: BLE advertising with service UUIDs, a full HAS GATT server, BAP audio sink and optionally audio source, volume control via VCS, binaural coordination via CSIP, and optionally microphone control via MICP.
This part walks through every HA-role requirement group by group, with BlueZ code examples for each. By the end of this tutorial you will understand what every data structure in a conforming hearing aid means, and you will have working Python BlueZ code that simulates an HA GATT server.
Prerequisite: Complete Part 1 first to understand the role definitions and profile stack.
Topics Covered in Part 2
1. LE Transport Requirements for the HA Role
1.1 LE 2M PHY — Mandatory
The HA must support the LE 2M PHY (2 Megabit per second Physical Layer). This is not optional — it is a hard requirement. LE 2M PHY doubles the data rate compared to LE 1M PHY, which is critical for getting high-quality audio into a hearing aid without excessive retransmissions eating into the latency budget. LE 1M PHY is still the default used for the connection setup (advertising), but the HA must be capable of switching to 2M PHY once connected.
In BlueZ/Linux, you can request LE 2M PHY using the HCI_LE_Set_PHY command after connection, or it gets negotiated via the L2CAP layer when CIS is set up.
# Check PHY capabilities of the local adapter
btmgmt phy
# Expected output should include LE2MTX and LE2MRX if BT 5.0+ hardware:
# Supported phys: BR1M1SLOT BR1M3SLOT BR1M5SLOT ... LE1MTX LE1MRX LE2MTX LE2MRX
# Enable 2M PHY (if not already active)
btmgmt phy LE2MTX LE2MRX LE1MTX LE1MRX
# Python: check if 2M PHY is supported on hci0
python3 <<EOF
import dbus
bus = dbus.SystemBus()
adapter_props = dbus.Interface(
bus.get_object('org.bluez', '/org/bluez/hci0'),
'org.freedesktop.DBus.Properties'
)
props = adapter_props.GetAll('org.bluez.Adapter1')
print("Adapter Address:", props.get('Address'))
# BlueZ exposes ExperimentalFeatures; PHY is checked at HCI level via btmgmt
EOF
1.2 CIS / BIS ISOAL Transport Parameters Explained
This is one of the most important technical sections in the HA role requirements. When a hearing aid accepts a CIS (unicast) connection or synchronises to a BIS (broadcast), it must support specific ISO Adaptation Layer (ISOAL) parameters. These parameters ensure that one LC3-encoded audio frame arrives in one PDU — no segmentation, no reassembly, no increased frame loss.
HAP specifies two parameter sets. The first is for framed ISOAL (avoids segmentation over the 7.5 ms transport interval). The second is for unframed ISOAL. Both use BN=1 (Burst Number = 1, meaning one PDU per sub-event) and Flush Timeout=1.
| Parameter | Framed ISOAL (Table 3.1) For 7.5ms transport interval |
Unframed ISOAL (Table 3.2) |
|---|---|---|
| Flush Timeout (FT) — unicast | 1 | 1 |
| Pre-Transmission Offset (PTO) — broadcast | 0 | 0 |
| Max_PDU (unicast C-to-P and P-to-C) | ≥ LC3 packet size at 10ms frame + 5 octet framed ISOAL header | ≥ LC3 packet size at 10ms frame (no extra header) |
| Max_PDU (broadcast) | ≥ LC3 packet size at 10ms frame + 5 octet header | ≥ LC3 packet size at 10ms frame |
| Framing | Framed | Unframed |
| BN (Burst Number) | 1 | 1 |
BN=1 means one burst (one PDU transmission) per CIS sub-event. FT=1 means flush immediately after one interval if undelivered. Together they minimise latency and prevent frame build-up.
1.3 How to Calculate the Maximum SDU Size
The Max_PDU value in the tables above depends on the LC3 codec configuration the hearing aid exposes in its PACS (Published Audio Capabilities Service) records. The formula is:
| Max SDU Size | = | Supported_Max_Codec_Frames_Per_SDU× Max_Octets_Per_Codec_Frame(octets 2–3 of Supported_Octets_Per_Codec_Frame LTV) |
Both Supported_Max_Codec_Frames_Per_SDU and Supported_Octets_Per_Codec_Frame are LTV structures defined in Bluetooth Assigned Numbers and exposed in the PAC records that the HA advertises in its PACS GATT service.
"""
SDU / Max_PDU size calculation for an HA advertising LC3:
- 16kHz / 10ms frame duration: 40 octets per codec frame
- 24kHz / 10ms frame duration: 60 octets per codec frame
- 32kHz / 10ms frame duration: 80 octets per codec frame
- 48kHz / 10ms frame duration: 120 octets per codec frame
"""
def calc_max_pdu_framed(octets_per_codec_frame, codec_frames_per_sdu=1):
"""
Calculate Max_PDU for framed ISOAL (add 5-byte framed header).
Args:
octets_per_codec_frame: e.g. 40 for LC3 16kHz/10ms
codec_frames_per_sdu: normally 1 for hearing aids
Returns:
minimum Max_PDU value to set in CIS parameters
"""
FRAMED_ISOAL_HEADER = 5 # octets added for framed ISOAL SDU header
max_sdu = octets_per_codec_frame * codec_frames_per_sdu
max_pdu = max_sdu + FRAMED_ISOAL_HEADER
return max_pdu
def calc_max_pdu_unframed(octets_per_codec_frame, codec_frames_per_sdu=1):
"""Calculate Max_PDU for unframed ISOAL (no extra header)."""
return octets_per_codec_frame * codec_frames_per_sdu
# Common LC3 10ms configurations
configs = {
"16kHz/10ms": 40,
"24kHz/10ms": 60,
"32kHz/10ms": 80,
"48kHz/10ms": 120,
}
for label, octets in configs.items():
framed = calc_max_pdu_framed(octets)
unframed = calc_max_pdu_unframed(octets)
print(f"LC3 {label}: SDU={octets}B Max_PDU(framed)={framed}B Max_PDU(unframed)={unframed}B")
# Output:
# LC3 16kHz/10ms: SDU=40B Max_PDU(framed)=45B Max_PDU(unframed)=40B
# LC3 24kHz/10ms: SDU=60B Max_PDU(framed)=65B Max_PDU(unframed)=60B
# LC3 32kHz/10ms: SDU=80B Max_PDU(framed)=85B Max_PDU(unframed)=80B
# LC3 48kHz/10ms: SDU=120B Max_PDU(framed)=125B Max_PDU(unframed)=120B
1.4 BAP Broadcast Sink — 20ms Rendering Deadline
For broadcast audio (BIS), the hearing aid must be able to render an LC3 audio stream no later than 20ms after the SDU Synchronisation Reference point. This is the maximum allowed rendering latency for broadcast. For unicast (CIS), the Presentation Delay range must include the value 20ms as a supported point — the hearing aid must declare this in its PACS Codec Configured state.
2. Advertising Requirements — Service UUID and Appearance AD Types
2.1 Service UUID AD Type Rules
When the hearing aid is in a GAP discoverable mode (general or limited), it must include the Hearing Access Service UUID (0x1854) in the Service UUID AD Type field of its advertising data or scan response data. This is how a HAUC or HARC finds a hearing aid during a BLE scan.
Important privacy exception: If the HA is in a GAP connectable mode and is using a resolvable private address (RPA), it must not include the HAS UUID in its advertising data. Including a stable service UUID alongside an RPA would negate the privacy benefit — any scanner could correlate the UUID across sessions. In RPA mode, the HAUC/HARC must use the IRK-based address resolution mechanism to find its bonded hearing aid.
2.2 Appearance AD Type Rules
The HA may include its Appearance characteristic value in the advertising data for better user experience (e.g., showing a hearing aid icon in the Bluetooth settings list). The same privacy rule applies: when using a resolvable private address in connectable mode, the Appearance value must not be included in the advertising data either.
| Scenario | HAS UUID in Adv Data? | Appearance in Adv Data? | Rule |
|---|---|---|---|
| GAP Discoverable mode, public or static address | MUST include | May include | Mandatory UUID |
| GAP Connectable mode, resolvable private address (RPA) | MUST NOT include | MUST NOT include | Privacy protection |
BlueZ: Setting Up HA BLE Advertisement with HAS UUID
#!/usr/bin/env python3
"""
BlueZ BLE Advertisement for Hearing Aid (HA) Role
Advertises the Hearing Access Service UUID (0x1854)
Uses BlueZ LEAdvertisement1 D-Bus interface
"""
import dbus
import dbus.service
import dbus.mainloop.glib
from gi.repository import GLib
BLUEZ_SERVICE = 'org.bluez'
LE_ADV_IFACE = 'org.bluez.LEAdvertisement1'
LE_ADV_MGR_IFACE = 'org.bluez.LEAdvertisingManager1'
DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager'
DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties'
# Bluetooth Assigned Numbers UUIDs
HAS_UUID = "00001854-0000-1000-8000-00805f9b34fb" # Hearing Access Service
BAS_UUID = "0000180f-0000-1000-8000-00805f9b34fb" # Battery Service
class HearingAidAdvertisement(dbus.service.Object):
"""
BLE Advertisement object for Hearing Aid (HA) role.
Publishes HAS UUID in Service UUIDs AD Type.
Using public address (not RPA) so UUID inclusion is allowed.
"""
PATH_BASE = '/org/bluez/ha_advertisement'
def __init__(self, bus, index):
self.path = self.PATH_BASE + str(index)
self.bus = bus
self.ad_type = 'peripheral'
self.service_uuids = [HAS_UUID, BAS_UUID]
self.local_name = 'HAP-HearingAid'
self.discoverable = dbus.Boolean(True)
dbus.service.Object.__init__(self, bus, self.path)
def get_properties(self):
return {
LE_ADV_IFACE: {
'Type': self.ad_type,
'ServiceUUIDs': dbus.Array(self.service_uuids, signature='s'),
'LocalName': dbus.String(self.local_name),
'Discoverable': self.discoverable,
# Appearance: 0x0541 = Hearing Aid (from Bluetooth Assigned Numbers)
'Appearance': dbus.UInt16(0x0541),
}
}
@dbus.service.method(DBUS_PROP_IFACE, in_signature='s', out_signature='a{sv}')
def GetAll(self, interface):
return self.get_properties()[LE_ADV_IFACE]
@dbus.service.method(LE_ADV_IFACE, in_signature='', out_signature='')
def Release(self):
print("Advertisement released")
def start_ha_advertisement():
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
# Find the LE Advertising Manager
mgr_obj = bus.get_object(BLUEZ_SERVICE, '/org/bluez/hci0')
adv_mgr = dbus.Interface(mgr_obj, LE_ADV_MGR_IFACE)
# Create and register advertisement
adv = HearingAidAdvertisement(bus, 0)
adv_mgr.RegisterAdvertisement(
adv.path,
{}, # options dict
reply_handler=lambda: print("HA advertisement registered OK"),
error_handler=lambda e: print(f"Advertisement error: {e}")
)
mainloop = GLib.MainLoop()
print(f"Advertising as '{adv.local_name}' with HAS UUID {HAS_UUID}")
print("Press Ctrl+C to stop...")
try:
mainloop.run()
except KeyboardInterrupt:
adv_mgr.UnregisterAdvertisement(adv.path)
print("Advertisement stopped")
if __name__ == '__main__':
start_ha_advertisement()
3. GATT Service Requirements for the HA Role
3.1 Hearing Access Service (HAS) — Mandatory
Every hearing aid must instantiate exactly one instance of the Hearing Access Service (HAS). The HAS contains three characteristics:
| Characteristic | UUID | Properties | Purpose |
|---|---|---|---|
| Hearing Aid Features | 0x2BDA | Read + Notify | 1-octet bitmask: Preset Sync Support, Independent Presets, Dynamic Presets, Writable Presets Support |
| Hearing Aid Preset Control Point | 0x2BDB | Write + Indicate/Notify | Opcodes for Read Presets, Set Active, Set Next, Set Previous, Write Name, Preset Changed |
| Active Preset Index | 0x2BDC | Read + Notify | Index of the currently active preset. Notified when user or controller changes preset. |
Service UUID: 0x1854 | All characteristics require LE Security Mode 1 Level 2 (encrypted link, 128-bit key)
3.2 Hearing Aid Features Characteristic — Bit Field Explained
| Bit | Field Name | Meaning |
|---|---|---|
| 0 | Hearing Aid Type (bit 0) | 0=Monaural, 1=Binaural (along with bit 1) |
| 1 | Hearing Aid Type (bit 1) | bits[1:0]: 0b00=Monaural, 0b01=Binaural, 0b10=Banded |
| 2 | Preset Synchronisation Support | 1 = HA can sync preset changes with its binaural pair locally |
| 3 | Independent Presets | 1 = Left and right aids have different preset lists |
| 4 | Dynamic Presets | 1 = Preset list can change; HA will notify via Preset Changed opcode |
| 5 | Writable Presets Support | 1 = HARC can write names to preset records via Write Preset Name |
| 6–7 | RFU | Reserved for future use — set to 0 |
#!/usr/bin/env python3
"""
BlueZ GATT Server — Hearing Access Service (HAS) for HA Role
Registers HAS with all three mandatory characteristics.
"""
import dbus, dbus.service, dbus.mainloop.glib
from gi.repository import GLib
BLUEZ_SERVICE = 'org.bluez'
GATT_MGR_IFACE = 'org.bluez.GattManager1'
GATT_SVC_IFACE = 'org.bluez.GattService1'
GATT_CHR_IFACE = 'org.bluez.GattCharacteristic1'
DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager'
DBUS_PROP_IFACE = 'org.freedesktop.DBus.Properties'
# HAS UUIDs (from Bluetooth Assigned Numbers)
HAS_UUID = "00001854-0000-1000-8000-00805f9b34fb"
FEAT_UUID = "00002bda-0000-1000-8000-00805f9b34fb" # Hearing Aid Features
CTRL_UUID = "00002bdb-0000-1000-8000-00805f9b34fb" # Preset Control Point
PIDX_UUID = "00002bdc-0000-1000-8000-00805f9b34fb" # Active Preset Index
# Hearing Aid Features bitmask for a simple monaural HA
# Bit 0&1 = 0b00 (Monaural), Bit 4 = 1 (Dynamic Presets), Bit 5 = 1 (Writable Presets)
HA_FEATURES_VALUE = 0b00110000 # 0x30 = Dynamic + Writable presets, Monaural
class Application(dbus.service.Object):
def __init__(self, bus):
self.path = '/'
self.services = []
dbus.service.Object.__init__(self, bus, self.path)
@dbus.service.method(DBUS_OM_IFACE, out_signature='a{oa{sa{sv}}}')
def GetManagedObjects(self):
response = {}
for svc in self.services:
response[dbus.ObjectPath(svc.path)] = svc.get_properties()
for chrc in svc.get_characteristics():
response[dbus.ObjectPath(chrc.path)] = chrc.get_properties()
return response
def add_service(self, svc):
self.services.append(svc)
class GattService(dbus.service.Object):
def __init__(self, bus, index, uuid, primary=True):
self.path = f'/org/bluez/ha/service{index}'
self.uuid = uuid
self.primary = primary
self.characteristics = []
dbus.service.Object.__init__(self, bus, self.path)
def get_properties(self):
return {
GATT_SVC_IFACE: {
'UUID': dbus.String(self.uuid),
'Primary': dbus.Boolean(self.primary),
'Characteristics': dbus.Array(
[dbus.ObjectPath(c.path) for c in self.characteristics],
signature='o'
)
}
}
def get_characteristics(self):
return self.characteristics
def add_characteristic(self, chrc):
self.characteristics.append(chrc)
class HearingAidFeaturesCharacteristic(dbus.service.Object):
def __init__(self, bus, index, service):
self.path = service.path + f'/char{index}'
self.uuid = FEAT_UUID
self.flags = ['read', 'notify']
self.service = service
self.value = [dbus.Byte(HA_FEATURES_VALUE)]
self.notifying = False
dbus.service.Object.__init__(self, bus, self.path)
def get_properties(self):
return {
GATT_CHR_IFACE: {
'Service': dbus.ObjectPath(self.service.path),
'UUID': dbus.String(self.uuid),
'Flags': dbus.Array(self.flags, signature='s'),
'Value': dbus.Array(self.value, signature='y'),
}
}
@dbus.service.method(GATT_CHR_IFACE, in_signature='a{sv}', out_signature='ay')
def ReadValue(self, options):
print(f"[HAS] ReadValue: Hearing Aid Features = 0x{HA_FEATURES_VALUE:02x}")
return self.value
@dbus.service.method(GATT_CHR_IFACE, in_signature='a{sv}', out_signature='')
def StartNotify(self, options):
self.notifying = True
print("[HAS] Notifications enabled for Hearing Aid Features")
@dbus.service.method(GATT_CHR_IFACE, out_signature='')
def StopNotify(self):
self.notifying = False
print("[HAS] Notifications disabled for Hearing Aid Features")
class ActivePresetIndexCharacteristic(dbus.service.Object):
def __init__(self, bus, index, service):
self.path = service.path + f'/char{index}'
self.uuid = PIDX_UUID
self.flags = ['read', 'notify']
self.service = service
self.preset_idx = 1 # active preset index starts at 1
self.notifying = False
dbus.service.Object.__init__(self, bus, self.path)
def get_properties(self):
return {
GATT_CHR_IFACE: {
'Service': dbus.ObjectPath(self.service.path),
'UUID': dbus.String(self.uuid),
'Flags': dbus.Array(self.flags, signature='s'),
'Value': dbus.Array([dbus.Byte(self.preset_idx)], signature='y'),
}
}
@dbus.service.method(GATT_CHR_IFACE, in_signature='a{sv}', out_signature='ay')
def ReadValue(self, options):
print(f"[HAS] ReadValue: Active Preset Index = {self.preset_idx}")
return [dbus.Byte(self.preset_idx)]
@dbus.service.method(GATT_CHR_IFACE, in_signature='a{sv}', out_signature='')
def StartNotify(self, options):
self.notifying = True
print("[HAS] Notifications enabled for Active Preset Index")
@dbus.service.method(GATT_CHR_IFACE, out_signature='')
def StopNotify(self):
self.notifying = False
def register_app_cb():
print("[GATT] HAS Application registered successfully")
def register_app_error_cb(error):
print(f"[GATT] Failed to register application: {error}")
def main():
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
# Build GATT application tree
app = Application(bus)
svc = GattService(bus, 0, HAS_UUID)
feat = HearingAidFeaturesCharacteristic(bus, 0, svc)
pidx = ActivePresetIndexCharacteristic(bus, 2, svc)
svc.add_characteristic(feat)
svc.add_characteristic(pidx)
app.add_service(svc)
# Register with BlueZ GATT Manager
gatt_mgr = dbus.Interface(
bus.get_object(BLUEZ_SERVICE, '/org/bluez/hci0'),
GATT_MGR_IFACE
)
gatt_mgr.RegisterApplication(
app.path, {},
reply_handler=register_app_cb,
error_handler=register_app_error_cb
)
mainloop = GLib.MainLoop()
print("[GATT] HAS GATT Server running. Waiting for connections...")
mainloop.run()
if __name__ == '__main__':
main()
4. Battery Service, IAS, and CSIP Set Member
4.1 Battery Service (BAS) — Recommended
If the hearing aid has one or more batteries, it should (recommended, not mandatory) instantiate the Battery Service (BAS, UUID 0x180F). This exposes the battery level as a 0–100% value. A binaural set would have two separate battery levels — one per aid. The BAS Battery Level characteristic (0x2A19) is typically a read + notify characteristic so the companion app can track when to prompt the user to recharge.
4.2 Immediate Alert Service (IAS) — Optional on HA
The HA may instantiate one instance of the Immediate Alert Service (IAS, UUID 0x1802). This gives the IAC role (doorbell, smoke alarm, etc.) a target to write to. When the Alert Level characteristic (0x2A06) receives the value 0x02 (High Alert), the hearing aid must generate an audio tone. When it receives 0x01 (Mild Alert), it should generate an audio alert. The duration and style of the alert are implementation-specific.
# IAS Alert Level values defined in Bluetooth Assigned Numbers
ALERT_NO_ALERT = 0x00 # Stop alerting
ALERT_MILD = 0x01 # Should alert — HA should respond with mild audio
ALERT_HIGH = 0x02 # Shall alert — HA must respond with prominent audio
# Demonstration: what the HA does on receiving each alert level
def handle_alert_level_write(new_value: int):
if new_value == ALERT_HIGH:
print("HIGH ALERT received — play prominent audio alert tone (mandatory)")
# Trigger HA speaker: e.g., 3 loud beeps at 1kHz
elif new_value == ALERT_MILD:
print("MILD ALERT received — play soft audio alert tone (recommended)")
# Trigger HA speaker: e.g., 1 soft beep
elif new_value == ALERT_NO_ALERT:
print("NO ALERT — stop any ongoing alert")
else:
print(f"Prohibited value {new_value} — ignore, do not respond")
4.3 CSIP Set Member — Mandatory for Binaural HAs
If the hearing aid is capable of being part of a Binaural Hearing Aid Set, it must instantiate the Coordinated Set Identification Service (CSIS, UUID 0x1846) and must expose the Coordinated Set Size characteristic. The Set Size value tells the HAUC how many members are in this set (value = 2 for a standard left+right binaural set). Both aids in a set share the same SIRK (Set Identity Resolving Key), which the HAUC uses to discover both aids and recognise them as a matched pair.
# CSIP / CSIS UUIDs (Bluetooth Assigned Numbers)
CSIS_UUID = "00001846-0000-1000-8000-00805f9b34fb" # Coordinated Set Identification Service
SIRK_UUID = "00002b84-0000-1000-8000-00805f9b34fb" # Set Identity Resolving Key char
SIZE_UUID = "00002b85-0000-1000-8000-00805f9b34fb" # Coordinated Set Size characteristic
RANK_UUID = "00002b86-0000-1000-8000-00805f9b34fb" # Member Rank characteristic
# For Left HA in a binaural set:
SET_SIZE = 2 # Total members in set
MEMBER_RANK = 1 # This aid's rank (Left=1, Right=2)
# SIRK (Set Identity Resolving Key) — 16-byte value
# Both hearing aids in the binaural set MUST have the same SIRK
# This allows the HAUC to identify and discover the partner aid
# In production this is generated during manufacturing and stored securely
SIRK_VALUE = bytes([
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10
]) # Example SIRK — use a real random key in production
# SIRK type: 0x01 = Encrypted SIRK, 0x02 = Plaintext SIRK
# For security, encrypted SIRK is preferred in real devices
SIRK_TYPE_ENCRYPTED = 0x01
SIRK_TYPE_PLAINTEXT = 0x02
5. Volume Renderer Role — VCS, AICS, and VOCS
The HA must implement the VCP Volume Renderer role, which means it exposes a Volume Control Service (VCS). The VCS Volume State characteristic reports the current volume level (0–255) and mute state. How the VCS volume setting maps to the actual hearing aid amplification is implementation-specific — it could be the overall output volume or just the received audio stream volume.
| VCS Server (Volume Control Service) — mandatory on HA | ||||
| ▼ includes | ▼ includes | ▼ includes | ▼ includes | ▼ includes (Volume Balance) |
| AICS Audio Input #1 Microphone (ambient sound) | AICS Audio Input #2 Telecoil (hearing loop) | AICS Audio Input #3 BLE LE Audio (streamed audio) | Receiver (HA speaker) Final output | VOCS Volume Offset 1 per aid (binaural) 2 instances (banded) |
| AICS = Audio Input Control Service (optional, multiple instances possible) | VOCS = Volume Offset Control Service (for Volume Balance feature) | ||||
The Volume Balance feature (optional) lets left and right aids have different volume levels. When supported, each HA in a binaural set must expose one VOCS instance; a banded HA must expose two VOCS instances (one per output channel).
6. MICP Microphone Device Role
If the HA supports the BAP Audio Source role (i.e., it can stream audio back to the phone, such as during a hands-free call), it must also expose one instance of the Microphone Input Control Service (MICS, UUID 0x184D). This lets the HARC (typically the smartphone) mute or unmute the hearing aid’s microphone that picks up the user’s voice during a call.
The HA may also expose a second MICS/AICS instance to give the HARC control over the ambient sound capture microphone separately. This is optional and up to the manufacturer.
# MICS UUID (Bluetooth Assigned Numbers)
MICS_UUID = "0000184d-0000-1000-8000-00805f9b34fb" # Microphone Input Control Service
MUTE_UUID = "00002bc3-0000-1000-8000-00805f9b34fb" # Mute characteristic
# Mute characteristic values
MUTE_NOT_MUTED = 0x00 # Microphone is active
MUTE_MUTED = 0x01 # Microphone is muted
MUTE_DISABLED = 0x02 # HA does not allow remote muting
# The HARC writes to the Mute characteristic to mute/unmute the HA mic.
# The Mute char supports: read, write (if not disabled), notify
# When value changes (e.g., user physically mutes on aid), HA sends notification.
def handle_mute_write(new_value: int, current_mute_state: int):
if current_mute_state == MUTE_DISABLED:
# Return ATT Error: Mute Disabled (0x80)
raise ValueError("Remote muting not allowed on this device")
elif new_value == MUTE_MUTED:
print("Muting voice microphone...")
# Disable voice pickup mic in audio pipeline
elif new_value == MUTE_NOT_MUTED:
print("Un-muting voice microphone...")
# Re-enable voice pickup mic
Part 2 Summary
Next: Part 3 — HAUC, HARC, IAC Roles & Security
Part 3 covers the client-side roles: HAUC (unicast streaming), HARC (all 9 preset procedures including Synchronized Locally), IAC (alert client), connection establishment, and the complete security model including LE Security Mode 1 Level 2 and 128-bit key requirements.
