跳到主要内容

iOS 文件与图像传输 SDK 1.0.0 接入指南

SDK Version: 1.0.0

XTalkFileTransferSdk 是独立的 SwiftPM package/product,负责点对点文件字节收发、进度、取消、入站所有权、radio 资源恢复,以及 JPEG/HEIC 图像转码。XTalkImageTranscoder 就在这个 package 内,不是第六个 package。最低平台为 iOS 17,工具链要求 Swift 5.9 或更高版本。

SDK 不直接扫描或连接蓝牙,不负责设备认证、联系人/密钥存储、文件落盘、相册权限、UI 或消息数据库。宿主必须先完成 BLE 连接和设备认证,再注入 transport、独占 radio lease、local short ID,以及按 source short ID 查找的 16-byte control key。

1. SwiftPM 接入

本 monorepo 的本地开发与源码交付从 consumer 目录使用精确路径 ../../Packages/XTalkFileTransferSdk,只给文件/图片 target 选择 XTalkFileTransferSdk product:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
name: "CustomerFileFeature",
platforms: [.iOS(.v17)],
dependencies: [
.package(
name: "XTalkFileTransferSdk",
path: "../../Packages/XTalkFileTransferSdk"
),
],
targets: [
.target(
name: "CustomerFileFeature",
dependencies: [
.product(name: "XTalkFileTransferSdk", package: "XTalkFileTransferSdk"),
]
),
]
)

源码只需要:

import XTalkFileTransferSdk

2. 必须实现的 transport 与 radio lease

public protocol FileTransferTransport: Sendable {
func runAtCommand(
_ command: String,
timeoutMs: Int,
log: Bool
) async throws -> [String]

func sendRfPayload(_ payload: Data, timeoutMs: Int) async throws
}
  • runAtCommand 必须在模块返回 ERROR、超时、断链或取消时抛错,不能把失败伪装成成功行数组。
  • sendRfPayload 必须等待成功的 SEND_FINISH 才返回;负终态、超时、断链和取消都必须抛错。
  • 两个方法都必须协作响应 Swift task cancellation。

FileTransferRadioLeaseAcquisition 必须取得文件传输期间的独占 radio 所有权,并在等待时响应取消。SDK 结束时调用 lease 的 end(.restored)end(.quarantined);宿主不得把 quarantined radio 立即交给 Mesh/PTT 等其他业务。

3. 创建 manager 与事件消费

let manager = XTalkFileTransferManager(
transport: appTransport,
acquireRadioLease: {
try await radioArbiter.acquireFileTransferLease()
}
)

manager.onEvent = { @MainActor event in
switch event {
case .sendProgress(let progress):
print("send", progress.fractionCompleted)
case .receiveStarted(let source, let totalBytes):
print("receive started", source, totalBytes)
case .receiveProgress(let progress):
print("receive", progress.fractionCompleted, progress.weakSignal)
case .receiveFinished(let source, let data, let result):
// Persist only after validating result.outcome == .success.
print("receive finished", source, data?.count as Any, result)
}
}

onEvent 在 MainActor 上串行投递。一个被接受的接收生命周期顺序固定为 receiveStarted → zero or more receiveProgress → exactly one receiveFinished;宿主应以 receiveFinished 为唯一接收终态。

4. 发送文件 bytes

let request = try XTalkFileTransferSendRequest(
data: fileData,
sourceShortID: localShortID,
targetShortID: peerShortID,
controlKey: peerControlKey
)
let result = await manager.send(request)

switch result.outcome {
case .success:
print("sent")
case .cancelledLocal, .cancelledRemote:
print("cancelled")
case .failure(let error):
print("failed", error)
}

发送数据必须为 1...524287 bytes。source/target 是 UInt8,control key 必须恰好 16 bytes。manager 同一时刻只允许一个 send 或 receive;并发请求返回 .failure(.busy)send 不抛协议错误,而是把发送终态和 radio 恢复状态放在 XTalkFileTransferResult;request 构造参数不合法时会抛 XTalkFileTransferError

5. 唯一入站路由

宿主收到每一个候选 RF payload 时,只调用一次:

let ownership = manager.handleInbound(
payload,
metadata: .init(snr: snr, rssiDbm: rssi),
localShortID: localShortID,
controlKeyForSource: { sourceShortID in
keyStore.controlKey(for: sourceShortID) // Data?,必须 16 bytes
}
)

返回值:

  • .notOwned:不是文件传输帧,可继续交给其他协议 owner。
  • .consumed:已由文件传输消费,禁止再交给 Mesh/PTT/Realtime。
  • .rejected(error):形状属于文件传输但被拒绝,同样禁止重复消费;记录错误用于诊断。

不要为缺失 key 设置固定/default 回退。非本机 target、错误 key、CRC/结构错误都必须 fail closed。ACK 由模块/PHY 层产生,应用层不得再发送额外 10-byte ACK。

6. 图像转码与发送

let options = XTalkImageTranscodeOptions(
maximumBytes: 300_000,
maximumPixelDimension: 2_048,
minimumPixelDimension: 320,
minimumQuality: 0.45,
preferredFormat: .jpeg
)
let image = try XTalkImageTranscoder.transcode(sourceImageData, options: options)

let request = try XTalkFileTransferSendRequest(
data: image.data,
sourceShortID: localShortID,
targetShortID: peerShortID,
controlKey: peerControlKey
)
let result = await manager.send(request)

转码器会应用 EXIF orientation、把 alpha 合成到白色背景、不复制源 metadata,并在质量和尺寸之间搜索;输出绝不超过 maximumBytesmaximumBytes 必须为 1...524287,像素尺寸必须为正且 minimum 不大于 maximum,minimumQuality 必须是有限的 0...1

.jpeg.heic 是严格格式要求,不是偏好列表。设备没有 HEIC encoder 时抛 .imageEncodingUnavailable(.heic),不会静默退回 JPEG;无法在最小尺寸/质量下满足预算时抛 .imageExceedsBudget。转码只返回 bytes,不会隐式发送。相册选择、权限和文件命名仍由宿主负责。

7. 取消、结果与恢复

  • cancelSend() -> Bool:仅在有活动发送时返回 true
  • cancelReceive() async -> Bool:等待接收清理和 radio lease 结束;无活动接收返回 false
  • .cancelledLocal.cancelledRemote 分开报告。
  • result.restoration.notRequired.restored.failed(error)。业务成功但恢复失败时也必须隔离 radio 并阻止其他业务继续使用。

文件传输会暂时改变 WORKMODEADDTLRATEANYRATELENFREQ 只在快照阶段读取,并在恢复阶段重写捕获值;传输阶段不会设置 FREQ。SDK 恢复它能够捕获的原值;模块目前没有可靠的 AT+ANYRATELEN? 查询合同,所以无法恢复一个未知旧值。宿主必须让文件传输独占 radio,并在 quarantined 后执行重连/重新认证/重新初始化。

8. 错误速查

错误含义与处理
invalidDataLength / invalidControlKey发送请求的数据长度或 control key 无效;修正输入,不重试 wire
invalidTarget入站 FILE_INIT 的目标 short ID 不是本机;拒绝该帧,不创建接收会话
notConnected / notAuthenticated先连接并认证,再重建请求
busy已有文件收发或 radio 被占用;串行排队
protocolMalformed / crcMismatch / cryptoFailure丢弃当前非法帧并记录,不降级到默认 key
timeout / retryExceeded检查链路与信号,确认 radio 已恢复后再由用户重试
transport(String)transport/模块失败;保留原因,通常需要重连
invalidImageOptions / imageDecodeFailed修正选项或源图片
imageEncodingUnavailable(format)当前平台没有所需 encoder;由用户显式选择另一格式
imageExceedsBudget放宽预算/最小尺寸/最低质量,或拒绝发送
internalFailure(String)不应出现的内部失败;保留日志和复现输入

9. 内存、线程和安全边界

SDK 以内存 Data 组装完整文件,最大约 512 KiB;宿主不要在主线程同步读取大文件或做额外副本。图像转码是同步 CPU 工作,建议从非 MainActor task 调用,再在 MainActor 更新 UI。事件回调是 MainActor;transport 和 key provider 是 Sendable,必须自行保证线程安全。

SDK 1.0.0 的兼容传输协议不提供端到端机密性或完整性保证。不要把文件传输当作端到端安全信道;敏感内容必须在传入 SDK 前由业务层使用 AEAD 加密。后续协议升级需要显式版本协商和明确的旧版本兼容策略,不能在 1.0.0 内静默改变 wire。