MFi PTT Protocol Specification

Version: 1.1
EA Protocol Name: com.pttsdk.ea.protocol
Date: May 15, 2026

A custom MFi protocol for certified accessories (e.g., headsets, speaker‑mics, control panels) to interact with iOS apps for Push‑to‑Talk with low latency and secure, reliable behavior across devices.

Introduction

A Custom MFi Protocol is a proprietary communication standard developed under Apple's Made for iPhone/iPad (MFi) program. It enables certified accessories to interact with iOS devices using specialized commands beyond standard USB or Bluetooth profiles.

Benefits

  • Optimized Performance: Real‑time control of PTT functions like press/release, channel switching, and emergency signaling with minimal latency.
  • Secure Communication: Uses Apple's authentication and optional encryption to ensure trusted accessory‑device interaction.
  • Extended Functionality: Supports advanced features like AI assistant activation and device status reporting.
  • User Experience Consistency: Reliable behavior across certified accessories, improving usability in mission‑critical environments.
  • Integration Flexibility: Tailorable to public safety, logistics, or enterprise communication workflows.

PTT commands

Command Name Command ID (Hex) Description
PTT_PRESS 0x1001 PTT button pressed
PTT_LONG_PRESS 0x1002 PTT button long pressed
PTT_DOUBLE_PRESS 0x1003 PTT button double pressed
PTT_TRIPLE_PRESS 0x1004 PTT button triple pressed
PTT_RELEASE 0x1005 PTT button released
CHANNEL_UP 0x100A Switch to next channel
CHANNEL_DOWN 0x100B Switch to previous channel

Call control commands

Command Name Command ID (Hex) Description
ACCEPT_CALL 0x2000 Accept incoming call
END_CALL 0x2001 End current call
HOLD_CALL 0x2002 Place call on hold

Other commands

Command Name Command ID (Hex) Description
MUTE 0x3000 Mute microphone
UNMUTE 0x3001 Unmute microphone
AI_ASSISTANT 0x4000 Activate AI assistant
REPLAY 0x4001 Replay last received message
STATUS_TOGGLE 0x5000 Toggle status (Available → Busy → Solo)
STATUS_AVAILABLE 0x5001 Set status available
STATUS_BUSY 0x5002 Set status busy
STATUS_SOLO 0x5003 Set status solo
GET_CONFIG 0x6000 Get configuration
SET_CONFIG 0x6001 Set configuration
EMERGENCY_TRIGGER 0x9999 Emergency alert

Packet Structures

Request / Notify Packet Structure

        
byte 1: 0x3C                        // Start 
byte 2: 0x00 or 0x02                // Command type (0x00 = Request, 0x02 = Notify)
byte 3-4: Command (UInt16)          // Command ID (big-endian)
byte 5-6: Data length (UInt16)      // Payload length (big-endian)
byte 7..(7+N): Data (optional)      // Payload bytes
byte 8+N: 0x3E                      // End byte
            
        

Response Packet Structure

        
byte 1: 0x3C                        // Start byte
byte 2: 0x01                        // Response type
byte 3-4: Command (UInt16)          // Original command ID (big-endian)
byte 5-6: Response code (UInt16)    // Response code (big-endian)
byte 7: 0x3E                        // End byte
            
        

Payload Examples

PTT_PRESS
3C 02 10 01 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x1001
Length: 0x0000
End: 0x3E

PTT_LONG_PRESS
3C 02 10 02 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x1002
Length: 0x0000
End: 0x3E

PTT_DOUBLE_PRESS
3C 02 10 03 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x1003
Length: 0x0000
End: 0x3E

PTT_TRIPLE_PRESS
3C 02 10 04 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x1004
Length: 0x0000
End: 0x3E

PTT_RELEASE
3C 02 10 05 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x1005
Length: 0x0000
End: 0x3E

CHANNEL_UP
3C 02 10 0A 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x100A
Length: 0x0000
End: 0x3E

CHANNEL_DOWN
3C 02 10 0B 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x100B
Length: 0x0000
End: 0x3E

ACCEPT_CALL
3C 02 20 00 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x2000
Length: 0x0000
End: 0x3E

END_CALL
3C 02 20 01 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x2001
Length: 0x0000
End: 0x3E

HOLD_CALL
3C 02 20 02 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x2002
Length: 0x0000
End: 0x3E

MUTE
3C 02 30 00 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x3000
Length: 0x0000
End: 0x3E

UNMUTE
3C 02 30 01 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x3001
Length: 0x0000
End: 0x3E

AI_ASSISTANT
3C 02 40 00 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x4000
Length: 0x0000
End: 0x3E

REPLAY
3C 02 40 01 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x4001
Length: 0x0000
End: 0x3E

STATUS_TOGGLE
3C 02 50 00 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x5000
Length: 0x0000
End: 0x3E

STATUS_AVAILABLE
3C 02 50 01 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x5001
Length: 0x0000
End: 0x3E

STATUS_BUSY
3C 02 50 02 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x5002
Length: 0x0000
End: 0x3E

STATUS_SOLO
3C 02 50 03 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x5003
Length: 0x0000
End: 0x3E

GET_CONFIG (Request)
3C 00 60 00 00 00 3E

Start: 0x3C
Type: 0x00 (Request)
Command: 0x6000
Length: 0x0000
End: 0x3E

GET_CONFIG (Notify)
3C 02 60 00 00 01 08 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x6000
Length: 0x0001
Data: 0x08
End: 0x3E

SET_CONFIG
3C 00 60 01 00 01 08 3E

Start: 0x3C
Type: 0x00 (Request)
Command: 0x6001
Length: 0x0001
Data: 0x08
End: 0x3E

EMERGENCY_TRIGGER
3C 02 99 99 00 00 3E

Start: 0x3C
Type: 0x02 (Notify)
Command: 0x9999
Length: 0x0000
End: 0x3E

iOS Reference Code Sample

Info.plist Changes


    <key>UISupportedExternalAccessoryProtocols</key>
    <array>
    <string>com.pttsdk.ea.protocol</string>
    </array>

Swift — Connect and Parse Changes


    import Foundation
    import ExternalAccessory

    class MFiAccessoryManager: NSObject, StreamDelegate {
    private var session: EASession?
    private var inputStream: InputStream?
    private var outputStream: OutputStream?
    private let protocolString = "com.pttsdk.ea.protocol"

    func connect() {
    let accessories = EAAccessoryManager.shared().connectedAccessories
    guard let accessory = accessories.first(where: { $0.protocolStrings.contains(protocolString) }) else {
    print("No accessory found for protocol \(protocolString)")
    return
    }

    session = EASession(accessory: accessory, forProtocol: protocolString)
    inputStream = session?.inputStream
    outputStream = session?.outputStream

    [inputStream, outputStream].forEach { stream in
    stream?.delegate = self
    stream?.schedule(in: .current, forMode: .default)
    stream?.open()
    }
    }

    // MARK: - StreamDelegate
    func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    switch eventCode {
    case .hasBytesAvailable:
    parseIncomingData()
    case .errorOccurred:
    print("Stream error: \(String(describing: aStream.streamError))")
    case .endEncountered:
    print("Stream ended")
    default:
    break
    }
    }

    private func parseIncomingData() {
    guard let inputStream = inputStream else { return }
    var buffer = [UInt8](repeating: 0, count: 1024)
    let bytesRead = inputStream.read(&buffer, maxLength: buffer.count)
    guard bytesRead > 0 else { return }

    let data = Array(buffer.prefix(bytesRead))
    var index = 0

    while index < data.count {
    guard data[index] == 0x3C else {
    index += 1
    continue
    }

    guard index + 6 < data.count else { break }

    let commandType = data[index + 1]
    let commandID = UInt16(data[index + 2]) << 8 | UInt16(data[index + 3])
    let dataLength = UInt16(data[index + 4]) << 8 | UInt16(data[index + 5])

    let payloadStart = index + 6
    let payloadEnd = payloadStart + Int(dataLength)

    guard payloadEnd < data.count, data[payloadEnd] == 0x3E else {
    print("Invalid or incomplete message")
    break
    }

    let payload = Array(data[payloadStart..<payloadEnd])
    handleCommand(commandType: commandType, commandID: commandID, payload: payload)
    index = payloadEnd + 1
    }
    }

    private func handleCommand(commandType: UInt8, commandID: UInt16, payload: [UInt8]) {
    let hexID = String(format: "0x%04X", commandID)
    print("Received Type: \(commandType), ID: \(hexID), Payload: \(payload)")

    switch commandID {
    case 0x1001: print("PTT_PRESS")
    case 0x1002: print("PTT_LONG_PRESS")
    case 0x1003: print("PTT_DOUBLE_PRESS")
    case 0x1004: print("PTT_TRIPLE_PRESS")
    case 0x1005: print("PTT_RELEASE")
    case 0x100A: print("CHANNEL_UP")
    case 0x100B: print("CHANNEL_DOWN")
    case 0x2000: print("ACCEPT_CALL")
    case 0x2001: print("END_CALL")
    case 0x2002: print("HOLD_CALL")
    case 0x3000: print("MUTE")
    case 0x3001: print("UNMUTE")
    case 0x4000: print("AI_ASSISTANT")
    case 0x4001: print("REPLAY")
    case 0x5000: print("STATUS_TOGGLE")
    case 0x5001: print("STATUS_AVAILABLE")
    case 0x5002: print("STATUS_BUSY")
    case 0x5003: print("STATUS_SOLO")
    case 0x9999: print("EMERGENCY_TRIGGER")
    default:     print("Unknown command: \(hexID)")
    }
    }
    }


Revisions

Rev Date Changes
1.1 May 15, 2026 Add Get and Set Config
1.0 Feb 18, 2026 Initial draft