You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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设置)、接收设备返回数据,验证功能正确性

三、常见问题排查

  1. 设备扫描不到:检查UISupportedExternalAccessoryProtocols中的协议与设备完全匹配;iOS端确认USB适配器支持配件模式(廉价适配器可能仅支持充电)。
  2. 权限被拒绝:在系统设置中手动开启应用的USB/蓝牙权限;macOS需在「系统设置-隐私与安全性」中允许应用访问USB。
  3. 数据传输异常:确保串口波特率、数据位、停止位等参数与设备配置一致,可在插件中添加参数配置方法适配不同设备。

内容的提问来源于stack exchange,提问作者Pratham Jaiswal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 02:02:26