diff --git a/Unreal/Plugins/PS_Win_BLE/Content/Sample/BP_BLEActor.uasset b/Unreal/Plugins/PS_Win_BLE/Content/Sample/BP_BLEActor.uasset new file mode 100644 index 0000000..a870fc8 Binary files /dev/null and b/Unreal/Plugins/PS_Win_BLE/Content/Sample/BP_BLEActor.uasset differ diff --git a/Unreal/Plugins/PS_Win_BLE/Content/Sample/BP_Pawn.uasset b/Unreal/Plugins/PS_Win_BLE/Content/Sample/BP_Pawn.uasset new file mode 100644 index 0000000..64a57b5 Binary files /dev/null and b/Unreal/Plugins/PS_Win_BLE/Content/Sample/BP_Pawn.uasset differ diff --git a/Unreal/Plugins/PS_Win_BLE/Content/Sample/GM_Bluetooth.uasset b/Unreal/Plugins/PS_Win_BLE/Content/Sample/GM_Bluetooth.uasset new file mode 100644 index 0000000..68adcfb Binary files /dev/null and b/Unreal/Plugins/PS_Win_BLE/Content/Sample/GM_Bluetooth.uasset differ diff --git a/Unreal/Plugins/PS_Win_BLE/Content/Sample/PS_BLETest.umap b/Unreal/Plugins/PS_Win_BLE/Content/Sample/PS_BLETest.umap new file mode 100644 index 0000000..390dcd2 Binary files /dev/null and b/Unreal/Plugins/PS_Win_BLE/Content/Sample/PS_BLETest.umap differ diff --git a/Unreal/Plugins/PS_Win_BLE/Resources/Icon128.png b/Unreal/Plugins/PS_Win_BLE/Resources/Icon128.png new file mode 100644 index 0000000..04e1928 Binary files /dev/null and b/Unreal/Plugins/PS_Win_BLE/Resources/Icon128.png differ diff --git a/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/PS_Win_BLE.Build.cs b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/PS_Win_BLE.Build.cs new file mode 100644 index 0000000..4be0dbb --- /dev/null +++ b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/PS_Win_BLE.Build.cs @@ -0,0 +1,56 @@ +// 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 + }); + + // Allow WinRT headers in C++ code + bEnableExceptions = true; + } + } +} diff --git a/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Private/PS_BLEDevice.cpp b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Private/PS_BLEDevice.cpp new file mode 100644 index 0000000..2c8b9aa --- /dev/null +++ b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Private/PS_BLEDevice.cpp @@ -0,0 +1,251 @@ +// Copyright (C) 2025 ASTERION VR + +#include "PS_BLEDevice.h" +#include "PS_BLEModule.h" +#include "PS_BLEManager.h" + +#if PLATFORM_WINDOWS +#include "Windows/AllowWindowsPlatformTypes.h" +#include "Windows/AllowWindowsPlatformAtomics.h" +#pragma warning(push) +#pragma warning(disable: 4668 4946 5204 5220) +#include +#pragma warning(pop) +#include "Windows/HideWindowsPlatformAtomics.h" +#include "Windows/HideWindowsPlatformTypes.h" +using namespace winrt; +#endif + +// ───────────────────────────────────────────────────────────────────────────── + +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 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(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; +} diff --git a/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Private/PS_BLELibrary.cpp b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Private/PS_BLELibrary.cpp new file mode 100644 index 0000000..5e3c5d8 --- /dev/null +++ b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Private/PS_BLELibrary.cpp @@ -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 +} diff --git a/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Private/PS_BLEManager.cpp b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Private/PS_BLEManager.cpp new file mode 100644 index 0000000..e9bcca8 --- /dev/null +++ b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Private/PS_BLEManager.cpp @@ -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(); + Dev->RefToModule = BLEModule; + Dev->RefToManager = this; + Dev->DeviceID = Rec.ID; + Dev->RSSI = (int32)Rec.RSSI; + Dev->DeviceName = Rec.Name.IsEmpty() ? TEXT("") : 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& 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 diff --git a/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Private/PS_BLEModule.cpp b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Private/PS_BLEModule.cpp new file mode 100644 index 0000000..18fa643 --- /dev/null +++ b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Private/PS_BLEModule.cpp @@ -0,0 +1,734 @@ +// 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 "Modules/ModuleManager.h" + +// ─── WinRT includes (Windows only) ─────────────────────────────────────────── +#if PLATFORM_WINDOWS + +// Unreal defines WIN32_LEAN_AND_MEAN which can strip some COM headers — we +// add the specific ones we need without touching the Unreal macros. +#include "Windows/AllowWindowsPlatformTypes.h" +#include "Windows/AllowWindowsPlatformAtomics.h" + +#pragma warning(push) +#pragma warning(disable: 4668 4946 5204 5220) + +#include +#include +#include +#include +#include +#include + +#pragma warning(pop) + +#include "Windows/HideWindowsPlatformAtomics.h" +#include "Windows/HideWindowsPlatformTypes.h" + +using namespace winrt; +using namespace Windows::Devices::Bluetooth; +using namespace Windows::Devices::Bluetooth::Advertisement; +using namespace Windows::Devices::Bluetooth::GenericAttributeProfile; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::Storage::Streams; + +// ─── Scanner state (allocated on heap so WinRT types don't leak into header) ─ +struct FPS_ScannerState +{ + BluetoothLEAdvertisementWatcher Watcher{ nullptr }; + winrt::event_token ReceivedToken; + winrt::event_token StoppedToken; +}; + +#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(); + LocalBLEManager->AddToRoot(); // prevent GC + LocalBLEManager->AttachModule(this); +#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(); + + if (LocalBLEManager) + { + LocalBLEManager->DisconnectAll(); + LocalBLEManager->RemoveFromRoot(); + 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 + if (ScannerHandle) + { + FPS_ScannerState* State = static_cast(ScannerHandle); + try + { + if (State->Watcher && State->Watcher.Status() == 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 + if (!Ref) return false; + ScannerCleanup(); + + FPS_ScannerState* State = new FPS_ScannerState(); + State->Watcher = BluetoothLEAdvertisementWatcher(); + State->Watcher.ScanningMode(BluetoothLEScanningMode::Active); + ScannerHandle = State; + + // Capture filter and manager ref + FString FilterCopy = Filter; + UPS_BLE_Manager* MgrRef = Ref; + + State->ReceivedToken = State->Watcher.Received( + [MgrRef, FilterCopy](BluetoothLEAdvertisementWatcher const&, BluetoothLEAdvertisementReceivedEventArgs const& Args) + { + FString Name = Args.Advertisement().LocalName().c_str(); + + // Apply name filter (comma-separated substrings) + if (!FilterCopy.IsEmpty()) + { + TArray 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](BluetoothLEAdvertisementWatcher const&, BluetoothLEAdvertisementWatcherStoppedEventArgs const&) + { + TArray Empty; + DispatchDiscoveryEnd(MgrRef, Empty); + }); + + State->Watcher.Start(); + + // Auto-stop after DurationMs + 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 + if (!Ref) return false; + ScannerCleanup(); + + // Collect all devices during scan, fire DiscoveryEnd at the end + TSharedPtr, ESPMode::ThreadSafe> Collected = + MakeShared, ESPMode::ThreadSafe>(); + + FPS_ScannerState* State = new FPS_ScannerState(); + State->Watcher = BluetoothLEAdvertisementWatcher(); + State->Watcher.ScanningMode(BluetoothLEScanningMode::Active); + ScannerHandle = State; + + FString FilterCopy = Filter; + UPS_BLE_Manager* MgrRef = Ref; + + State->ReceivedToken = State->Watcher.Received( + [MgrRef, FilterCopy, Collected](BluetoothLEAdvertisementWatcher const&, BluetoothLEAdvertisementReceivedEventArgs const& Args) + { + FString Name = Args.Advertisement().LocalName().c_str(); + + // Dedup by address + uint64 Addr = Args.BluetoothAddress(); + for (const FPS_DeviceRecord& R : *Collected) + { + if (R.ID == Addr) return; + } + + if (!FilterCopy.IsEmpty()) + { + TArray 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](BluetoothLEAdvertisementWatcher const&, 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 + if (ScannerHandle) + { + FPS_ScannerState* State = static_cast(ScannerHandle); + try + { + if (State->Watcher && State->Watcher.Status() == 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; + UPS_BLE_Module* ModRef = this; + + // WinRT async connect + service discovery on background thread + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [ModRef, Device, Addr]() + { + try + { + // FromBluetoothAddressAsync is the standard WinRT connect path + auto Op = BluetoothLEDevice::FromBluetoothAddressAsync(Addr); + BluetoothLEDevice BLEDev = Op.get(); + + if (!BLEDev) + { + DispatchDeviceDisconnected(Device); + return; + } + + // Store native handle (AddRef via IUnknown kept alive by winrt wrapper) + Device->NativeDeviceHandle = new winrt::Windows::Devices::Bluetooth::BluetoothLEDevice(BLEDev); + + // Subscribe to connection-status change + BLEDev.ConnectionStatusChanged( + [Device](BluetoothLEDevice const& Dev, IInspectable const&) + { + if (Dev.ConnectionStatus() == BluetoothConnectionStatus::Disconnected) + { + DispatchDeviceDisconnected(Device); + } + }); + + // Discover GATT services + auto SvcResult = BLEDev.GetGattServicesAsync(BluetoothCacheMode::Uncached).get(); + if (SvcResult.Status() != 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); + Device->NativeGattServices.Add(new GattDeviceService(Svc)); + + FPS_ServiceItem SvcItem; + GUID g = Svc.Uuid(); + SvcItem.ServiceUUID = UPS_BLE_Device::GUIDToString(&g); + SvcItem.ServiceName = SvcItem.ServiceUUID; // WinRT doesn't give a friendly name + + // Discover characteristics + auto CharResult = Svc.GetCharacteristicsAsync(BluetoothCacheMode::Uncached).get(); + if (CharResult.Status() == GattCommunicationStatus::Success) + { + auto Chars = CharResult.Characteristics(); + for (uint32_t ci = 0; ci < Chars.Size(); ci++) + { + auto Ch = Chars.GetAt(ci); + FPS_CharacteristicItem ChItem; + GUID cg = Ch.Uuid(); + ChItem.CharacteristicUUID = UPS_BLE_Device::GUIDToString(&cg); + ChItem.CharacteristicName = ChItem.CharacteristicUUID; + + // Map WinRT properties to our descriptor bits + auto Props = Ch.CharacteristicProperties(); + uint8 Desc = 0; + if ((Props & GattCharacteristicProperties::Broadcast) != GattCharacteristicProperties::None) Desc |= 0x01; + if ((Props & GattCharacteristicProperties::ExtendedProperties) != GattCharacteristicProperties::None) Desc |= 0x02; + if ((Props & GattCharacteristicProperties::Notify) != GattCharacteristicProperties::None) Desc |= 0x04; + if ((Props & GattCharacteristicProperties::Indicate) != GattCharacteristicProperties::None) Desc |= 0x08; + if ((Props & GattCharacteristicProperties::Read) != GattCharacteristicProperties::None) Desc |= 0x10; + if ((Props & GattCharacteristicProperties::Write) != GattCharacteristicProperties::None) Desc |= 0x20; + if ((Props & GattCharacteristicProperties::WriteWithoutResponse)!= GattCharacteristicProperties::None) Desc |= 0x40; + if ((Props & GattCharacteristicProperties::AuthenticatedSignedWrites)!= GattCharacteristicProperties::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 + if (!Device) return false; + + // Close native GATT service handles + for (void* SvcPtr : Device->NativeGattServices) + { + if (SvcPtr) + { + delete static_cast(SvcPtr); + } + } + Device->NativeGattServices.Empty(); + + // Close device handle + if (Device->NativeDeviceHandle) + { + delete static_cast(Device->NativeDeviceHandle); + Device->NativeDeviceHandle = nullptr; + } + return true; +#else + return false; +#endif +} + +bool UPS_BLE_Module::IsDeviceConnected(UPS_BLE_Device* Device) +{ +#if PLATFORM_WINDOWS + if (!Device || !Device->NativeDeviceHandle) return false; + try + { + BluetoothLEDevice* BLEDev = static_cast(Device->NativeDeviceHandle); + return BLEDev->ConnectionStatus() == 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 >= Device->NativeGattServices.Num()) return false; + + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI]() + { + try + { + GattDeviceService* Svc = static_cast(Device->NativeGattServices[SI]); + auto Chars = Svc->GetCharacteristicsAsync(BluetoothCacheMode::Cached).get(); + if (Chars.Status() != GattCommunicationStatus::Success) return; + + auto Ch = Chars.Characteristics().GetAt(CI); + auto Result = Ch.ReadValueAsync(BluetoothCacheMode::Uncached).get(); + + EPS_GATTStatus Status = (Result.Status() == GattCommunicationStatus::Success) + ? EPS_GATTStatus::Success : EPS_GATTStatus::Failure; + + TArray Data; + if (Result.Status() == GattCommunicationStatus::Success) + { + auto Reader = 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& Data) +{ +#if PLATFORM_WINDOWS + if (!Device || SI >= Device->NativeGattServices.Num()) return false; + + TArray DataCopy = Data; + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI, DataCopy]() + { + try + { + GattDeviceService* Svc = static_cast(Device->NativeGattServices[SI]); + auto Chars = Svc->GetCharacteristicsAsync(BluetoothCacheMode::Cached).get(); + if (Chars.Status() != GattCommunicationStatus::Success) return; + + auto Ch = Chars.Characteristics().GetAt(CI); + + auto Writer = DataWriter(); + for (uint8 B : DataCopy) Writer.WriteByte(B); + + auto Status = Ch.WriteValueAsync(Writer.DetachBuffer(), + GattWriteOption::WriteWithResponse).get(); + + EPS_GATTStatus GattStatus = (Status == 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 >= Device->NativeGattServices.Num()) return false; + + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI]() + { + try + { + GattDeviceService* Svc = static_cast(Device->NativeGattServices[SI]); + auto Chars = Svc->GetCharacteristicsAsync(BluetoothCacheMode::Cached).get(); + if (Chars.Status() != GattCommunicationStatus::Success) return; + + auto Ch = Chars.Characteristics().GetAt(CI); + + // Write CCCD to enable notifications + auto WriteStatus = Ch.WriteClientCharacteristicConfigurationDescriptorAsync( + GattClientCharacteristicConfigurationDescriptorValue::Notify).get(); + + EPS_GATTStatus GattStatus = (WriteStatus == GattCommunicationStatus::Success) + ? EPS_GATTStatus::Success : EPS_GATTStatus::Failure; + + if (WriteStatus == GattCommunicationStatus::Success) + { + // Register value-changed callback + auto Token = Ch.ValueChanged( + [Device, SI, CI](GattCharacteristic const&, GattValueChangedEventArgs const& Args) + { + auto Reader = DataReader::FromBuffer(Args.CharacteristicValue()); + TArray Data; + Data.SetNumUninitialized(Reader.UnconsumedBufferLength()); + for (uint8& B : Data) B = Reader.ReadByte(); + DispatchNotify(Device, SI, CI, EPS_GATTStatus::Success, MoveTemp(Data)); + }); + + // Store token (key = packed SI<<8|CI) + 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 >= Device->NativeGattServices.Num()) return false; + + AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI]() + { + try + { + GattDeviceService* Svc = static_cast(Device->NativeGattServices[SI]); + auto Chars = Svc->GetCharacteristicsAsync(BluetoothCacheMode::Cached).get(); + if (Chars.Status() != GattCommunicationStatus::Success) return; + + auto Ch = Chars.Characteristics().GetAt(CI); + + // Remove event token + uint64 Key = ((uint64)SI << 8) | CI; + if (winrt::event_token** TokenPtr = reinterpret_cast(Device->NotifyTokens.Find(Key))) + { + Ch.ValueChanged(**TokenPtr); + delete *TokenPtr; + Device->NotifyTokens.Remove(Key); + } + + // Write CCCD to disable notifications + auto WriteStatus = Ch.WriteClientCharacteristicConfigurationDescriptorAsync( + GattClientCharacteristicConfigurationDescriptorValue::None).get(); + + EPS_GATTStatus GattStatus = (WriteStatus == 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& Devices) +{ + if (!Mgr) return; + TArray 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 { AsyncTask(ENamedThreads::GameThread, [Dev]() { Dev->RefToManager->JustConnectedDevice(Dev); Dev->OnConnect.Broadcast(Dev, Dev->ActiveServices); }); } +} + +void UPS_BLE_Module::DispatchDeviceDisconnected(UPS_BLE_Device* Dev) +{ + if (!Dev) return; + if (IsInGameThread()) { Dev->RefToManager->JustDisconnectedDevice(Dev); Dev->OnDisconnect.Broadcast(Dev); } + else { AsyncTask(ENamedThreads::GameThread, [Dev]() { Dev->RefToManager->JustDisconnectedDevice(Dev); Dev->OnDisconnect.Broadcast(Dev); }); } +} + +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 { AsyncTask(ENamedThreads::GameThread, [Dev]() { Dev->RefToManager->JustDiscoveredServices(Dev); Dev->OnServicesDiscovered.Broadcast(Dev, Dev->ActiveServices); }); } +} + +void UPS_BLE_Module::DispatchRead(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status, TArray 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 + { + AsyncTask(ENamedThreads::GameThread, [Dev, SI, CI, Status, Data]() + { + 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); + }); + } +} + +void UPS_BLE_Module::DispatchNotify(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status, TArray 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 + { + AsyncTask(ENamedThreads::GameThread, [Dev, SI, CI, Status, Data]() + { + 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); + }); + } +} + +void UPS_BLE_Module::DispatchWrite(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status) +{ + if (!Dev) return; + auto Fire = [Dev, SI, CI, Status]() + { + if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num()) + Dev->OnWrite.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID, Dev->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; + auto Fire = [Dev, SI, CI, Status]() + { + if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num()) + { + Dev->ActiveServices[SI].Characteristics[CI].subscribed = true; + Dev->OnSubscribe.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID, Dev->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; + auto Fire = [Dev, SI, CI, Status]() + { + if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num()) + { + Dev->ActiveServices[SI].Characteristics[CI].subscribed = false; + Dev->OnUnsubscribe.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID, Dev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID); + } + }; + if (IsInGameThread()) Fire(); + else AsyncTask(ENamedThreads::GameThread, Fire); +} + +IMPLEMENT_MODULE(UPS_BLE_Module, PS_Win_BLE) + +#undef LOCTEXT_NAMESPACE diff --git a/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLEDevice.h b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLEDevice.h new file mode 100644 index 0000000..225ee91 --- /dev/null +++ b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLEDevice.h @@ -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 = "PS 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 = "PS BLE", meta = (DisplayName = "Address as String", ReturnDisplayName = "Address")) + FString DeviceAddressAsString(); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS BLE", meta = (DisplayName = "Address as Int64", ReturnDisplayName = "Int64")) + int64 DeviceAddressAsInt64(); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS BLE", meta = (DisplayName = "Address as MAC", ReturnDisplayName = "MAC")) + FPS_MACAddress DeviceAddressAsMAC(); + + // ─── Actions ────────────────────────────────────────────────────────────── + UFUNCTION(BlueprintCallable, Category = "PS BLE") + void Connect(); + + UFUNCTION(BlueprintCallable, Category = "PS BLE") + void Disconnect(); + + UFUNCTION(BlueprintCallable, Category = "PS BLE", meta = (DisplayName = "Is Connected", ReturnDisplayName = "Connected")) + bool IsConnected(); + + UFUNCTION(BlueprintCallable, Category = "PS BLE") + void DiscoverServices(); + + UFUNCTION(BlueprintCallable, Category = "PS BLE") + void Read(const FString& ServiceUUID, const FString& CharacteristicUUID); + + UFUNCTION(BlueprintCallable, Category = "PS BLE") + void Write(const FString& ServiceUUID, const FString& CharacteristicUUID, TArray Data); + + UFUNCTION(BlueprintCallable, Category = "PS BLE") + void Subscribe(const FString& ServiceUUID, const FString& CharacteristicUUID); + + UFUNCTION(BlueprintCallable, Category = "PS BLE") + void Unsubscribe(const FString& ServiceUUID, const FString& CharacteristicUUID); + + // ─── Properties ─────────────────────────────────────────────────────────── + UPROPERTY(BlueprintReadOnly, Category = "PS BLE") + FString DeviceName = ""; + + UPROPERTY(BlueprintReadOnly, BlueprintGetter = "DeviceAddressAsString", Category = "PS BLE") + FString AddressAsString; + + UPROPERTY(BlueprintReadOnly, BlueprintGetter = "DeviceAddressAsInt64", Category = "PS BLE") + int64 AddressAsInt64 = 0; + + UPROPERTY(BlueprintReadOnly, BlueprintGetter = "DeviceAddressAsMAC", Category = "PS BLE") + FPS_MACAddress AddressAsMAC; + + UPROPERTY(BlueprintReadOnly, Category = "PS BLE") + int32 RSSI = 0; + + UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "PS BLE") + TArray ActiveServices; + + // ─── Delegates ──────────────────────────────────────────────────────────── + UPROPERTY(BlueprintAssignable, Category = "PS BLE") FPS_OnConnect OnConnect; + UPROPERTY(BlueprintAssignable, Category = "PS BLE") FPS_OnDisconnect OnDisconnect; + UPROPERTY(BlueprintAssignable, Category = "PS BLE") FPS_OnServicesDiscovered OnServicesDiscovered; + UPROPERTY(BlueprintAssignable, Category = "PS BLE") FPS_OnRead OnRead; + UPROPERTY(BlueprintAssignable, Category = "PS BLE") FPS_OnNotify OnNotify; + UPROPERTY(BlueprintAssignable, Category = "PS BLE") FPS_OnWrite OnWrite; + UPROPERTY(BlueprintAssignable, Category = "PS BLE") FPS_OnSubscribe OnSubscribe; + UPROPERTY(BlueprintAssignable, Category = "PS 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 NativeGattServices; // per-service GattDeviceService handles + TMap 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); +}; diff --git a/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLELibrary.h b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLELibrary.h new file mode 100644 index 0000000..1adbb99 --- /dev/null +++ b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLELibrary.h @@ -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 = "PS 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 = "PS 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 = "PS 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 = "PS 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 = "PS BLE", meta = (DisplayName = "Descriptor To String", ReturnDisplayName = "Descriptor")) + static FString DescriptorToString(const uint8& Descriptor); + + /** True if running inside the Unreal Editor */ + UFUNCTION(BlueprintPure, Category = "PS BLE", meta = (DisplayName = "Is Editor Running", ReturnDisplayName = "EditorRunning")) + static bool IsEditorRunning(); +}; diff --git a/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLEManager.h b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLEManager.h new file mode 100644 index 0000000..3984b8c --- /dev/null +++ b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLEManager.h @@ -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 = "PS 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 = "PS 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 = "PS 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 = "PS BLE") + void StopDiscovery(); + + UFUNCTION(BlueprintCallable, Category = "PS BLE") + void DisconnectAll(); + + UFUNCTION(BlueprintCallable, Category = "PS BLE", meta = (DisplayName = "Reset BLE Adapter", ReturnDisplayName = "Success")) + bool ResetBLEAdapter(); + + // ─── MAC utils ──────────────────────────────────────────────────────────── + UFUNCTION(BlueprintPure, Category = "PS BLE", meta = (DisplayName = "MAC to String", ReturnDisplayName = "Address")) + static FString MACToString(const FPS_MACAddress& Address); + + UFUNCTION(BlueprintPure, Category = "PS BLE", meta = (DisplayName = "String to MAC", ReturnDisplayName = "MAC")) + static FPS_MACAddress StringToMAC(const FString& Address, bool& Valid); + + // ─── Properties ─────────────────────────────────────────────────────────── + UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "PS BLE") + TArray FoundDevices; + + UPROPERTY(BlueprintReadOnly, Category = "PS BLE") + bool bIsScanInProgress = false; + + // ─── Delegates ──────────────────────────────────────────────────────────── + UPROPERTY(BlueprintAssignable, Category = "PS BLE") FPS_OnAnyBLEDeviceConnected OnAnyDeviceConnected; + UPROPERTY(BlueprintAssignable, Category = "PS BLE") FPS_OnAnyBLEDeviceDisconnected OnAnyDeviceDisconnected; + UPROPERTY(BlueprintAssignable, Category = "PS BLE") FPS_OnAnyServiceDiscovered OnAnyServiceDiscovered; + UPROPERTY(BlueprintAssignable, Category = "PS BLE") FPS_OnBLEError OnBLEError; + + // ─── Internal callbacks (called from UPS_BLE_Module dispatch) ───────────── + void JustDiscoveredDevice(const FPS_DeviceRecord& Rec); + void JustDiscoveryEnd(const TArray& 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); +}; diff --git a/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLEModule.h b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLEModule.h new file mode 100644 index 0000000..df21de1 --- /dev/null +++ b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLEModule.h @@ -0,0 +1,74 @@ +// 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; + +// ─── Internal device record (equivalent to TUE_dev_rec from old plugin) ────── +struct FPS_DeviceRecord +{ + uint64 ID = 0; + int64 RSSI = 0; + FString Name; + void* NativeDeviceRef = nullptr; // WinRT IBluetoothLEDevice* (opaque ptr for forward compat) +}; + +// ─── 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& 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; + +private: + bool bInitialized = false; + + // 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& 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 Data); + static void DispatchNotify(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status, TArray 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); +}; diff --git a/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLETypes.h b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLETypes.h new file mode 100644 index 0000000..e1d4d95 --- /dev/null +++ b/Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLETypes.h @@ -0,0 +1,122 @@ +// Copyright (C) 2025 ASTERION VR + +#pragma once + +#include "CoreMinimal.h" +#include "PS_BLETypes.generated.h" + +// ─── ENUMS ─────────────────────────────────────────────────────────────────── + +UENUM(BlueprintType, Category = "PS 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 = "PS 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 = "PS 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 = "PS BLE") +struct FPS_MACAddress +{ + GENERATED_BODY() +public: + UPROPERTY(BlueprintReadWrite, Category = "PS BLE") uint8 b0 = 0; + UPROPERTY(BlueprintReadWrite, Category = "PS BLE") uint8 b1 = 0; + UPROPERTY(BlueprintReadWrite, Category = "PS BLE") uint8 b2 = 0; + UPROPERTY(BlueprintReadWrite, Category = "PS BLE") uint8 b3 = 0; + UPROPERTY(BlueprintReadWrite, Category = "PS BLE") uint8 b4 = 0; + UPROPERTY(BlueprintReadWrite, Category = "PS BLE") uint8 b5 = 0; +}; + +USTRUCT(BlueprintType, Atomic, Category = "PS BLE") +struct FPS_CharacteristicItem +{ + GENERATED_BODY() +public: + FGuid cUUID; + bool subscribed = false; + UPROPERTY(BlueprintReadOnly, Category = "PS BLE") uint8 Descriptor = 0; + UPROPERTY(BlueprintReadOnly, Category = "PS BLE") FString CharacteristicName; + UPROPERTY(BlueprintReadOnly, Category = "PS BLE") FString CharacteristicUUID; +}; + +USTRUCT(BlueprintType, Atomic, Category = "PS BLE") +struct FPS_ServiceItem +{ + GENERATED_BODY() +public: + FGuid sUUID; + UPROPERTY(BlueprintReadOnly, Category = "PS BLE") FString ServiceName; + UPROPERTY(BlueprintReadOnly, Category = "PS BLE") FString ServiceUUID; + UPROPERTY(BlueprintReadOnly, Category = "PS BLE") TArray Characteristics; +}; + +// ─── 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&, DiscoveredDevices); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FPS_OnServicesDiscovered, UPS_BLE_Device* const, Device, const TArray&, ServicesList); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FPS_OnConnect, UPS_BLE_Device* const, Device, const TArray&, 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&, ServicesList); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(FPS_OnRead, EPS_GATTStatus, Status, UPS_BLE_Device*, Device, FString, ServiceUUID, FString, CharacteristicUUID, const TArray&, Data); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(FPS_OnNotify, EPS_GATTStatus, Status, UPS_BLE_Device*, Device, FString, ServiceUUID, FString, CharacteristicUUID, const TArray&, 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);