Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/

Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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/`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 27 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -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<br/>NatNet broadcast] --> Adapter[NatNet adapter<br/>OptiTrack.NatNet]
Adapter --> Core[OptiTrack core models<br/>OptiTrack.Core]
Adapter --> Telemetry[Telemetry abstraction<br/>NoOp by default]
Core --> Grasshopper[Grasshopper components<br/>Tracker]
Grasshopper --> Rhino[Rhino geometry outputs<br/>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.
50 changes: 50 additions & 0 deletions docs/developer-guide.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 14 additions & 1 deletion docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 21 additions & 0 deletions src/Tracker/OptiTrack/Core/IOptiTrackClient.cs
Original file line number Diff line number Diff line change
@@ -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<OptiTrackFrameEventArgs> FrameReceived;

event EventHandler<OptiTrackConnectionEventArgs> ConnectionChanged;

Task ConnectAsync( OptiTrackConnectionOptions options, CancellationToken cancellationToken );

Task DisconnectAsync();
}
}
23 changes: 23 additions & 0 deletions src/Tracker/OptiTrack/Core/OptiTrackConnectionInfo.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
}
26 changes: 26 additions & 0 deletions src/Tracker/OptiTrack/Core/OptiTrackConnectionOptions.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
10 changes: 10 additions & 0 deletions src/Tracker/OptiTrack/Core/OptiTrackConnectionStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace OptiTrack.Core {

public enum OptiTrackConnectionStatus {
Disconnected,
Connecting,
Connected,
Disconnecting,
Faulted
}
}
25 changes: 25 additions & 0 deletions src/Tracker/OptiTrack/Core/OptiTrackEventArgs.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
71 changes: 71 additions & 0 deletions src/Tracker/OptiTrack/Core/OptiTrackModels.cs
Original file line number Diff line number Diff line change
@@ -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<OptiTrackMarker>();
RigidBodies = new List<OptiTrackRigidBody>();
Skeletons = new List<OptiTrackSkeleton>();
StatusMessages = new List<string>();
}

public int FrameNumber { get; set; }

public bool IsRecording { get; set; }

public bool AssetsChanged { get; set; }

public IReadOnlyList<OptiTrackMarker> Markers { get; set; }

public IReadOnlyList<OptiTrackRigidBody> RigidBodies { get; set; }

public IReadOnlyList<OptiTrackSkeleton> Skeletons { get; set; }

public IReadOnlyList<string> StatusMessages { get; set; }
}
}
65 changes: 65 additions & 0 deletions src/Tracker/OptiTrack/NatNet/NatNetFrameConverter.cs
Original file line number Diff line number Diff line change
@@ -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<RigidBody> rigidBodyDescriptions, OptiTrackConnectionOptions options, IReadOnlyList<string> statusMessages ) {
List<OptiTrackMarker> markers = new List<OptiTrackMarker>();
List<OptiTrackRigidBody> rigidBodies = new List<OptiTrackRigidBody>();

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 )
} );
}
Comment on lines +30 to +51
}
}

return new OptiTrackFrame {
FrameNumber = data.iFrame,
IsRecording = data.bRecording,
AssetsChanged = data.bTrackingModelsChanged,
Markers = markers,
RigidBodies = rigidBodies,
StatusMessages = statusMessages
};
}
}
}
Loading
Loading