rainbow-dragon/Sources/ForgeCameraARRI/CAPProtocol.swift

208 lines
7.3 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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