如何用Swift实现ExternalAccessory Framework对接IoT设备苹果平台串口通信?
问题解答:Flutter苹果平台(iOS/iPadOS/macOS)USB串口对接IoT设备实现
一、个人使用的可行性说明
- 个人自用无需MFi认证:MFi认证仅针对上架App Store公开分发的应用,如果你只是自己使用(通过Xcode直接部署到自有设备,或用Apple Developer个人账号打包签名安装),完全不需要申请MFi认证,直接使用ExternalAccessory框架即可。
- 签名有效期说明:免费个人账号部署的应用有效期为7天,付费个人账号为1年,到期后重新部署即可继续使用。
二、分步实现指南(Flutter插件+ExternalAccessory)
1. 前置准备
- 拥有Apple Developer账号(免费/付费均可,付费账号支持部署到更多设备)
- 安装最新版Xcode,熟悉Flutter插件基础结构(你已有Swift插件开发经验,此环节可快速推进)
2. 创建Flutter插件项目
- 执行命令生成插件:
flutter create --template=plugin --platforms=ios,macos usb_serial_apple - 进入插件目录,分别打开iOS和macOS的Xcode工程:
ios/usb_serial_apple.xcworkspace、macos/usb_serial_apple.xcworkspace
3. 权限与Info.plist配置
iOS/iPadOS配置
在ios/Runner/Info.plist中添加以下键值对:
<!-- 声明支持的外部配件协议(需与IoT设备的协议完全匹配,比如ESP设备常用自定义协议,提前确认设备协议标识) --> <key>UISupportedExternalAccessoryProtocols</key> <array> <string>com.yourcompany.iotserial</string> <!-- 替换为设备实际协议 --> </array> <!-- 权限申请提示文案 --> <key>NSBluetoothAlwaysUsageDescription</key> <string>需要访问蓝牙以连接USB串口设备</string> <key>NSUSBUsageDescription</key> <string>需要访问USB接口以连接IoT设备</string>
macOS配置
在macos/Runner/Info.plist中添加:
<key>UISupportedExternalAccessoryProtocols</key> <array> <string>com.yourcompany.iotserial</string> <!-- 替换为设备实际协议 --> </array> <key>NSBluetoothAlwaysUsageDescription</key> <string>需要访问蓝牙以连接USB串口设备</string> <key>NSUSBUsageDescription</key> <string>需要访问USB接口以连接IoT设备</string> <!-- macOS专属USB访问权限 --> <key>com.apple.security.device.usb</key> <true/>
4. 原生端(Swift)核心功能实现
在插件的Swift实现文件(如UsbSerialApplePlugin.swift)中,基于EAAccessoryManager和EASession开发核心功能:
4.1 扫描设备
import ExternalAccessory class UsbSerialApplePlugin: NSObject, FlutterPlugin { private var accessoryManager: EAAccessoryManager! private var currentSession: EASession? private var eventChannel: FlutterEventChannel? static func register(with registrar: FlutterPluginRegistrar) { let methodChannel = FlutterMethodChannel(name: "usb_serial_apple", binaryMessenger: registrar.messenger()) let eventChannel = FlutterEventChannel(name: "usb_serial_apple_events", binaryMessenger: registrar.messenger()) let instance = UsbSerialApplePlugin() registrar.addMethodCallDelegate(instance, channel: methodChannel) eventChannel.setStreamHandler(instance) instance.accessoryManager = EAAccessoryManager.shared() instance.eventChannel = eventChannel } func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "scanDevices": let matchedAccessories = accessoryManager.connectedAccessories.filter { $0.protocolStrings.contains("com.yourcompany.iotserial") } let deviceList = matchedAccessories.map { ["name": $0.name, "serialNumber": $0.serialNumber ?? ""] } result(deviceList) // 后续方法:connect、disconnect、sendData等 default: result(FlutterMethodNotImplemented) } } }
4.2 设备连接与数据发送
case "connect": guard let args = call.arguments as? [String: String], let serialNumber = args["serialNumber"] else { result(FlutterError(code: "INVALID_ARGS", message: "缺少设备序列号", details: nil)) return } guard let targetDevice = accessoryManager.connectedAccessories.first(where: { $0.serialNumber == serialNumber }) else { result(FlutterError(code: "DEVICE_NOT_FOUND", message: "未找到指定设备", details: nil)) return } currentSession = EASession(accessory: targetDevice, forProtocol: "com.yourcompany.iotserial") if let session = currentSession { session.inputStream?.open() session.outputStream?.open() session.inputStream?.delegate = self result(true) } else { result(FlutterError(code: "CONNECT_FAILED", message: "设备连接失败", details: nil)) } case "sendData": guard let args = call.arguments as? [String: String], let dataStr = args["data"], let data = dataStr.data(using: .utf8) else { result(FlutterError(code: "INVALID_DATA", message: "数据格式错误", details: nil)) return } currentSession?.outputStream?.write(data, maxLength: data.count) result(true)
4.3 接收设备数据(实现StreamDelegate与FlutterStreamHandler)
extension UsbSerialApplePlugin: StreamDelegate, FlutterStreamHandler { func stream(_ aStream: Stream, handle eventCode: Stream.Event) { switch eventCode { case .hasBytesAvailable: guard let inputStream = aStream as? InputStream else { return } let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: 1024) defer { buffer.deallocate() } let bytesRead = inputStream.read(buffer, maxLength: 1024) if bytesRead > 0 { let receivedData = Data(bytes: buffer, count: bytesRead) if let receivedStr = String(data: receivedData, encoding: .utf8) { eventSink?(["data": receivedStr]) } } case .endEncountered: currentSession?.inputStream?.close() currentSession?.outputStream?.close() currentSession = nil default: break } } func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) { eventSink = events } func onCancel(withArguments arguments: Any?) { eventSink = nil } private var eventSink: FlutterEventSink? }
5. Flutter端封装调用
创建usb_serial_apple.dart,封装原生方法调用:
import 'package:flutter/services.dart'; class UsbSerialApple { static const MethodChannel _methodChannel = MethodChannel('usb_serial_apple'); static const EventChannel _eventChannel = EventChannel('usb_serial_apple_events'); static Future<List<Map<String, dynamic>>> scanDevices() async { final List<dynamic> result = await _methodChannel.invokeMethod('scanDevices'); return result.cast<Map<String, dynamic>>(); } static Future<bool> connect(String serialNumber) async { return await _methodChannel.invokeMethod('connect', {'serialNumber': serialNumber}); } static Future<bool> sendData(String data) async { return await _methodChannel.invokeMethod('sendData', {'data': data}); } static Stream<Map<String, dynamic>> get onDataReceived { return _eventChannel.receiveBroadcastStream().map((event) => event as Map<String, dynamic>); } }
6. 测试与验证
- 用适配的USB转接线将IoT设备连接到苹果设备(iOS/iPadOS用Lightning转USB适配器,macOS直接插USB)
- 在Xcode中选择目标设备运行Flutter应用
- 调用扫描方法确认设备可被识别,测试连接、发送配置指令(如WiFi SSID设置)、接收设备返回数据,验证功能正确性
三、常见问题排查
- 设备扫描不到:检查
UISupportedExternalAccessoryProtocols中的协议与设备完全匹配;iOS端确认USB适配器支持配件模式(廉价适配器可能仅支持充电)。 - 权限被拒绝:在系统设置中手动开启应用的USB/蓝牙权限;macOS需在「系统设置-隐私与安全性」中允许应用访问USB。
- 数据传输异常:确保串口波特率、数据位、停止位等参数与设备配置一致,可在插件中添加参数配置方法适配不同设备。
内容的提问来源于stack exchange,提问作者Pratham Jaiswal
相关产品推荐
相关产品推荐

