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.
| 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 |
| 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 |
| 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 |
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
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
3C 02 10 01 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x1001
Length: 0x0000
End: 0x3E
3C 02 10 02 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x1002
Length: 0x0000
End: 0x3E
3C 02 10 03 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x1003
Length: 0x0000
End: 0x3E
3C 02 10 04 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x1004
Length: 0x0000
End: 0x3E
3C 02 10 05 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x1005
Length: 0x0000
End: 0x3E
3C 02 10 0A 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x100A
Length: 0x0000
End: 0x3E
3C 02 10 0B 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x100B
Length: 0x0000
End: 0x3E
3C 02 20 00 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x2000
Length: 0x0000
End: 0x3E
3C 02 20 01 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x2001
Length: 0x0000
End: 0x3E
3C 02 20 02 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x2002
Length: 0x0000
End: 0x3E
3C 02 30 00 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x3000
Length: 0x0000
End: 0x3E
3C 02 30 01 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x3001
Length: 0x0000
End: 0x3E
3C 02 40 00 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x4000
Length: 0x0000
End: 0x3E
3C 02 40 01 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x4001
Length: 0x0000
End: 0x3E
3C 02 50 00 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x5000
Length: 0x0000
End: 0x3E
3C 02 50 01 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x5001
Length: 0x0000
End: 0x3E
3C 02 50 02 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x5002
Length: 0x0000
End: 0x3E
3C 02 50 03 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x5003
Length: 0x0000
End: 0x3E
3C 00 60 00 00 00 3E
Start: 0x3C
Type: 0x00 (Request)
Command: 0x6000
Length: 0x0000
End: 0x3E
3C 02 60 00 00 01 08 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x6000
Length: 0x0001
Data: 0x08
End: 0x3E
3C 00 60 01 00 01 08 3E
Start: 0x3C
Type: 0x00 (Request)
Command: 0x6001
Length: 0x0001
Data: 0x08
End: 0x3E
3C 02 99 99 00 00 3E
Start: 0x3C
Type: 0x02 (Notify)
Command: 0x9999
Length: 0x0000
End: 0x3E
<key>UISupportedExternalAccessoryProtocols</key>
<array>
<string>com.pttsdk.ea.protocol</string>
</array>
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)")
}
}
}
| Rev | Date | Changes |
|---|---|---|
| 1.1 | May 15, 2026 | Add Get and Set Config |
| 1.0 | Feb 18, 2026 | Initial draft |