import Foundation /// ARRI Camera Access Protocol (CAP) — binary over TCP :5055, big-endian. /// Frame: [length U16][msgType U8][msgId U16][cmdCode U16][payload]. /// length includes the 7-byte header. Verified: docs/research/arri-cap-protocol.md. public enum CAP { public enum MsgType: UInt8, Sendable { case command = 0x01 case reply = 0x02 case event = 0x03 } /// Command codes (verified subset). public enum Command: UInt16, Sendable { case live = 0x0080 case requestPwdChallenge = 0x0081 case password = 0x0082 case clientName = 0x0083 case requestVariables = 0x0084 case unRequestVariables = 0x0085 case setVariable = 0x0086 case welcome = 0x0087 case getFrameGrab = 0x0088 case set3DLutData = 0x008c case saveLookFile = 0x008d case getVariable = 0x0090 case recordStart = 0x00a0 case recordStop = 0x00a1 case getClipList = 0x00a4 } /// Result codes in reply cmdCode field. public enum Result: UInt16, Sendable { case ok = 0x0000 case noSuchCommand = 0x0001 case notAllowed = 0x0002 case noSuchVariables = 0x0003 case wrongPassword = 0x0004 case wrongType = 0x0006 case notAuthorized = 0x0007 case tooManyClients = 0x0008 case protocolError = 0x0009 } /// Variable IDs (verified subset). public enum Variable: UInt16, Sendable, CaseIterable { case cameraState = 0x0040 case lookFilename = 0x0041 case cdlValues = 0x0050 case colorTemperature = 0x0051 case tint = 0x0052 case exposureIndex = 0x0053 case currentReel = 0x005C case clipNumber = 0x005D case sensorFPS = 0x0061 case timecode = 0x0078 } /// Camera state bitfield (variable 0x0040). public struct CameraStateBits: OptionSet, Sendable { public let rawValue: UInt16 public init(rawValue: UInt16) { self.rawValue = rawValue } public static let recording = CameraStateBits(rawValue: 0x0001) public static let playback = CameraStateBits(rawValue: 0x0002) public static let standbyReady = CameraStateBits(rawValue: 0x0004) public static let idle = CameraStateBits(rawValue: 0x0080) } public struct Frame: Equatable, Sendable { public var msgType: UInt8 public var msgId: UInt16 public var cmdCode: UInt16 public var payload: Data public init(msgType: UInt8, msgId: UInt16, cmdCode: UInt16, payload: Data = Data()) { self.msgType = msgType self.msgId = msgId self.cmdCode = cmdCode self.payload = payload } } public enum CodecError: Error, Equatable { case frameTooShort case lengthMismatch } // MARK: Encoding primitives (big-endian) public static func putU16(_ v: UInt16, into data: inout Data) { data.append(UInt8(v >> 8)) data.append(UInt8(v & 0xFF)) } public static func putU32(_ v: UInt32, into data: inout Data) { data.append(UInt8((v >> 24) & 0xFF)) data.append(UInt8((v >> 16) & 0xFF)) data.append(UInt8((v >> 8) & 0xFF)) data.append(UInt8(v & 0xFF)) } public static func putF32(_ v: Float, into data: inout Data) { putU32(v.bitPattern, into: &data) } /// CAP string: U16 length + UTF-8. public static func putString(_ s: String, into data: inout Data) { let bytes = Data(s.utf8) putU16(UInt16(bytes.count), into: &data) data.append(bytes) } public static func readU16(_ data: Data, at offset: Int) -> UInt16? { guard data.count >= offset + 2 else { return nil } let i = data.startIndex + offset return UInt16(data[i]) << 8 | UInt16(data[i + 1]) } public static func readU32(_ data: Data, at offset: Int) -> UInt32? { guard data.count >= offset + 4 else { return nil } let i = data.startIndex + offset return UInt32(data[i]) << 24 | UInt32(data[i + 1]) << 16 | UInt32(data[i + 2]) << 8 | UInt32(data[i + 3]) } public static func readF32(_ data: Data, at offset: Int) -> Float? { readU32(data, at: offset).map { Float(bitPattern: $0) } } public static func readString(_ data: Data, at offset: Int) -> (value: String, consumed: Int)? { guard let len = readU16(data, at: offset), data.count >= offset + 2 + Int(len) else { return nil } let start = data.startIndex + offset + 2 let s = String(data: data.subdata(in: start.. Data { var out = Data() let total = UInt16(7 + frame.payload.count) putU16(total, into: &out) out.append(frame.msgType) putU16(frame.msgId, into: &out) putU16(frame.cmdCode, into: &out) out.append(frame.payload) return out } /// Decode first complete frame from buffer, consuming it. nil if incomplete. public static func decodeFirst(_ buffer: inout Data) throws -> Frame? { guard buffer.count >= 2 else { return nil } guard let length = readU16(buffer, at: 0) else { return nil } guard length >= 7 else { throw CodecError.frameTooShort } guard buffer.count >= Int(length) else { return nil } let base = buffer.startIndex let msgType = buffer[base + 2] guard let msgId = readU16(buffer, at: 3), let cmdCode = readU16(buffer, at: 5) else { return nil } let payload = buffer.subdata(in: base + 7.. "HH:MM:SS:FF". public static func decodeTimecodeBCD(_ v: UInt32) -> String { func bcd(_ byte: UInt32) -> UInt32 { (byte >> 4) * 10 + (byte & 0xF) } let h = bcd((v >> 24) & 0xFF) let m = bcd((v >> 16) & 0xFF) let s = bcd((v >> 8) & 0xFF) let f = bcd(v & 0xFF) return String(format: "%02d:%02d:%02d:%02d", h, m, s, f) } public static func encodeTimecodeBCD(_ tc: String) -> UInt32? { let parts = tc.split(separator: ":").compactMap { UInt32($0) } guard parts.count == 4 else { return nil } func toBCD(_ v: UInt32) -> UInt32 { (v / 10) << 4 | (v % 10) } return toBCD(parts[0]) << 24 | toBCD(parts[1]) << 16 | toBCD(parts[2]) << 8 | toBCD(parts[3]) } /// CDL BLOB: 10×F32 slope(3) offset(3) power(3) sat — order per ASC convention (INFERRED). public static func encodeCDLBlob(slope: (Float, Float, Float), offset: (Float, Float, Float), power: (Float, Float, Float), saturation: Float) -> Data { var out = Data() for v in [slope.0, slope.1, slope.2, offset.0, offset.1, offset.2, power.0, power.1, power.2, saturation] { putF32(v, into: &out) } return out } public static func decodeCDLBlob(_ data: Data) -> [Float]? { guard data.count == 40 else { return nil } return (0..<10).compactMap { readF32(data, at: $0 * 4) } } }