|
| 1 | +using System.Runtime.InteropServices; |
| 2 | +using Microsoft.Extensions.Logging; |
| 3 | + |
| 4 | +namespace Baballonia.VFTCapture.Linux; |
| 5 | + |
| 6 | +public partial class LinuxUsbCommunicator : IDisposable |
| 7 | +{ |
| 8 | + /// <summary> |
| 9 | + /// Native interop functions for Linux. |
| 10 | + /// </summary> |
| 11 | + private sealed partial class LinuxNative |
| 12 | + { |
| 13 | + [LibraryImport("libc", EntryPoint = "close", SetLastError = true)] |
| 14 | + public static partial int Close(IntPtr handle); |
| 15 | + [LibraryImport("libc", EntryPoint = "ioctl", SetLastError = true)] |
| 16 | + public static partial int Ioctl(IntPtr handle, int request, ref UvcXuControlQuery capability); |
| 17 | + |
| 18 | + [LibraryImport("libc", EntryPoint = "open", SetLastError = true)] |
| 19 | + public static partial IntPtr Open([MarshalAs(UnmanagedType.LPStr)] string path, FileOpenFlags flags); |
| 20 | + } |
| 21 | + |
| 22 | + public enum FileOpenFlags |
| 23 | + { |
| 24 | + O_RDONLY = 0x00, |
| 25 | + O_RDWR = 0x02, |
| 26 | + O_NONBLOCK = 0x800, |
| 27 | + O_SYNC = 0x101000 |
| 28 | + } |
| 29 | + |
| 30 | + [StructLayout(LayoutKind.Sequential)] |
| 31 | + private struct UvcXuControlQuery |
| 32 | + { |
| 33 | + public byte unit; |
| 34 | + public byte selector; |
| 35 | + public UvcQuery query; |
| 36 | + public ushort size; |
| 37 | + public IntPtr data; |
| 38 | + } |
| 39 | + |
| 40 | + public enum XuTask : byte |
| 41 | + { |
| 42 | + SET = 0x50, |
| 43 | + GET = 0x51, |
| 44 | + } |
| 45 | + |
| 46 | + public enum XuReg : byte |
| 47 | + { |
| 48 | + SENSOR = 0xab, |
| 49 | + } |
| 50 | + |
| 51 | + public enum UvcQuery : byte |
| 52 | + { |
| 53 | + SET_CUR = 0x01, |
| 54 | + GET_CUR = 0x81, |
| 55 | + GET_MIN = 0x82, |
| 56 | + GET_MAX = 0x83, |
| 57 | + GET_RES = 0x84, |
| 58 | + GET_LEN = 0x85, |
| 59 | + GET_INFO = 0x86, |
| 60 | + GET_DEF = 0x87, |
| 61 | + } |
| 62 | + |
| 63 | + const int _IOC_NRBITS = 8; |
| 64 | + const int _IOC_TYPEBITS = 8; |
| 65 | + const int _IOC_SIZEBITS = 14; |
| 66 | + const int _IOC_DIRBITS = 2; |
| 67 | + |
| 68 | + const int _IOC_NRSHIFT = 0; |
| 69 | + const int _IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS; |
| 70 | + const int _IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS; |
| 71 | + const int _IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS; |
| 72 | + |
| 73 | + const int _IOC_NONE = 0; |
| 74 | + const int _IOC_WRITE = 1; |
| 75 | + const int _IOC_READ = 2; |
| 76 | + |
| 77 | + private static int Ioc(int dir, int type, int nr, int size) |
| 78 | + => (dir << _IOC_DIRSHIFT) | (type << _IOC_TYPESHIFT) | (nr << _IOC_NRSHIFT) | (size << _IOC_SIZESHIFT); |
| 79 | + private static int IocTypeCheck<T>() where T : unmanaged |
| 80 | + => Marshal.SizeOf<T>(); |
| 81 | + private static readonly int _UVC_IOC_CTRL_QUERY = Ioc(_IOC_READ | _IOC_WRITE, 'u', 0x21, IocTypeCheck<UvcXuControlQuery>()); |
| 82 | + |
| 83 | + /// <summary> |
| 84 | + /// The file handle. Should not really be used outside of this clase. |
| 85 | + /// </summary> |
| 86 | + internal IntPtr Handle { get; private set; } = IntPtr.Zero; |
| 87 | + |
| 88 | + /// <summary> |
| 89 | + /// Device control buffer size. For the Vive Face Tracker this |
| 90 | + /// is either 384 or 64. |
| 91 | + /// </summary> |
| 92 | + public ushort BufferSize { get; private set; } = 0; |
| 93 | + |
| 94 | + /// <summary> |
| 95 | + /// Check if the held handle is still valid. |
| 96 | + /// </summary> |
| 97 | + public bool IsValid { get => Handle != IntPtr.Zero; } |
| 98 | + |
| 99 | + private readonly ILogger log; |
| 100 | + |
| 101 | + /// <summary> |
| 102 | + /// Opens a file and wraps the handle. It must point to a Vive Face Tracker device. |
| 103 | + /// </summary> |
| 104 | + /// <exception cref="Exception"></exception> |
| 105 | + public LinuxUsbCommunicator(ILogger logger, string path, FileOpenFlags flags = FileOpenFlags.O_RDWR) |
| 106 | + { |
| 107 | + log = logger; |
| 108 | + |
| 109 | + log.LogDebug($"VFT: opening '{path}' as '{flags}'"); |
| 110 | + Handle = LinuxNative.Open(path, flags); |
| 111 | + var err = Marshal.GetLastPInvokeError(); |
| 112 | + if (err != 0) |
| 113 | + throw new Exception($"Error while opening native file:\n{Marshal.GetLastPInvokeErrorMessage()}"); |
| 114 | + |
| 115 | + log.LogDebug("VFT: validating buffer size"); |
| 116 | + var deviceBufferSz = GetLen(); |
| 117 | + |
| 118 | + log.LogDebug($"VFT: get buffer size: {deviceBufferSz}"); |
| 119 | + if (deviceBufferSz != 384 && deviceBufferSz != 64) |
| 120 | + throw new Exception($"Got unexpected device buffer size: {deviceBufferSz}"); |
| 121 | + |
| 122 | + BufferSize = deviceBufferSz; |
| 123 | + } |
| 124 | + |
| 125 | + /// <summary> |
| 126 | + /// Explicit closing of the handle without waiting for GC. |
| 127 | + /// </summary> |
| 128 | + public void Dispose() |
| 129 | + { |
| 130 | + // We must ensure we actually hold a valid pointer. |
| 131 | + if (IsValid) |
| 132 | + LinuxNative.Close(Handle); |
| 133 | + // After closing, we must ensure the handle cannot be used anymore! |
| 134 | + Handle = IntPtr.Zero; |
| 135 | + } |
| 136 | + |
| 137 | + ~LinuxUsbCommunicator() => Dispose(); |
| 138 | + |
| 139 | + private void XuQueryCur(byte unit, byte selector, UvcQuery query, byte[] data) |
| 140 | + { |
| 141 | + var dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned); |
| 142 | + try |
| 143 | + { |
| 144 | + UvcXuControlQuery c = new() |
| 145 | + { |
| 146 | + unit = unit, |
| 147 | + selector = selector, |
| 148 | + query = query, |
| 149 | + size = (ushort)data.Length, |
| 150 | + data = dataHandle.AddrOfPinnedObject() |
| 151 | + }; |
| 152 | + var res = LinuxNative.Ioctl(Handle, _UVC_IOC_CTRL_QUERY, ref c); |
| 153 | + if (res != 0) |
| 154 | + { |
| 155 | + int err = Marshal.GetLastPInvokeError(); |
| 156 | + string msg = Marshal.GetLastPInvokeErrorMessage(); |
| 157 | + throw new Exception( |
| 158 | + $"Error in ioctl uvc_xu_control_query request:\n" + |
| 159 | + $" {{q:{query},sz:{data.Length}}}\n" + |
| 160 | + $" {{r:{res},e:{err}}}: {msg}"); |
| 161 | + } |
| 162 | + } |
| 163 | + finally |
| 164 | + { |
| 165 | + dataHandle.Free(); |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + private void XuSetCur(byte selector, byte[] data) |
| 170 | + => XuQueryCur(4, selector, UvcQuery.SET_CUR, data); |
| 171 | + |
| 172 | + private byte[] XuGetCur(byte selector, int len) |
| 173 | + { |
| 174 | + byte[] data = new byte[len]; |
| 175 | + XuQueryCur(4, selector, UvcQuery.GET_CUR, data); |
| 176 | + return data; |
| 177 | + } |
| 178 | + |
| 179 | + private ushort GetLen(byte selector = 2) |
| 180 | + { |
| 181 | + byte[] data = new byte[2]; |
| 182 | + XuQueryCur(4, selector, UvcQuery.GET_LEN, data); |
| 183 | + return (ushort)(data[1] << 8 | data[0]); |
| 184 | + } |
| 185 | + |
| 186 | + private byte SetCur(byte[] data, int timeout = 1000) |
| 187 | + { |
| 188 | + if (data.Length != BufferSize) |
| 189 | + throw new Exception($"Got incorrect buffer size: {data.Length} expected: {BufferSize}"); |
| 190 | + |
| 191 | + XuSetCur(2, data); |
| 192 | + |
| 193 | + if (timeout <= 0) |
| 194 | + return 0; |
| 195 | + |
| 196 | + CancellationTokenSource cts = new(timeout); |
| 197 | + while (!cts.Token.IsCancellationRequested) |
| 198 | + { |
| 199 | + byte[] result = XuGetCur(2, BufferSize); |
| 200 | + switch (result[0]) |
| 201 | + { |
| 202 | + case 0x55: // Not ready. |
| 203 | + // Can do a lil' eep. |
| 204 | + Thread.Sleep(1); |
| 205 | + break; |
| 206 | + case 0x56: |
| 207 | + if (Enumerable.SequenceEqual(data[0..16], result[1..17])) |
| 208 | + { |
| 209 | + return result[17]; |
| 210 | + } |
| 211 | + throw new Exception($"Invalid response sequence: {result[1..17]} expected: {data[0..16]}"); |
| 212 | + default: |
| 213 | + throw new Exception($"Unexpected response from XU command: {result[0]}"); |
| 214 | + } |
| 215 | + } |
| 216 | + throw new TimeoutException("Got no rersponse from device."); |
| 217 | + } |
| 218 | + |
| 219 | + /// <summary> |
| 220 | + /// Applies a task to a register. |
| 221 | + /// </summary> |
| 222 | + private byte RegisterTask(XuTask task, XuReg reg, byte addr, byte value = 0x00) |
| 223 | + { |
| 224 | + byte[] data = new byte[BufferSize]; |
| 225 | + |
| 226 | + // Because we only need to set bytes, we can hardcode |
| 227 | + // most of the values here. |
| 228 | + data[00] = (byte)task; |
| 229 | + data[01] = (byte)reg; |
| 230 | + data[02] = 0x60; |
| 231 | + data[03] = 0x01; // 1 byte sized address. |
| 232 | + data[04] = 0x01; // 1 byte sized value. |
| 233 | + // Address. |
| 234 | + data[05] = 0x00; |
| 235 | + data[06] = 0x00; |
| 236 | + data[07] = 0x00; |
| 237 | + data[08] = addr; |
| 238 | + // Page address. |
| 239 | + data[09] = 0x90; |
| 240 | + data[10] = 0x01; |
| 241 | + data[11] = 0x00; |
| 242 | + data[12] = 0x01; |
| 243 | + // Value. |
| 244 | + data[13] = 0x00; |
| 245 | + data[14] = 0x00; |
| 246 | + data[15] = 0x00; |
| 247 | + data[16] = value; |
| 248 | + |
| 249 | + return SetCur(data); |
| 250 | + } |
| 251 | + |
| 252 | + private void SetRegister(XuReg reg, byte addr, byte value) |
| 253 | + => RegisterTask(XuTask.SET, reg, addr, value); |
| 254 | + |
| 255 | + private byte GetRegister(XuReg reg, byte addr) |
| 256 | + => RegisterTask(XuTask.GET, reg, addr); |
| 257 | + |
| 258 | + private void SetRegisterSensor(byte addr, byte value) |
| 259 | + => SetRegister(XuReg.SENSOR, addr, value); |
| 260 | + |
| 261 | + private void GetRegisterSensor(byte addr) |
| 262 | + => GetRegister(XuReg.SENSOR, addr); |
| 263 | + |
| 264 | + private void SetEnableStream(bool enable) |
| 265 | + { |
| 266 | + log.LogDebug($"VFT: set stream: {(enable ? "on" : "off")}"); |
| 267 | + byte[] data = new byte[BufferSize]; |
| 268 | + data[0] = (byte)XuTask.SET; |
| 269 | + data[1] = 0x14; // Magic numbers, this does not have |
| 270 | + data[2] = 0x00; // the same pattern as RegisterTask! |
| 271 | + data[3] = (byte)(enable ? 0x01 : 0x00); |
| 272 | + SetCur(data); |
| 273 | + } |
| 274 | + |
| 275 | + private void SendMagicPacket() |
| 276 | + { |
| 277 | + // I have no clue why we need to send this magic packet. |
| 278 | + log.LogDebug("VFT: sending magic packet"); |
| 279 | + byte[] data = new byte[BufferSize]; |
| 280 | + data[0] = 0x51; // Magic numbers, this does not have |
| 281 | + data[1] = 0x52; // the same pattern as RegisterTask! |
| 282 | + if (BufferSize >= 256) |
| 283 | + { |
| 284 | + // Need to set magic numbers on large buffers. |
| 285 | + data[254] = 0x53; |
| 286 | + data[255] = 0x54; |
| 287 | + } |
| 288 | + SetCur(data); |
| 289 | + } |
| 290 | + |
| 291 | + /// <summary> |
| 292 | + /// Toggles the state of the camera. |
| 293 | + /// </summary> |
| 294 | + public void SetState(bool enabled) |
| 295 | + { |
| 296 | + SendMagicPacket(); |
| 297 | + SetEnableStream(false); |
| 298 | + |
| 299 | + Thread.Sleep(100); |
| 300 | + |
| 301 | + // Set infra-red LED state. |
| 302 | + byte irLedState = (byte)(enabled ? 0xff : 0x00); |
| 303 | + log.LogDebug($"VFT: set camera: {(enabled ? "on" : "off")}"); |
| 304 | + SendMagicPacket(); |
| 305 | + SetRegisterSensor(0x00, 0x40); |
| 306 | + SetRegisterSensor(0x08, 0x01); |
| 307 | + SetRegisterSensor(0x70, 0x00); |
| 308 | + SetRegisterSensor(0x02, irLedState); |
| 309 | + SetRegisterSensor(0x03, irLedState); |
| 310 | + SetRegisterSensor(0x04, irLedState); |
| 311 | + SetRegisterSensor(0x0e, 0x00); |
| 312 | + SetRegisterSensor(0x05, 0xb2); |
| 313 | + SetRegisterSensor(0x06, 0xb2); |
| 314 | + SetRegisterSensor(0x07, 0xb2); |
| 315 | + SetRegisterSensor(0x0f, 0x03); |
| 316 | + |
| 317 | + // On enable, restore the stream. |
| 318 | + // On disable, skip this. |
| 319 | + if (enabled) |
| 320 | + { |
| 321 | + Thread.Sleep(100); |
| 322 | + |
| 323 | + SendMagicPacket(); |
| 324 | + SetEnableStream(true); |
| 325 | + } |
| 326 | + } |
| 327 | +} |
0 commit comments