Compare commits

...

8 Commits

Author SHA1 Message Date
d6f928ec31 UE: update Blueprint and test map assets
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:30:57 +01:00
aa8df59af2 PS_Win_BLE: fix crash on disconnect - use TWeakObjectPtr in all async dispatches
All Dispatch* functions now capture UPS_BLE_Device via TWeakObjectPtr instead of
a raw pointer, preventing an access violation when the UObject is garbage collected
before the GameThread lambda executes (EXCEPTION_ACCESS_VIOLATION @ 0x60).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:25:16 +01:00
10ca1e19c7 Arduino: disable IMU BLE streaming when not in debug mode
IMU data (roll/pitch/yaw) is now only sent over BLE when debugMode != DEBUG_OFF,
as originally intended for production. Removes the TODO_TEST workaround.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:12:03 +01:00
7c2cb387f2 PS_Win_BLE : fix crash shutdown + cache caractéristiques GATT
Crash shutdown (Index >= 0) :
- OnPreExit se déclenche encore trop tard (GC déjà en cours).
- Remplacé par OnEnginePreExit qui fire avant le shutdown des core
  modules, donc avant toute destruction d'UObject. RemoveFromRoot()
  est maintenant appelé au bon moment.

Cache caractéristiques GATT :
- Bug : Read/Write/Subscribe rappelaient GetCharacteristicsAsync(Cached)
  à chaque opération, ce qui peut retourner un ordre différent du
  discovery initial (Uncached) → mauvaise caractéristique ciblée.
- Fix : les GattCharacteristic sont maintenant stockées dans
  FPS_GattServiceHandle::Characteristics (std::vector) lors du
  ConnectDevice, et réutilisées directement via [CI] dans toutes
  les opérations ultérieures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 19:28:05 +01:00
d53dd36194 Fix crash shutdown + IMU BLE toujours actif pour tests
PS_Win_BLE : crash à la fermeture d'Unreal (assertion Index >= 0 dans
UObjectArray) car RemoveFromRoot() était appelé dans ShutdownModule()
alors que l'UObject array est déjà partiellement détruit. Fix : cleanup
du BLEManager déplacé dans un delegate FCoreDelegates::OnPreExit qui
s'exécute plus tôt, avant la destruction des UObjects.

uproject : désactivation explicite de WinBluetoothLE (les nœuds
Blueprint restaient dans le groupe Bluetooth Low Energy car le plugin
était chargé automatiquement depuis le dossier Plugins/).

Firmware : envoi IMU BLE découplé du debug mode pour pouvoir tester
la caractéristique 6E400002 à distance (TODO_TEST — remettre la
condition debugMode != DEBUG_OFF quand Pico Motion Tracker intégré).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 19:23:13 +01:00
0573cd9a57 PS_Win_BLE : catégories Blueprint → ASTERION|Win_BLE
Toutes les entrées Blueprint (UFUNCTION, UPROPERTY, UCLASS, USTRUCT, UENUM)
sont désormais rangées dans ASTERION|Win_BLE au lieu de "PS BLE".
64 occurrences mises à jour dans les 4 headers publics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 16:54:35 +01:00
113eddef46 PS_Win_BLE : fix erreurs de compilation WinRT
- Suppression des 'using namespace Windows::*' au scope global
  (conflit avec le namespace Windows d'Unreal via AllowWindowsPlatformTypes)
- Remplacement par un macro PS_BLE_WINRT_NS avec aliases locaux
  (WinBT, WinAdv, WinGAP, WinStr) utilisés dans chaque fonction
- Ajout de FPS_BLEDeviceHandle et FPS_GattServiceHandle : wrappers
  heap-alloués pour les types WinRT qui suppriment operator new
- Suppression warning C4265 (dtor non-virtual interne aux headers WinRT)
- Plugin charge sans erreur dans UE 5.5

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 16:46:01 +01:00
d593bbd9fd Ajout plugin PS_Win_BLE : réécriture native WinRT sans DLL tierce
Remplace WinBluetoothLE (dépendant de BLEproto64.dll) par une
implémentation C++ native utilisant les APIs WinRT Windows.Devices.Bluetooth.

Structure :
- PS_Win_BLE.uplugin / PS_Win_BLE.Build.cs
- PS_BLETypes.h     : enums (EPS_GATTStatus, EPS_BLEError, EPS_CharacteristicDescriptor),
                      structs (FPS_MACAddress, FPS_ServiceItem, FPS_CharacteristicItem),
                      delegates (FPS_OnConnect, FPS_OnNotify, FPS_OnRead, etc.)
- PS_BLEModule      : startup WinRT, scan BLE (Live/Background), connect/disconnect,
                      Read/Write/Subscribe/Unsubscribe, dispatch GameThread
- PS_BLEDevice      : UObject par périphérique BLE, gestion handles WinRT natifs
- PS_BLEManager     : gestionnaire global, discovery, MAC utils
- PS_BLELibrary     : Blueprint function library (GetBLEManager, descriptors, etc.)
- Content/Sample    : assets Blueprint copiés depuis WinBluetoothLE

Préfixe PS_ sur tous les fichiers, classes, structs et enums.
Copyright (C) 2025 ASTERION VR

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 16:27:23 +01:00
18 changed files with 1874 additions and 2 deletions

View File

@ -574,8 +574,7 @@ void loop() {
sendDebugData(accelMag, gyroMag, audioLevel);
printDebugSerial(accelMag, gyroMag, audioLevel);
// ─── IMU BLE 10 Hz (debug uniquement) ───
// En production (debugMode OFF), pas d'envoi IMU → charge BLE réduite pour Unreal
// ─── IMU BLE 10 Hz ───
if (debugMode != DEBUG_OFF && now-lastSend>=100) {
lastSend=now;
float dt=(now-lastUpdate)/1000.0f; lastUpdate=now;

View File

@ -17,6 +17,14 @@
"TargetAllowList": [
"Editor"
]
},
{
"Name": "WinBluetoothLE",
"Enabled": false
},
{
"Name": "PS_Win_BLE",
"Enabled": true
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -0,0 +1,63 @@
// Copyright (C) 2025 ASTERION VR
using UnrealBuildTool;
using System.IO;
public class PS_Win_BLE : ModuleRules
{
public PS_Win_BLE(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
// WinRT support requires C++17 and specific Windows SDK headers
CppStandard = CppStandardVersion.Cpp17;
PublicIncludePaths.AddRange(
new string[] {
}
);
PrivateIncludePaths.AddRange(
new string[] {
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"Projects",
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
}
);
if (Target.Platform == UnrealTargetPlatform.Win64)
{
// WinRT / Windows BLE native APIs
PublicSystemLibraries.AddRange(new string[]
{
"windowsapp.lib", // WinRT APIs (Windows.Devices.Bluetooth, etc.)
"runtimeobject.lib" // RoInitialize / WinRT activation
});
// C++/WinRT headers (winrt/Windows.Devices.Bluetooth.h, etc.)
// Located in the Windows SDK — we pick the version used by UE5.5
string WinSDKDir = "C:/Program Files (x86)/Windows Kits/10";
string WinSDKVersion = "10.0.22621.0";
PrivateIncludePaths.Add(Path.Combine(WinSDKDir, "Include", WinSDKVersion, "cppwinrt"));
PrivateIncludePaths.Add(Path.Combine(WinSDKDir, "Include", WinSDKVersion, "winrt"));
// Allow WinRT headers in C++ code
bEnableExceptions = true;
}
}
}

View File

@ -0,0 +1,239 @@
// Copyright (C) 2025 ASTERION VR
#include "PS_BLEDevice.h"
#include "PS_BLEModule.h"
#include "PS_BLEManager.h"
// ─────────────────────────────────────────────────────────────────────────────
UPS_BLE_Device::UPS_BLE_Device()
{
}
UPS_BLE_Device::~UPS_BLE_Device()
{
if (RefToModule)
{
RefToModule->DisconnectDevice(this);
}
ActiveServices.Empty();
}
// ─── Actions ─────────────────────────────────────────────────────────────────
void UPS_BLE_Device::Connect()
{
if (IsConnected())
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::AlreadyConnected, AddressAsString, "", "");
return;
}
if (!RefToModule->ConnectDevice(this))
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::ConnectFailed, AddressAsString, "", "");
}
}
void UPS_BLE_Device::DiscoverServices()
{
ActiveServices.Empty();
if (!RefToModule->ConnectDevice(this))
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::ConnectFailed, AddressAsString, "", "");
}
}
void UPS_BLE_Device::Disconnect()
{
if (!IsConnected())
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::CanNotDisconnect, AddressAsString, "", "");
return;
}
if (!RefToModule->DisconnectDevice(this))
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::DisconnectFailed, AddressAsString, "", "");
}
}
bool UPS_BLE_Device::IsConnected()
{
if (bDestroyInProgress) return false;
return RefToModule->IsDeviceConnected(this);
}
void UPS_BLE_Device::Read(const FString& ServiceUUID, const FString& CharacteristicUUID)
{
if (!IsConnected()) { RefToManager->OnBLEError.Broadcast(EPS_BLEError::NeedConnectionFirst, AddressAsString, "", ""); return; }
EPS_CharacteristicDescriptor Desc;
uint16 SrvChar = FindInList(ServiceUUID, CharacteristicUUID, Desc);
if (SrvChar == 0xFFFF) { RefToManager->OnBLEError.Broadcast(EPS_BLEError::NonReadableChar, AddressAsString, ServiceUUID, CharacteristicUUID); return; }
if (((uint8)Desc & (uint8)EPS_CharacteristicDescriptor::Readable) == 0)
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::NonReadableChar, AddressAsString, ServiceUUID, CharacteristicUUID);
return;
}
uint8 SI = (SrvChar >> 8);
uint8 CI = (SrvChar & 0xFF);
if (!RefToModule->ReadCharacteristic(this, SI, CI))
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::ReadingFailed, AddressAsString, ServiceUUID, CharacteristicUUID);
}
}
void UPS_BLE_Device::Write(const FString& ServiceUUID, const FString& CharacteristicUUID, TArray<uint8> Data)
{
if (!IsConnected()) { RefToManager->OnBLEError.Broadcast(EPS_BLEError::NeedConnectionFirst, AddressAsString, "", ""); return; }
if (Data.Num() == 0) { RefToManager->OnBLEError.Broadcast(EPS_BLEError::ZeroLengthWrite, AddressAsString, ServiceUUID, CharacteristicUUID); return; }
EPS_CharacteristicDescriptor Desc;
uint16 SrvChar = FindInList(ServiceUUID, CharacteristicUUID, Desc);
if (SrvChar == 0xFFFF || ((uint8)Desc & (uint8)EPS_CharacteristicDescriptor::Writable) == 0)
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::NonWritableChar, AddressAsString, ServiceUUID, CharacteristicUUID);
return;
}
uint8 SI = (SrvChar >> 8);
uint8 CI = (SrvChar & 0xFF);
if (!RefToModule->WriteCharacteristic(this, SI, CI, Data))
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::WritingFailed, AddressAsString, ServiceUUID, CharacteristicUUID);
}
}
void UPS_BLE_Device::Subscribe(const FString& ServiceUUID, const FString& CharacteristicUUID)
{
if (!IsConnected()) { RefToManager->OnBLEError.Broadcast(EPS_BLEError::NeedConnectionFirst, AddressAsString, "", ""); return; }
EPS_CharacteristicDescriptor Desc;
uint16 SrvChar = FindInList(ServiceUUID, CharacteristicUUID, Desc);
const uint8 Mask = (uint8)EPS_CharacteristicDescriptor::Notifiable | (uint8)EPS_CharacteristicDescriptor::Indicable;
if (SrvChar == 0xFFFF || ((uint8)Desc & Mask) == 0)
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::CanNotSubscribe, AddressAsString, ServiceUUID, CharacteristicUUID);
return;
}
uint8 SI = (SrvChar >> 8);
uint8 CI = (SrvChar & 0xFF);
if (ActiveServices[SI].Characteristics[CI].subscribed)
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::AlreadySubscribed, AddressAsString, ServiceUUID, CharacteristicUUID);
return;
}
if (!RefToModule->SubscribeCharacteristic(this, SI, CI))
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::SubscriptionFailed, AddressAsString, ServiceUUID, CharacteristicUUID);
}
}
void UPS_BLE_Device::Unsubscribe(const FString& ServiceUUID, const FString& CharacteristicUUID)
{
if (!IsConnected()) { RefToManager->OnBLEError.Broadcast(EPS_BLEError::NeedConnectionFirst, AddressAsString, "", ""); return; }
EPS_CharacteristicDescriptor Desc;
uint16 SrvChar = FindInList(ServiceUUID, CharacteristicUUID, Desc);
const uint8 Mask = (uint8)EPS_CharacteristicDescriptor::Notifiable | (uint8)EPS_CharacteristicDescriptor::Indicable;
if (SrvChar == 0xFFFF || ((uint8)Desc & Mask) == 0)
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::CanNotSubscribe, AddressAsString, ServiceUUID, CharacteristicUUID);
return;
}
uint8 SI = (SrvChar >> 8);
uint8 CI = (SrvChar & 0xFF);
if (!ActiveServices[SI].Characteristics[CI].subscribed)
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::NotSubscribed, AddressAsString, ServiceUUID, CharacteristicUUID);
return;
}
if (!RefToModule->UnsubscribeCharacteristic(this, SI, CI))
{
RefToManager->OnBLEError.Broadcast(EPS_BLEError::UnsubscribeFailed, AddressAsString, ServiceUUID, CharacteristicUUID);
}
}
// ─── Address getters ──────────────────────────────────────────────────────────
FString UPS_BLE_Device::DeviceAddressAsString()
{
FString Result;
for (int32 i = 5; i >= 0; i--)
{
uint8 B = (DeviceID >> (i * 8)) & 0xFF;
ByteToHex(B, Result);
if (i > 0) Result.AppendChar(TEXT(':'));
}
return Result;
}
int64 UPS_BLE_Device::DeviceAddressAsInt64()
{
return (int64)(DeviceID & 0x0000FFFFFFFFFFFF);
}
FPS_MACAddress UPS_BLE_Device::DeviceAddressAsMAC()
{
FPS_MACAddress MAC;
MAC.b0 = (DeviceID) & 0xFF;
MAC.b1 = (DeviceID >> 8) & 0xFF;
MAC.b2 = (DeviceID >> 16) & 0xFF;
MAC.b3 = (DeviceID >> 24) & 0xFF;
MAC.b4 = (DeviceID >> 32) & 0xFF;
MAC.b5 = (DeviceID >> 40) & 0xFF;
return MAC;
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
uint16 UPS_BLE_Device::FindInList(const FString& ServiceUUID, const FString& CharUUID, EPS_CharacteristicDescriptor& OutDescriptor) const
{
for (int32 SI = 0; SI < ActiveServices.Num(); SI++)
{
if (ActiveServices[SI].ServiceUUID.Equals(ServiceUUID, ESearchCase::IgnoreCase))
{
for (int32 CI = 0; CI < ActiveServices[SI].Characteristics.Num(); CI++)
{
if (ActiveServices[SI].Characteristics[CI].CharacteristicUUID.Equals(CharUUID, ESearchCase::IgnoreCase))
{
OutDescriptor = (EPS_CharacteristicDescriptor)ActiveServices[SI].Characteristics[CI].Descriptor;
return (uint16)((SI << 8) | (CI & 0xFF));
}
}
}
}
OutDescriptor = EPS_CharacteristicDescriptor::Unknown;
return 0xFFFF;
}
FString UPS_BLE_Device::GUIDToString(const void* WinRTGuid)
{
// WinRT GUID layout matches Win32 GUID: Data1(u32) Data2(u16) Data3(u16) Data4[8]
const uint8* Raw = static_cast<const uint8*>(WinRTGuid);
auto HexByte = [](uint8 B) -> FString { FString S; ByteToHex(B, S); return S; };
// Data1 — little-endian uint32
FString Result = TEXT("{");
Result += HexByte(Raw[3]); Result += HexByte(Raw[2]);
Result += HexByte(Raw[1]); Result += HexByte(Raw[0]);
Result += TEXT("-");
// Data2 — little-endian uint16
Result += HexByte(Raw[5]); Result += HexByte(Raw[4]);
Result += TEXT("-");
// Data3 — little-endian uint16
Result += HexByte(Raw[7]); Result += HexByte(Raw[6]);
Result += TEXT("-");
// Data4[0..1]
Result += HexByte(Raw[8]); Result += HexByte(Raw[9]);
Result += TEXT("-");
// Data4[2..7]
for (int32 i = 10; i < 16; i++) Result += HexByte(Raw[i]);
Result += TEXT("}");
return Result;
}

View File

@ -0,0 +1,98 @@
// Copyright (C) 2025 ASTERION VR
#include "PS_BLELibrary.h"
#include "PS_BLEModule.h"
#include "PS_BLEManager.h"
#include "Modules/ModuleManager.h"
UPS_BLE_Library::UPS_BLE_Library(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
UPS_BLE_Manager* UPS_BLE_Library::GetBLEManager(bool& IsPluginLoaded, bool& BLEAdapterFound)
{
UPS_BLE_Module* Mod = (UPS_BLE_Module*)FModuleManager::Get().GetModule(FName("PS_Win_BLE"));
if (!Mod)
{
IsPluginLoaded = false;
BLEAdapterFound = false;
return nullptr;
}
IsPluginLoaded = true;
BLEAdapterFound = Mod->bInitialized;
return Mod->LocalBLEManager;
}
uint8 UPS_BLE_Library::NibbleToByte(const FString& Nibble, bool& Valid)
{
Valid = false;
if (Nibble.IsEmpty()) return 0;
TCHAR C = Nibble[0];
Valid = true;
if (C >= TEXT('0') && C <= TEXT('9')) return C - TEXT('0');
if (C >= TEXT('A') && C <= TEXT('F')) return C - TEXT('A') + 10;
if (C >= TEXT('a') && C <= TEXT('f')) return C - TEXT('a') + 10;
Valid = false;
return 0;
}
void UPS_BLE_Library::GetDescriptorBits(
const EPS_CharacteristicDescriptor& Descriptor,
bool& Broadcastable, bool& ExtendedProperties,
bool& Notifiable, bool& Indicable,
bool& Readable, bool& Writable,
bool& WriteNoResponse, bool& SignedWrite)
{
uint8 D = (uint8)Descriptor;
Broadcastable = (D & 0x01) != 0;
ExtendedProperties = (D & 0x02) != 0;
Notifiable = (D & 0x04) != 0;
Indicable = (D & 0x08) != 0;
Readable = (D & 0x10) != 0;
Writable = (D & 0x20) != 0;
WriteNoResponse = (D & 0x40) != 0;
SignedWrite = (D & 0x80) != 0;
}
EPS_CharacteristicDescriptor UPS_BLE_Library::MakeDescriptor(
const bool Broadcastable, const bool ExtendedProperties,
const bool Notifiable, const bool Indicable,
const bool Readable, const bool Writable,
const bool WriteNoResponse, const bool SignedWrite)
{
uint8 Result = 0;
if (Broadcastable) Result |= (uint8)EPS_CharacteristicDescriptor::Broadcast;
if (ExtendedProperties) Result |= (uint8)EPS_CharacteristicDescriptor::ExtendedProps;
if (Notifiable) Result |= (uint8)EPS_CharacteristicDescriptor::Notifiable;
if (Indicable) Result |= (uint8)EPS_CharacteristicDescriptor::Indicable;
if (Readable) Result |= (uint8)EPS_CharacteristicDescriptor::Readable;
if (Writable) Result |= (uint8)EPS_CharacteristicDescriptor::Writable;
if (WriteNoResponse) Result |= (uint8)EPS_CharacteristicDescriptor::WriteNoResponse;
if (SignedWrite) Result |= (uint8)EPS_CharacteristicDescriptor::SignedWrite;
return (EPS_CharacteristicDescriptor)Result;
}
FString UPS_BLE_Library::DescriptorToString(const uint8& Descriptor)
{
FString Result;
if (Descriptor & 0x01) Result += TEXT("Broadcastable ");
if (Descriptor & 0x02) Result += TEXT("ExtendedProperties ");
if (Descriptor & 0x04) Result += TEXT("Notifiable ");
if (Descriptor & 0x08) Result += TEXT("Indicable ");
if (Descriptor & 0x10) Result += TEXT("Readable ");
if (Descriptor & 0x20) Result += TEXT("Writable ");
if (Descriptor & 0x40) Result += TEXT("WriteNoResponse ");
if (Descriptor & 0x80) Result += TEXT("SignedWrite ");
Result.TrimEndInline();
return Result;
}
bool UPS_BLE_Library::IsEditorRunning()
{
#if WITH_EDITOR
return true;
#else
return false;
#endif
}

View File

@ -0,0 +1,202 @@
// Copyright (C) 2025 ASTERION VR
#include "PS_BLEManager.h"
#include "PS_BLEModule.h"
#include "PS_BLEDevice.h"
#include "Misc/MessageDialog.h"
#define LOCTEXT_NAMESPACE "PS_Win_BLE"
// ─────────────────────────────────────────────────────────────────────────────
UPS_BLE_Manager::UPS_BLE_Manager()
{
if (!bAttached)
{
UPS_BLE_Module* Mod = (UPS_BLE_Module*)FModuleManager::Get().GetModule(FName("PS_Win_BLE"));
if (Mod) AttachModule(Mod);
}
}
UPS_BLE_Manager::~UPS_BLE_Manager()
{
FoundDevices.Empty();
}
void UPS_BLE_Manager::AttachModule(UPS_BLE_Module* Module)
{
if (!bAttached && Module)
{
BLEModule = Module;
bAttached = true;
}
}
// ─── Discovery ────────────────────────────────────────────────────────────────
void UPS_BLE_Manager::StartDiscoveryLive(
const FPS_OnNewBLEDeviceDiscovered& OnNewDevice,
const FPS_OnDiscoveryEnd& OnEnd,
const int64 DurationMs, const FString& NameFilter)
{
bIsScanInProgress = true;
FoundDevices.Empty();
PendingOnNewDevice = OnNewDevice;
PendingOnDiscoveryEnd = OnEnd;
BLEModule->StartDiscoveryLive(this, (int32)DurationMs, NameFilter);
}
void UPS_BLE_Manager::StartDiscoveryInBackground(
const FPS_OnDiscoveryEnd& OnEnd,
const int64 DurationMs, const FString& NameFilter)
{
bIsScanInProgress = true;
FoundDevices.Empty();
PendingOnDiscoveryEnd = OnEnd;
BLEModule->StartDiscoveryInBackground(this, (int32)DurationMs, NameFilter);
}
void UPS_BLE_Manager::StopDiscovery()
{
BLEModule->StopDiscovery();
bIsScanInProgress = false;
}
void UPS_BLE_Manager::DisconnectAll()
{
for (UPS_BLE_Device* Dev : FoundDevices)
{
if (Dev && Dev->IsConnected())
{
Dev->bDestroyInProgress = true;
BLEModule->DisconnectDevice(Dev);
}
}
}
bool UPS_BLE_Manager::ResetBLEAdapter()
{
// Nothing to reset in native WinRT — the BLE adapter is managed by the OS.
// We just verify BLE is available by checking module init state.
if (BLEModule && BLEModule->bInitialized) return true;
FMessageDialog::Open(EAppMsgType::Ok,
LOCTEXT("PS_BLE_NoAdapter", "Bluetooth adapter not found or does not support LE mode."));
return false;
}
// ─── Internal callbacks ───────────────────────────────────────────────────────
UPS_BLE_Device* UPS_BLE_Manager::MakeNewDevice(const FPS_DeviceRecord& Rec)
{
UPS_BLE_Device* Dev = NewObject<UPS_BLE_Device>();
Dev->RefToModule = BLEModule;
Dev->RefToManager = this;
Dev->DeviceID = Rec.ID;
Dev->RSSI = (int32)Rec.RSSI;
Dev->DeviceName = Rec.Name.IsEmpty() ? TEXT("<noname>") : Rec.Name;
Dev->AddressAsString = Dev->DeviceAddressAsString();
Dev->AddressAsInt64 = Dev->DeviceAddressAsInt64();
Dev->AddressAsMAC = Dev->DeviceAddressAsMAC();
FoundDevices.Add(Dev);
return Dev;
}
int32 UPS_BLE_Manager::GetIndexByID(uint64 DeviceID) const
{
for (int32 i = 0; i < FoundDevices.Num(); i++)
{
if (FoundDevices[i] && FoundDevices[i]->DeviceID == DeviceID) return i;
}
return -1;
}
void UPS_BLE_Manager::JustDiscoveredDevice(const FPS_DeviceRecord& Rec)
{
// Skip if already in list
if (GetIndexByID(Rec.ID) >= 0) return;
UPS_BLE_Device* Dev = MakeNewDevice(Rec);
PendingOnNewDevice.ExecuteIfBound(Dev);
}
void UPS_BLE_Manager::JustDiscoveryEnd(const TArray<FPS_DeviceRecord>& Devices)
{
// For background discovery: build FoundDevices from the collected records
for (const FPS_DeviceRecord& Rec : Devices)
{
if (GetIndexByID(Rec.ID) < 0)
{
MakeNewDevice(Rec);
}
}
bIsScanInProgress = false;
PendingOnDiscoveryEnd.ExecuteIfBound(FoundDevices);
}
void UPS_BLE_Manager::JustConnectedDevice(UPS_BLE_Device* Dev)
{
OnAnyDeviceConnected.Broadcast(Dev);
}
void UPS_BLE_Manager::JustDisconnectedDevice(UPS_BLE_Device* Dev)
{
OnAnyDeviceDisconnected.Broadcast(Dev);
}
void UPS_BLE_Manager::JustDiscoveredServices(UPS_BLE_Device* Dev)
{
OnAnyServiceDiscovered.Broadcast(Dev, Dev->ActiveServices);
}
// ─── MAC utils ────────────────────────────────────────────────────────────────
FString UPS_BLE_Manager::MACToString(const FPS_MACAddress& Addr)
{
FString Result;
ByteToHex(Addr.b5, Result); Result += TEXT(":");
ByteToHex(Addr.b4, Result); Result += TEXT(":");
ByteToHex(Addr.b3, Result); Result += TEXT(":");
ByteToHex(Addr.b2, Result); Result += TEXT(":");
ByteToHex(Addr.b1, Result); Result += TEXT(":");
ByteToHex(Addr.b0, Result);
return Result;
}
uint8 UPS_BLE_Manager::HexNibble(TCHAR C, bool& Valid)
{
Valid = true;
if (C >= TEXT('0') && C <= TEXT('9')) return C - TEXT('0');
if (C >= TEXT('A') && C <= TEXT('F')) return C - TEXT('A') + 10;
if (C >= TEXT('a') && C <= TEXT('f')) return C - TEXT('a') + 10;
Valid = false;
return 0;
}
FPS_MACAddress UPS_BLE_Manager::StringToMAC(const FString& Address, bool& Valid)
{
FPS_MACAddress Blank, Result;
FString Tmp = Address.TrimStartAndEnd();
Valid = false;
if (Tmp.Len() != 17) return Blank;
if (Tmp[2] != TEXT(':') || Tmp[5] != TEXT(':') || Tmp[8] != TEXT(':') ||
Tmp[11] != TEXT(':') || Tmp[14] != TEXT(':')) return Blank;
auto ReadByte = [&](int32 Idx) -> uint8
{
bool V1, V2;
uint8 Hi = HexNibble(Tmp[Idx], V1);
uint8 Lo = HexNibble(Tmp[Idx+1], V2);
if (!V1 || !V2) { Valid = false; }
return (Hi << 4) | Lo;
};
Valid = true;
Result.b0 = ReadByte(0);
Result.b1 = ReadByte(3);
Result.b2 = ReadByte(6);
Result.b3 = ReadByte(9);
Result.b4 = ReadByte(12);
Result.b5 = ReadByte(15);
return Valid ? Result : Blank;
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,814 @@
// Copyright (C) 2025 ASTERION VR
#include "PS_BLEModule.h"
#include "PS_BLEManager.h"
#include "PS_BLEDevice.h"
#include "Async/Async.h"
#include "Misc/MessageDialog.h"
#include "Misc/CoreDelegates.h"
#include "Modules/ModuleManager.h"
// ─── WinRT includes (Windows only) ───────────────────────────────────────────
// NOTE: AllowWindowsPlatformTypes must wrap the WinRT headers.
// We do NOT place any "using namespace" at file scope because Unreal also
// defines a ::Windows namespace (via AllowWindowsPlatformTypes) and that would
// cause "ambiguous symbol" errors. All WinRT types are fully qualified with
// winrt:: inside each function body.
#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
#include "Windows/AllowWindowsPlatformAtomics.h"
#pragma warning(push)
#pragma warning(disable: 4265 4668 4946 5204 5220) // 4265: WinRT internal non-virtual dtor (harmless)
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Devices.Bluetooth.h>
#include <winrt/Windows.Devices.Bluetooth.Advertisement.h>
#include <winrt/Windows.Devices.Bluetooth.GenericAttributeProfile.h>
#include <winrt/Windows.Storage.Streams.h>
#pragma warning(pop)
#include "Windows/HideWindowsPlatformAtomics.h"
#include "Windows/HideWindowsPlatformTypes.h"
// ─── Convenient namespace aliases used ONLY inside function bodies ────────────
// (Defined as macros so we can paste them at the top of each #if PLATFORM_WINDOWS block)
#define PS_BLE_WINRT_NS \
namespace WinBT = winrt::Windows::Devices::Bluetooth; \
namespace WinAdv = winrt::Windows::Devices::Bluetooth::Advertisement; \
namespace WinGAP = winrt::Windows::Devices::Bluetooth::GenericAttributeProfile; \
namespace WinFnd = winrt::Windows::Foundation; \
namespace WinStr = winrt::Windows::Storage::Streams;
// ─── Scanner state (heap-allocated so WinRT types don't appear in the header) ─
struct FPS_ScannerState
{
winrt::Windows::Devices::Bluetooth::Advertisement::BluetoothLEAdvertisementWatcher Watcher{ nullptr };
winrt::event_token ReceivedToken;
winrt::event_token StoppedToken;
};
// ─── WinRT object wrappers ────────────────────────────────────────────────────
// WinRT types delete operator new — we wrap them in plain structs so they can
// live on the heap via regular new/delete (stored as void* in UObject headers).
struct FPS_BLEDeviceHandle
{
winrt::Windows::Devices::Bluetooth::BluetoothLEDevice Device{ nullptr };
winrt::event_token ConnectionStatusToken;
};
struct FPS_GattServiceHandle
{
winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattDeviceService Service{ nullptr };
// Characteristics cached at discovery time (same order as ActiveServices[SI].Characteristics)
std::vector<winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristic> Characteristics;
};
#endif // PLATFORM_WINDOWS
#define LOCTEXT_NAMESPACE "PS_Win_BLE"
// ─────────────────────────────────────────────────────────────────────────────
// Module startup / shutdown
// ─────────────────────────────────────────────────────────────────────────────
void UPS_BLE_Module::StartupModule()
{
#if PLATFORM_WINDOWS
try
{
winrt::init_apartment(winrt::apartment_type::multi_threaded);
bWinRTCoInitialized = true;
}
catch (...)
{
// Already initialized in this apartment — that's fine.
bWinRTCoInitialized = false;
}
bInitialized = true;
LocalBLEManager = NewObject<UPS_BLE_Manager>();
LocalBLEManager->AddToRoot(); // prevent GC
LocalBLEManager->AttachModule(this);
// Cleanup UObjects before Unreal's UObject array is destroyed.
// Both ShutdownModule() and OnPreExit fire too late (UObjectArray already
// partially torn down). OnEnginePreExit fires earlier, before GC teardown.
FCoreDelegates::OnEnginePreExit.AddLambda([this]()
{
if (LocalBLEManager)
{
LocalBLEManager->DisconnectAll();
LocalBLEManager->RemoveFromRoot();
LocalBLEManager = nullptr;
}
});
#else
FMessageDialog::Open(EAppMsgType::Ok,
LOCTEXT("PS_Win_BLE_Platform", "PS_Win_BLE: BLE is only supported on Windows 64-bit."));
#endif
}
void UPS_BLE_Module::ShutdownModule()
{
#if PLATFORM_WINDOWS
ScannerCleanup();
// NOTE: LocalBLEManager cleanup (DisconnectAll + RemoveFromRoot) is handled
// in the OnPreExit delegate registered in StartupModule, because ShutdownModule
// is called after the UObject array has started to be destroyed (causes Index >= 0 crash).
LocalBLEManager = nullptr;
if (bWinRTCoInitialized)
{
winrt::uninit_apartment();
bWinRTCoInitialized = false;
}
bInitialized = false;
#endif
}
UPS_BLE_Manager* UPS_BLE_Module::GetBLEManager(const UPS_BLE_Module* Mod)
{
return Mod ? Mod->LocalBLEManager : nullptr;
}
// ─────────────────────────────────────────────────────────────────────────────
// Discovery
// ─────────────────────────────────────────────────────────────────────────────
void UPS_BLE_Module::ScannerCleanup()
{
#if PLATFORM_WINDOWS
PS_BLE_WINRT_NS
if (ScannerHandle)
{
FPS_ScannerState* State = static_cast<FPS_ScannerState*>(ScannerHandle);
try
{
if (State->Watcher &&
State->Watcher.Status() == WinAdv::BluetoothLEAdvertisementWatcherStatus::Started)
{
State->Watcher.Stop();
}
}
catch (...) {}
delete State;
ScannerHandle = nullptr;
}
#endif
}
bool UPS_BLE_Module::StartDiscoveryLive(UPS_BLE_Manager* Ref, int32 DurationMs, const FString& Filter)
{
#if PLATFORM_WINDOWS
PS_BLE_WINRT_NS
if (!Ref) return false;
ScannerCleanup();
FPS_ScannerState* State = new FPS_ScannerState();
State->Watcher = WinAdv::BluetoothLEAdvertisementWatcher();
State->Watcher.ScanningMode(WinAdv::BluetoothLEScanningMode::Active);
ScannerHandle = State;
FString FilterCopy = Filter;
UPS_BLE_Manager* MgrRef = Ref;
State->ReceivedToken = State->Watcher.Received(
[MgrRef, FilterCopy](WinAdv::BluetoothLEAdvertisementWatcher const&,
WinAdv::BluetoothLEAdvertisementReceivedEventArgs const& Args)
{
FString Name = Args.Advertisement().LocalName().c_str();
if (!FilterCopy.IsEmpty())
{
TArray<FString> Parts;
FilterCopy.ParseIntoArray(Parts, TEXT(","), true);
bool bMatch = false;
for (const FString& Part : Parts)
{
if (Name.Contains(Part.TrimStartAndEnd())) { bMatch = true; break; }
}
if (!bMatch) return;
}
FPS_DeviceRecord Rec;
Rec.ID = Args.BluetoothAddress();
Rec.RSSI = Args.RawSignalStrengthInDBm();
Rec.Name = Name;
DispatchDeviceDiscovered(MgrRef, Rec);
});
State->StoppedToken = State->Watcher.Stopped(
[MgrRef](WinAdv::BluetoothLEAdvertisementWatcher const&,
WinAdv::BluetoothLEAdvertisementWatcherStoppedEventArgs const&)
{
TArray<FPS_DeviceRecord> Empty;
DispatchDiscoveryEnd(MgrRef, Empty);
});
State->Watcher.Start();
if (DurationMs > 0)
{
int32 Ms = DurationMs;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, Ms]()
{
FPlatformProcess::Sleep(Ms / 1000.0f);
StopDiscovery();
});
}
return true;
#else
return false;
#endif
}
bool UPS_BLE_Module::StartDiscoveryInBackground(UPS_BLE_Manager* Ref, int32 DurationMs, const FString& Filter)
{
#if PLATFORM_WINDOWS
PS_BLE_WINRT_NS
if (!Ref) return false;
ScannerCleanup();
TSharedPtr<TArray<FPS_DeviceRecord>, ESPMode::ThreadSafe> Collected =
MakeShared<TArray<FPS_DeviceRecord>, ESPMode::ThreadSafe>();
FPS_ScannerState* State = new FPS_ScannerState();
State->Watcher = WinAdv::BluetoothLEAdvertisementWatcher();
State->Watcher.ScanningMode(WinAdv::BluetoothLEScanningMode::Active);
ScannerHandle = State;
FString FilterCopy = Filter;
UPS_BLE_Manager* MgrRef = Ref;
State->ReceivedToken = State->Watcher.Received(
[MgrRef, FilterCopy, Collected](WinAdv::BluetoothLEAdvertisementWatcher const&,
WinAdv::BluetoothLEAdvertisementReceivedEventArgs const& Args)
{
FString Name = Args.Advertisement().LocalName().c_str();
uint64 Addr = Args.BluetoothAddress();
for (const FPS_DeviceRecord& R : *Collected)
{
if (R.ID == Addr) return;
}
if (!FilterCopy.IsEmpty())
{
TArray<FString> Parts;
FilterCopy.ParseIntoArray(Parts, TEXT(","), true);
bool bMatch = false;
for (const FString& Part : Parts)
{
if (Name.Contains(Part.TrimStartAndEnd())) { bMatch = true; break; }
}
if (!bMatch) return;
}
FPS_DeviceRecord Rec;
Rec.ID = Addr;
Rec.RSSI = Args.RawSignalStrengthInDBm();
Rec.Name = Name;
Collected->Add(Rec);
});
State->StoppedToken = State->Watcher.Stopped(
[MgrRef, Collected](WinAdv::BluetoothLEAdvertisementWatcher const&,
WinAdv::BluetoothLEAdvertisementWatcherStoppedEventArgs const&)
{
DispatchDiscoveryEnd(MgrRef, *Collected);
});
State->Watcher.Start();
if (DurationMs > 0)
{
int32 Ms = DurationMs;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this, Ms]()
{
FPlatformProcess::Sleep(Ms / 1000.0f);
StopDiscovery();
});
}
return true;
#else
return false;
#endif
}
bool UPS_BLE_Module::StopDiscovery()
{
#if PLATFORM_WINDOWS
PS_BLE_WINRT_NS
if (ScannerHandle)
{
FPS_ScannerState* State = static_cast<FPS_ScannerState*>(ScannerHandle);
try
{
if (State->Watcher &&
State->Watcher.Status() == WinAdv::BluetoothLEAdvertisementWatcherStatus::Started)
{
State->Watcher.Stop();
}
}
catch (...) { return false; }
return true;
}
#endif
return false;
}
// ─────────────────────────────────────────────────────────────────────────────
// Connect / Disconnect / IsConnected
// ─────────────────────────────────────────────────────────────────────────────
bool UPS_BLE_Module::ConnectDevice(UPS_BLE_Device* Device)
{
#if PLATFORM_WINDOWS
if (!Device) return false;
uint64 Addr = Device->DeviceID;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, Addr]()
{
PS_BLE_WINRT_NS
try
{
auto BLEDev = WinBT::BluetoothLEDevice::FromBluetoothAddressAsync(Addr).get();
if (!BLEDev)
{
DispatchDeviceDisconnected(Device);
return;
}
FPS_BLEDeviceHandle* Handle = new FPS_BLEDeviceHandle();
Handle->Device = BLEDev;
Handle->ConnectionStatusToken = BLEDev.ConnectionStatusChanged(
[Device](WinBT::BluetoothLEDevice const& Dev, winrt::Windows::Foundation::IInspectable const&)
{
if (Dev.ConnectionStatus() == WinBT::BluetoothConnectionStatus::Disconnected)
{
DispatchDeviceDisconnected(Device);
}
});
Device->NativeDeviceHandle = Handle;
auto SvcResult = BLEDev.GetGattServicesAsync(WinBT::BluetoothCacheMode::Uncached).get();
if (SvcResult.Status() != WinGAP::GattCommunicationStatus::Success)
{
DispatchDeviceDisconnected(Device);
return;
}
Device->ActiveServices.Empty();
Device->NativeGattServices.Empty();
auto Services = SvcResult.Services();
for (uint32_t si = 0; si < Services.Size(); si++)
{
auto Svc = Services.GetAt(si);
FPS_GattServiceHandle* SvcHandle = new FPS_GattServiceHandle();
SvcHandle->Service = Svc;
Device->NativeGattServices.Add(SvcHandle);
FPS_ServiceItem SvcItem;
GUID g = Svc.Uuid();
SvcItem.ServiceUUID = UPS_BLE_Device::GUIDToString(&g);
SvcItem.ServiceName = SvcItem.ServiceUUID;
auto CharResult = SvcHandle->Service.GetCharacteristicsAsync(WinBT::BluetoothCacheMode::Uncached).get();
if (CharResult.Status() == WinGAP::GattCommunicationStatus::Success)
{
auto Chars = CharResult.Characteristics();
for (uint32_t ci = 0; ci < Chars.Size(); ci++)
{
auto Ch = Chars.GetAt(ci);
// Cache the characteristic object so all subsequent operations
// (Read / Write / Subscribe) use the exact same instance and index
// as what was discovered here — no re-fetch needed.
SvcHandle->Characteristics.push_back(Ch);
FPS_CharacteristicItem ChItem;
GUID cg = Ch.Uuid();
ChItem.CharacteristicUUID = UPS_BLE_Device::GUIDToString(&cg);
ChItem.CharacteristicName = ChItem.CharacteristicUUID;
auto Props = Ch.CharacteristicProperties();
uint8 Desc = 0;
using GP = WinGAP::GattCharacteristicProperties;
if ((Props & GP::Broadcast) != GP::None) Desc |= 0x01;
if ((Props & GP::ExtendedProperties) != GP::None) Desc |= 0x02;
if ((Props & GP::Notify) != GP::None) Desc |= 0x04;
if ((Props & GP::Indicate) != GP::None) Desc |= 0x08;
if ((Props & GP::Read) != GP::None) Desc |= 0x10;
if ((Props & GP::Write) != GP::None) Desc |= 0x20;
if ((Props & GP::WriteWithoutResponse) != GP::None) Desc |= 0x40;
if ((Props & GP::AuthenticatedSignedWrites) != GP::None) Desc |= 0x80;
ChItem.Descriptor = Desc;
SvcItem.Characteristics.Add(ChItem);
}
}
Device->ActiveServices.Add(SvcItem);
}
DispatchServicesDiscovered(Device);
DispatchDeviceConnected(Device);
}
catch (...)
{
DispatchDeviceDisconnected(Device);
}
});
return true;
#else
return false;
#endif
}
bool UPS_BLE_Module::DisconnectDevice(UPS_BLE_Device* Device)
{
#if PLATFORM_WINDOWS
PS_BLE_WINRT_NS
if (!Device) return false;
for (void* SvcPtr : Device->NativeGattServices)
{
if (SvcPtr) delete static_cast<FPS_GattServiceHandle*>(SvcPtr);
}
Device->NativeGattServices.Empty();
if (Device->NativeDeviceHandle)
{
delete static_cast<FPS_BLEDeviceHandle*>(Device->NativeDeviceHandle);
Device->NativeDeviceHandle = nullptr;
}
return true;
#else
return false;
#endif
}
bool UPS_BLE_Module::IsDeviceConnected(UPS_BLE_Device* Device)
{
#if PLATFORM_WINDOWS
PS_BLE_WINRT_NS
if (!Device || !Device->NativeDeviceHandle) return false;
try
{
auto* Handle = static_cast<FPS_BLEDeviceHandle*>(Device->NativeDeviceHandle);
return Handle->Device.ConnectionStatus() == WinBT::BluetoothConnectionStatus::Connected;
}
catch (...) {}
#endif
return false;
}
// ─────────────────────────────────────────────────────────────────────────────
// GATT Read / Write / Subscribe / Unsubscribe
// ─────────────────────────────────────────────────────────────────────────────
bool UPS_BLE_Module::ReadCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8 CI)
{
#if PLATFORM_WINDOWS
if (!Device || SI >= (uint8)Device->NativeGattServices.Num()) return false;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI]()
{
PS_BLE_WINRT_NS
try
{
auto* SvcH = static_cast<FPS_GattServiceHandle*>(Device->NativeGattServices[SI]);
if (CI >= SvcH->Characteristics.size()) return;
auto Ch = SvcH->Characteristics[CI];
auto Result = Ch.ReadValueAsync(WinBT::BluetoothCacheMode::Uncached).get();
EPS_GATTStatus Status = (Result.Status() == WinGAP::GattCommunicationStatus::Success)
? EPS_GATTStatus::Success : EPS_GATTStatus::Failure;
TArray<uint8> Data;
if (Result.Status() == WinGAP::GattCommunicationStatus::Success)
{
auto Reader = WinStr::DataReader::FromBuffer(Result.Value());
Data.SetNumUninitialized(Reader.UnconsumedBufferLength());
for (uint8& B : Data) B = Reader.ReadByte();
}
DispatchRead(Device, SI, CI, Status, MoveTemp(Data));
}
catch (...) {}
});
return true;
#else
return false;
#endif
}
bool UPS_BLE_Module::WriteCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8 CI, const TArray<uint8>& Data)
{
#if PLATFORM_WINDOWS
if (!Device || SI >= (uint8)Device->NativeGattServices.Num()) return false;
TArray<uint8> DataCopy = Data;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI, DataCopy]()
{
PS_BLE_WINRT_NS
try
{
auto* SvcH = static_cast<FPS_GattServiceHandle*>(Device->NativeGattServices[SI]);
if (CI >= SvcH->Characteristics.size()) return;
auto Ch = SvcH->Characteristics[CI];
auto Writer = WinStr::DataWriter();
for (uint8 B : DataCopy) Writer.WriteByte(B);
auto WriteStatus = Ch.WriteValueAsync(Writer.DetachBuffer(),
WinGAP::GattWriteOption::WriteWithResponse).get();
EPS_GATTStatus GattStatus = (WriteStatus == WinGAP::GattCommunicationStatus::Success)
? EPS_GATTStatus::Success : EPS_GATTStatus::Failure;
DispatchWrite(Device, SI, CI, GattStatus);
}
catch (...) {}
});
return true;
#else
return false;
#endif
}
bool UPS_BLE_Module::SubscribeCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8 CI)
{
#if PLATFORM_WINDOWS
if (!Device || SI >= (uint8)Device->NativeGattServices.Num()) return false;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI]()
{
PS_BLE_WINRT_NS
try
{
auto* SvcH = static_cast<FPS_GattServiceHandle*>(Device->NativeGattServices[SI]);
if (CI >= SvcH->Characteristics.size()) return;
auto Ch = SvcH->Characteristics[CI];
auto WriteStatus = Ch.WriteClientCharacteristicConfigurationDescriptorAsync(
WinGAP::GattClientCharacteristicConfigurationDescriptorValue::Notify).get();
EPS_GATTStatus GattStatus = (WriteStatus == WinGAP::GattCommunicationStatus::Success)
? EPS_GATTStatus::Success : EPS_GATTStatus::Failure;
if (WriteStatus == WinGAP::GattCommunicationStatus::Success)
{
auto Token = Ch.ValueChanged(
[Device, SI, CI](WinGAP::GattCharacteristic const&,
WinGAP::GattValueChangedEventArgs const& Args)
{
PS_BLE_WINRT_NS
auto Reader = WinStr::DataReader::FromBuffer(Args.CharacteristicValue());
TArray<uint8> Data;
Data.SetNumUninitialized(Reader.UnconsumedBufferLength());
for (uint8& B : Data) B = Reader.ReadByte();
DispatchNotify(Device, SI, CI, EPS_GATTStatus::Success, MoveTemp(Data));
});
uint64 Key = ((uint64)SI << 8) | CI;
Device->NotifyTokens.Add(Key, new winrt::event_token(Token));
}
DispatchSubscribe(Device, SI, CI, GattStatus);
}
catch (...) {}
});
return true;
#else
return false;
#endif
}
bool UPS_BLE_Module::UnsubscribeCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8 CI)
{
#if PLATFORM_WINDOWS
if (!Device || SI >= (uint8)Device->NativeGattServices.Num()) return false;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI]()
{
PS_BLE_WINRT_NS
try
{
auto* SvcH = static_cast<FPS_GattServiceHandle*>(Device->NativeGattServices[SI]);
if (CI >= SvcH->Characteristics.size()) return;
auto Ch = SvcH->Characteristics[CI];
uint64 Key = ((uint64)SI << 8) | CI;
if (winrt::event_token** TokenPtr = reinterpret_cast<winrt::event_token**>(Device->NotifyTokens.Find(Key)))
{
Ch.ValueChanged(**TokenPtr);
delete *TokenPtr;
Device->NotifyTokens.Remove(Key);
}
auto WriteStatus = Ch.WriteClientCharacteristicConfigurationDescriptorAsync(
WinGAP::GattClientCharacteristicConfigurationDescriptorValue::None).get();
EPS_GATTStatus GattStatus = (WriteStatus == WinGAP::GattCommunicationStatus::Success)
? EPS_GATTStatus::Success : EPS_GATTStatus::Failure;
DispatchUnsubscribe(Device, SI, CI, GattStatus);
}
catch (...) {}
});
return true;
#else
return false;
#endif
}
// ─────────────────────────────────────────────────────────────────────────────
// GameThread dispatchers
// ─────────────────────────────────────────────────────────────────────────────
void UPS_BLE_Module::DispatchDeviceDiscovered(UPS_BLE_Manager* Mgr, const FPS_DeviceRecord& Rec)
{
if (!Mgr) return;
FPS_DeviceRecord RecCopy = Rec;
if (IsInGameThread()) { Mgr->JustDiscoveredDevice(RecCopy); }
else { AsyncTask(ENamedThreads::GameThread, [Mgr, RecCopy]() { Mgr->JustDiscoveredDevice(RecCopy); }); }
}
void UPS_BLE_Module::DispatchDiscoveryEnd(UPS_BLE_Manager* Mgr, const TArray<FPS_DeviceRecord>& Devices)
{
if (!Mgr) return;
TArray<FPS_DeviceRecord> Copy = Devices;
if (IsInGameThread()) { Mgr->JustDiscoveryEnd(Copy); }
else { AsyncTask(ENamedThreads::GameThread, [Mgr, Copy]() { Mgr->JustDiscoveryEnd(Copy); }); }
}
void UPS_BLE_Module::DispatchDeviceConnected(UPS_BLE_Device* Dev)
{
if (!Dev) return;
if (IsInGameThread())
{
Dev->RefToManager->JustConnectedDevice(Dev);
Dev->OnConnect.Broadcast(Dev, Dev->ActiveServices);
}
else
{
TWeakObjectPtr<UPS_BLE_Device> WeakDev(Dev);
AsyncTask(ENamedThreads::GameThread, [WeakDev]()
{
if (!WeakDev.IsValid()) return;
WeakDev->RefToManager->JustConnectedDevice(WeakDev.Get());
WeakDev->OnConnect.Broadcast(WeakDev.Get(), WeakDev->ActiveServices);
});
}
}
void UPS_BLE_Module::DispatchDeviceDisconnected(UPS_BLE_Device* Dev)
{
if (!Dev) return;
if (IsInGameThread())
{
Dev->RefToManager->JustDisconnectedDevice(Dev);
Dev->OnDisconnect.Broadcast(Dev);
}
else
{
TWeakObjectPtr<UPS_BLE_Device> WeakDev(Dev);
AsyncTask(ENamedThreads::GameThread, [WeakDev]()
{
if (!WeakDev.IsValid()) return;
WeakDev->RefToManager->JustDisconnectedDevice(WeakDev.Get());
WeakDev->OnDisconnect.Broadcast(WeakDev.Get());
});
}
}
void UPS_BLE_Module::DispatchServicesDiscovered(UPS_BLE_Device* Dev)
{
if (!Dev) return;
if (IsInGameThread())
{
Dev->RefToManager->JustDiscoveredServices(Dev);
Dev->OnServicesDiscovered.Broadcast(Dev, Dev->ActiveServices);
}
else
{
TWeakObjectPtr<UPS_BLE_Device> WeakDev(Dev);
AsyncTask(ENamedThreads::GameThread, [WeakDev]()
{
if (!WeakDev.IsValid()) return;
WeakDev->RefToManager->JustDiscoveredServices(WeakDev.Get());
WeakDev->OnServicesDiscovered.Broadcast(WeakDev.Get(), WeakDev->ActiveServices);
});
}
}
void UPS_BLE_Module::DispatchRead(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status, TArray<uint8> Data)
{
if (!Dev) return;
if (IsInGameThread())
{
if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num())
Dev->OnRead.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID,
Dev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID, Data);
}
else
{
TWeakObjectPtr<UPS_BLE_Device> WeakDev(Dev);
AsyncTask(ENamedThreads::GameThread, [WeakDev, SI, CI, Status, Data]()
{
if (!WeakDev.IsValid()) return;
if (SI < WeakDev->ActiveServices.Num() && CI < WeakDev->ActiveServices[SI].Characteristics.Num())
WeakDev->OnRead.Broadcast(Status, WeakDev.Get(), WeakDev->ActiveServices[SI].ServiceUUID,
WeakDev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID, Data);
});
}
}
void UPS_BLE_Module::DispatchNotify(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status, TArray<uint8> Data)
{
if (!Dev) return;
if (IsInGameThread())
{
if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num())
Dev->OnNotify.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID,
Dev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID, Data);
}
else
{
TWeakObjectPtr<UPS_BLE_Device> WeakDev(Dev);
AsyncTask(ENamedThreads::GameThread, [WeakDev, SI, CI, Status, Data]()
{
if (!WeakDev.IsValid()) return;
if (SI < WeakDev->ActiveServices.Num() && CI < WeakDev->ActiveServices[SI].Characteristics.Num())
WeakDev->OnNotify.Broadcast(Status, WeakDev.Get(), WeakDev->ActiveServices[SI].ServiceUUID,
WeakDev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID, Data);
});
}
}
void UPS_BLE_Module::DispatchWrite(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status)
{
if (!Dev) return;
TWeakObjectPtr<UPS_BLE_Device> WeakDev(Dev);
auto Fire = [WeakDev, SI, CI, Status]()
{
if (!WeakDev.IsValid()) return;
if (SI < WeakDev->ActiveServices.Num() && CI < WeakDev->ActiveServices[SI].Characteristics.Num())
WeakDev->OnWrite.Broadcast(Status, WeakDev.Get(), WeakDev->ActiveServices[SI].ServiceUUID,
WeakDev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID);
};
if (IsInGameThread()) Fire();
else AsyncTask(ENamedThreads::GameThread, Fire);
}
void UPS_BLE_Module::DispatchSubscribe(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status)
{
if (!Dev) return;
TWeakObjectPtr<UPS_BLE_Device> WeakDev(Dev);
auto Fire = [WeakDev, SI, CI, Status]()
{
if (!WeakDev.IsValid()) return;
if (SI < WeakDev->ActiveServices.Num() && CI < WeakDev->ActiveServices[SI].Characteristics.Num())
{
WeakDev->ActiveServices[SI].Characteristics[CI].subscribed = true;
WeakDev->OnSubscribe.Broadcast(Status, WeakDev.Get(), WeakDev->ActiveServices[SI].ServiceUUID,
WeakDev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID);
}
};
if (IsInGameThread()) Fire();
else AsyncTask(ENamedThreads::GameThread, Fire);
}
void UPS_BLE_Module::DispatchUnsubscribe(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status)
{
if (!Dev) return;
TWeakObjectPtr<UPS_BLE_Device> WeakDev(Dev);
auto Fire = [WeakDev, SI, CI, Status]()
{
if (!WeakDev.IsValid()) return;
if (SI < WeakDev->ActiveServices.Num() && CI < WeakDev->ActiveServices[SI].Characteristics.Num())
{
WeakDev->ActiveServices[SI].Characteristics[CI].subscribed = false;
WeakDev->OnUnsubscribe.Broadcast(Status, WeakDev.Get(), WeakDev->ActiveServices[SI].ServiceUUID,
WeakDev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID);
}
};
if (IsInGameThread()) Fire();
else AsyncTask(ENamedThreads::GameThread, Fire);
}
IMPLEMENT_MODULE(UPS_BLE_Module, PS_Win_BLE)
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,106 @@
// Copyright (C) 2025 ASTERION VR
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "PS_BLETypes.h"
#include "PS_BLEDevice.generated.h"
class UPS_BLE_Manager;
class UPS_BLE_Module;
UCLASS(ClassGroup = (Custom), Category = "ASTERION|Win_BLE", meta = (BlueprintSpawnableComponent))
class PS_WIN_BLE_API UPS_BLE_Device : public UObject
{
GENERATED_BODY()
friend class UPS_BLE_Module;
friend class UPS_BLE_Manager;
public:
UPS_BLE_Device();
virtual ~UPS_BLE_Device() override;
// ─── Getters Blueprint ────────────────────────────────────────────────────
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "ASTERION|Win_BLE", meta = (DisplayName = "Address as String", ReturnDisplayName = "Address"))
FString DeviceAddressAsString();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "ASTERION|Win_BLE", meta = (DisplayName = "Address as Int64", ReturnDisplayName = "Int64"))
int64 DeviceAddressAsInt64();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "ASTERION|Win_BLE", meta = (DisplayName = "Address as MAC", ReturnDisplayName = "MAC"))
FPS_MACAddress DeviceAddressAsMAC();
// ─── Actions ──────────────────────────────────────────────────────────────
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE")
void Connect();
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE")
void Disconnect();
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE", meta = (DisplayName = "Is Connected", ReturnDisplayName = "Connected"))
bool IsConnected();
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE")
void DiscoverServices();
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE")
void Read(const FString& ServiceUUID, const FString& CharacteristicUUID);
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE")
void Write(const FString& ServiceUUID, const FString& CharacteristicUUID, TArray<uint8> Data);
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE")
void Subscribe(const FString& ServiceUUID, const FString& CharacteristicUUID);
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE")
void Unsubscribe(const FString& ServiceUUID, const FString& CharacteristicUUID);
// ─── Properties ───────────────────────────────────────────────────────────
UPROPERTY(BlueprintReadOnly, Category = "ASTERION|Win_BLE")
FString DeviceName = "<noname>";
UPROPERTY(BlueprintReadOnly, BlueprintGetter = "DeviceAddressAsString", Category = "ASTERION|Win_BLE")
FString AddressAsString;
UPROPERTY(BlueprintReadOnly, BlueprintGetter = "DeviceAddressAsInt64", Category = "ASTERION|Win_BLE")
int64 AddressAsInt64 = 0;
UPROPERTY(BlueprintReadOnly, BlueprintGetter = "DeviceAddressAsMAC", Category = "ASTERION|Win_BLE")
FPS_MACAddress AddressAsMAC;
UPROPERTY(BlueprintReadOnly, Category = "ASTERION|Win_BLE")
int32 RSSI = 0;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "ASTERION|Win_BLE")
TArray<FPS_ServiceItem> ActiveServices;
// ─── Delegates ────────────────────────────────────────────────────────────
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Win_BLE") FPS_OnConnect OnConnect;
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Win_BLE") FPS_OnDisconnect OnDisconnect;
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Win_BLE") FPS_OnServicesDiscovered OnServicesDiscovered;
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Win_BLE") FPS_OnRead OnRead;
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Win_BLE") FPS_OnNotify OnNotify;
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Win_BLE") FPS_OnWrite OnWrite;
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Win_BLE") FPS_OnSubscribe OnSubscribe;
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Win_BLE") FPS_OnUnsubscribe OnUnsubscribe;
// ─── Internal state ───────────────────────────────────────────────────────
uint64 DeviceID = 0;
UPS_BLE_Manager* RefToManager = nullptr;
UPS_BLE_Module* RefToModule = nullptr;
bool bDestroyInProgress = false;
// Native WinRT handles (opaque void* — cast to WinRT types in .cpp)
void* NativeDeviceHandle = nullptr; // IBluetoothLEDevice
void* NativeGattSession = nullptr; // GattSession
TArray<void*> NativeGattServices; // per-service GattDeviceService handles
TMap<uint64, void*> NotifyTokens; // characteristic handle -> event token
// Finds service+char indices from UUID strings, returns 0xFFFF if not found
uint16 FindInList(const FString& ServiceUUID, const FString& CharUUID, EPS_CharacteristicDescriptor& OutDescriptor) const;
// Converts a 128-bit GUID (from WinRT GUID struct) to "{xxxxxxxx-xxxx-...}" FString
static FString GUIDToString(const void* WinRTGuid);
};

View File

@ -0,0 +1,53 @@
// Copyright (C) 2025 ASTERION VR
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "PS_BLETypes.h"
#include "PS_BLELibrary.generated.h"
class UPS_BLE_Manager;
UCLASS()
class PS_WIN_BLE_API UPS_BLE_Library : public UBlueprintFunctionLibrary
{
GENERATED_UCLASS_BODY()
public:
/** Return the BLE Manager singleton */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "ASTERION|Win_BLE", meta = (
DisplayName = "Get BLE Manager",
ReturnDisplayName = "BLE Manager",
Keywords = "Bluetooth LE BLE manager PS"))
static UPS_BLE_Manager* GetBLEManager(bool& IsPluginLoaded, bool& BLEAdapterFound);
/** Convert a hex nibble character to byte (0-15). Valid=false if invalid character. */
UFUNCTION(BlueprintPure, Category = "ASTERION|Win_BLE", meta = (DisplayName = "Nibble To Byte", ReturnDisplayName = "Byte"))
static uint8 NibbleToByte(const FString& Nibble, bool& Valid);
/** Decompose a characteristic descriptor byte into individual capability booleans */
UFUNCTION(BlueprintPure, Category = "ASTERION|Win_BLE", meta = (DisplayName = "Get Descriptor Bits"))
static void GetDescriptorBits(
const EPS_CharacteristicDescriptor& Descriptor,
bool& Broadcastable, bool& ExtendedProperties,
bool& Notifiable, bool& Indicable,
bool& Readable, bool& Writable,
bool& WriteNoResponse, bool& SignedWrite);
/** Build a characteristic descriptor byte from individual capability booleans */
UFUNCTION(BlueprintPure, Category = "ASTERION|Win_BLE", meta = (DisplayName = "Make BLE Descriptor"))
static EPS_CharacteristicDescriptor MakeDescriptor(
const bool Broadcastable, const bool ExtendedProperties,
const bool Notifiable, const bool Indicable,
const bool Readable, const bool Writable,
const bool WriteNoResponse, const bool SignedWrite);
/** Convert a descriptor byte to a human-readable string */
UFUNCTION(BlueprintPure, Category = "ASTERION|Win_BLE", meta = (DisplayName = "Descriptor To String", ReturnDisplayName = "Descriptor"))
static FString DescriptorToString(const uint8& Descriptor);
/** True if running inside the Unreal Editor */
UFUNCTION(BlueprintPure, Category = "ASTERION|Win_BLE", meta = (DisplayName = "Is Editor Running", ReturnDisplayName = "EditorRunning"))
static bool IsEditorRunning();
};

View File

@ -0,0 +1,91 @@
// Copyright (C) 2025 ASTERION VR
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "PS_BLETypes.h"
#include "PS_BLEManager.generated.h"
class UPS_BLE_Module;
class UPS_BLE_Device;
UCLASS(ClassGroup = (Custom), Category = "ASTERION|Win_BLE", meta = (BlueprintSpawnableComponent))
class PS_WIN_BLE_API UPS_BLE_Manager : public UObject
{
GENERATED_BODY()
friend class UPS_BLE_Module;
public:
UPS_BLE_Manager();
virtual ~UPS_BLE_Manager() override;
void AttachModule(UPS_BLE_Module* Module);
// ─── Discovery ────────────────────────────────────────────────────────────
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE", meta = (
AutoCreateRefTerm = "OnNewDeviceDiscovered",
ToolTip = "Live scan: fires OnNewDeviceDiscovered each time a device is found. Filter is comma-separated name substrings."))
void StartDiscoveryLive(
const FPS_OnNewBLEDeviceDiscovered& OnNewDeviceDiscovered,
const FPS_OnDiscoveryEnd& OnDiscoveryEnd,
const int64 DurationMs = 5000,
const FString& NameFilter = "");
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE", meta = (
AutoCreateRefTerm = "OnDiscoveryEnd",
ToolTip = "Background scan: fires OnDiscoveryEnd once finished. Filter is comma-separated name substrings."))
void StartDiscoveryInBackground(
const FPS_OnDiscoveryEnd& OnDiscoveryEnd,
const int64 DurationMs = 5000,
const FString& NameFilter = "");
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE")
void StopDiscovery();
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE")
void DisconnectAll();
UFUNCTION(BlueprintCallable, Category = "ASTERION|Win_BLE", meta = (DisplayName = "Reset BLE Adapter", ReturnDisplayName = "Success"))
bool ResetBLEAdapter();
// ─── MAC utils ────────────────────────────────────────────────────────────
UFUNCTION(BlueprintPure, Category = "ASTERION|Win_BLE", meta = (DisplayName = "MAC to String", ReturnDisplayName = "Address"))
static FString MACToString(const FPS_MACAddress& Address);
UFUNCTION(BlueprintPure, Category = "ASTERION|Win_BLE", meta = (DisplayName = "String to MAC", ReturnDisplayName = "MAC"))
static FPS_MACAddress StringToMAC(const FString& Address, bool& Valid);
// ─── Properties ───────────────────────────────────────────────────────────
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "ASTERION|Win_BLE")
TArray<UPS_BLE_Device*> FoundDevices;
UPROPERTY(BlueprintReadOnly, Category = "ASTERION|Win_BLE")
bool bIsScanInProgress = false;
// ─── Delegates ────────────────────────────────────────────────────────────
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Win_BLE") FPS_OnAnyBLEDeviceConnected OnAnyDeviceConnected;
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Win_BLE") FPS_OnAnyBLEDeviceDisconnected OnAnyDeviceDisconnected;
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Win_BLE") FPS_OnAnyServiceDiscovered OnAnyServiceDiscovered;
UPROPERTY(BlueprintAssignable, Category = "ASTERION|Win_BLE") FPS_OnBLEError OnBLEError;
// ─── Internal callbacks (called from UPS_BLE_Module dispatch) ─────────────
void JustDiscoveredDevice(const FPS_DeviceRecord& Rec);
void JustDiscoveryEnd(const TArray<FPS_DeviceRecord>& Devices);
void JustConnectedDevice(UPS_BLE_Device* Dev);
void JustDisconnectedDevice(UPS_BLE_Device* Dev);
void JustDiscoveredServices(UPS_BLE_Device* Dev);
private:
UPS_BLE_Module* BLEModule = nullptr;
bool bAttached = false;
UPS_BLE_Device* MakeNewDevice(const FPS_DeviceRecord& Rec);
int32 GetIndexByID(uint64 DeviceID) const;
FPS_OnNewBLEDeviceDiscovered PendingOnNewDevice;
FPS_OnDiscoveryEnd PendingOnDiscoveryEnd;
static uint8 HexNibble(TCHAR C, bool& Valid);
};

View File

@ -0,0 +1,65 @@
// Copyright (C) 2025 ASTERION VR
#pragma once
#include "Modules/ModuleManager.h"
#include "CoreMinimal.h"
#include "PS_BLETypes.h"
class UPS_BLE_Device;
class UPS_BLE_Manager;
// ─── Module ───────────────────────────────────────────────────────────────────
class UPS_BLE_Module : public IModuleInterface
{
friend class UPS_BLE_Device;
friend class UPS_BLE_Manager;
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
static UPS_BLE_Manager* GetBLEManager(const UPS_BLE_Module* Mod);
// Discovery
bool StartDiscoveryLive(UPS_BLE_Manager* Ref, int32 DurationMs, const FString& Filter);
bool StartDiscoveryInBackground(UPS_BLE_Manager* Ref, int32 DurationMs, const FString& Filter);
bool StopDiscovery();
// Device lifecycle
bool ConnectDevice(UPS_BLE_Device* Device);
bool DisconnectDevice(UPS_BLE_Device* Device);
bool IsDeviceConnected(UPS_BLE_Device* Device);
// GATT operations
bool ReadCharacteristic(UPS_BLE_Device* Device, uint8 ServiceIndex, uint8 CharIndex);
bool WriteCharacteristic(UPS_BLE_Device* Device, uint8 ServiceIndex, uint8 CharIndex, const TArray<uint8>& Data);
bool SubscribeCharacteristic(UPS_BLE_Device* Device, uint8 ServiceIndex, uint8 CharIndex);
bool UnsubscribeCharacteristic(UPS_BLE_Device* Device, uint8 ServiceIndex, uint8 CharIndex);
UPS_BLE_Manager* LocalBLEManager = nullptr;
bool bInitialized = false;
private:
// WinRT scanner (opaque handle — implementation in .cpp using WinRT types)
void* ScannerHandle = nullptr;
// WinRT COM initialized
bool bWinRTCoInitialized = false;
void ScannerCleanup();
// Internal callbacks dispatched to GameThread
static void DispatchDeviceDiscovered(UPS_BLE_Manager* Mgr, const FPS_DeviceRecord& Rec);
static void DispatchDiscoveryEnd(UPS_BLE_Manager* Mgr, const TArray<FPS_DeviceRecord>& Devices);
static void DispatchDeviceConnected(UPS_BLE_Device* Dev);
static void DispatchDeviceDisconnected(UPS_BLE_Device* Dev);
static void DispatchServicesDiscovered(UPS_BLE_Device* Dev);
static void DispatchRead(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status, TArray<uint8> Data);
static void DispatchNotify(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status, TArray<uint8> Data);
static void DispatchWrite(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status);
static void DispatchSubscribe(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status);
static void DispatchUnsubscribe(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status);
};

View File

@ -0,0 +1,134 @@
// Copyright (C) 2025 ASTERION VR
#pragma once
#include "CoreMinimal.h"
#include "PS_BLETypes.generated.h"
// ─── ENUMS ───────────────────────────────────────────────────────────────────
UENUM(BlueprintType, Category = "ASTERION|Win_BLE")
enum class EPS_GATTStatus : uint8
{
Success = 0x00 UMETA(DisplayName = "Success"),
ReadNotPermitted = 0x02 UMETA(DisplayName = "Read Not Permitted"),
WriteNotPermitted = 0x03 UMETA(DisplayName = "Write Not Permitted"),
InsuficientAutentication = 0x05 UMETA(DisplayName = "Insufficient Authentication"),
RequestNotSupported = 0x06 UMETA(DisplayName = "Request Not Supported"),
InvalidOffset = 0x07 UMETA(DisplayName = "Invalid Offset"),
InvalidAttributeLength = 0x0D UMETA(DisplayName = "Invalid Attribute Length"),
InsufficientEncryption = 0x0F UMETA(DisplayName = "Insufficient Encryption"),
Failure = 0xFF UMETA(DisplayName = "Failure")
};
UENUM(BlueprintType, Category = "ASTERION|Win_BLE")
enum class EPS_CharacteristicDescriptor : uint8
{
Unknown = 0x00 UMETA(DisplayName = "Unknown"),
Broadcast = 0x01 UMETA(DisplayName = "Broadcastable"),
ExtendedProps = 0x02 UMETA(DisplayName = "Extended Properties"),
Notifiable = 0x04 UMETA(DisplayName = "Notifiable"),
Indicable = 0x08 UMETA(DisplayName = "Indicable"),
Readable = 0x10 UMETA(DisplayName = "Readable"),
Writable = 0x20 UMETA(DisplayName = "Writable"),
WriteNoResponse = 0x40 UMETA(DisplayName = "Write No Response"),
SignedWrite = 0x80 UMETA(DisplayName = "Signed Write")
};
UENUM(BlueprintType, Category = "ASTERION|Win_BLE")
enum class EPS_BLEError : uint8
{
NoError = 0x00 UMETA(DisplayName = "Success"),
NonReadableChar = 0x01 UMETA(DisplayName = "Non Readable Characteristic"),
NonWritableChar = 0x03 UMETA(DisplayName = "Non Writable Characteristic"),
CanNotSubscribe = 0x04 UMETA(DisplayName = "Cannot subscribe to Non Notifiable/Indicable Characteristic"),
CanNotUnsubscribe = 0x05 UMETA(DisplayName = "Cannot unsubscribe if not previously subscribed"),
AlreadyConnected = 0x06 UMETA(DisplayName = "Device is already connected"),
CanNotDisconnect = 0x07 UMETA(DisplayName = "Cannot disconnect if not connected"),
NeedConnectionFirst = 0x08 UMETA(DisplayName = "Operation requires connection first"),
ZeroLengthWrite = 0x09 UMETA(DisplayName = "Zero length write"),
AlreadySubscribed = 0x0A UMETA(DisplayName = "Already subscribed"),
NotSubscribed = 0x0B UMETA(DisplayName = "Not subscribed"),
CommunicationFailed = 0x0C UMETA(DisplayName = "Communication Failed: No BLE adapter"),
WritingFailed = 0x0D UMETA(DisplayName = "Writing Failed: No BLE adapter"),
SubscriptionFailed = 0x0E UMETA(DisplayName = "Subscription Failed: No BLE adapter"),
UnsubscribeFailed = 0x0F UMETA(DisplayName = "Unsubscribe Failed: No BLE adapter"),
ReadingFailed = 0x10 UMETA(DisplayName = "Reading Failed: No BLE adapter"),
ConnectFailed = 0x11 UMETA(DisplayName = "Connect Failed: No BLE adapter"),
DisconnectFailed = 0x12 UMETA(DisplayName = "Disconnect Failed: No BLE adapter"),
Failure = 0xFF UMETA(DisplayName = "Unknown Failure")
};
// ─── STRUCTS ─────────────────────────────────────────────────────────────────
USTRUCT(BlueprintType, Atomic, Category = "ASTERION|Win_BLE")
struct FPS_MACAddress
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite, Category = "ASTERION|Win_BLE") uint8 b0 = 0;
UPROPERTY(BlueprintReadWrite, Category = "ASTERION|Win_BLE") uint8 b1 = 0;
UPROPERTY(BlueprintReadWrite, Category = "ASTERION|Win_BLE") uint8 b2 = 0;
UPROPERTY(BlueprintReadWrite, Category = "ASTERION|Win_BLE") uint8 b3 = 0;
UPROPERTY(BlueprintReadWrite, Category = "ASTERION|Win_BLE") uint8 b4 = 0;
UPROPERTY(BlueprintReadWrite, Category = "ASTERION|Win_BLE") uint8 b5 = 0;
};
USTRUCT(BlueprintType, Atomic, Category = "ASTERION|Win_BLE")
struct FPS_CharacteristicItem
{
GENERATED_BODY()
public:
FGuid cUUID;
bool subscribed = false;
UPROPERTY(BlueprintReadOnly, Category = "ASTERION|Win_BLE") uint8 Descriptor = 0;
UPROPERTY(BlueprintReadOnly, Category = "ASTERION|Win_BLE") FString CharacteristicName;
UPROPERTY(BlueprintReadOnly, Category = "ASTERION|Win_BLE") FString CharacteristicUUID;
};
USTRUCT(BlueprintType, Atomic, Category = "ASTERION|Win_BLE")
struct FPS_ServiceItem
{
GENERATED_BODY()
public:
FGuid sUUID;
UPROPERTY(BlueprintReadOnly, Category = "ASTERION|Win_BLE") FString ServiceName;
UPROPERTY(BlueprintReadOnly, Category = "ASTERION|Win_BLE") FString ServiceUUID;
UPROPERTY(BlueprintReadOnly, Category = "ASTERION|Win_BLE") TArray<FPS_CharacteristicItem> Characteristics;
};
// ─── INTERNAL DEVICE RECORD (forward-declared here so Manager can use it) ────
// This is a plain C++ struct (no UCLASS/USTRUCT) used internally between
// the module and the manager — not exposed to Blueprint.
struct FPS_DeviceRecord
{
uint64 ID = 0;
int64 RSSI = 0;
FString Name;
void* NativeDeviceRef = nullptr;
};
// ─── FORWARD DECLARATIONS ────────────────────────────────────────────────────
class UPS_BLE_Device;
class UPS_BLE_Manager;
// ─── DELEGATES ───────────────────────────────────────────────────────────────
DECLARE_DYNAMIC_DELEGATE_OneParam(FPS_OnNewBLEDeviceDiscovered, UPS_BLE_Device* const, Device);
DECLARE_DYNAMIC_DELEGATE_OneParam(FPS_OnDiscoveryEnd, const TArray<UPS_BLE_Device*>&, DiscoveredDevices);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FPS_OnServicesDiscovered, UPS_BLE_Device* const, Device, const TArray<FPS_ServiceItem>&, ServicesList);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FPS_OnConnect, UPS_BLE_Device* const, Device, const TArray<FPS_ServiceItem>&, ServicesList);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam (FPS_OnDisconnect, UPS_BLE_Device* const, Device);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam (FPS_OnAnyBLEDeviceConnected, UPS_BLE_Device* const, Device);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam (FPS_OnAnyBLEDeviceDisconnected,UPS_BLE_Device* const, Device);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FPS_OnAnyServiceDiscovered, UPS_BLE_Device* const, Device, const TArray<FPS_ServiceItem>&, ServicesList);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(FPS_OnRead, EPS_GATTStatus, Status, UPS_BLE_Device*, Device, FString, ServiceUUID, FString, CharacteristicUUID, const TArray<uint8>&, Data);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(FPS_OnNotify, EPS_GATTStatus, Status, UPS_BLE_Device*, Device, FString, ServiceUUID, FString, CharacteristicUUID, const TArray<uint8>&, Data);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FPS_OnWrite, EPS_GATTStatus, Status, UPS_BLE_Device*, Device, FString, ServiceUUID, FString, CharacteristicUUID);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FPS_OnSubscribe, EPS_GATTStatus, Status, UPS_BLE_Device*, Device, FString, ServiceUUID, FString, CharacteristicUUID);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FPS_OnUnsubscribe, EPS_GATTStatus, Status, UPS_BLE_Device*, Device, FString, ServiceUUID, FString, CharacteristicUUID);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FPS_OnBLEError, EPS_BLEError, ErrorCode, FString, DeviceAddress, FString, ServiceUUID, FString, CharacteristicUUID);