diff --git a/.gitignore b/.gitignore index 8cefc90..ccda3ce 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ bld/ # Visual Studio 2015 cache/options directory .vs/ +/.vscode/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b469fa4..5fe766a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v1.3.0 + +- Added SDK-independent OptiTrack core models and `IOptiTrackClient`. +- Added a NatNet-backed adapter that isolates direct `NatNetML` usage under `OptiTrack.NatNet`. +- Updated the Grasshopper component to consume internal domain models instead of raw NatNet SDK frame types. +- Added a default no-op telemetry abstraction and sanitizer for future privacy-aware error/performance reporting. +- Added developer and architecture documentation, including a Mermaid diagram of the Motive-to-Grasshopper flow. +- Updated assembly metadata and documentation version references to `1.3.0`. + ## v1.2.0 - Reorganized the repository around `src/`, `lib/`, `docs/`, and `examples/`. diff --git a/README.md b/README.md index 5ead36c..c65da0d 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Maintainers may use the Codex Sentry plugin for read-only issue review, but that ## Version -Current modernization target: `v1.2.0`. +Current modernization target: `v1.3.0`. ## Contributors diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..df25a38 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,27 @@ +# Architecture + +Tracker separates Grasshopper UI code from NatNet SDK-specific code so future NatNet upgrades, tests, and telemetry work can happen behind stable internal boundaries. + +```mermaid +flowchart LR + Motive[OptiTrack Motive
NatNet broadcast] --> Adapter[NatNet adapter
OptiTrack.NatNet] + Adapter --> Core[OptiTrack core models
OptiTrack.Core] + Adapter --> Telemetry[Telemetry abstraction
NoOp by default] + Core --> Grasshopper[Grasshopper components
Tracker] + Grasshopper --> Rhino[Rhino geometry outputs
points, labels, planes] + Grasshopper --> Telemetry +``` + +## Boundaries + +`OptiTrack.NatNet` is the only layer that should reference `NatNetML` classes. It owns the NatNet client, data descriptors, frame event callback, and conversion into SDK-independent models. + +`OptiTrack.Core` contains internal domain models and `IOptiTrackClient`. This layer does not know about Rhino, Grasshopper, or Sentry. + +`TrackerComponent` consumes `IOptiTrackClient`, receives `OptiTrackFrame` instances, and converts them into Grasshopper outputs. Rhino geometry creation remains in the Grasshopper layer. + +`OptiTrack.Telemetry` defines a future reporting boundary. The active implementation is `NoOpTelemetryService`, so telemetry calls are local no-ops unless a later release explicitly configures a transport. + +## Privacy + +The abstraction does not make motion-capture data safe for telemetry. Domain models can contain marker coordinates and rigid body names for local component outputs. Telemetry integrations must use `TelemetrySanitizer` and must avoid raw frame data, marker coordinates, rigid body names, file paths, IP addresses, usernames, machine names, and Rhino document names. diff --git a/docs/developer-guide.md b/docs/developer-guide.md new file mode 100644 index 0000000..4c0cc53 --- /dev/null +++ b/docs/developer-guide.md @@ -0,0 +1,50 @@ +# Developer Guide + +## Project Layout + +- `src/Tracker` contains the Grasshopper plugin project. +- `src/Tracker/OptiTrack/Core` contains SDK-independent domain models and client interfaces. +- `src/Tracker/OptiTrack/NatNet` contains the NatNetML-backed adapter and conversion helpers. +- `src/Tracker/OptiTrack/Telemetry` contains the default-disabled telemetry abstraction. +- `lib/NatNet` contains the current bundled NatNet SDK 4.0 files. + +## NatNet Boundary + +Grasshopper components should depend on `OptiTrack.Core` models and interfaces instead of `NatNetML` types. Keep direct usage of `NatNetClientML`, `FrameOfMocapData`, `DataDescriptor`, and related SDK classes inside `OptiTrack.NatNet`. + +The current adapter is `NatNetOptiTrackClient`, which implements `IOptiTrackClient`. + +## Domain Models + +Use these models when passing capture data across internal boundaries: + +- `OptiTrackFrame` +- `OptiTrackMarker` +- `OptiTrackRigidBody` +- `OptiTrackSkeleton` +- `OptiTrackConnectionOptions` +- `OptiTrackConnectionInfo` +- `OptiTrackConnectionStatus` +- `OptiTrackFrameEventArgs` +- `OptiTrackConnectionEventArgs` + +These models may contain motion-capture values for local Grasshopper output. They must not be serialized into telemetry payloads. + +## Telemetry + +Telemetry is represented by `ITelemetryService` and defaults to `NoOpTelemetryService`. New code may call the interface for sanitized operation boundaries, but must not add a real Sentry transport without a separate review. + +Only send coarse operational fields to telemetry in future integrations. Never send marker coordinates, rigid body names, raw frame payloads, IP addresses, file paths, usernames, machine names, or Rhino document names. + +## Conversion Helpers + +`NatNetFrameConverter` performs the SDK-to-domain conversion. Keep conversion behavior small and testable. If a test project is added later, cover: + +- NatNet frame metadata maps to `OptiTrackFrame`. +- Marker IDs map to marker labels without sending coordinates to telemetry. +- Rigid body position/quaternion values map to `OptiTrackRigidBody`. +- Missing or untracked rigid bodies do not create Grasshopper planes. + +## Build + +Use the local build command in [build.md](build.md). Rhino and Motive integration still require manual validation on a machine with the relevant runtime dependencies. diff --git a/docs/telemetry.md b/docs/telemetry.md index 3a8ad7f..31905b0 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -22,9 +22,22 @@ Recognized placeholder settings: - `SENTRY_DSN` - optional DSN; telemetry remains disabled when absent or empty. - `SENTRY_ENVIRONMENT` - optional environment name such as `development`, `lab`, or `production`. -- `SENTRY_RELEASE` - optional release identifier, for example `tracker@1.2.0`. +- `SENTRY_RELEASE` - optional release identifier, for example `tracker@1.3.0`. - `SENTRY_TRACES_SAMPLE_RATE` - optional numeric sample rate for aggregate performance telemetry. +## Current v1.3.0 Boundary + +Tracker now includes an internal telemetry boundary: + +- `ITelemetryService` +- `NoOpTelemetryService` +- `TelemetryEvent` +- `TelemetryContext` +- `TelemetryScope` +- `TelemetrySanitizer` + +The Grasshopper component and NatNet adapter use this boundary for sanitized exception and operation hooks. The default implementation is no-op, so no data leaves the process. + ## Sentry Plugin Operations Repository maintainers may use the Codex Sentry plugin for read-only issue and event inspection during maintenance. That workflow is separate from Tracker runtime telemetry. diff --git a/src/Tracker/OptiTrack/Core/IOptiTrackClient.cs b/src/Tracker/OptiTrack/Core/IOptiTrackClient.cs new file mode 100644 index 0000000..ec6ae11 --- /dev/null +++ b/src/Tracker/OptiTrack/Core/IOptiTrackClient.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace OptiTrack.Core { + + public interface IOptiTrackClient { + + bool IsConnected { get; } + + OptiTrackConnectionInfo ConnectionInfo { get; } + + event EventHandler FrameReceived; + + event EventHandler ConnectionChanged; + + Task ConnectAsync( OptiTrackConnectionOptions options, CancellationToken cancellationToken ); + + Task DisconnectAsync(); + } +} diff --git a/src/Tracker/OptiTrack/Core/OptiTrackConnectionInfo.cs b/src/Tracker/OptiTrack/Core/OptiTrackConnectionInfo.cs new file mode 100644 index 0000000..f16a0b1 --- /dev/null +++ b/src/Tracker/OptiTrack/Core/OptiTrackConnectionInfo.cs @@ -0,0 +1,23 @@ +namespace OptiTrack.Core { + + public sealed class OptiTrackConnectionInfo { + + public OptiTrackConnectionInfo() { + Status = OptiTrackConnectionStatus.Disconnected; + } + + public OptiTrackConnectionStatus Status { get; set; } + + public string LocalAddress { get; set; } = string.Empty; + + public string ServerAddress { get; set; } = string.Empty; + + public string HostApplication { get; set; } = string.Empty; + + public string NatNetVersion { get; set; } = string.Empty; + + public bool IsConnected { + get { return Status == OptiTrackConnectionStatus.Connected; } + } + } +} diff --git a/src/Tracker/OptiTrack/Core/OptiTrackConnectionOptions.cs b/src/Tracker/OptiTrack/Core/OptiTrackConnectionOptions.cs new file mode 100644 index 0000000..73d94d0 --- /dev/null +++ b/src/Tracker/OptiTrack/Core/OptiTrackConnectionOptions.cs @@ -0,0 +1,26 @@ +namespace OptiTrack.Core { + + public enum OptiTrackConnectionType { + Multicast, + Unicast + } + + public sealed class OptiTrackConnectionOptions { + + public string LocalAddress { get; set; } = "127.0.0.1"; + + public string ServerAddress { get; set; } = "127.0.0.1"; + + public OptiTrackConnectionType ConnectionType { get; set; } = OptiTrackConnectionType.Multicast; + + public bool IncludeMarkers { get; set; } = true; + + public bool IncludeRigidBodies { get; set; } + + public bool IncludeSkeletons { get; set; } + + public bool IncludeForcePlates { get; set; } + + public int FrameDivisor { get; set; } = 4; + } +} diff --git a/src/Tracker/OptiTrack/Core/OptiTrackConnectionStatus.cs b/src/Tracker/OptiTrack/Core/OptiTrackConnectionStatus.cs new file mode 100644 index 0000000..ed04616 --- /dev/null +++ b/src/Tracker/OptiTrack/Core/OptiTrackConnectionStatus.cs @@ -0,0 +1,10 @@ +namespace OptiTrack.Core { + + public enum OptiTrackConnectionStatus { + Disconnected, + Connecting, + Connected, + Disconnecting, + Faulted + } +} diff --git a/src/Tracker/OptiTrack/Core/OptiTrackEventArgs.cs b/src/Tracker/OptiTrack/Core/OptiTrackEventArgs.cs new file mode 100644 index 0000000..0e12996 --- /dev/null +++ b/src/Tracker/OptiTrack/Core/OptiTrackEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace OptiTrack.Core { + + public sealed class OptiTrackFrameEventArgs : EventArgs { + + public OptiTrackFrameEventArgs( OptiTrackFrame frame ) { + Frame = frame; + } + + public OptiTrackFrame Frame { get; private set; } + } + + public sealed class OptiTrackConnectionEventArgs : EventArgs { + + public OptiTrackConnectionEventArgs( OptiTrackConnectionInfo connectionInfo, string message ) { + ConnectionInfo = connectionInfo; + Message = message; + } + + public OptiTrackConnectionInfo ConnectionInfo { get; private set; } + + public string Message { get; private set; } + } +} diff --git a/src/Tracker/OptiTrack/Core/OptiTrackModels.cs b/src/Tracker/OptiTrack/Core/OptiTrackModels.cs new file mode 100644 index 0000000..43ae413 --- /dev/null +++ b/src/Tracker/OptiTrack/Core/OptiTrackModels.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; + +namespace OptiTrack.Core { + + public sealed class OptiTrackMarker { + + public int Id { get; set; } + + public string Label { get; set; } = string.Empty; + + public double X { get; set; } + + public double Y { get; set; } + + public double Z { get; set; } + } + + public sealed class OptiTrackRigidBody { + + public int Id { get; set; } + + public string Name { get; set; } = string.Empty; + + public bool IsTracked { get; set; } + + public double X { get; set; } + + public double Y { get; set; } + + public double Z { get; set; } + + public double Qx { get; set; } + + public double Qy { get; set; } + + public double Qz { get; set; } + + public double Qw { get; set; } + } + + public sealed class OptiTrackSkeleton { + + public int Id { get; set; } + + public string Name { get; set; } = string.Empty; + } + + public sealed class OptiTrackFrame { + + public OptiTrackFrame() { + Markers = new List(); + RigidBodies = new List(); + Skeletons = new List(); + StatusMessages = new List(); + } + + public int FrameNumber { get; set; } + + public bool IsRecording { get; set; } + + public bool AssetsChanged { get; set; } + + public IReadOnlyList Markers { get; set; } + + public IReadOnlyList RigidBodies { get; set; } + + public IReadOnlyList Skeletons { get; set; } + + public IReadOnlyList StatusMessages { get; set; } + } +} diff --git a/src/Tracker/OptiTrack/NatNet/NatNetFrameConverter.cs b/src/Tracker/OptiTrack/NatNet/NatNetFrameConverter.cs new file mode 100644 index 0000000..68f29e6 --- /dev/null +++ b/src/Tracker/OptiTrack/NatNet/NatNetFrameConverter.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + +using NatNetML; + +using OptiTrack.Core; + +namespace OptiTrack.NatNet { + + internal static class NatNetFrameConverter { + + internal static OptiTrackFrame ConvertFrame( FrameOfMocapData data, IReadOnlyList rigidBodyDescriptions, OptiTrackConnectionOptions options, IReadOnlyList statusMessages ) { + List markers = new List(); + List rigidBodies = new List(); + + if ( options.IncludeMarkers ) { + for ( int i = 0; i < data.nOtherMarkers; i++ ) { + Marker marker = data.OtherMarkers[ i ]; + markers.Add( new OptiTrackMarker { + Id = marker.ID, + Label = marker.ID.ToString(), + X = Math.Round( marker.x, 4 ), + Y = Math.Round( marker.y, 4 ), + Z = Math.Round( marker.z, 4 ) + } ); + } + } + + if ( options.IncludeRigidBodies ) { + for ( int i = 0; i < rigidBodyDescriptions.Count; i++ ) { + RigidBody description = rigidBodyDescriptions[ i ]; + + for ( int j = 0; j < data.nRigidBodies; j++ ) { + RigidBodyData rigidBodyData = data.RigidBodies[ j ]; + if ( !rigidBodyData.Tracked ) { + continue; + } + + rigidBodies.Add( new OptiTrackRigidBody { + Id = description.ID, + Name = description.Name, + IsTracked = rigidBodyData.Tracked, + X = Math.Round( rigidBodyData.x, 4 ), + Y = Math.Round( rigidBodyData.y, 4 ), + Z = Math.Round( rigidBodyData.z, 4 ), + Qx = Math.Round( rigidBodyData.qx, 6 ), + Qy = Math.Round( rigidBodyData.qy, 6 ), + Qz = Math.Round( rigidBodyData.qz, 6 ), + Qw = Math.Round( rigidBodyData.qw, 6 ) + } ); + } + } + } + + return new OptiTrackFrame { + FrameNumber = data.iFrame, + IsRecording = data.bRecording, + AssetsChanged = data.bTrackingModelsChanged, + Markers = markers, + RigidBodies = rigidBodies, + StatusMessages = statusMessages + }; + } + } +} diff --git a/src/Tracker/OptiTrack/NatNet/NatNetOptiTrackClient.cs b/src/Tracker/OptiTrack/NatNet/NatNetOptiTrackClient.cs new file mode 100644 index 0000000..17e3f8e --- /dev/null +++ b/src/Tracker/OptiTrack/NatNet/NatNetOptiTrackClient.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using NatNetML; + +using OptiTrack.Core; +using OptiTrack.Telemetry; + +namespace OptiTrack.NatNet { + + public sealed class NatNetOptiTrackClient : IOptiTrackClient { + private readonly object syncRoot = new object(); + private readonly ITelemetryService telemetryService; + private NatNetClientML natNetClient; + private OptiTrackConnectionOptions options; + private List dataDescriptors; + private List rigidBodies; + private List skeletons; + private List forcePlates; + private List statusMessages; + private bool assetsChanged; + + public NatNetOptiTrackClient( ITelemetryService telemetryService ) { + this.telemetryService = telemetryService ?? new NoOpTelemetryService(); + natNetClient = new NatNetClientML(); + options = new OptiTrackConnectionOptions(); + ConnectionInfo = new OptiTrackConnectionInfo(); + dataDescriptors = new List(); + rigidBodies = new List(); + skeletons = new List(); + forcePlates = new List(); + statusMessages = new List(); + } + + public bool IsConnected { + get { return ConnectionInfo.IsConnected; } + } + + public OptiTrackConnectionInfo ConnectionInfo { get; private set; } + + public event EventHandler FrameReceived; + + public event EventHandler ConnectionChanged; + + public Task ConnectAsync( OptiTrackConnectionOptions connectionOptions, CancellationToken cancellationToken ) { + return Task.Run( () => Connect( connectionOptions, cancellationToken ), cancellationToken ); + } + + public Task DisconnectAsync() { + return Task.Run( () => Disconnect() ); + } + + private void Connect( OptiTrackConnectionOptions connectionOptions, CancellationToken cancellationToken ) { + OptiTrackConnectionOptions requestedOptions = connectionOptions ?? new OptiTrackConnectionOptions(); + using ( telemetryService.StartSpan( "natnet.connect", new TelemetryContext().SetTag( "connection_type", requestedOptions.ConnectionType.ToString() ) ) ) { + cancellationToken.ThrowIfCancellationRequested(); + options = requestedOptions; + options.FrameDivisor = Math.Max( 1, options.FrameDivisor ); + + SetConnectionStatus( OptiTrackConnectionStatus.Connecting, "Attempting connection to server..." ); + + try { + lock ( syncRoot ) { + natNetClient = new NatNetClientML(); + statusMessages.Clear(); + statusMessages.Add( "NatNet managed client application starting..." ); + statusMessages.Add( "Local IP set." ); + statusMessages.Add( "Server IP set." ); + + int[] natNetVersion = natNetClient.NatNetVersion(); + ConnectionInfo.NatNetVersion = string.Format( "{0}.{1}.{2}.{3}", natNetVersion[ 0 ], natNetVersion[ 1 ], natNetVersion[ 2 ], natNetVersion[ 3 ] ); + + NatNetClientML.ConnectParams connectParams = new NatNetClientML.ConnectParams { + ConnectionType = ToNatNetConnectionType( options.ConnectionType ), + ServerAddress = options.ServerAddress, + LocalAddress = options.LocalAddress + }; + + natNetClient.Connect( connectParams ); + FetchServerDescriptor(); + FetchDataDescriptions(); + natNetClient.OnFrameReady += OnFrameReady; + } + + SetConnectionStatus( OptiTrackConnectionStatus.Connected, "Success: Data Port Connected." ); + } catch ( Exception exception ) { + telemetryService.CaptureException( exception, new TelemetryContext().SetTag( "operation", "natnet_connect" ) ); + SetConnectionStatus( OptiTrackConnectionStatus.Faulted, "Error: Failed to connect. Check the connection settings." ); + throw; + } + } + } + + private void Disconnect() { + SetConnectionStatus( OptiTrackConnectionStatus.Disconnecting, "Disconnecting from server..." ); + + try { + lock ( syncRoot ) { + natNetClient.OnFrameReady -= OnFrameReady; + natNetClient.Disconnect(); + dataDescriptors.Clear(); + rigidBodies.Clear(); + skeletons.Clear(); + forcePlates.Clear(); + statusMessages.Clear(); + statusMessages.Add( "Service stopped. Activate module to begin streaming." ); + } + + SetConnectionStatus( OptiTrackConnectionStatus.Disconnected, "Service stopped. Activate module to begin streaming." ); + } catch ( Exception exception ) { + telemetryService.CaptureException( exception, new TelemetryContext().SetTag( "operation", "natnet_disconnect" ) ); + SetConnectionStatus( OptiTrackConnectionStatus.Faulted, "Error: Failed to disconnect cleanly." ); + } + } + + private void FetchServerDescriptor() { + ServerDescription serverDescription = new ServerDescription(); + int errorCode = natNetClient.GetServerDescription( serverDescription ); + + if ( errorCode == 0 ) { + statusMessages.Add( "Success: Connected to the server." ); + statusMessages.Add( "Server Info:" ); + statusMessages.Add( "Host: " + serverDescription.HostComputerName ); + statusMessages.Add( "Application Name: " + serverDescription.HostApp ); + ConnectionInfo.HostApplication = serverDescription.HostApp; + ConnectionInfo.LocalAddress = options.LocalAddress; + ConnectionInfo.ServerAddress = options.ServerAddress; + } else { + statusMessages.Add( "Error: Failed to connect. Check the connection settings." ); + statusMessages.Add( "Program terminated." ); + } + } + + private void FetchDataDescriptions() { + bool result = natNetClient.GetDataDescriptions( out dataDescriptors ); + if ( !result ) { + statusMessages.Add( "Error: Could not get the Data Descriptions" ); + return; + } + + statusMessages.Add( "Success: Data Descriptions obtained from the server." ); + statusMessages.Add( "Total " + dataDescriptors.Count + " data sets in the capture:" ); + rigidBodies.Clear(); + skeletons.Clear(); + forcePlates.Clear(); + + for ( int i = 0; i < dataDescriptors.Count; i++ ) { + DataDescriptor descriptor = dataDescriptors[ i ]; + switch ( descriptor.type ) { + case (int) DataDescriptorType.eMarkerSetData: + MarkerSet markerSet = (MarkerSet) descriptor; + statusMessages.Add( "MarkerSet (" + markerSet.Name + ")" ); + break; + + case (int) DataDescriptorType.eRigidbodyData: + RigidBody rigidBody = (RigidBody) descriptor; + statusMessages.Add( "RigidBody (" + rigidBody.Name + ")" ); + rigidBodies.Add( rigidBody ); + break; + + case (int) DataDescriptorType.eSkeletonData: + skeletons.Add( (Skeleton) descriptor ); + break; + + case (int) DataDescriptorType.eForcePlateData: + forcePlates.Add( (ForcePlate) descriptor ); + break; + + default: + statusMessages.Add( "Error: Invalid Data Set" ); + break; + } + } + } + + private void OnFrameReady( FrameOfMocapData data, NatNetClientML client ) { + if ( data.iFrame % options.FrameDivisor != 0 ) { + return; + } + + try { + OptiTrackFrame frame; + + lock ( syncRoot ) { + if ( data.bTrackingModelsChanged == true || data.nRigidBodies != rigidBodies.Count || data.nSkeletons != skeletons.Count || data.nForcePlates != forcePlates.Count || assetsChanged ) { + assetsChanged = false; + FetchDataDescriptions(); + } + + frame = NatNetFrameConverter.ConvertFrame( data, rigidBodies, options, new List( statusMessages ) ); + } + + EventHandler handler = FrameReceived; + if ( handler != null ) { + handler( this, new OptiTrackFrameEventArgs( frame ) ); + } + } catch ( Exception exception ) { + telemetryService.CaptureException( exception, new TelemetryContext().SetTag( "operation", "natnet_frame_conversion" ) ); + } + } + + private void SetConnectionStatus( OptiTrackConnectionStatus status, string message ) { + ConnectionInfo.Status = status; + + EventHandler handler = ConnectionChanged; + if ( handler != null ) { + handler( this, new OptiTrackConnectionEventArgs( ConnectionInfo, message ) ); + } + } + + private static ConnectionType ToNatNetConnectionType( OptiTrackConnectionType connectionType ) { + return connectionType == OptiTrackConnectionType.Unicast ? ConnectionType.Unicast : ConnectionType.Multicast; + } + } +} diff --git a/src/Tracker/OptiTrack/Telemetry/ITelemetryService.cs b/src/Tracker/OptiTrack/Telemetry/ITelemetryService.cs new file mode 100644 index 0000000..0f87f15 --- /dev/null +++ b/src/Tracker/OptiTrack/Telemetry/ITelemetryService.cs @@ -0,0 +1,13 @@ +using System; + +namespace OptiTrack.Telemetry { + + public interface ITelemetryService { + + void CaptureException( Exception exception, TelemetryContext context ); + + void CaptureMessage( string message, TelemetrySeverity severity, TelemetryContext context ); + + TelemetryScope StartSpan( string operationName, TelemetryContext context ); + } +} diff --git a/src/Tracker/OptiTrack/Telemetry/NoOpTelemetryService.cs b/src/Tracker/OptiTrack/Telemetry/NoOpTelemetryService.cs new file mode 100644 index 0000000..c9bed33 --- /dev/null +++ b/src/Tracker/OptiTrack/Telemetry/NoOpTelemetryService.cs @@ -0,0 +1,17 @@ +using System; + +namespace OptiTrack.Telemetry { + + public sealed class NoOpTelemetryService : ITelemetryService { + + public void CaptureException( Exception exception, TelemetryContext context ) { + } + + public void CaptureMessage( string message, TelemetrySeverity severity, TelemetryContext context ) { + } + + public TelemetryScope StartSpan( string operationName, TelemetryContext context ) { + return new TelemetryScope( this, operationName, context ); + } + } +} diff --git a/src/Tracker/OptiTrack/Telemetry/TelemetryContext.cs b/src/Tracker/OptiTrack/Telemetry/TelemetryContext.cs new file mode 100644 index 0000000..8381f3d --- /dev/null +++ b/src/Tracker/OptiTrack/Telemetry/TelemetryContext.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace OptiTrack.Telemetry { + + public sealed class TelemetryContext { + + public TelemetryContext() { + Tags = new Dictionary(); + Metrics = new Dictionary(); + } + + public IDictionary Tags { get; private set; } + + public IDictionary Metrics { get; private set; } + + public TelemetryContext SetTag( string key, string value ) { + Tags[ TelemetrySanitizer.SanitizeKey( key ) ] = TelemetrySanitizer.SanitizeTagValue( key, value ); + return this; + } + + public TelemetryContext SetMetric( string key, double value ) { + Metrics[ TelemetrySanitizer.SanitizeKey( key ) ] = value; + return this; + } + } +} diff --git a/src/Tracker/OptiTrack/Telemetry/TelemetryEvent.cs b/src/Tracker/OptiTrack/Telemetry/TelemetryEvent.cs new file mode 100644 index 0000000..ad0e6c1 --- /dev/null +++ b/src/Tracker/OptiTrack/Telemetry/TelemetryEvent.cs @@ -0,0 +1,22 @@ +using System; + +namespace OptiTrack.Telemetry { + + public sealed class TelemetryEvent { + + public TelemetryEvent( string message, TelemetrySeverity severity, TelemetryContext context ) { + Message = TelemetrySanitizer.SanitizeValue( message ); + Severity = severity; + Context = context ?? new TelemetryContext(); + TimestampUtc = DateTime.UtcNow; + } + + public string Message { get; private set; } + + public TelemetrySeverity Severity { get; private set; } + + public TelemetryContext Context { get; private set; } + + public DateTime TimestampUtc { get; private set; } + } +} diff --git a/src/Tracker/OptiTrack/Telemetry/TelemetrySanitizer.cs b/src/Tracker/OptiTrack/Telemetry/TelemetrySanitizer.cs new file mode 100644 index 0000000..1b84a3b --- /dev/null +++ b/src/Tracker/OptiTrack/Telemetry/TelemetrySanitizer.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace OptiTrack.Telemetry { + + public static class TelemetrySanitizer { + private static readonly Regex IPv4Pattern = new Regex( @"\b(?:\d{1,3}\.){3}\d{1,3}\b", RegexOptions.Compiled ); + private static readonly Regex WindowsPathPattern = new Regex( @"[A-Za-z]:\\[^\s]+", RegexOptions.Compiled ); + private static readonly Regex EmailPattern = new Regex( @"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}", RegexOptions.Compiled | RegexOptions.IgnoreCase ); + + private static readonly HashSet SensitiveKeyParts = new HashSet( StringComparer.OrdinalIgnoreCase ) { + "address", + "body", + "capture", + "file", + "host", + "ip", + "machine", + "marker", + "model", + "name", + "path", + "position", + "project", + "rigidbody", + "token", + "user" + }; + + public static string SanitizeKey( string key ) { + if ( string.IsNullOrWhiteSpace( key ) ) { + return "unknown"; + } + + return key.Trim().Replace( " ", "_" ).ToLowerInvariant(); + } + + public static string SanitizeValue( string value ) { + if ( string.IsNullOrEmpty( value ) ) { + return string.Empty; + } + + string sanitized = EmailPattern.Replace( value, "[redacted-email]" ); + sanitized = IPv4Pattern.Replace( sanitized, "[redacted-ip]" ); + sanitized = WindowsPathPattern.Replace( sanitized, "[redacted-path]" ); + return sanitized; + } + + public static bool IsSensitiveKey( string key ) { + if ( string.IsNullOrWhiteSpace( key ) ) { + return false; + } + + foreach ( string part in SensitiveKeyParts ) { + if ( key.IndexOf( part, StringComparison.OrdinalIgnoreCase ) >= 0 ) { + return true; + } + } + + return false; + } + + public static string SanitizeTagValue( string key, string value ) { + if ( IsSensitiveKey( key ) ) { + return "[redacted]"; + } + + return SanitizeValue( value ); + } + } +} diff --git a/src/Tracker/OptiTrack/Telemetry/TelemetryScope.cs b/src/Tracker/OptiTrack/Telemetry/TelemetryScope.cs new file mode 100644 index 0000000..4ff6d36 --- /dev/null +++ b/src/Tracker/OptiTrack/Telemetry/TelemetryScope.cs @@ -0,0 +1,31 @@ +using System; +using System.Diagnostics; + +namespace OptiTrack.Telemetry { + + public sealed class TelemetryScope : IDisposable { + private readonly ITelemetryService telemetryService; + private readonly string operationName; + private readonly TelemetryContext context; + private readonly Stopwatch stopwatch; + private bool disposed; + + public TelemetryScope( ITelemetryService telemetryService, string operationName, TelemetryContext context ) { + this.telemetryService = telemetryService; + this.operationName = TelemetrySanitizer.SanitizeValue( operationName ); + this.context = context ?? new TelemetryContext(); + stopwatch = Stopwatch.StartNew(); + } + + public void Dispose() { + if ( disposed ) { + return; + } + + disposed = true; + stopwatch.Stop(); + context.SetMetric( "duration_ms", stopwatch.Elapsed.TotalMilliseconds ); + telemetryService.CaptureMessage( operationName, TelemetrySeverity.Debug, context ); + } + } +} diff --git a/src/Tracker/OptiTrack/Telemetry/TelemetrySeverity.cs b/src/Tracker/OptiTrack/Telemetry/TelemetrySeverity.cs new file mode 100644 index 0000000..ec26858 --- /dev/null +++ b/src/Tracker/OptiTrack/Telemetry/TelemetrySeverity.cs @@ -0,0 +1,10 @@ +namespace OptiTrack.Telemetry { + + public enum TelemetrySeverity { + Debug, + Info, + Warning, + Error, + Fatal + } +} diff --git a/src/Tracker/Properties/AssemblyInfo.cs b/src/Tracker/Properties/AssemblyInfo.cs index 4246855..28869c3 100644 --- a/src/Tracker/Properties/AssemblyInfo.cs +++ b/src/Tracker/Properties/AssemblyInfo.cs @@ -27,6 +27,6 @@ // // You can specify all the values or you can default the Build and Revision Numbers by // using the '*' as shown below: [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion( "1.2.0.0" )] -[assembly: AssemblyFileVersion( "1.2.0.0" )] -[assembly: AssemblyInformationalVersion( "1.2.0" )] +[assembly: AssemblyVersion( "1.3.0.0" )] +[assembly: AssemblyFileVersion( "1.3.0.0" )] +[assembly: AssemblyInformationalVersion( "1.3.0" )] diff --git a/src/Tracker/Tracker.csproj b/src/Tracker/Tracker.csproj index a839105..97254b1 100644 --- a/src/Tracker/Tracker.csproj +++ b/src/Tracker/Tracker.csproj @@ -63,6 +63,21 @@ True Icons.resx + + + + + + + + + + + + + + + diff --git a/src/Tracker/TrackerComponent.cs b/src/Tracker/TrackerComponent.cs index 5f33c34..ddffb88 100644 --- a/src/Tracker/TrackerComponent.cs +++ b/src/Tracker/TrackerComponent.cs @@ -1,11 +1,13 @@ -using System; -using System.Collections; +using System; using System.Collections.Generic; +using System.Threading; using System.Windows.Forms; using Grasshopper.Kernel; -using NatNetML; +using OptiTrack.Core; +using OptiTrack.NatNet; +using OptiTrack.Telemetry; using Rhino.Geometry; @@ -15,6 +17,23 @@ namespace Tracker { /// The main tracker component. /// public class TrackerComponent : GH_Component { + private static readonly ITelemetryService Telemetry = new NoOpTelemetryService(); + private static IOptiTrackClient optiTrackClient = CreateClient(); + private static OptiTrackFrame currentFrame; + private static bool handlersAttached; + private static bool connectionConfirmed; + private static int counter; + + private static bool RigidBody; + private static bool Skeleton; + private static bool ForcePlate; + private static bool yUp; + + public static List Log = new List(); + protected static List mPoints = new List(); + protected static List mLabels = new List(); + private static readonly List rBodyNames = new List(); + private static readonly List rBodyPlanes = new List(); public TrackerComponent() : base( "OptiTrack Stream", @@ -24,59 +43,15 @@ public TrackerComponent() "OptiTrack" ) { } - public static List Log = new List(); - - // [NatNet] Network connection configuration - private static NatNetClientML mNatNet = new NatNetClientML(); - - private static string mStrLocalIP = "127.0.0.1"; // Local IP address (string) - private static string mStrServerIP = "127.0.0.1"; // Server IP address (string) - private static readonly ConnectionType mConnectionType = ConnectionType.Multicast; // Multicast or Unicast mode. Multicast recommended. - - // List for saving each of the data descriptors - private static List mDataDescriptor = new List(); - - // Lists and Hashtables for saving data descriptions - public static Hashtable mHtSkelRBs = new Hashtable(); - - public static List mMarkerSet = new List(); - public static List mRigidBodies = new List(); - public static List mSkeletons = new List(); - public static List mForcePlates = new List(); - - // Boolean value for detecting change in assset - private static bool mAssetChanged = false; - - private static bool connectionConfirmed = false; - private static int counter; - - // Flags are appended to a list for checking at SolveInstance. - private static bool RigidBody = false; - - private static bool Skeleton = false; - private static bool ForcePlate = false; - - // Marker points - protected static List mPoints = new List(); - - protected static List mLabels = new List(); - - // Rigid body data. - private static readonly List rBodyNames = new List(); - - private static readonly List rBodyPos = new List(); - private static readonly List rBodyQuats = new List(); - private static readonly List rBodyPlanes = new List(); - - //Setup Configurations - private static bool yUp = false; + private static IOptiTrackClient CreateClient() { + return new NatNetOptiTrackClient( Telemetry ); + } protected override void RegisterInputParams( GH_InputParamManager pManager ) { pManager.AddBooleanParameter( "Activate", "Activate", "Activate the streaming module.", GH_ParamAccess.item, false ); pManager.AddBooleanParameter( "Reset", "Reset", "Reset the streaming module.", GH_ParamAccess.item, false ); pManager.AddTextParameter( "Local IP", "Local IP", "IP address for the receiver.", GH_ParamAccess.item, "127.0.0.1" ); pManager.AddTextParameter( "Server IP", "Server IP", "IP address for the server.", GH_ParamAccess.item, "127.0.0.1" ); - // TODO Frame rate variable input either here or in the menu. } protected override void RegisterOutputParams( GH_OutputParamManager pManager ) { @@ -85,10 +60,6 @@ protected override void RegisterOutputParams( GH_OutputParamManager pManager ) { pManager.AddTextParameter( "Labels", "Labels", "Marker labels.", GH_ParamAccess.list ); pManager.AddTextParameter( "RB Name", "RB Name", "List of Rigid Body name data.", GH_ParamAccess.list ); pManager.AddPlaneParameter( "BR Plane", "BR Plane", "List of planes assosieated with the Ridgid Bodys", GH_ParamAccess.list ); - // TODO pManager.AddTextParameter("RB Position", "RB Pos", "List of Rigid Body - // position data.", GH_ParamAccess.list); TODO pManager.AddTextParameter("RB - // Quaternion", "RB Quat", "List of Rigid Body quaternion data.", - // GH_ParamAccess.list); } protected override void SolveInstance( IGH_DataAccess DA ) { @@ -106,77 +77,21 @@ protected override void SolveInstance( IGH_DataAccess DA ) { if ( !DA.GetData( 3, ref serverIP ) ) return; - // Reset the service if the flag is active. if ( reset ) { - mDataDescriptor.Clear(); - mHtSkelRBs.Clear(); - mRigidBodies.Clear(); - mSkeletons.Clear(); - mForcePlates.Clear(); - Log.Clear(); - + ClearFrameState(); + DisconnectClient(); counter = 0; } - // Activate the streaming module and attempt to find a Motive broadcast. if ( activate && counter == 0 ) { - Log.Add( "NatNet managed client application starting..." ); - - // If not default, set the IP address - mStrLocalIP = localIP; - Log.Add( "Local IP set." ); - - mStrServerIP = serverIP; - Log.Add( "Server IP set." ); - - // Attempt connection to the server. - Log.Add( "Attempting connection to server..." ); - ConnectToServer(); - Log.Add( "Fetching the Server Descriptor." ); - connectionConfirmed = FetchServerDescriptor(); //Fetch and parse data descriptor - - Log.Add( "Fetching the Frame Data." ); - /* [NatNet] Assigning a event handler function for fetching frame data each time a frame is received */ - mNatNet.OnFrameReady += new FrameReadyEventHandler( FetchFrameData ); - Log.Add( "Success: Data Port Connected." ); - + ConnectClient( localIP, serverIP ); counter++; - } else if ( activate && connectionConfirmed ) // If the service is activated and we have a confirmed connection to the server. - { - /* [NatNet] Assigning a event handler function for fetching frame data each time a frame is received */ - mNatNet.OnFrameReady += new FrameReadyEventHandler( FetchFrameData ); - - // Exception handler for updated assets list. - if ( mAssetChanged == true ) { - Log.Add( "Change in the list of the assets. Refetching the descriptions" ); - - /* Clear out existing lists */ - mDataDescriptor.Clear(); - mHtSkelRBs.Clear(); - mRigidBodies.Clear(); - mSkeletons.Clear(); - mForcePlates.Clear(); - - /* [NatNet] Re-fetch the updated list of descriptors */ - FetchDataDescriptor(); - mAssetChanged = false; - } - - /* [NatNet] Disabling data handling function */ - mNatNet.OnFrameReady -= FetchFrameData; - + } else if ( activate && connectionConfirmed ) { + ProcessFrameData( currentFrame ); counter++; } else if ( !activate ) { - /* Clearing Saved Descriptions */ - mRigidBodies.Clear(); - mSkeletons.Clear(); - mHtSkelRBs.Clear(); - mForcePlates.Clear(); - - if ( connectionConfirmed ) { - mNatNet.Disconnect(); - } - Log.Clear(); + DisconnectClient(); + ClearFrameState(); Log.Add( "Service stopped. Activate module to begin streaming." ); } @@ -185,382 +100,151 @@ protected override void SolveInstance( IGH_DataAccess DA ) { DA.SetDataList( "Markers", mPoints ); DA.SetDataList( "Labels", mLabels ); DA.SetDataList( "RB Name", rBodyNames ); - //DA.SetDataList("RB Position", rigidBodyPos); - //DA.SetDataList("RB Quaternion", rigidBodyQuat); DA.SetDataList( "BR Plane", rBodyPlanes ); - } catch ( Exception ) { - //throw; + } catch ( Exception exception ) { + Telemetry.CaptureException( exception, new TelemetryContext().SetTag( "operation", "grasshopper_set_output" ) ); } ExpireSolution( true ); } - /// - /// [NatNet] parseFrameData will be called when a frame of Mocap data has is received - /// from the server application. - /// - /// Note: This callback is on the network service thread, so it is important to return - /// from this function quickly as possible to prevent incoming frames of data from - /// buffering up on the network socket. - /// - /// Note: "data" is a reference structure to the current frame of data. NatNet re-uses - /// this same instance for each incoming frame, so it should not be kept (the values - /// contained in "data" will become replaced after this callback function has exited). - /// - /// The actual frame of mocap data - /// The NatNet client instance - private static void FetchFrameData( FrameOfMocapData data, NatNetClientML client ) { - /* Exception handler for cases where assets are added or removed. - Data description is re-obtained in the main function so that contents - in the frame handler is kept minimal. */ - if ( data.bTrackingModelsChanged == true || data.nRigidBodies != mRigidBodies.Count || data.nSkeletons != mSkeletons.Count || data.nForcePlates != mForcePlates.Count ) { - mAssetChanged = true; - } + private static void ConnectClient( string localIP, string serverIP ) { + EnsureClientHandlers(); + Log.Clear(); + Log.Add( "NatNet managed client application starting..." ); + Log.Add( "Local IP set." ); + Log.Add( "Server IP set." ); + Log.Add( "Attempting connection to server..." ); + + OptiTrackConnectionOptions options = new OptiTrackConnectionOptions { + LocalAddress = localIP, + ServerAddress = serverIP, + ConnectionType = OptiTrackConnectionType.Multicast, + IncludeMarkers = true, + IncludeRigidBodies = RigidBody, + IncludeSkeletons = Skeleton, + IncludeForcePlates = ForcePlate, + FrameDivisor = 4 + }; - /* Processing and ouputting frame data every 200th frame. - This conditional statement is included in order to simplify the program output */ - if ( data.iFrame % 4 == 0 ) // 120 FPS Flex 13 cameras (every 4th frame will give a solid 30 FPS in GH) - { - if ( data.bRecording == false ) { - //Log.Add("Frame # " + data.iFrame.ToString() + " Received:"); - } else if ( data.bRecording == true ) { - //Log.Add("[Recording] Frame #" + data.iFrame.ToString() + " Received:"); + try { + optiTrackClient.ConnectAsync( options, CancellationToken.None ).GetAwaiter().GetResult(); + connectionConfirmed = optiTrackClient.IsConnected; + if ( connectionConfirmed ) { + Log.Add( "Fetching the Frame Data." ); + Log.Add( "Success: Data Port Connected." ); } + } catch ( Exception exception ) { + connectionConfirmed = false; + Log.Add( "Error: Failed to connect. Check the connection settings." ); + Telemetry.CaptureException( exception, new TelemetryContext().SetTag( "operation", "component_connect" ) ); + } + } + + private static void DisconnectClient() { + if ( optiTrackClient == null || !optiTrackClient.IsConnected ) { + connectionConfirmed = false; + return; + } - ProcessFrameData( data ); + optiTrackClient.DisconnectAsync().GetAwaiter().GetResult(); + connectionConfirmed = false; + } + + private static void EnsureClientHandlers() { + if ( handlersAttached ) { + return; + } + + optiTrackClient.FrameReceived += OnFrameReceived; + optiTrackClient.ConnectionChanged += OnConnectionChanged; + handlersAttached = true; + } + + private static void OnFrameReceived( object sender, OptiTrackFrameEventArgs e ) { + currentFrame = e.Frame; + } + + private static void OnConnectionChanged( object sender, OptiTrackConnectionEventArgs e ) { + if ( !string.IsNullOrWhiteSpace( e.Message ) ) { + Log.Add( e.Message ); } } - /// - /// [NatNet] Process mocap frame data. - /// - /// - /// - private static void ProcessFrameData( FrameOfMocapData data ) { + private static void ClearFrameState() { + currentFrame = null; + Log.Clear(); + mPoints.Clear(); + mLabels.Clear(); + rBodyNames.Clear(); + rBodyPlanes.Clear(); + } + + private static void ProcessFrameData( OptiTrackFrame frame ) { + if ( frame == null ) { + return; + } + mPoints.Clear(); + mLabels.Clear(); + rBodyNames.Clear(); + rBodyPlanes.Clear(); - Point3d markerPoint; - bool createPoints = false; // Set tp true to create points for each marker. - bool allPoints = false; // Set to true to output all marker points. - - // [NatNet] Fetch Labeled Markers (Point Cloud) - for ( int i = 0; i < data.nOtherMarkers; i++ ) { - if ( createPoints ) { - // Recreate point data. - markerPoint = new Point3d( - Math.Round( data.LabeledMarkers[ i ].x, 4 ), - Math.Round( data.LabeledMarkers[ i ].y, 4 ), - Math.Round( data.LabeledMarkers[ i ].z, 4 ) ); - - mPoints.Add( markerPoint ); - - if ( allPoints ) { - for ( int j = 0; j < data.MarkerSets[ i ].nMarkers; j++ ) { - // Recreate point data. - markerPoint = new Point3d( - Math.Round( data.LabeledMarkers[ i ].x, 4 ), - Math.Round( data.LabeledMarkers[ i ].y, 4 ), - Math.Round( data.LabeledMarkers[ i ].z, 4 ) ); - - mPoints.Add( markerPoint ); - } - } - } + Log.Clear(); + foreach ( string message in frame.StatusMessages ) { + Log.Add( message ); + } - // Create marker labels. - string markerLabel = data.OtherMarkers[ i ].ID.ToString(); - mLabels.Add( markerLabel ); + foreach ( OptiTrackMarker marker in frame.Markers ) { + mLabels.Add( marker.Label ); } if ( RigidBody ) { - rBodyNames.Clear(); - rBodyPos.Clear(); - rBodyQuats.Clear(); - rBodyPlanes.Clear(); - - // [NatNet] Fetch Rigid Body Frame Data - for ( int i = 0; i < mRigidBodies.Count; i++ ) { - int rbID = mRigidBodies[ i ].ID; // Fetch the rigid body IDs. - - // For each rigid body saved in the description list, fetch the actual object. - for ( int j = 0; j < data.nRigidBodies; j++ ) { - RigidBody rb = mRigidBodies[ i ]; // Saved rigid body descriptions - RigidBodyData rbData = data.RigidBodies[ j ]; // Received rigid body descriptions - - if ( rbData.Tracked == true ) { - // Rigid Body ID - rBodyNames.Add( rb.Name ); - - // Rigid Body Position - rBodyPos.Add( Math.Round( rbData.x, 4 ) ); - rBodyPos.Add( Math.Round( rbData.y, 4 ) ); - rBodyPos.Add( Math.Round( rbData.z, 4 ) ); - - // Rigid Body Quaternion - float[] quat = new float[ 4 ] { rbData.qx, rbData.qy, rbData.qz, rbData.qw }; - - // Attempt to normalise the rotation notation to fit with robot programming - // (WXYZ). - rBodyQuats.Add( Math.Round( rbData.qw, 6 ) ); - rBodyQuats.Add( Math.Round( rbData.qx, 6 ) ); - rBodyQuats.Add( Math.Round( rbData.qy, 6 ) ); - rBodyQuats.Add( Math.Round( rbData.qz, 6 ) ); - - // Attempt to convert Quaternion to Euler - try { - float[] eulers = new float[ 3 ]; - - //Converting quat orientation into XYZ Euler representation. - eulers = NatNetClientML.QuatToEuler( quat, NATEulerOrder.NAT_XYZr ); - double xrot = Utlities.RadiansToDegrees( eulers[ 0 ] ); - double yrot = Utlities.RadiansToDegrees( eulers[ 1 ] ); - double zrot = Utlities.RadiansToDegrees( eulers[ 2 ] ); - - Console.WriteLine( "\t\tori ({0:N3}, {1:N3}, {2:N3})", xrot, yrot, zrot ); - } catch ( Exception ) { - var msg = "Failed to convert quaternion to Euler."; - Console.WriteLine( msg ); - throw new Exception( msg ); - } - - // Create Plane - Quaternion rbQuat = new Quaternion( Math.Round( rbData.qw, 6 ), Math.Round( rbData.qx, 6 ), Math.Round( rbData.qy, 6 ), Math.Round( rbData.qz, 6 ) ); - rbQuat.GetRotation( out Plane rbPlane ); - rbPlane.Origin = new Point3d( Math.Round( rbData.x, 4 ), Math.Round( rbData.y, 4 ), Math.Round( rbData.z, 4 ) ); - - // If the document is in mm, scale the plane to mm. - var doc = Rhino.RhinoDoc.ActiveDoc; - if ( doc != null && doc.ModelUnitSystem == Rhino.UnitSystem.Millimeters ) { - rbPlane.Transform( Transform.Scale( new Point3d( 0, 0, 0 ), 1000 ) ); - } - - // Y Rotation Matrix (Up) - Transform xformYup = new Transform(); - xformYup.M01 = xformYup.M02 = xformYup.M03 = xformYup.M10 = xformYup.M11 = xformYup.M13 = xformYup.M20 = xformYup.M22 = xformYup.M33 = xformYup.M30 = xformYup.M31 = xformYup.M32 = 0; - xformYup.M00 = xformYup.M21 = xformYup.M33 = 1; - xformYup.M12 = -1; - - // X Rotation Matrix - Transform xRotate = new Transform(); - xRotate.M00 = xRotate.M02 = xRotate.M03 = xRotate.M11 = xRotate.M12 = xRotate.M13 = xRotate.M20 = xRotate.M21 = xRotate.M33 = xRotate.M30 = xRotate.M31 = xRotate.M32 = 0; - xRotate.M10 = xRotate.M22 = xRotate.M33 = 1; - xRotate.M01 = -1; - - if ( yUp ) //Detect if Y is pointing up - { - rbPlane.Transform( xformYup ); - } - - rbPlane.Transform( xRotate ); - rBodyPlanes.Add( rbPlane ); - } else { - Log.Add( rb.Name.ToString() + " is not tracked in current frame." ); - } + foreach ( OptiTrackRigidBody rigidBody in frame.RigidBodies ) { + if ( !rigidBody.IsTracked ) { + continue; } + + rBodyNames.Add( rigidBody.Name ); + rBodyPlanes.Add( CreateRigidBodyPlane( rigidBody ) ); } } - - //if (Skeleton) - //{ - // /* Parsing Skeleton Frame Data */ - // for (int i = 0; i < mSkeletons.Count; i++) // Fetching skeleton IDs from the saved descriptions - // { - // int sklID = mSkeletons[i].ID; - - // for (int j = 0; j < data.nSkeletons; j++) { if (sklID == data.Skeletons[j].ID) // - // When skeleton ID of the description matches skeleton ID of the frame data. { - // NatNetML.Skeleton skl = mSkeletons[i]; // Saved skeleton descriptions - // NatNetML.SkeletonData sklData = data.Skeletons[j]; // Received skeleton frame - // data - - // Console.WriteLine("\tSkeleton ({0}):", skl.Name); Console.WriteLine("\t\tSegment - // count: {0}", sklData.nRigidBodies); - - // /* Now, for each of the skeleton segments */ for (int k = 0; k < - // sklData.nRigidBodies; k++) { NatNetML.RigidBodyData boneData = - // sklData.RigidBodies[k]; - - // /* Decoding skeleton bone ID */ int skeletonID = HighWord(boneData.ID); int - // rigidBodyID = LowWord(boneData.ID); int uniqueID = skeletonID * 1000 + - // rigidBodyID; int key = uniqueID.GetHashCode(); - - // NatNetML.RigidBody bone = (RigidBody)mHtSkelRBs[key]; //Fetching saved skeleton - // bone descriptions - - // //Outputting only the hip segment data for the purpose of this sample. - // if (k == 0) - // Console.WriteLine("\t\t{0:N3}: pos({1:N3}, {2:N3}, {3:N3})", bone.Name, boneData.x, boneData.y, boneData.z); - // } - // } - // } - // } - //} - - //if (ForcePlate) - //{ - // /* Parsing Force Plate Frame Data */ - // for (int i = 0; i < mForcePlates.Count; i++) - // { - // int fpID = mForcePlates[i].ID; // Fetching force plate IDs from the saved descriptions - - // for (int j = 0; j < data.nForcePlates; j++) { if (fpID == data.ForcePlates[j].ID) - // // When force plate ID of the descriptions matches force plate ID of the frame - // data. { NatNetML.ForcePlate fp = mForcePlates[i]; // Saved force plate - // descriptions NatNetML.ForcePlateData fpData = data.ForcePlates[i]; // Received - // forceplate frame data - - // Console.WriteLine("\tForce Plate ({0}):", fp.Serial); - - // // Here we will be printing out only the first force plate "subsample" (index 0) that was collected with the mocap frame. - // for (int k = 0; k < fpData.nChannels; k++) - // { - // Console.WriteLine("\t\tChannel {0}: {1}", fp.ChannelNames[k], fpData.ChannelData[k].Values[0]); - // } - // } - // } - // } - //} } - /// - /// [NatNet] Establish a NatNet Client connection (if not established already). - /// - private static void ConnectToServer() { - // [NatNet] Instantiate the client object - mNatNet = new NatNetClientML(); - - // [NatNet] Checking verions of the NatNet SDK library - int[] verNatNet = new int[ 4 ]; // Saving NatNet SDK version number - verNatNet = mNatNet.NatNetVersion(); - Console.WriteLine( "NatNet SDK Version: {0}.{1}.{2}.{3}", verNatNet[ 0 ], - verNatNet[ 1 ], verNatNet[ 2 ], verNatNet[ 3 ] ); - - // [NatNet] Connecting to the Server - Console.WriteLine( "\nConnecting...\n\tLocal IP address: {0}\n\tServer IP Address: {1}\n\n", mStrLocalIP, mStrServerIP ); - - NatNetClientML.ConnectParams connectParams = new NatNetClientML.ConnectParams { - ConnectionType = mConnectionType, - ServerAddress = mStrServerIP, - LocalAddress = mStrLocalIP - }; - - mNatNet.Connect( connectParams ); - } + private static Plane CreateRigidBodyPlane( OptiTrackRigidBody rigidBody ) { + Quaternion rbQuat = new Quaternion( rigidBody.Qw, rigidBody.Qx, rigidBody.Qy, rigidBody.Qz ); + rbQuat.GetRotation( out Plane rbPlane ); + rbPlane.Origin = new Point3d( rigidBody.X, rigidBody.Y, rigidBody.Z ); - /// - /// [NatNet] Fetch server description to confirm connection. - /// - /// - private static bool FetchServerDescriptor() { - ServerDescription m_ServerDescriptor = new ServerDescription(); - int errorCode = mNatNet.GetServerDescription( m_ServerDescriptor ); - - if ( errorCode == 0 ) { - Log.Add( "Success: Connected to the server." ); - ParseSeverDescriptor( m_ServerDescriptor ); - return true; - } else { - Log.Add( "Error: Failed to connect. Check the connection settings." ); - Log.Add( "Program terminated." ); - return false; + var doc = Rhino.RhinoDoc.ActiveDoc; + if ( doc != null && doc.ModelUnitSystem == Rhino.UnitSystem.Millimeters ) { + rbPlane.Transform( Transform.Scale( new Point3d( 0, 0, 0 ), 1000 ) ); } - } - /// - /// [NatNet] Parse server information and display in console window. - /// - /// - private static void ParseSeverDescriptor( ServerDescription server ) { - Log.Add( "Server Info:" ); - Log.Add( "Host: " + server.HostComputerName ); - Log.Add( "Application Name: " + server.HostApp ); - // Console.WriteLine("\tApplication Version: {0}.{1}.{2}.{3}", - // server.HostAppVersion[0], server.HostAppVersion[1], server.HostAppVersion[2], - // server.HostAppVersion[3]); Console.WriteLine("\tNatNet Version: - // {0}.{1}.{2}.{3}\n", server.NatNetVersion[0], server.NatNetVersion[1], - // server.NatNetVersion[2], server.NatNetVersion[3]); - } + Transform xformYup = new Transform(); + xformYup.M01 = xformYup.M02 = xformYup.M03 = xformYup.M10 = xformYup.M11 = xformYup.M13 = xformYup.M20 = xformYup.M22 = xformYup.M33 = xformYup.M30 = xformYup.M31 = xformYup.M32 = 0; + xformYup.M00 = xformYup.M21 = xformYup.M33 = 1; + xformYup.M12 = -1; - /// - /// [NatNet] Fetch Data Descriptions from the server and populate the appropriate - /// lists. - /// - private static void FetchDataDescriptor() { - // [NatNet] Fetch Data Descriptions. Instantiate objects for saving data - // descriptions and frame data. - bool result = mNatNet.GetDataDescriptions( out mDataDescriptor ); - if ( result ) { - Log.Add( "Success: Data Descriptions obtained from the server." ); - ParseDataDescriptor( mDataDescriptor ); - } else { - Log.Add( "Error: Could not get the Data Descriptions" ); - } - } + Transform xRotate = new Transform(); + xRotate.M00 = xRotate.M02 = xRotate.M03 = xRotate.M11 = xRotate.M12 = xRotate.M13 = xRotate.M20 = xRotate.M21 = xRotate.M33 = xRotate.M30 = xRotate.M31 = xRotate.M32 = 0; + xRotate.M10 = xRotate.M22 = xRotate.M33 = 1; + xRotate.M01 = -1; - /// - /// [NatNet] Parse Data Descriptions and display in console window. - /// - /// - private static void ParseDataDescriptor( List description ) { - // [NatNet] Request a description of the Active Model List from the server. This - // sample will list only names of the data sets, but you can access - int numDataSet = description.Count; - Log.Add( "Total " + numDataSet.ToString() + " data sets in the capture:" ); - - for ( int i = 0; i < numDataSet; ++i ) { - int dataSetType = description[ i ].type; - // Parse Data Descriptions for each data sets and save them in the delcared lists - // and hashtables for later uses. - switch ( dataSetType ) { - case (int) DataDescriptorType.eMarkerSetData: - MarkerSet mkset = (MarkerSet) description[ i ]; - Log.Add( "MarkerSet (" + mkset.Name + ")" ); - break; - - case (int) DataDescriptorType.eRigidbodyData: - RigidBody rb = (RigidBody) description[ i ]; - Log.Add( "RigidBody (" + rb.Name + ")" ); - - // Saving Rigid Body Descriptions - mRigidBodies.Add( rb ); - break; - - //case ((int)NatNetML.DataDescriptorType.eSkeletonData): - // NatNetML.Skeleton skl = (NatNetML.Skeleton)description[i]; - // Log.Add("Skeleton (" + skl.Name + ", Bones:"); - - // //Saving Skeleton Descriptions mSkeletons.Add(skl); - - // // Saving Individual Bone Descriptions for (int j = 0; j < skl.nRigidBodies; - // j++) { Console.WriteLine("\t\t{0}. {1}", j + 1, skl.RigidBodies[j].Name); int - // uniqueID = skl.ID * 1000 + skl.RigidBodies[j].ID; int key = - // uniqueID.GetHashCode(); mHtSkelRBs.Add(key, skl.RigidBodies[j]); //Saving the - // bone segments onto the hashtable } break; - - //case ((int)NatNetML.DataDescriptorType.eForcePlateData): - // NatNetML.ForcePlate fp = (NatNetML.ForcePlate)description[i]; - // Console.WriteLine("\tForcePlate ({0})", fp.Serial); - - // // Saving Force Plate Channel Names mForcePlates.Add(fp); - - // for (int j = 0; j < fp.ChannelCount; j++) { Console.WriteLine("\t\tChannel - // {0}: {1}", j + 1, fp.ChannelNames[j]); } break; - - default: - // When a Data Set does not match any of the descriptions provided by the SDK. - Log.Add( "Error: Invalid Data Set" ); - break; - } + if ( yUp ) { + rbPlane.Transform( xformYup ); } + + rbPlane.Transform( xRotate ); + return rbPlane; } - // The following functions append menu items and then handle the item clicked event. protected override void AppendAdditionalComponentMenuItems( ToolStripDropDown menu ) { ToolStripMenuItem cSystem = Menu_AppendItem( menu, "Y up", Menu_yUp, true, yUp ); cSystem.ToolTipText = "When checked, component will expect Y up"; - ToolStripMenuItem rBody = Menu_AppendItem( menu, "Rigid Body", Menu_rBodyClick, true, RigidBody ); // Append the item to the menu. - rBody.ToolTipText = "When checked, component will stream Rigid Body data."; // Specifically assign a tooltip text to the menu item. + ToolStripMenuItem rBody = Menu_AppendItem( menu, "Rigid Body", Menu_rBodyClick, true, RigidBody ); + rBody.ToolTipText = "When checked, component will stream Rigid Body data."; ToolStripMenuItem skeleton = Menu_AppendItem( menu, "Skeleton", Menu_skeletonClick, true, Skeleton ); skeleton.ToolTipText = "When checked, component will stream Skeleton data."; @@ -569,64 +253,38 @@ protected override void AppendAdditionalComponentMenuItems( ToolStripDropDown me fPlate.ToolTipText = "When checked, component will stream Force Plate data."; } - /// - /// Handle the item clicked event of the menu item. - /// - /// - /// private void Menu_yUp( object sender, EventArgs e ) { RecordUndoEvent( "Y Up" ); yUp = !yUp; ExpireSolution( true ); } - /// - /// Handle the item clicked event of the menu item. - /// - /// - /// private void Menu_rBodyClick( object sender, EventArgs e ) { RecordUndoEvent( "Rigid Body" ); RigidBody = !RigidBody; ExpireSolution( true ); } - /// - /// Handle the item clicked event of the menu item. - /// - /// - /// private void Menu_skeletonClick( object sender, EventArgs e ) { RecordUndoEvent( "Skeleton" ); Skeleton = !Skeleton; ExpireSolution( true ); } - /// - /// Handle the item clicked event of the menu item. - /// - /// - /// private void Menu_fPlateClick( object sender, EventArgs e ) { RecordUndoEvent( "Force Plate" ); ForcePlate = !ForcePlate; ExpireSolution( true ); } - /// - /// Retuns a bitmap to represent the component. - /// protected override System.Drawing.Bitmap Icon { get { return Properties.Icons.Tracker; } } - /// - /// Gets the unique ID for this component. Do not copy this. - /// public override Guid ComponentGuid { get { return new Guid( "D1C724B2-FEB4-405E-9570-382F8FD553F8" ); } } } -} \ No newline at end of file +}