Skip to content

nalathethird/bHapticsLib

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

bHapticsLib (Modernized)

Modernized Fork of bHapticsLib - Optimized for .NET Standard 2.1 and .NET 9

An Open-Source .NET Library for bHaptics Support with native WebSocket implementation.

.NET License

Special Thanks to bHaptics for making the bHaptics Gear and supporting the community!


What's New in This Fork (v1.0.9)

Modernization

  • Native WebSockets - Uses System.Net.WebSockets.ClientWebSocket (no external dependencies unlike the main branch!)
  • Full Async/Await - Modern Task-based asynchronous patterns
  • Simplified Targets - Only .NET Standard 2.1 and .NET 9 (removed legacy frameworks due to security issues and legacy)
  • Better Performance - Improved connection handling and message buffering

New Features

  • MessagePack Support - High-performance binary serialization (4-5x faster, 60-70% smaller) NEW in v1.0.9!
  • Event System - Real-time device and connection state notifications
  • Battery Monitoring - Query and monitor device battery levels
  • Debug Logging - Comprehensive connection diagnostics
  • Resonite/FrooxEngine Ready - Tested and optimized for modern VR applications

Table of Contents


Installation

NuGet Package (Coming Soon)

dotnet add package bHapticsLib

Manual Installation

  1. Download the latest release from Releases
  2. Add bHapticsLib.dll as a reference to your project
  3. Start coding!

Build from Source

git clone https://github.com/nalathethird/bHapticsLib.git
cd bHapticsLib
dotnet build -c Release

Quick Start

using bHapticsLib;

// Connect to bHaptics Player
bHapticsManager.Connect("MyApp", "My Application");

// Check connection
if (bHapticsManager.Status == bHapticsStatus.Connected)
{
    Console.WriteLine("Connected to bHaptics Player!");
    
    // Check connected devices
    int deviceCount = bHapticsManager.GetConnectedDeviceCount();
    Console.WriteLine($"Connected devices: {deviceCount}");
    
    // Play simple haptic feedback
    bHapticsManager.Play("test", 1000, PositionID.Vest, 
        new DotPoint[] { new DotPoint(0, 100) });
}

// Subscribe to events
bHapticsManager.DeviceStatusChanged += (sender, e) =>
{
    Console.WriteLine($"{e.Position} {(e.IsConnected ? "connected" : "disconnected")}");
};

// Cleanup
bHapticsManager.Disconnect();

Device IDs & PositionID Reference

Each bHaptics device has a fixed, permanent ID that never changes:

Device Name PositionID Integer ID Description
TactSuit (X40/X16) PositionID.Vest 3 Full vest (front + back combined)
Tactal PositionID.Head 4 Face mask / head device
Tactosy Hand (Left) PositionID.HandLeft 6 Left hand/wrist device
Tactosy Hand (Right) PositionID.HandRight 7 Right hand/wrist device
Tactosy Foot (Left) PositionID.FootLeft 8 Left foot device
Tactosy Foot (Right) PositionID.FootRight 9 Right foot device
Tactosy2 Arm (Left) PositionID.ArmLeft 10 Left arm device
Tactosy2 Arm (Right) PositionID.ArmRight 11 Right arm device
Vest Front PositionID.VestFront 201 Front of vest only
Vest Back PositionID.VestBack 202 Back of vest only
TactGlove (Left) PositionID.GloveLeft 203 Left glove
TactGlove (Right) PositionID.GloveRight 204 Right glove

Example:

// Get device ID as integer
int vestID = (int)PositionID.Vest;  // Always 3

// Get device name
string name = PositionID.Vest.ToString();  // "Vest"

// Use in battery queries
int? battery = bHapticsManager.GetBatteryLevel(PositionID.Vest);  // Query vest battery

Connection Management

Connect to bHaptics Player

// Simple connection
bHapticsManager.Connect("MyApp", "My Application");

// With auto-reconnect
bHapticsManager.Connect("MyApp", "My Application", 
    tryToReconnect: true, 
    maxRetries: 10);

Check Connection Status

bHapticsStatus status = bHapticsManager.Status;
// Disconnected, Connecting, or Connected

if (status == bHapticsStatus.Connected)
    Console.WriteLine("Connected!");

Connection Events

bHapticsManager.ConnectionEstablished += (sender, e) =>
{
    Console.WriteLine($"Connected! ({e.PreviousStatus} -> {e.NewStatus})");
};

bHapticsManager.ConnectionLost += (sender, e) =>
{
    Console.WriteLine($"Disconnected! ({e.PreviousStatus} -> {e.NewStatus})");
};

bHapticsManager.StatusChanged += (sender, e) =>
{
    Console.WriteLine($"Status: {e.PreviousStatus} -> {e.NewStatus}");
};

Disconnect

bHapticsManager.Disconnect();

Debug Connection Issues

// Get last error
string error = bHapticsManager.GetLastError();
Console.WriteLine($"Error: {error}");

// Get detailed connection log
string log = bHapticsManager.GetConnectionLog();
Console.WriteLine(log);

Device Queries

Device Count

int count = bHapticsManager.GetConnectedDeviceCount();
bool anyDevices = bHapticsManager.IsAnyDevicesConnected();

Check Device Connection

bool vestConnected = bHapticsManager.IsDeviceConnected(PositionID.Vest);
bool headConnected = bHapticsManager.IsDeviceConnected(PositionID.Head);

if (vestConnected)
    Console.WriteLine("TactSuit is connected!");

Device Status (Motor Values)

int[] motorValues = bHapticsManager.GetDeviceStatus(PositionID.Vest);
// Returns array of intensity values (0-100) for each motor

bool anyMotorActive = bHapticsManager.IsAnyMotorActive(PositionID.Vest);

Device Events

bHapticsManager.DeviceStatusChanged += (sender, e) =>
{
    if (e.IsConnected)
        Console.WriteLine($"{e.Position} connected");
    else
        Console.WriteLine($"{e.Position} disconnected");
};

Battery Monitoring

Query Battery Level

// Get battery for specific device
int? vestBattery = bHapticsManager.GetBatteryLevel(PositionID.Vest);
int? headBattery = bHapticsManager.GetBatteryLevel(PositionID.Head);

if (vestBattery.HasValue)
    Console.WriteLine($"Vest battery: {vestBattery.Value}%");
else
    Console.WriteLine("Battery info not available");

Battery Change Events

bHapticsManager.BatteryLevelChanged += (sender, e) =>
{
    Console.WriteLine($"{e.Position} battery: {e.BatteryLevel}%");
    
    // Low battery warning
    if (e.BatteryLevel.HasValue && e.BatteryLevel.Value < 20)
        Console.WriteLine($"LOW BATTERY: {e.Position}");
};

Monitor All Device Batteries

var devices = new[] 
{ 
    PositionID.Vest, PositionID.Head,
    PositionID.HandLeft, PositionID.HandRight,
    PositionID.ArmLeft, PositionID.ArmRight 
};

foreach (var device in devices)
{
    if (bHapticsManager.IsDeviceConnected(device))
    {
        int deviceID = (int)device;  // Get integer ID
        int? battery = bHapticsManager.GetBatteryLevel(device);
        
        Console.WriteLine($"Device {deviceID} ({device}): {battery?.ToString() ?? "N/A"}%");
    }
}

See BATTERY_LEVEL_GUIDE.md for advanced battery monitoring patterns.


Event System

Available Events

// Device connection/disconnection
bHapticsManager.DeviceStatusChanged += (sender, e) => { };

// Connection established
bHapticsManager.ConnectionEstablished += (sender, e) => { };

// Connection lost
bHapticsManager.ConnectionLost += (sender, e) => { };

// Any status change
bHapticsManager.StatusChanged += (sender, e) => { };

// Battery level changes
bHapticsManager.BatteryLevelChanged += (sender, e) => { };

Event Args

// DeviceStatusChangedEventArgs
public PositionID Position { get; }
public bool IsConnected { get; }
public DateTime Timestamp { get; }

// ConnectionStatusChangedEventArgs
public bHapticsStatus PreviousStatus { get; }
public bHapticsStatus NewStatus { get; }
public DateTime Timestamp { get; }

// BatteryLevelChangedEventArgs
public PositionID Position { get; }
public int? BatteryLevel { get; }
public int? PreviousBatteryLevel { get; }
public DateTime Timestamp { get; }

Thread Safety Warning

All events fire on a background thread! For UI updates:

bHapticsManager.DeviceStatusChanged += (s, e) =>
{
    // Dispatch to UI thread
    Dispatcher.Invoke(() => UpdateUI(e.Position, e.IsConnected));
};

See EVENT_SYSTEM_GUIDE.md for comprehensive event examples.


Pattern Playback

Load and Play .tact Files

// Load pattern from file
HapticPattern pattern = HapticPattern.LoadFromFile("myPattern", "path/to/pattern.tact");

// Play pattern
pattern.Play();

// Play with options
pattern.Play(
    scaleOption: new ScaleOption(intensity: 0.8f, duration: 1.0f),
    rotationOption: new RotationOption(offsetAngleX: 45f, offsetY: 0f)
);

// Check if playing
bool isPlaying = pattern.IsPlaying();

// Stop pattern
pattern.Stop();

Play Registered Patterns

// Register pattern first
bHapticsManager.RegisterPatternFromFile("myPattern", "path/to/pattern.tact");

// Play registered pattern
bHapticsManager.PlayRegistered("myPattern");

// With options
bHapticsManager.PlayRegistered("myPattern",
    scaleOption: new ScaleOption(0.8f, 1.0f));

Pattern Swapping (Mirror Left/Right)

// Load swapped pattern (mirrors left/right)
HapticPattern swapped = HapticPattern.LoadSwappedFromFile("swapped", "path/to/pattern.tact");
swapped.Play();

Pattern Registration

Register from File

bHapticsManager.RegisterPatternFromFile("impact", "patterns/impact.tact");

Register from JSON

string json = File.ReadAllText("pattern.tact");
bHapticsManager.RegisterPatternFromJson("impact", json);

Register Swapped Pattern

bHapticsManager.RegisterPatternSwappedFromFile("impactSwapped", "patterns/impact.tact");

Check if Registered

bool isRegistered = bHapticsManager.IsPatternRegistered("impact");

Manual Feedback

Using DotPoint (Specific Motors)

// Activate motor 0 at 100% intensity for 1 second
bHapticsManager.Play("test", 1000, PositionID.Vest,
    new DotPoint[] 
    {
        new DotPoint(0, 100),  // Motor 0 at 100%
        new DotPoint(5, 80),   // Motor 5 at 80%
        new DotPoint(10, 60)   // Motor 10 at 60%
    });

Using Byte/Int Arrays

// 20 motors (max for vest)
byte[] motorArray = new byte[20] 
{
    100, 80, 60, 40, 20,  // Top row
    100, 80, 60, 40, 20,  // Second row
    100, 80, 60, 40, 20,  // Third row
    100, 80, 60, 40, 20   // Bottom row
};

bHapticsManager.Play("pattern", 1000, PositionID.Vest, motorArray);

Mirrored Playback

bHapticsManager.PlayMirrored("test", 1000, PositionID.Vest,
    new DotPoint[] { new DotPoint(0, 100) },
    MirrorDirection.Both);  // Mirror horizontally and vertically

PathPoint (Animated Paths)

bHapticsManager.Play("sweep", 2000, PositionID.Vest,
    new PathPoint[]
    {
        new PathPoint(0.0f, 0.0f, 100, 3),   // Start top-left
        new PathPoint(1.0f, 0.0f, 100, 3),   // Move to top-right
        new PathPoint(1.0f, 1.0f, 100, 3),   // Move to bottom-right
        new PathPoint(0.0f, 1.0f, 100, 3)    // Move to bottom-left
    });

Playback Control

Check if Playing

bool playing = bHapticsManager.IsPlaying("patternKey");
bool anyPlaying = bHapticsManager.IsPlayingAny();

Stop Playback

bHapticsManager.StopPlaying("patternKey");  // Stop specific pattern
bHapticsManager.StopPlayingAll();           // Stop all patterns

Complete API Reference

Connection

  • bool Connect(string id, string name, bool tryToReconnect = true, int maxRetries = 5)
  • bool Disconnect()
  • bHapticsStatus Status { get; }
  • string GetLastError()
  • string GetConnectionLog()

Device Queries

  • int GetConnectedDeviceCount()
  • bool IsAnyDevicesConnected()
  • bool IsDeviceConnected(PositionID type)
  • int[] GetDeviceStatus(PositionID type)
  • bool IsAnyMotorActive(PositionID type)
  • int? GetBatteryLevel(PositionID type) NEW!

Pattern Management

  • bool IsPatternRegistered(string key)
  • void RegisterPatternFromFile(string key, string tactFilePath)
  • void RegisterPatternFromJson(string key, string tactFileJson)
  • void RegisterPatternSwappedFromFile(string key, string tactFilePath)
  • void RegisterPatternSwappedFromJson(string key, string tactFileJson)

Playback

  • void Play(string key, int durationMillis, PositionID position, DotPoint[] dotPoints)
  • void Play(string key, int durationMillis, PositionID position, byte[] dotPoints)
  • void Play(string key, int durationMillis, PositionID position, PathPoint[] pathPoints)
  • void PlayMirrored(string key, int durationMillis, PositionID position, DotPoint[] dotPoints, MirrorDirection direction)
  • void PlayRegistered(string key)
  • void PlayRegistered(string key, ScaleOption scaleOption, RotationOption rotationOption)

MessagePack Playback NEW in v1.0.9!

  • void PlayFromMessagePack(string key, int durationMillis, PositionID position, byte[] messagePackData)
  • void PlayFromMessagePackMirrored(string key, int durationMillis, PositionID position, byte[] messagePackData, MirrorDirection mirrorDirection)
  • void PlayPathFromMessagePack(string key, int durationMillis, PositionID position, byte[] messagePackData)
  • void PlayCombinedFromMessagePack(string key, int durationMillis, PositionID position, byte[] dotPointsMessagePack, byte[] pathPointsMessagePack)

Playback Control

  • bool IsPlaying(string key)
  • bool IsPlayingAny()
  • void StopPlaying(string key)
  • void StopPlayingAll()

Events

  • event EventHandler<DeviceStatusChangedEventArgs> DeviceStatusChanged NEW!
  • event EventHandler<ConnectionStatusChangedEventArgs> ConnectionEstablished NEW!
  • event EventHandler<ConnectionStatusChangedEventArgs> ConnectionLost NEW!
  • event EventHandler<ConnectionStatusChangedEventArgs> StatusChanged NEW!
  • event EventHandler<BatteryLevelChangedEventArgs> BatteryLevelChanged NEW!

Constants

  • const int MaxIntensityInInt = 500
  • const byte MaxIntensityInByte = 200
  • const int MaxMotorsPerDotPoint = 20
  • const int MaxMotorsPerPathPoint = 3

Documentation

Getting Started

Modernization

Advanced Features

Debugging


Use Cases

VR Applications (Resonite, VRChat, Unity)

// Real-time haptic feedback in VR
public class VRHaptics : MonoBehaviour
{
    void Start()
    {
        bHapticsManager.Connect("VRApp", "My VR Application");
        bHapticsManager.DeviceStatusChanged += OnDeviceChanged;
    }

    void OnCollision(Collision collision)
    {
        // Play impact on vest
        bHapticsManager.Play("impact", 500, PositionID.Vest,
            new DotPoint[] { new DotPoint(0, 100) });
    }
}

Game Development

// Damage feedback system
public void TakeDamage(int damage, Vector3 direction)
{
    int intensity = Mathf.Clamp(damage * 10, 0, 100);
    bHapticsManager.Play("damage", 800, PositionID.Vest,
        new DotPoint[] { new DotPoint(GetMotorFromDirection(direction), intensity) });
}

Health/Fitness Apps

// Heart rate training feedback
public void OnHeartRateZone(int zone)
{
    if (zone > 4)  // High intensity zone
        bHapticsManager.Play("alert", 1000, PositionID.Vest,
            new DotPoint[] { new DotPoint(0, 100) });
}

Building from Source

Prerequisites

  • .NET 9 SDK
  • Visual Studio 2022 or VS Code (optional)

Build Commands

# Clone repository
git clone https://github.com/nalathethird/bHapticsLib.git
cd bHapticsLib

# Restore packages
dotnet restore

# Build
dotnet build -c Release

# Output location
Output\Release\netstandard2.1\bHapticsLib.dll
Output\Release\net9.0\bHapticsLib.dll

Run Tests

dotnet run --project TestApplication

Credits

Original Library

Created by Herp Derpinstine

This Fork (v1.0.9+)

Modernized by nalathethird for .NET 9 and Resonite/FrooxEngine

  • Repository: https://github.com/nalathethird/bHapticsLib
  • Native WebSocket implementation
  • MessagePack support - High-performance binary serialization (4-5x faster, 60-70% smaller)
  • Event system and battery monitoring
  • Modern async/await patterns

Third-Party Libraries

Special Thanks

  • bHaptics for creating amazing haptic hardware and supporting the community
  • Herp Derpinstine for the excellent original implementation

See CREDITS.md for detailed attribution.


License

MIT License - See LICENSE.md for full details.

This fork maintains the same MIT License as the original project.

You are free to:

  • Use commercially
  • Modify and redistribute
  • Use in closed-source projects
  • Create derivative works

You must:

  • Include original copyright notice
  • Include license text

Disclaimer

bHaptics is not liable for any issues or problems that may arise from using this library.

This is a community-driven project created and developed by passionate users and content creators.

For questions or issues:


Links


Why This Fork?

The original bHapticsLib is excellent but targets a wide range of .NET frameworks including very old ones. This fork:

  • Removes legacy framework support - Focus on modern .NET
  • Uses native WebSockets - No external dependencies
  • Full async/await - Modern C# patterns throughout
  • Event-driven architecture - Real-time notifications
  • Battery monitoring - Track device battery levels
  • Better diagnostics - Comprehensive logging and debugging

Perfect for modern applications that don't need .NET Framework 3.5 support!


Comparison

Feature Original This Fork
Target Frameworks .NET 3.5 - .NET 9 .NET 9
WebSocket Library WebSocketDotNet Native ClientWebSocket
Async Support Partial Full async/await
MessagePack Support No Yes (v1.0.9+)
Event System No Yes
Battery Monitoring No Yes
Debug Logging Basic Comprehensive
External Dependencies 1 0 (MessagePack is local fork)

MessagePack High-Performance Input (NEW!)

What is MessagePack Support?

This library now supports MessagePack serialization for incoming haptic data from mods/games, providing:

  • ⚑ 4-5x faster deserialization than JSON
  • πŸ“¦ 60-70% smaller data size (saves bandwidth)
  • 🎯 Zero changes to bHaptics Player (still receives JSON)

Data Flow

Mod/Game          β†’ [MessagePack] β†’ bHapticsLib β†’ [JSON] β†’ bHaptics Player β†’ [Bluetooth] β†’ Device
(Resonite/Unity)      FAST ⚑        Deserialize    SAME        ???          5-15ms       Physical
                      SMALL πŸ“¦       & Convert      RATE

Key Point: MessagePack only speeds up INPUT to your library. Output to bHaptics Player is unchanged (still JSON).

Performance Benchmarks

Metric JSON MessagePack Improvement
Deserialization Speed 1x (baseline) 4-5x faster ⚑ 5x faster
Data Size (20 motors) ~400 bytes ~150 bytes πŸ“¦ 62% smaller
Network Bandwidth 100% 38% 🌐 62% saved

Usage Example

using MessagePack;
using bHapticsLib;

// In your mod/game (sender side)
var points = new DotPoint[]
{
    new DotPoint(0, 100),   // Motor 0 at 100%
    new DotPoint(5, 80),    // Motor 5 at 80%
    new DotPoint(10, 60)    // Motor 10 at 60%
};

// Serialize to MessagePack (FAST + SMALL)
byte[] messagePackData = MessagePackSerializer.Serialize(points);

// Send to bHapticsLib
bHapticsManager.PlayFromMessagePack("haptic", 500, PositionID.Vest, messagePackData);

Available MessagePack Methods

// Basic DotPoint array
bHapticsManager.PlayFromMessagePack(
    string key, 
    int durationMillis, 
    PositionID position, 
    byte[] messagePackData);

// With mirroring
bHapticsManager.PlayFromMessagePackMirrored(
    string key, 
    int durationMillis, 
    PositionID position, 
    byte[] messagePackData, 
    MirrorDirection mirrorDirection);

// PathPoint array
bHapticsManager.PlayPathFromMessagePack(
    string key, 
    int durationMillis, 
    PositionID position, 
    byte[] messagePackData);

// Combined DotPoint + PathPoint
bHapticsManager.PlayCombinedFromMessagePack(
    string key, 
    int durationMillis, 
    PositionID position, 
    byte[] dotPointsMessagePack, 
    byte[] pathPointsMessagePack);

Performance & Bottlenecks

Your Library (Fast! ⚑)

  • MessagePack deserialization: 4-5x faster than JSON
  • Converts to JSON internally for bHaptics Player

bHaptics Player (Unknown ❓)

  • Closed source - Internal processing speed is unknown
  • Still receives JSON at the same rate (unchanged)
  • May have internal queuing/rate limiting

Bluetooth 5 (Hardware Limit πŸ”Œ)

  • ~5-15ms latency - Fixed hardware limitation
  • Cannot be improved by faster serialization

Physical Motors (Slowest 🐌)

  • ~10-50ms response time - Motors need time to spin up
  • Maximum practical update rate: ~20-60 Hz

⚠️ Important: Rate Limiting

Even though MessagePack is fast, don't flood bHaptics Player with updates:

// ❌ BAD: Too fast (will overwhelm player/motors)
for (int i = 0; i < 1000; i++)
{
    bHapticsManager.PlayFromMessagePack("spam", 10, PositionID.Vest, data);
}

// βœ… GOOD: Reasonable update rate (30-60 Hz max)
void Update()  // Called ~60 times per second
{
    if (ShouldSendHaptics())
        bHapticsManager.PlayFromMessagePack("feedback", 50, PositionID.Vest, data);
}

Best Practice: Keep update rate at 20-60 Hz maximum regardless of how fast MessagePack is. The physical hardware can't respond faster!

Backwards Compatibility

βœ… 100% compatible - All existing methods still work:

// Old way (still works!)
var points = new DotPoint[] { new DotPoint(0, 100) };
bHapticsManager.Play("test", 500, PositionID.Vest, points);

// New way (faster input!)
byte[] data = MessagePackSerializer.Serialize(points);
bHapticsManager.PlayFromMessagePack("test", 500, PositionID.Vest, data);

Both send the exact same JSON to bHaptics Player!

Testing

Run the MessagePack test:

dotnet run --project TestApplication
# Choose option 8: MessagePack Integration Test

The test demonstrates:

  • βœ… Basic MessagePack deserialization
  • βœ… Performance comparisons (speed + size)
  • βœ… PathPoint patterns
  • βœ… Mirrored playback
  • βœ… Combined DotPoint + PathPoint

About

An Open-Source .NET Library for bHaptics Support

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C# 100.0%