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,并在质量和尺寸之间搜索;输出绝不超过 maximumBytes。maximumBytes 必须为 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 并阻止其他业务继续使用。
文件传输会暂时改变 WORKMODE、ADDTL、RATE 和 ANYRATELEN。FREQ 只在快照阶段读取,并在恢复阶段重写捕获值;传输阶段不会设置 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。