Refactor to introduce NatNet and telemetry abstraction layers#8
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the Tracker Grasshopper plugin to decouple it from direct NatNet SDK usage by introducing SDK-independent OptiTrack core models (OptiTrack.Core), a NatNet-backed adapter (OptiTrack.NatNet), and a default-disabled telemetry boundary (OptiTrack.Telemetry). It also updates docs and versioning to v1.3.0.
Changes:
- Added
OptiTrack.Coredomain models +IOptiTrackClientinterface and updated the Grasshopper component to consume them. - Implemented
NatNetOptiTrackClient+NatNetFrameConverterto isolateNatNetMLintegration and perform frame conversion. - Introduced a no-op telemetry abstraction with sanitization utilities and documented architecture/privacy boundaries.
Reviewed changes
Copilot reviewed 23 out of 24 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Tracker/TrackerComponent.cs | Switches Grasshopper component from direct NatNetML usage to IOptiTrackClient + core models; adds telemetry hooks. |
| src/Tracker/Tracker.csproj | Includes new Core/NatNet/Telemetry source files in the project. |
| src/Tracker/Properties/AssemblyInfo.cs | Bumps assembly versions to 1.3.0. |
| src/Tracker/OptiTrack/Core/IOptiTrackClient.cs | Defines SDK-independent client interface used by Grasshopper layer. |
| src/Tracker/OptiTrack/Core/OptiTrackConnectionInfo.cs | Introduces connection info model surfaced through the client boundary. |
| src/Tracker/OptiTrack/Core/OptiTrackConnectionOptions.cs | Introduces connection option model (include flags, frame divisor, connection type). |
| src/Tracker/OptiTrack/Core/OptiTrackConnectionStatus.cs | Defines connection lifecycle status enum. |
| src/Tracker/OptiTrack/Core/OptiTrackEventArgs.cs | Defines event args for connection/frame events. |
| src/Tracker/OptiTrack/Core/OptiTrackModels.cs | Adds SDK-independent frame/marker/rigid body/skeleton models. |
| src/Tracker/OptiTrack/NatNet/NatNetOptiTrackClient.cs | Implements IOptiTrackClient using NatNetML; manages connection, descriptors, eventing. |
| src/Tracker/OptiTrack/NatNet/NatNetFrameConverter.cs | Converts NatNet frames into core models for the Grasshopper layer. |
| src/Tracker/OptiTrack/Telemetry/ITelemetryService.cs | Defines telemetry abstraction interface. |
| src/Tracker/OptiTrack/Telemetry/NoOpTelemetryService.cs | Provides default no-op telemetry implementation. |
| src/Tracker/OptiTrack/Telemetry/TelemetryContext.cs | Adds tag/metric container with sanitization on keys/values. |
| src/Tracker/OptiTrack/Telemetry/TelemetryEvent.cs | Defines telemetry event payload object with timestamp. |
| src/Tracker/OptiTrack/Telemetry/TelemetrySanitizer.cs | Adds privacy-oriented redaction/sanitization helpers. |
| src/Tracker/OptiTrack/Telemetry/TelemetryScope.cs | Adds span-style timing scope that emits duration metrics. |
| src/Tracker/OptiTrack/Telemetry/TelemetrySeverity.cs | Defines severity enum for telemetry messages. |
| README.md | Updates modernization target version reference to v1.3.0. |
| docs/telemetry.md | Documents telemetry boundary and updates example release string to 1.3.0. |
| docs/developer-guide.md | Adds developer guide describing boundaries and project layout. |
| docs/architecture.md | Adds architecture overview + Mermaid diagram and boundary/privacy notes. |
| CHANGELOG.md | Adds v1.3.0 changelog entry describing refactor and docs. |
| .gitignore | Ignores root .vscode/ directory. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Create marker labels. | ||
| string markerLabel = data.OtherMarkers[ i ].ID.ToString(); | ||
| mLabels.Add( markerLabel ); | ||
| foreach ( OptiTrackMarker marker in frame.Markers ) { |
| private static void OnConnectionChanged( object sender, OptiTrackConnectionEventArgs e ) { | ||
| if ( !string.IsNullOrWhiteSpace( e.Message ) ) { | ||
| Log.Add( e.Message ); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// [NatNet] Process mocap frame data. | ||
| /// </summary> | ||
| /// <param name="data"> </param> | ||
| /// <exception cref="Exception"> </exception> | ||
| 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 ); | ||
| } |
| 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 ) | ||
| } ); | ||
| } |
| 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." ); |
| private List<ForcePlate> forcePlates; | ||
| private List<string> statusMessages; | ||
| private bool assetsChanged; | ||
|
|
There was a problem hiding this comment.
@codex[agent] Implement the signalling mechanism mentioned here if it makes sense and is obvious as to how to do so. If it doesn't make sense to you, then remove the unchanged variable.
There was a problem hiding this comment.
To use Codex here, create a Codex account and connect to github.
| IncludeRigidBodies = RigidBody, | ||
| IncludeSkeletons = Skeleton, | ||
| IncludeForcePlates = ForcePlate, |
rhughes42
left a comment
There was a problem hiding this comment.
@copilot fix the issue with Rigid body conversion currently nests for each rigidBodyDescriptions inside for each data.RigidBodies without any ID match. This produces duplicates (N descriptions × M tracked bodies) and also assigns the description's ID/Name to every tracked rigid body regardless of which body the frame data belongs to. Convert by matching the frame rigid body data to the corresponding description (typically by rigid body ID), and emit at most one OptiTrackRigidBody per tracked rigid body.
This pull request introduces a major architectural refactor to decouple the Grasshopper plugin from the NatNet SDK, establish clear internal boundaries, and lay the foundation for privacy-aware telemetry. The update adds SDK-independent OptiTrack core models and interfaces, a NatNet-backed adapter, and a no-op telemetry abstraction. Comprehensive developer and architecture documentation is also included to clarify the new design and boundaries.
Core Architecture and Abstraction Layers:
OptiTrackFrame,OptiTrackMarker,OptiTrackRigidBody, etc.) and theIOptiTrackClientinterface inOptiTrack.Core, enabling the Grasshopper component to consume internal domain models instead of raw NatNet SDK types. (src/Tracker/OptiTrack/Core/OptiTrackModels.cs,src/Tracker/OptiTrack/Core/IOptiTrackClient.cs,src/Tracker/OptiTrack/Core/OptiTrackConnectionOptions.cs,src/Tracker/OptiTrack/Core/OptiTrackConnectionInfo.cs,src/Tracker/OptiTrack/Core/OptiTrackConnectionStatus.cs,src/Tracker/OptiTrack/Core/OptiTrackEventArgs.cs) [1] [2] [3] [4] [5] [6]NatNetOptiTrackClient) that isolates all directNatNetMLusage underOptiTrack.NatNet, handling connection, frame conversion, and event propagation using the new core models. (src/Tracker/OptiTrack/NatNet/NatNetOptiTrackClient.cs,src/Tracker/OptiTrack/NatNet/NatNetFrameConverter.cs) [1] [2]Telemetry and Privacy:
ITelemetryService,NoOpTelemetryService,TelemetryContext,TelemetryScope,TelemetrySanitizer) to support future privacy-aware error and performance reporting, defaulting to no-op behavior unless explicitly configured. (src/Tracker/OptiTrack/Telemetry/ITelemetryService.cs,src/Tracker/OptiTrack/Telemetry/NoOpTelemetryService.cs) [1] [2]docs/telemetry.md,docs/architecture.md,docs/developer-guide.md) [1] [2] [3]Documentation and Versioning:
docs/architecture.md,docs/developer-guide.md) [1] [2]v1.3.0release. (CHANGELOG.md,README.md,docs/telemetry.md) [1] [2] [3]This refactor sets up the codebase for easier future upgrades, testing, and privacy-compliant telemetry, while making the plugin architecture clearer and more maintainable.