// // PTPUtilities.swift // otg_swift // // Created by hanqiu on 2026/7/3. // import Foundation @preconcurrency import ImageCaptureCore /// PTP Container 类型字段(ISO 15740)。 enum PTPContainerType: UInt16 { /// 主机发出的操作命令包 case command = 0x0001 /// 伴随命令的数据阶段(out/in Data) case data = 0x0002 /// 设备对命令的响应包 case response = 0x0003 /// 设备主动上报的事件包(如 Sony SDIE) case event = 0x0004 } /// 解析后的 PTP 容器:命令、数据、响应或事件包。 struct PTPContainer { let length: UInt32 let type: UInt16 let code: UInt16 let transactionID: UInt32 let params: [UInt32] } /// PTP Container 解析失败原因。 enum PTPParseError: Error { /// 数据长度不足 12 字节(Container 最小头) case tooShort /// length 字段与 buffer 实际长度不一致 case invalidLength } /// 标准 PTP 操作码与响应码(ISO 15740)。 enum PTPStandardCommand { /// GetDeviceInfo (0x1001),用于 PTP 管线 Probe static let getDeviceInfo: UInt16 = 0x1001 /// GetObjectInfo (0x1008),Probe 虚拟对象元数据 static let getObjectInfo: UInt16 = 0x1008 /// GetObject (0x1009),下载对象二进制(Step 6) static let getObject: UInt16 = 0x1009 /// ObjectAdded 事件 (0x4002) static let objectAdded: UInt16 = 0x4002 /// RequestObjectTransfer 事件 (0x4009) static let requestObjectTransfer: UInt16 = 0x4009 /// Response OK (0x2001),命令成功响应码 static let responseOK: UInt16 = 0x2001 } /// PTP 包构建、解析与经 ImageCaptureCore 发送的通用工具。 enum PTPHelper { // MARK: - Build /// 构建 PTP Command Container(小端序,length = 12 + 4×参数个数)。 static func buildCommand( code: UInt16, transactionID: UInt32, parameters: [UInt32] = [] ) -> Data { let length = UInt32(12 + parameters.count * 4) var data = Data() appendUInt32LE(length, to: &data) appendUInt16LE(PTPContainerType.command.rawValue, to: &data) appendUInt16LE(code, to: &data) appendUInt32LE(transactionID, to: &data) for parameter in parameters { appendUInt32LE(parameter, to: &data) } return data } // MARK: - Parse /// 从二进制数据解析 PTP Container;参数区按 4 字节对齐读取。 static func parseContainer(_ data: Data) throws -> PTPContainer { guard data.count >= 12 else { throw PTPParseError.tooShort } let length = readUInt32LE(data, offset: 0) guard length >= 12, data.count >= Int(length) else { throw PTPParseError.invalidLength } let type = readUInt16LE(data, offset: 4) let code = readUInt16LE(data, offset: 6) let transactionID = readUInt32LE(data, offset: 8) var params: [UInt32] = [] var offset = 12 while offset + 4 <= Int(length) { params.append(readUInt32LE(data, offset: offset)) offset += 4 } return PTPContainer( length: length, type: type, code: code, transactionID: transactionID, params: params ) } /// 从 PTP Event Container 提取事件码(如 Sony 0xC203)。 static func parseEventCode(from eventData: Data) -> UInt16? { try? parseContainer(eventData).code } static func isResponseOK(_ response: Data) -> Bool { guard !response.isEmpty else { return true } guard let parsed = try? parseContainer(response) else { return false } return parsed.code == PTPStandardCommand.responseOK } static func hexString(_ data: Data) -> String { data.map { String(format: "%02X", $0) }.joined(separator: " ") } /// 同一文件名+大小视为同一张待传照片,用于边拍边传去重。 static func objectSignature(filename: String, size: UInt32) -> String { "\(filename)_\(size)" } /// ObjectInfo 固定布局:offset 8 为 CompressedSize(UInt32 LE)。 static func parseObjectCompressedSize(_ data: Data) -> UInt32? { guard data.count >= 12 else { return nil } return readUInt32LE(data, offset: 8) } /// 从 ObjectInfo 解析文件名;优先 PTP 字符串格式,失败则扫描 UTF-16LE。 static func parseObjectInfoFilename(_ data: Data) -> String? { guard data.count >= 53 else { return nil } if let filename = parsePTPString(data, offset: 52)?.string, !filename.isEmpty { return sanitizeFilename(filename) } var offset = 52 var codeUnits: [UInt16] = [] while offset + 1 < data.count { let unit = readUInt16LE(data, offset: offset) if unit == 0 { break } codeUnits.append(unit) offset += 2 } guard !codeUnits.isEmpty else { return nil } let raw = String(utf16CodeUnits: codeUnits, count: codeUnits.count) return sanitizeFilename(raw) } /// PTP 字符串:1 字节字符数 + UTF-16LE 字符序列(无 NUL 终止)。 static func parsePTPString(_ data: Data, offset start: Int) -> (string: String, nextOffset: Int)? { guard start < data.count else { return nil } let charCount = Int(data[start]) var offset = start + 1 guard charCount > 0, offset + charCount * 2 <= data.count else { return nil } var codeUnits: [UInt16] = [] codeUnits.reserveCapacity(charCount) for index in 0.. String { let withoutNulls = name.replacingOccurrences(of: "\0", with: "") let trimmed = withoutNulls.trimmingCharacters(in: .whitespacesAndNewlines.union(.controlCharacters)) let invalid = CharacterSet(charactersIn: "/\\:?%*|\"<>") let cleaned = trimmed.components(separatedBy: invalid).joined(separator: "_") return cleaned.isEmpty ? "photo.jpg" : cleaned } /// 粗略判断 inData 是否为 JPEG/PNG 或足够大的二进制块。 static func isLikelyImageData(_ data: Data) -> Bool { guard data.count >= 3 else { return false } if data[0] == 0xFF, data[1] == 0xD8, data[2] == 0xFF { return true } if data.count >= 4, data[0] == 0x89, data[1] == 0x50, data[2] == 0x4E, data[3] == 0x47 { return true } return data.count > 1024 } // MARK: - Send struct CommandResult { let success: Bool let response: Data let inData: Data } /// 经 `requestSendPTPCommand` 发送 PTP 命令;调用方传入并递增 `transactionID`。 /// 必须在主线程发起(ImageCaptureCore 要求),completion 内 resume continuation。 @MainActor static func sendCommand( on camera: ICCameraDevice, code: UInt16, transactionID: inout UInt32, parameters: [UInt32] = [], outData: Data? = nil, label: String ) async -> CommandResult { let currentID = transactionID transactionID += 1 let commandData = buildCommand( code: code, transactionID: currentID, parameters: parameters ) OTGLog.info(.ptp, "PTP command sending: \(label), bytes=\(commandData.count)") return await withCheckedContinuation { continuation in let finish: (CommandResult) -> Void = { result in continuation.resume(returning: result) } let sendPTP = { camera.requestSendPTPCommand( commandData, outData: outData, completion: { inData, response, error in OTGLog.info( .ptp, "PTP callback received: \(label), inData=\(inData.count), response=\(response.count)" ) if let error { OTGLog.error(.ptp, "PTP \(label) failed: \(error.localizedDescription)") finish(CommandResult(success: false, response: response, inData: inData)) return } var ok = true if !response.isEmpty { if let parsed = try? parseContainer(response) { // 非 0x2001 ResponseOK 视为失败 if parsed.code != PTPStandardCommand.responseOK { ok = false OTGLog.warning( .ptp, "PTP \(label) response code=0x\(String(format: "%04X", parsed.code))" ) } } else { ok = false OTGLog.warning(.ptp, "PTP \(label) response parse failed") } } if ok { OTGLog.info(.ptp, "PTP \(label) OK, inData=\(inData.count) bytes") } finish(CommandResult(success: ok, response: response, inData: inData)) } ) } // ImageCaptureCore PTP API 要求从主线程调用,方法本身由 MainActor 隔离。 sendPTP() } } // MARK: - Little Endian static func appendUInt16LE(_ value: UInt16, to data: inout Data) { var littleEndian = value.littleEndian withUnsafeBytes(of: &littleEndian) { raw in data.append(contentsOf: raw) } } static func appendUInt32LE(_ value: UInt32, to data: inout Data) { var littleEndian = value.littleEndian withUnsafeBytes(of: &littleEndian) { raw in data.append(contentsOf: raw) } } static func readUInt16LE(_ data: Data, offset: Int) -> UInt16 { UInt16(data[offset]) | (UInt16(data[offset + 1]) << 8) } static func readUInt32LE(_ data: Data, offset: Int) -> UInt32 { UInt32(data[offset]) | (UInt32(data[offset + 1]) << 8) | (UInt32(data[offset + 2]) << 16) | (UInt32(data[offset + 3]) << 24) } }