rainbow-dragon/Sources/ForgeCameraARRI/CAPProtocol.swift

209 lines
7.3 KiB
Swift
Raw Permalink Normal View History

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) }
}
}