From 3105fa057c3f684893697cd163e27a7a8ce0b276 Mon Sep 17 00:00:00 2001 From: ka-ART <25586944+ka-ART@users.noreply.github.com> Date: Sat, 4 May 2024 22:11:31 +0200 Subject: [PATCH] UDP connection through stateful firewall; fixing GitHub issue #6 --- CHANGELOG.md | 5 + .../dtrack-fingertracking-administration.png | Bin 0 -> 30319 bytes Documentation~/images/unity-dtrack-source.png | Bin 24740 -> 28940 bytes README.md | 44 +++- .../Source/DTrackObjects/DTrackMeaTool.cs | 56 +++++ .../DTrackObjects/DTrackMeaTool.cs.meta | 11 + .../Source/DTrackObjects/Frame.cs | 69 +++++- Sdk/DTrackSDK-CSharp/Source/DTrackSDK.cs | 232 +++++++++++++++++- .../Source/Parsers/BodyParser.cs | 11 +- .../Source/Parsers/FlystickParser.cs | 8 +- .../Source/Parsers/HandParser.cs | 11 +- .../Source/Parsers/MeaToolParser.cs | 129 ++++++++++ .../Source/Parsers/MeaToolParser.cs.meta | 11 + Sdk/DTrackSDK-CSharp/Source/Parsers/Parser.cs | 76 +++++- .../Source/Parsers/TimestampParser.cs | 20 +- Sdk/DTrackSDK-CSharp/Source/Util/Statics.cs | 23 +- Source/DTrack.cs | 21 +- Source/Receivers/DTrackReceiver.cs | 4 + Source/Receivers/DTrackReceiver6Dof.cs | 7 +- Source/Receivers/DTrackReceiverFlystick.cs | 8 +- Source/Receivers/DTrackReceiverHand.cs | 7 +- package.json | 2 +- 22 files changed, 695 insertions(+), 60 deletions(-) create mode 100644 Documentation~/images/dtrack-fingertracking-administration.png create mode 100644 Sdk/DTrackSDK-CSharp/Source/DTrackObjects/DTrackMeaTool.cs create mode 100644 Sdk/DTrackSDK-CSharp/Source/DTrackObjects/DTrackMeaTool.cs.meta create mode 100644 Sdk/DTrackSDK-CSharp/Source/Parsers/MeaToolParser.cs create mode 100644 Sdk/DTrackSDK-CSharp/Source/Parsers/MeaToolParser.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 8382f5f..cf5034f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # List of ART DTRACK Plugin for Unity Game Engine releases +## v1.1.3 (2024-05-03) + +- (Optionally) enables UDP connection through stateful firewall +- Fixing DTrack1 parsing error (cf. GitHub issue #6) + ## v1.1.2 (2023-04-01) - Some reorganization to allow release as UPM package, esp. in OpenUPM diff --git a/Documentation~/images/dtrack-fingertracking-administration.png b/Documentation~/images/dtrack-fingertracking-administration.png new file mode 100644 index 0000000000000000000000000000000000000000..b13057c9872b68b5c747763f8751a929c35ebaf1 GIT binary patch literal 30319 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 BrWODI literal 0 HcmV?d00001 diff --git a/Documentation~/images/unity-dtrack-source.png b/Documentation~/images/unity-dtrack-source.png index dd87830496b28d6b3bb3832d5ce5dfeed3f58f32..0f4b6725c95b4e5bdb46ddd035e5f1bdf4121f85 100644 GIT binary patch literal 28940 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

Kyh)#!r6or1cm9XTTW*RBO>)Gz(4c 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 i{m%yX*MnT+d0Q#1a) -- Validated with Unity Editors 2019.4 to 2022.3 +- Validated with Unity Editors 2019.4 to 2023.2 (but should work with any later Unity) - Supports DTRACK room calibration modes 'Powerwall' and 'Normal' - Supports tracking of 6DOF Standard Bodies and Flystick2, Flystick2+ and Flystick3 - Supports Flystick buttons and joystick/trigger (emitting Unity events) - Supports Fingertracking, with 'Leap Motion Realistic Male/Female Hands' (by Storkplay) +- (Optionally) enables UDP connection through stateful firewall ## Installing the ART DTRACK Plugin There are several ways to get the ART DTRACK Plugin. -### GitHub +### Unity Asset Store -You can download a ready-to-use custom Unity package ( UnityDTrackPlugin-vX.X.X.unitypackage ) at
-[https://github.com/ar-tracking/UnityDTrackPlugin/releases](https://github.com/ar-tracking/UnityDTrackPlugin/releases). +The plugin is available (for free) in the Unity Asset Store +[https://assetstore.unity.com/packages/tools/integration/art-dtrack-plugin-246990](https://assetstore.unity.com/packages/tools/integration/art-dtrack-plugin-246990). -In order to install the package, follow the steps below. +After 'purchasing' you can install it via the Unity Package Manager: 1. Launch Unity Hub - Create/Open Unity project -- Import package ( *Assets* → *Import Package...* → *Custom Package...* ) +- Open Package Manager ( *Window* → *Package Manager* → *Packages: My Assets* ) +- Download the package (button *Download* ), then install it (button *Install* ) -The plugin will be installed in your project's assets folder:
+The plugin will be installed in your project's Assets folder:
/path/to/unity/projects/*MyUnityProject*/**Assets/ARTDTrackPlugin**/ . ### OpenUPM @@ -51,6 +53,7 @@ The ART DTRACK plugin is available (for free) via To install it, follow the instructions mentioned at the above website; roughly: 1. Launch Unity Hub +- Create/Open Unity project - Open *Edit* → *Project Settings...* → *Package Manager* - Add a new Scoped Registry (corresponding to the above OpenUPM link), then click *Save* or *Apply* - Open Package Manager ( *Window* → *Package Manager* → *Packages: My Registries* ) @@ -59,6 +62,20 @@ To install it, follow the instructions mentioned at the above website; roughly: The plugin will be installed in your project's Packages folder:
/path/to/unity/projects/*MyUnityProject*/**Packages/ART DTRACK Plugin**/ . +### GitHub + +You can download a ready-to-use custom Unity package ( UnityDTrackPlugin-vX.X.X.unitypackage ) at
+[https://github.com/ar-tracking/UnityDTrackPlugin/releases](https://github.com/ar-tracking/UnityDTrackPlugin/releases). + +In order to install the package, follow the steps below. + +1. Launch Unity Hub +- Create/Open Unity project +- Import package ( *Assets* → *Import Package...* → *Custom Package...* ) + +The plugin will be installed in your project's Assets folder:
+/path/to/unity/projects/*MyUnityProject*/**Assets/ARTDTrackPlugin**/ . + ### Plain Sources You can download or clone sources for this plugin at @@ -203,6 +220,14 @@ this prefix must be removed.
+### Fingertracking + +When configuring ART Fingertracking hand devices with 3 thimbles, be sure to activate the _Simulate 5 fingers_ setting. + +![Figure: DTRACK3 Fingertracking administration dialog](Documentation~/images/dtrack-fingertracking-administration.png) +
+ + ## Plugin Configuration Streaming position, rotation and button events data from DTRACK @@ -214,6 +239,9 @@ it a name, e.g. **DTrackSource**. To this object attach the - Set *Listen Port* number matching the setting for DTRACK (see section [**Setting Outputs**](#dtracksettingoutputs)) +- Optionally set *Controller Host* with hostname or IP address of your ART Controller, if there is a + 'stateful firewall' installed between Controller and Unity; normally that's sufficient to pass UDP packets + with tracking data to the plugin - Set *DTrack Coordinates* matching the calibration mode used in DTRACK (see section [**Room Calibration**](#dtrackroomcalibration)) diff --git a/Sdk/DTrackSDK-CSharp/Source/DTrackObjects/DTrackMeaTool.cs b/Sdk/DTrackSDK-CSharp/Source/DTrackObjects/DTrackMeaTool.cs new file mode 100644 index 0000000..6c54ca6 --- /dev/null +++ b/Sdk/DTrackSDK-CSharp/Source/DTrackObjects/DTrackMeaTool.cs @@ -0,0 +1,56 @@ +/* DTrackSDK in C#: DTrackMeaTool.cs + * + * Data object containing DTRACK output data of one Measurement Tool. + * + * Copyright (c) 2024 Advanced Realtime Tracking GmbH & Co. KG + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using DTrackSDK.Interfaces; + +namespace DTrackSDK +{ + + +public class DTrackMeaTool : DTrackBody +{ + public float TipRadius { get; } + public int NumButtons { get; } + public bool[] Buttons { get; } + + public DTrackMeaTool( int id, float quality, float sx, float sy, float sz, + float r0, float r1, float r2, float r3, float r4, float r5, float r6, float r7, float r8, + float tipradius, bool[] buttons, int numButtons ) + : base( id, quality, sx, sy, sz, r0, r1, r2 ,r3, r4, r5, r6, r7, r8 ) + { + this.TipRadius = tipradius; + this.NumButtons = numButtons; + this.Buttons = buttons; + } +} + + +} // namespace DTrackSDK + diff --git a/Sdk/DTrackSDK-CSharp/Source/DTrackObjects/DTrackMeaTool.cs.meta b/Sdk/DTrackSDK-CSharp/Source/DTrackObjects/DTrackMeaTool.cs.meta new file mode 100644 index 0000000..6e7a0c7 --- /dev/null +++ b/Sdk/DTrackSDK-CSharp/Source/DTrackObjects/DTrackMeaTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 90c0b523bb95de695bdb0e6d905f44ff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Sdk/DTrackSDK-CSharp/Source/DTrackObjects/Frame.cs b/Sdk/DTrackSDK-CSharp/Source/DTrackObjects/Frame.cs index 2101892..bcadc41 100644 --- a/Sdk/DTrackSDK-CSharp/Source/DTrackObjects/Frame.cs +++ b/Sdk/DTrackSDK-CSharp/Source/DTrackObjects/Frame.cs @@ -2,7 +2,7 @@ * * Data object containing DTRACK output data of one frame. * - * Copyright (c) 2019-2022 Advanced Realtime Tracking GmbH & Co. KG + * Copyright (c) 2019-2024 Advanced Realtime Tracking GmbH & Co. KG * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -39,21 +39,88 @@ public class Frame // frame counter, timestamp public uint FrameCounter { get; set; } public double TimeStamp { get; set; } + public uint TimeStampSec { get; set; } + public uint TimeStampUsec { get; set; } + public uint LatencyUsec { get; set; } // Standard Bodies public Dictionary< int, DTrackBody > Bodies { get; set; } + public int NumBodies { get; set; } // number of calibrated Standard Bodies (as far as known) // Flysticks public Dictionary< int, DTrackFlystick > Flysticks { get; set; } + public int NumFlysticks { get; set; } // number of calibrated Flysticks + + // Measurement Tools + public Dictionary< int, DTrackMeaTool > MeaTools { get; set; } + public int NumMeaTools { get; set; } // number of calibrated Measurement Tools // Fingertracking hands public Dictionary< int, DTrackHand > Hands { get; set; } + public int NumHands { get; set; } // number of calibrated Fingertracking hands (as far as known) public Frame() { this.FrameCounter = 0; this.TimeStamp = -1.0; + this.TimeStampSec = 0; + this.TimeStampUsec = 0; + this.LatencyUsec = 0; + + this.NumBodies = 0; + this.NumFlysticks = 0; + this.NumMeaTools = 0; + this.NumHands = 0; + } + + + // Get Standard Body data. + + public DTrackBody GetBody( int id ) + { + if ( this.Bodies == null ) return null; + + DTrackBody body; + if ( this.Bodies.TryGetValue( id, out body ) ) return body; + + return null; + } + + // Get Flystick data. + + public DTrackFlystick GetFlystick( int id ) + { + if ( this.Flysticks == null ) return null; + + DTrackFlystick flystick; + if ( this.Flysticks.TryGetValue( id, out flystick ) ) return flystick; + + return null; + } + + // Get Measurement Tool data. + + public DTrackMeaTool GetMeaTool( int id ) + { + if ( this.MeaTools == null ) return null; + + DTrackMeaTool meatool; + if ( this.MeaTools.TryGetValue( id, out meatool ) ) return meatool; + + return null; + } + + // Get Fingertracking hand data. + + public DTrackHand GetHand( int id ) + { + if ( this.Hands == null ) return null; + + DTrackHand hand; + if ( this.Hands.TryGetValue( id, out hand ) ) return hand; + + return null; } } diff --git a/Sdk/DTrackSDK-CSharp/Source/DTrackSDK.cs b/Sdk/DTrackSDK-CSharp/Source/DTrackSDK.cs index 9b7a1f4..f477c36 100644 --- a/Sdk/DTrackSDK-CSharp/Source/DTrackSDK.cs +++ b/Sdk/DTrackSDK-CSharp/Source/DTrackSDK.cs @@ -1,9 +1,8 @@ /* DTrackSDK in C#: DTrackSDK.cs * - * Functions to receive and process DTRACK UDP packets (ASCII protocol), as - * well as to exchange DTrack2/DTRACK3 TCP command strings. + * Functions to receive and process DTRACK UDP packets (ASCII protocol). * - * Copyright (c) 2019-2022 Advanced Realtime Tracking GmbH & Co. KG + * Copyright (c) 2019-2024 Advanced Realtime Tracking GmbH & Co. KG * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -28,11 +27,11 @@ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * - * Version v0.1.0 + * Version v0.2.0 * * Purpose: * - receives DTRACK UDP packets (ASCII protocol) and converts them into easier to handle data - * - sends and receives DTrack2/DTRACK3 commands (TCP) + * - sends DTRACK3 feedback data commands (UDP) * - DTRACK network protocol according to: * 'DTrack2 User Manual, Technical Appendix' or 'DTRACK3 Programmer's Guide' */ @@ -54,13 +53,19 @@ public class DTrackSDK { private int _dataPort = 0; private UdpClient _dataChannel = null; + private int _dataTimeout = Statics.DEFAULT_UDP_TIMEOUT; private IPEndPoint _dataEndPoint = null; + private IPEndPoint _udpSenderEndPoint = null; + + private Parser _parser = new Parser(); private string _dataBuffer; private Frame _frame; private string _lastErrorMessage; + private UdpClient _feedbackChannel = null; + // Constructor, use for pure listening mode. Using this constructor, only a UDP receiver to get // tracking data from the Controller will be established. Please start measurement manually. @@ -75,18 +80,24 @@ private bool Initialize( int dataPort ) _dataPort = dataPort; _dataChannel = null; _dataEndPoint = null; + _udpSenderEndPoint = null; + _feedbackChannel = null; if ( _dataPort != 0 ) { try { + _dataChannel = new UdpClient(); + _dataChannel.ExclusiveAddressUse = true; + var ep = new IPEndPoint( IPAddress.Any, _dataPort ); - _dataChannel = new UdpClient( ep ); + _dataChannel.Client.Bind( ep ); + _dataChannel.Client.ReceiveTimeout = _dataTimeout; + _dataChannel.Client.SendTimeout = _dataTimeout; } catch ( Exception e ) { - _lastErrorMessage = Convert.ToString( e ); - + _lastErrorMessage = e.Message; _dataChannel = null; return false; } @@ -100,9 +111,28 @@ private bool Initialize( int dataPort ) // Destructor. ~DTrackSDK() + { + this.Close(); + } + + + // Close connection to Controller, especially close all UDP sockets. + + public bool Close() { if ( _dataChannel != null ) + { _dataChannel.Close(); + _dataChannel = null; + } + + if ( _feedbackChannel != null ) + { + _feedbackChannel.Close(); + _feedbackChannel = null; + } + + return true; } @@ -119,6 +149,65 @@ private bool Initialize( int dataPort ) public int DataPort => _dataPort; + // UDP timeout for receiving tracking data (in microseconds). + + public int DataTimeoutUS + { + get + { return _dataTimeout; } + set + { + if ( value <= 0 ) + { + _dataTimeout = Statics.DEFAULT_UDP_TIMEOUT; + } else { + _dataTimeout = value / 1000; + } + + if ( _dataChannel != null ) + { + _dataChannel.Client.ReceiveTimeout = _dataTimeout; + _dataChannel.Client.SendTimeout = _dataTimeout; + } + + if ( _feedbackChannel != null ) _feedbackChannel.Client.SendTimeout = _dataTimeout; + } + } + + + // Enable UDP connection through a stateful firewall. + // + // In order to enable UDP traffic through a stateful firewall. Just necessary for listening modes, will be done + // automatically for communicating mode. Default port is working just for DTrack3 v3.1.1 or newer. + + public bool EnableStatefulFirewallConnection( string senderHost, int senderPort = Statics.CONTROLLER_PORT_UDPSENDER ) + { + try + { + IPAddress[] addrlist = Dns.GetHostAddresses( senderHost ); + if ( addrlist.Length == 0 ) return false; + + foreach ( IPAddress addr in addrlist ) + { + if ( addr.AddressFamily == AddressFamily.InterNetwork ) + { + _udpSenderEndPoint = new IPEndPoint( addr, senderPort ); + break; + } + } + } + catch ( Exception e ) + { + _lastErrorMessage = e.Message; + return false; + } + + this.SendStatefulFirewallPacket(); // try enabling UDP connection + + return true; + } + + // Receive and process one tracking data packet. // // This method waits until a data packet becomes available, but no longer @@ -137,11 +226,11 @@ public bool Receive() packet = _dataChannel.Receive( ref _dataEndPoint ); _dataBuffer = Encoding.ASCII.GetString( packet ); - _frame = RawParser.Parse( _dataBuffer ); + _frame = _parser.Parse( _dataBuffer ); } catch ( Exception e ) { - _lastErrorMessage = Convert.ToString( e ); + _lastErrorMessage = e.Message; return false; } @@ -166,11 +255,11 @@ public async Task< bool > ReceiveAsync() _dataEndPoint = res.RemoteEndPoint; _dataBuffer = Encoding.ASCII.GetString( res.Buffer ); - _frame = RawParser.Parse( _dataBuffer ); + _frame = _parser.Parse( _dataBuffer ); } catch ( Exception e ) { - _lastErrorMessage = Convert.ToString( e ); + _lastErrorMessage = e.Message; return false; } @@ -200,6 +289,125 @@ public string GetLastErrorMessage() { return _lastErrorMessage; } + + + // Send dummy UDP packet for stateful firewall. + // + // Sends a packet to the Controller, in order to enable UDP traffic through a stateful firewall. + + private bool SendStatefulFirewallPacket() + { + if ( _dataChannel == null ) return false; + + if ( _udpSenderEndPoint == null ) return false; + + int sent = 0; + byte[] msg = Encoding.ASCII.GetBytes( "fw4dtsdkcs" ); + try + { + sent = _dataChannel.Send( msg, msg.Length, _udpSenderEndPoint ); + } + catch ( Exception e ) + { + _lastErrorMessage = e.Message; + return false; + } + + return ( sent == msg.Length ); + } + + + // Send tactile Fingertracking command to set feedback on a specific finger of a specific hand. + + public bool TactileFinger( int handId, int fingerId, float strength ) + { + strength = Math.Max( 0.0f, Math.Min( strength, 1.0f ) ); + + return this.SendFeedbackCommand( $"tfb 1 [{handId} {fingerId} 1.0 {strength}]\0" ); + } + + + // Send tactile Fingertracking command to set feedback on all fingers of a specific hand. + + public bool TactileHand( int handId, float[] strength ) + { + string s = $"tfb {strength.Length} "; + + for ( int i = 0; i < strength.Length; i++ ) + { + float st = Math.Max( 0.0f, Math.Min( strength[ i ], 1.0f ) ); + s += $"[{handId} {i} 1.0 {st}]"; + } + + return this.SendFeedbackCommand( s + "\0" ); + } + + + // Send tactile Fingertracking command to turn off tactile feedback on all fingers of a specific hand. + + public bool TactileHandOff( int handId, int numFinger ) + { + float[] strength = new float[ numFinger ]; + + for ( int i = 0; i < strength.Length; i++ ) + strength[ i ] = 0.0f; + + return this.TactileHand( handId, strength ); + } + + + // Send Flystick feedback command to start a beep on a specific Flystick. + + public bool FlystickBeep( int flystickId, float durationMs, float frequencyHz ) + { + return this.SendFeedbackCommand( $"ffb 1 [{flystickId} {( int )durationMs} {( int )frequencyHz} 0 0][]\0" ); + } + + + // Send Flystick feedback command to start a vibration pattern on a specific Flystick. + + public bool FlystickVibration( int flystickId, int vibrationPattern ) + { + return this.SendFeedbackCommand( $"ffb 1 [{flystickId} 0 0 {vibrationPattern} 0][]\0" ); + } + + + // Send feedback command via UDP. + + private bool SendFeedbackCommand( string command ) + { + if ( _feedbackChannel == null ) + { + if ( _dataEndPoint == null ) return false; + + try + { + _feedbackChannel = new UdpClient( _dataEndPoint.Address.ToString(), Statics.CONTROLLER_PORT_FEEDBACK ); + // establishes default remote host + _feedbackChannel.Client.SendTimeout = _dataTimeout; + } + catch ( Exception e ) + { + _lastErrorMessage = e.Message; + _feedbackChannel = null; + return false; + } + } + + int sent = 0; + byte[] msg = Encoding.ASCII.GetBytes( command ); + try + { + sent = _feedbackChannel.Send( msg, msg.Length ); + } + catch ( Exception e ) + { + _lastErrorMessage = e.Message; + return false; + } + + return ( sent == msg.Length ); + } } diff --git a/Sdk/DTrackSDK-CSharp/Source/Parsers/BodyParser.cs b/Sdk/DTrackSDK-CSharp/Source/Parsers/BodyParser.cs index 380c5f1..8145901 100644 --- a/Sdk/DTrackSDK-CSharp/Source/Parsers/BodyParser.cs +++ b/Sdk/DTrackSDK-CSharp/Source/Parsers/BodyParser.cs @@ -2,7 +2,7 @@ * * Parsing Standard Bodies of DTRACK output data. * - * Copyright (c) 2019-2022 Advanced Realtime Tracking GmbH & Co. KG + * Copyright (c) 2019-2023 Advanced Realtime Tracking GmbH & Co. KG * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -38,13 +38,16 @@ namespace DTrackSDK.Parsers public static class BodyParser { - public static Dictionary< int, DTrackBody > Parse( string raw ) + public static Dictionary< int, DTrackBody > Parse( string raw, out int num ) { string[] bodyCountSplit = raw.Split( Statics.NumberSplit, 3 ); int bodyCount = Convert.ToInt32( bodyCountSplit[ 1 ] ); if ( bodyCount <= 0 ) + { + num = 0; return null; + } var bodies = new Dictionary< int, DTrackBody >(); @@ -52,13 +55,14 @@ public static Dictionary< int, DTrackBody > Parse( string raw ) string[] sectionSplit = trimmed.Split( Statics.SectionSplit, StringSplitOptions.None ); int iblk = 0; + int id = -1; for ( int ibody = 0; ibody < bodyCount; ibody++ ) { string[] m = sectionSplit[ iblk++ ].Split( ' ' ); string[] s = sectionSplit[ iblk++ ].Split( ' ' ); string[] r = sectionSplit[ iblk++ ].Split( ' ' ); - int id = Convert.ToInt32( m[ 0 ] ); + id = Convert.ToInt32( m[ 0 ] ); float qu = Convert.ToSingle( m[ 1 ], CultureInfo.InvariantCulture ); float sx = Convert.ToSingle( s[ 0 ], CultureInfo.InvariantCulture ); @@ -79,6 +83,7 @@ public static Dictionary< int, DTrackBody > Parse( string raw ) new DTrackBody( id, qu, sx, sy, sz, r0, r1, r2, r3, r4, r5, r6, r7, r8 ) ); } + num = id + 1; return bodies; } } diff --git a/Sdk/DTrackSDK-CSharp/Source/Parsers/FlystickParser.cs b/Sdk/DTrackSDK-CSharp/Source/Parsers/FlystickParser.cs index ab56e51..a0ffcb1 100644 --- a/Sdk/DTrackSDK-CSharp/Source/Parsers/FlystickParser.cs +++ b/Sdk/DTrackSDK-CSharp/Source/Parsers/FlystickParser.cs @@ -2,7 +2,7 @@ * * Parsing Flysticks of DTRACK output data. * - * Copyright (c) 2019-2022 Advanced Realtime Tracking GmbH & Co. KG + * Copyright (c) 2019-2024 Advanced Realtime Tracking GmbH & Co. KG * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -38,13 +38,16 @@ namespace DTrackSDK.Parsers public static class FlystickParser { - public static Dictionary< int, DTrackFlystick > Parse( string raw ) + public static Dictionary< int, DTrackFlystick > Parse( string raw, out int num ) { string[] flystickCountSplit = raw.Split( Statics.NumberSplit, 4 ); int flystickCount = Convert.ToInt32( flystickCountSplit[ 2 ] ); if ( flystickCount <= 0 ) + { + num = 0; return null; + } var bodies = new Dictionary< int, DTrackFlystick >(); @@ -125,6 +128,7 @@ public static Dictionary< int, DTrackFlystick > Parse( string raw ) buttons, buttonsCount, analogs, analogsCount ) ); } + num = flystickCount; return bodies; } } diff --git a/Sdk/DTrackSDK-CSharp/Source/Parsers/HandParser.cs b/Sdk/DTrackSDK-CSharp/Source/Parsers/HandParser.cs index d8add02..214351c 100644 --- a/Sdk/DTrackSDK-CSharp/Source/Parsers/HandParser.cs +++ b/Sdk/DTrackSDK-CSharp/Source/Parsers/HandParser.cs @@ -2,7 +2,7 @@ * * Parsing Fingertracking hands of DTRACK output data. * - * Copyright (c) 2020-2022 Advanced Realtime Tracking GmbH & Co. KG + * Copyright (c) 2020-2023 Advanced Realtime Tracking GmbH & Co. KG * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -38,13 +38,16 @@ namespace DTrackSDK.Parsers public static class HandParser { - public static Dictionary< int, DTrackHand > Parse( string raw ) + public static Dictionary< int, DTrackHand > Parse( string raw, out int num ) { string[] handsSplit = raw.Split( Statics.NumberSplit, 3 ); int handsCount = Convert.ToInt32( handsSplit[ 1 ] ); if ( handsCount <= 0 ) + { + num = 0; return null; + } var hands = new Dictionary< int, DTrackHand >(); @@ -52,10 +55,11 @@ public static Dictionary< int, DTrackHand > Parse( string raw ) string[] handsData = trimmed.Split( Statics.SectionSplit, StringSplitOptions.None ); int blk = 0; + int id = -1; for ( int gl = 0; gl < handsCount; gl++ ) { string[] meta = handsData[ blk++ ].Split( ' ' ); - int id = Convert.ToInt32( meta[ 0 ] ); + id = Convert.ToInt32( meta[ 0 ] ); float qu = Convert.ToSingle( meta[ 1 ], CultureInfo.InvariantCulture ); int lr = Convert.ToInt32( meta[ 2 ] ); int nf = Convert.ToInt32( meta[ 3 ] ); @@ -111,6 +115,7 @@ public static Dictionary< int, DTrackHand > Parse( string raw ) new DTrackHand( id, qu, lr, sx, sy, sz, r0, r1, r2, r3, r4, r5, r6, r7, r8, fingers ) ); } + num = id + 1; return hands; } } diff --git a/Sdk/DTrackSDK-CSharp/Source/Parsers/MeaToolParser.cs b/Sdk/DTrackSDK-CSharp/Source/Parsers/MeaToolParser.cs new file mode 100644 index 0000000..616b4d9 --- /dev/null +++ b/Sdk/DTrackSDK-CSharp/Source/Parsers/MeaToolParser.cs @@ -0,0 +1,129 @@ +/* DTrackSDK in C#: MeaToolParser.cs + * + * Parsing Measurement Tools of DTRACK output data. + * + * Copyright (c) 2024 Advanced Realtime Tracking GmbH & Co. KG + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace DTrackSDK.Parsers +{ + + +public static class MeaToolParser +{ + public static Dictionary< int, DTrackMeaTool > Parse( string raw, out int num ) + { + string[] meatoolCountSplit = raw.Split( Statics.NumberSplit, 4 ); + + int meatoolCount = Convert.ToInt32( meatoolCountSplit[ 2 ] ); + if ( meatoolCount <= 0 ) + { + num = 0; + return null; + } + + var bodies = new Dictionary< int, DTrackMeaTool >(); + + string trimmed = meatoolCountSplit[ 3 ].Trim( Statics.SectionTrim ); + string[] sectionSplit = trimmed.Split( Statics.SectionSplit, StringSplitOptions.None ); + + int iblk = 0; + for ( int imt = 0; imt < meatoolCount; imt++ ) + { + string[] metaSection = sectionSplit[ iblk++ ].Split( ' ' ); + int id = Convert.ToInt32( metaSection[ 0 ] ); + float qu = Convert.ToSingle( metaSection[ 1 ], CultureInfo.InvariantCulture ); + int buttonsCount = Convert.ToInt32( metaSection[ 2 ] ); + float tipradius = Convert.ToSingle( metaSection[ 3 ], CultureInfo.InvariantCulture ); + + string[] s = sectionSplit[ iblk++ ].Split( ' ' ); + float sx = Convert.ToSingle( s[ 0 ], CultureInfo.InvariantCulture ); + float sy = Convert.ToSingle( s[ 1 ], CultureInfo.InvariantCulture ); + float sz = Convert.ToSingle( s[ 2 ], CultureInfo.InvariantCulture ); + + string[] r = sectionSplit[ iblk++ ].Split( ' ' ); + float r0 = Convert.ToSingle( r[ 0 ], CultureInfo.InvariantCulture ); + float r1 = Convert.ToSingle( r[ 1 ], CultureInfo.InvariantCulture ); + float r2 = Convert.ToSingle( r[ 2 ], CultureInfo.InvariantCulture ); + float r3 = Convert.ToSingle( r[ 3 ], CultureInfo.InvariantCulture ); + float r4 = Convert.ToSingle( r[ 4 ], CultureInfo.InvariantCulture ); + float r5 = Convert.ToSingle( r[ 5 ], CultureInfo.InvariantCulture ); + float r6 = Convert.ToSingle( r[ 6 ], CultureInfo.InvariantCulture ); + float r7 = Convert.ToSingle( r[ 7 ], CultureInfo.InvariantCulture ); + float r8 = Convert.ToSingle( r[ 8 ], CultureInfo.InvariantCulture ); + + string[] inputSection = null; + if ( buttonsCount > 0 ) + { + inputSection = sectionSplit[ iblk ].Split( ' ' ); + } + iblk++; + + bool[] buttons = null; + + int buttonSlots = 0; + if ( buttonsCount > 0 ) + { + buttons = new bool[ buttonsCount ]; + + buttonSlots = ( int )Math.Ceiling( ( double )buttonsCount / 32 ); + + int m = 0; + for ( int slot = 0; slot < buttonSlots; slot++ ) + { + int buttonGroup = Convert.ToInt32( inputSection[ slot ] ); + for ( int k = 0; k < 32; k++ ) + { + buttons[ m ] = ( ( buttonGroup & 0x01 ) != 0 ); + + m++; + if ( m == buttonsCount ) + break; + + buttonGroup >>= 1; + } + } + } + + iblk++; // ignore covariance + + bodies.Add( id, + new DTrackMeaTool( id, qu, sx, sy, sz, r0, r1, r2, r3, r4, r5, r6, r7, r8, + tipradius, buttons, buttonsCount ) ); + } + + num = meatoolCount; + return bodies; + } +} + + +} // namespace DTrackSDK.Parsers + diff --git a/Sdk/DTrackSDK-CSharp/Source/Parsers/MeaToolParser.cs.meta b/Sdk/DTrackSDK-CSharp/Source/Parsers/MeaToolParser.cs.meta new file mode 100644 index 0000000..4b6f099 --- /dev/null +++ b/Sdk/DTrackSDK-CSharp/Source/Parsers/MeaToolParser.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ca5959514263949eda7339a3bc167a2e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Sdk/DTrackSDK-CSharp/Source/Parsers/Parser.cs b/Sdk/DTrackSDK-CSharp/Source/Parsers/Parser.cs index 4486f42..aa1e728 100644 --- a/Sdk/DTrackSDK-CSharp/Source/Parsers/Parser.cs +++ b/Sdk/DTrackSDK-CSharp/Source/Parsers/Parser.cs @@ -2,7 +2,7 @@ * * Parsing a frame of DTRACK output data. * - * Copyright (c) 2019-2022 Advanced Realtime Tracking GmbH & Co. KG + * Copyright (c) 2019-2024 Advanced Realtime Tracking GmbH & Co. KG * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -34,12 +34,18 @@ namespace DTrackSDK.Parsers { -public static class RawParser +public class Parser { - public static Frame Parse( string packet ) + private int _numBodies = 0; + private int _numHands = 0; + + public Frame Parse( string packet ) { var frame = new Frame(); + int calNumBodies = -1; + int calNumHands = -1; + string[] lines = packet.Split( Statics.LineSplit, StringSplitOptions.RemoveEmptyEntries ); foreach ( string line in lines ) @@ -54,30 +60,82 @@ public static Frame Parse( string packet ) { frame.TimeStamp = TimestampParser.Parse( line ); } + else if ( line.StartsWith( Statics.Prefix_ts2 ) ) + { + uint tssec, tsusec, lat; + frame.TimeStamp = Timestamp2Parser.Parse( line, out tssec, out tsusec, out lat ); + frame.TimeStampSec = tssec; + frame.TimeStampUsec = tsusec; + frame.LatencyUsec = lat; + } else if ( line.StartsWith( Statics.Prefix_6dcal ) ) { - CalibratedBodiesParser.Parse( line ); + calNumBodies = CalibratedBodiesParser.Parse( line ); } else if ( line.StartsWith( Statics.Prefix_6d ) ) { - frame.Bodies = BodyParser.Parse( line ); + int num; + frame.Bodies = BodyParser.Parse( line, out num ); + frame.NumBodies = num; } else if ( line.StartsWith( Statics.Prefix_6df2 ) ) { - frame.Flysticks = FlystickParser.Parse( line ); + int num; + frame.Flysticks = FlystickParser.Parse( line, out num ); + frame.NumFlysticks = num; + } + else if ( line.StartsWith( Statics.Prefix_6dmt2 ) ) + { + int num; + frame.MeaTools = MeaToolParser.Parse( line, out num ); + frame.NumMeaTools = num; } else if ( line.StartsWith( Statics.Prefix_glcal ) ) { - CalibratedHandsParser.Parse( line ); + calNumHands = CalibratedHandsParser.Parse( line ); } else if ( line.StartsWith( Statics.Prefix_gl ) ) { - frame.Hands = HandParser.Parse( line ); + int num; + frame.Hands = HandParser.Parse( line, out num ); + frame.NumHands = num; } } catch ( Exception e ) { - throw new Exception( $"Error parsing line: {line} {Environment.NewLine}Exception: {e}" ); + throw new Exception( $"Error parsing line '{line.Substring( 0, 6 )}': {e.Message}" ); + } + } + + if ( calNumBodies >= 0 ) + { + frame.NumBodies = calNumBodies; + } + else + { + if ( frame.NumBodies > _numBodies ) + { + _numBodies = frame.NumBodies; + } + else + { + frame.NumBodies = _numBodies; + } + } + + if ( calNumHands >= 0 ) + { + frame.NumHands = calNumHands; + } + else + { + if ( frame.NumHands > _numHands ) + { + _numHands = frame.NumHands; + } + else + { + frame.NumHands = _numHands; } } diff --git a/Sdk/DTrackSDK-CSharp/Source/Parsers/TimestampParser.cs b/Sdk/DTrackSDK-CSharp/Source/Parsers/TimestampParser.cs index 5b8bb1e..306f0f7 100644 --- a/Sdk/DTrackSDK-CSharp/Source/Parsers/TimestampParser.cs +++ b/Sdk/DTrackSDK-CSharp/Source/Parsers/TimestampParser.cs @@ -1,8 +1,8 @@ /* DTrackSDK in C#: TimestampParser.cs * - * Parsing timestamp of DTRACK output data. + * Parsing timestamp data of DTRACK output data. * - * Copyright (c) 2019-2022 Advanced Realtime Tracking GmbH & Co. KG + * Copyright (c) 2019-2024 Advanced Realtime Tracking GmbH & Co. KG * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -40,10 +40,26 @@ public static class TimestampParser public static double Parse( string raw ) { string[] split = raw.Split( ' ' ); + return Convert.ToDouble( split[ 1 ], CultureInfo.InvariantCulture ); } } +public static class Timestamp2Parser +{ + public static double Parse( string raw, out uint tssec, out uint tsusec, out uint lat ) + { + string[] split = raw.Split( ' ' ); + + tssec = Convert.ToUInt32( split[ 1 ] ); + tsusec = Convert.ToUInt32( split[ 2 ] ); + lat = Convert.ToUInt32( split[ 3 ] ); + + return ( double )( tssec % ( 24 * 3600 ) ) + ( double )tsusec / 1000000.0; + } +} + + } // namespace DTrackSDK.Parsers diff --git a/Sdk/DTrackSDK-CSharp/Source/Util/Statics.cs b/Sdk/DTrackSDK-CSharp/Source/Util/Statics.cs index 478f0ce..0c97eb9 100644 --- a/Sdk/DTrackSDK-CSharp/Source/Util/Statics.cs +++ b/Sdk/DTrackSDK-CSharp/Source/Util/Statics.cs @@ -1,8 +1,8 @@ /* DTrackSDK in C#: Statics.cs * - * DTrackSDK: Constants. + * Miscellaneous constants. * - * Copyright (c) 2019-2022 Advanced Realtime Tracking GmbH & Co. KG + * Copyright (c) 2019-2024 Advanced Realtime Tracking GmbH & Co. KG * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -41,21 +41,38 @@ public class FingerIndex public const int PINKY = 4; } +public class FlystickVibrationPattern +{ + public const int SINGLE_CLICK = 1; + public const int DOUBLE_CLICK = 2; + public const int BUMP = 3; + public const int TRANSITION_HUM = 4; + public const int RAMP_UP = 5; + public const int RAMP_DOWN = 6; +} + public class Statics { + // connection properties + public const int CONTROLLER_PORT_UDPSENDER = 50107; + public const int CONTROLLER_PORT_FEEDBACK = 50110; + public const int DEFAULT_UDP_TIMEOUT = 1000; + // properties for parsers public static readonly char[] LineSplit = { ( char )0x0d, ( char )0x0a }; public static readonly char[] NumberSplit = { ' ' }; - public static readonly char[] SectionTrim = { '[', ']' }; + public static readonly char[] SectionTrim = { '[', ']', ' ' }; public static readonly string[] SectionSplit = { "][", "] [" }; // DTRACK output (ASCII): prefixes of data types public const string Prefix_fr = "fr "; public const string Prefix_ts = "ts "; + public const string Prefix_ts2 = "ts2 "; public const string Prefix_6dcal = "6dcal "; public const string Prefix_6d = "6d "; public const string Prefix_6df2 = "6df2 "; + public const string Prefix_6dmt2 = "6dmt2 "; public const string Prefix_glcal = "glcal "; public const string Prefix_gl = "gl "; } diff --git a/Source/DTrack.cs b/Source/DTrack.cs index d21b8a6..2310cba 100644 --- a/Source/DTrack.cs +++ b/Source/DTrack.cs @@ -2,7 +2,7 @@ * * Main script providing DTRACK tracking data to Unity. * - * Copyright (c) 2019-2023 Advanced Realtime Tracking GmbH & Co. KG + * Copyright (c) 2019-2024 Advanced Realtime Tracking GmbH & Co. KG * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -27,7 +27,7 @@ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * - * Version v1.1.2 + * Version v1.1.3 */ using System; @@ -49,6 +49,9 @@ public class DTrack : MonoBehaviour [ Tooltip( "UDP port for incoming DTRACK tracking data" ) ] public int listenPort = 0; // final default see OnValidate() + [ Tooltip( "Optional hostname/IP of DTRACK Controller to enable UDP connection through stateful firewall" ) ] + public string controllerHost; + [ Serializable ] public enum DTrackCoordinates { @@ -94,13 +97,19 @@ void Awake() void Start() { - this.sdk = new DTrackSDK.DTrackSDK( listenPort ); + this.sdk = new DTrackSDK.DTrackSDK( this.listenPort ); if ( ! this.sdk.IsDataInterfaceValid ) { Debug.Log( $"Cannot initialize SDK to receive DTRACK tracking data: {this.sdk.GetLastErrorMessage()}" ); return; } + if ( ! String.IsNullOrEmpty( this.controllerHost ) ) + { + if ( ! this.sdk.EnableStatefulFirewallConnection( this.controllerHost ) ) + Debug.Log( $"Cannot send UDP packet to {this.controllerHost}" ); + } + this.thread = new Thread( new ThreadStart( ReceiveThread ) ); this.runReceiveThread = true; @@ -112,7 +121,11 @@ void OnDestroy() this.runReceiveThread = false; if ( this.thread != null ) this.thread.Abort(); - this.sdk = null; + if ( this.sdk != null ) + { + this.sdk.Close(); + this.sdk = null; + } } diff --git a/Source/Receivers/DTrackReceiver.cs b/Source/Receivers/DTrackReceiver.cs index 2b6a3a2..d8aa3d1 100644 --- a/Source/Receivers/DTrackReceiver.cs +++ b/Source/Receivers/DTrackReceiver.cs @@ -49,7 +49,11 @@ public abstract class DTrackReceiver : MonoBehaviour, IDTrackReceiver protected void Register() { +#if UNITY_2023_1_OR_NEWER + DTrack dtr = FindFirstObjectByType< DTrack >(); +#else DTrack dtr = FindObjectOfType< DTrack >(); +#endif if ( dtr != null ) { diff --git a/Source/Receivers/DTrackReceiver6Dof.cs b/Source/Receivers/DTrackReceiver6Dof.cs index 675c095..2125ae5 100644 --- a/Source/Receivers/DTrackReceiver6Dof.cs +++ b/Source/Receivers/DTrackReceiver6Dof.cs @@ -2,7 +2,7 @@ * * Script providing DTRACK Standard Body data to a GameObject. * - * Copyright (c) 2020-2023 Advanced Realtime Tracking GmbH & Co. KG + * Copyright (c) 2020-2024 Advanced Realtime Tracking GmbH & Co. KG * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -65,13 +65,12 @@ void Update() { DTrackSDK.Frame frame = GetDTrackFrame(); // ensures data integrity against DTrack class if ( frame == null ) return; // no new tracking data - if ( frame.Bodies == null ) return; try { - DTrackSDK.DTrackBody dtBody; + DTrackSDK.DTrackBody dtBody = frame.GetBody( this.bodyId - 1 ); - if ( frame.Bodies.TryGetValue( this.bodyId - 1, out dtBody ) ) + if ( dtBody != null ) { if ( dtBody.IsTracked ) { diff --git a/Source/Receivers/DTrackReceiverFlystick.cs b/Source/Receivers/DTrackReceiverFlystick.cs index e1c1159..6ee8bb4 100644 --- a/Source/Receivers/DTrackReceiverFlystick.cs +++ b/Source/Receivers/DTrackReceiverFlystick.cs @@ -2,7 +2,7 @@ * * Script providing DTRACK Flystick data to a GameObject. * - * Copyright (c) 2020-2023 Advanced Realtime Tracking GmbH & Co. KG + * Copyright (c) 2020-2024 Advanced Realtime Tracking GmbH & Co. KG * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -156,12 +156,12 @@ void Update() { DTrackSDK.Frame frame = GetDTrackFrame(); // ensures data integrity against DTrack class if ( frame == null ) return; // no new tracking data - if ( frame.Flysticks == null ) return; try { - DTrackSDK.DTrackFlystick dtFlystick; - if ( frame.Flysticks.TryGetValue( flystickId - 1, out dtFlystick ) ) + DTrackSDK.DTrackFlystick dtFlystick = frame.GetFlystick( this.flystickId - 1 ); + + if ( dtFlystick != null ) { if ( dtFlystick.IsTracked ) { diff --git a/Source/Receivers/DTrackReceiverHand.cs b/Source/Receivers/DTrackReceiverHand.cs index c160b08..75fc0ce 100644 --- a/Source/Receivers/DTrackReceiverHand.cs +++ b/Source/Receivers/DTrackReceiverHand.cs @@ -2,7 +2,7 @@ * * Script providing DTRACK Fingertracking data to a hand mapper script. * - * Copyright (c) 2021-2023 Advanced Realtime Tracking GmbH & Co. KG + * Copyright (c) 2021-2024 Advanced Realtime Tracking GmbH & Co. KG * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -216,13 +216,12 @@ public DTrackReceiverHand.Hand GetHand() { DTrackSDK.Frame frame = GetDTrackFrame(); // ensures data integrity against DTrack class if ( frame == null ) return null; // no new tracking data - if ( frame.Hands == null ) return null; try { - DTrackSDK.DTrackHand dtHand; + DTrackSDK.DTrackHand dtHand = frame.GetHand( this.handId - 1 ); - if ( frame.Hands.TryGetValue( this.handId - 1, out dtHand ) ) + if ( dtHand != null ) { if ( dtHand.IsTracked ) { diff --git a/package.json b/package.json index a3dd57d..e91591d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.ar-tracking.dtrack", - "version": "1.1.2", + "version": "1.1.3", "displayName": "ART DTRACK Plugin", "description": "Plugin for integration of Advanced Realtime Tracking DTRACK tracking solutions.", "unity": "2019.4",