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.
Special Thanks to bHaptics for making the bHaptics Gear and supporting the community!
- 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
- 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
- Installation
- Quick Start
- Device IDs
- Connection Management
- Device Queries
- Battery Monitoring
- Event System
- Pattern Playback
- Pattern Registration
- Manual Feedback
- MessagePack High-Performance Input NEW!
- API Reference
- Documentation
- Credits
dotnet add package bHapticsLib- Download the latest release from Releases
- Add
bHapticsLib.dllas a reference to your project - Start coding!
git clone https://github.com/nalathethird/bHapticsLib.git
cd bHapticsLib
dotnet build -c Releaseusing 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();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// Simple connection
bHapticsManager.Connect("MyApp", "My Application");
// With auto-reconnect
bHapticsManager.Connect("MyApp", "My Application",
tryToReconnect: true,
maxRetries: 10);bHapticsStatus status = bHapticsManager.Status;
// Disconnected, Connecting, or Connected
if (status == bHapticsStatus.Connected)
Console.WriteLine("Connected!");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}");
};bHapticsManager.Disconnect();// Get last error
string error = bHapticsManager.GetLastError();
Console.WriteLine($"Error: {error}");
// Get detailed connection log
string log = bHapticsManager.GetConnectionLog();
Console.WriteLine(log);int count = bHapticsManager.GetConnectedDeviceCount();
bool anyDevices = bHapticsManager.IsAnyDevicesConnected();bool vestConnected = bHapticsManager.IsDeviceConnected(PositionID.Vest);
bool headConnected = bHapticsManager.IsDeviceConnected(PositionID.Head);
if (vestConnected)
Console.WriteLine("TactSuit is connected!");int[] motorValues = bHapticsManager.GetDeviceStatus(PositionID.Vest);
// Returns array of intensity values (0-100) for each motor
bool anyMotorActive = bHapticsManager.IsAnyMotorActive(PositionID.Vest);bHapticsManager.DeviceStatusChanged += (sender, e) =>
{
if (e.IsConnected)
Console.WriteLine($"{e.Position} connected");
else
Console.WriteLine($"{e.Position} disconnected");
};// 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");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}");
};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.
// 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) => { };// 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; }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.
// 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();// 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));// Load swapped pattern (mirrors left/right)
HapticPattern swapped = HapticPattern.LoadSwappedFromFile("swapped", "path/to/pattern.tact");
swapped.Play();bHapticsManager.RegisterPatternFromFile("impact", "patterns/impact.tact");string json = File.ReadAllText("pattern.tact");
bHapticsManager.RegisterPatternFromJson("impact", json);bHapticsManager.RegisterPatternSwappedFromFile("impactSwapped", "patterns/impact.tact");bool isRegistered = bHapticsManager.IsPatternRegistered("impact");// 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%
});// 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);bHapticsManager.PlayMirrored("test", 1000, PositionID.Vest,
new DotPoint[] { new DotPoint(0, 100) },
MirrorDirection.Both); // Mirror horizontally and verticallybHapticsManager.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
});bool playing = bHapticsManager.IsPlaying("patternKey");
bool anyPlaying = bHapticsManager.IsPlayingAny();bHapticsManager.StopPlaying("patternKey"); // Stop specific pattern
bHapticsManager.StopPlayingAll(); // Stop all patternsbool Connect(string id, string name, bool tryToReconnect = true, int maxRetries = 5)bool Disconnect()bHapticsStatus Status { get; }string GetLastError()string GetConnectionLog()
int GetConnectedDeviceCount()bool IsAnyDevicesConnected()bool IsDeviceConnected(PositionID type)int[] GetDeviceStatus(PositionID type)bool IsAnyMotorActive(PositionID type)int? GetBatteryLevel(PositionID type)NEW!
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)
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)
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)
bool IsPlaying(string key)bool IsPlayingAny()void StopPlaying(string key)void StopPlayingAll()
event EventHandler<DeviceStatusChangedEventArgs> DeviceStatusChangedNEW!event EventHandler<ConnectionStatusChangedEventArgs> ConnectionEstablishedNEW!event EventHandler<ConnectionStatusChangedEventArgs> ConnectionLostNEW!event EventHandler<ConnectionStatusChangedEventArgs> StatusChangedNEW!event EventHandler<BatteryLevelChangedEventArgs> BatteryLevelChangedNEW!
const int MaxIntensityInInt = 500const byte MaxIntensityInByte = 200const int MaxMotorsPerDotPoint = 20const int MaxMotorsPerPathPoint = 3
- Quick Start Guide - Get up and running fast
- TestApplication Guide - Interactive test harness
- Modernization Notes - Technical details of changes
- Changelog - Version history
- Rebuild Instructions - How to build the library
- Event System Guide - Comprehensive event documentation
- Event Quick Reference - Copy-paste examples
- Battery Level Guide - Battery monitoring patterns
- Battery Quick Reference - Battery examples
- Connection Debug Guide - Troubleshooting connections
- Critical Debug Guide - Advanced diagnostics
// 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) });
}
}// 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) });
}// 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) });
}- .NET 9 SDK
- Visual Studio 2022 or VS Code (optional)
# 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.dlldotnet run --project TestApplicationCreated by Herp Derpinstine
- Original Repository: https://github.com/HerpDerpinstine/bHapticsLib
- Full C# implementation of bHaptics SDK
- Pattern system and protocol design
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
- SimpleJSON by Bunny83 - MIT License
- MessagePack-CSharp - .NET 9 fork for high-performance serialization
- System.Net.WebSockets.Client - Microsoft, MIT License
- bHaptics for creating amazing haptic hardware and supporting the community
- Herp Derpinstine for the excellent original implementation
See CREDITS.md for detailed attribution.
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
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:
- GitHub Issues for this fork
- Original Repository Issues for base library
- bHaptics Discord for general support
- This Fork: https://github.com/nalathethird/bHapticsLib
- Forked From: https://github.com/HerpDerpinstine/bHapticsLib
- bHaptics Website: https://www.bhaptics.com
- bHaptics Designer: https://designer.bhaptics.com
- bHaptics Discord: https://discord.gg/JDw423Wskf
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!
| 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) |
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)
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).
| 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 |
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);// 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);- MessagePack deserialization: 4-5x faster than JSON
- Converts to JSON internally for bHaptics Player
- Closed source - Internal processing speed is unknown
- Still receives JSON at the same rate (unchanged)
- May have internal queuing/rate limiting
- ~5-15ms latency - Fixed hardware limitation
- Cannot be improved by faster serialization
- ~10-50ms response time - Motors need time to spin up
- Maximum practical update rate: ~20-60 Hz
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!
β 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!
Run the MessagePack test:
dotnet run --project TestApplication
# Choose option 8: MessagePack Integration TestThe test demonstrates:
- β Basic MessagePack deserialization
- β Performance comparisons (speed + size)
- β PathPoint patterns
- β Mirrored playback
- β Combined DotPoint + PathPoint