209 lines
7.3 KiB
Swift
209 lines
7.3 KiB
Swift
|
|
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..<start + Int(len)), encoding: .utf8) ?? ""
|
|||
|
|
return (s, 2 + Int(len))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: Frame codec
|
|||
|
|
|
|||
|
|
public static func encode(_ frame: Frame) -> 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..<base + Int(length))
|
|||
|
|
buffer.removeFirst(Int(length))
|
|||
|
|
return Frame(msgType: msgType, msgId: msgId, cmdCode: cmdCode, payload: payload)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: Timecode
|
|||
|
|
|
|||
|
|
/// U32 BCD HHMMSSFF -> "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) }
|
|||
|
|
}
|
|||
|
|
}
|