Skip to content

Commit 7e48467

Browse files
dfgHiatusNaraendafeilenhyblocker
authored
Make the VFT work for real this time (#202)
* refactor/port VFT's OpenCV to V4L2 on Linux Co-Authored-By: Nara <18581205+naraenda@users.noreply.github.com> Co-Authored-By: Chelsea Jaggi <1109288+feilen@users.noreply.github.com> * Partition Windows and Linux implementations Co-Authored-By: Hyblocker <7695629+hyblocker@users.noreply.github.com> * Remove Linux code from Windows capture * Make common Utils class, WIP windows (too dark) --------- Co-authored-by: Nara <18581205+naraenda@users.noreply.github.com> Co-authored-by: Chelsea Jaggi <1109288+feilen@users.noreply.github.com> Co-authored-by: Hyblocker <7695629+hyblocker@users.noreply.github.com>
1 parent 1d99001 commit 7e48467

File tree

16 files changed

+1407
-368
lines changed

16 files changed

+1407
-368
lines changed

src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ private async Task VideoCapture_UpdateLoop(CancellationToken ct)
8585
await Task.Delay(1, ct);
8686
}
8787
}
88+
// catch (TaskCanceledException)
89+
// {
90+
// return;
91+
// }
8892
catch(Exception e)
8993
{
9094
SetRawMat(new Mat());

src/Baballonia.OpenCVCapture/OpenCVCaptureFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public bool CanConnect(string address)
2828

2929
return lowered.StartsWith("/dev/video") ||
3030
lowered.EndsWith("appsink") ||
31+
address == "HTC Multimedia Camera" ||
3132
int.TryParse(address, out _) ||
3233
Uri.TryCreate(address, UriKind.Absolute, out _);
3334
}

src/Baballonia.VFTCapture/Baballonia.VFTCapture.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,10 @@
1111
<ProjectReference Include="..\Baballonia.SDK\Baballonia.SDK.csproj" />
1212
</ItemGroup>
1313

14+
<ItemGroup>
15+
<None Update="blluc.dll" Condition="$([MSBuild]::IsOSPlatform('Windows'))">
16+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
17+
</None>
18+
</ItemGroup>
19+
1420
</Project>
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
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

Comments
 (0)