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>
This commit is contained in:
parent
1836e4e2b1
commit
d593bbd9fd
BIN
Unreal/Plugins/PS_Win_BLE/Content/Sample/BP_BLEActor.uasset
Normal file
BIN
Unreal/Plugins/PS_Win_BLE/Content/Sample/BP_BLEActor.uasset
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Win_BLE/Content/Sample/BP_Pawn.uasset
Normal file
BIN
Unreal/Plugins/PS_Win_BLE/Content/Sample/BP_Pawn.uasset
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Win_BLE/Content/Sample/GM_Bluetooth.uasset
Normal file
BIN
Unreal/Plugins/PS_Win_BLE/Content/Sample/GM_Bluetooth.uasset
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Win_BLE/Content/Sample/PS_BLETest.umap
Normal file
BIN
Unreal/Plugins/PS_Win_BLE/Content/Sample/PS_BLETest.umap
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Win_BLE/Resources/Icon128.png
Normal file
BIN
Unreal/Plugins/PS_Win_BLE/Resources/Icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 <winrt/Windows.Foundation.h>
|
||||
#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<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;
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
@ -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 <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"
|
||||
|
||||
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<UPS_BLE_Manager>();
|
||||
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<FPS_ScannerState*>(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<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](BluetoothLEAdvertisementWatcher const&, BluetoothLEAdvertisementWatcherStoppedEventArgs const&)
|
||||
{
|
||||
TArray<FPS_DeviceRecord> 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<TArray<FPS_DeviceRecord>, ESPMode::ThreadSafe> Collected =
|
||||
MakeShared<TArray<FPS_DeviceRecord>, 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<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](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<FPS_ScannerState*>(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<GattDeviceService*>(SvcPtr);
|
||||
}
|
||||
}
|
||||
Device->NativeGattServices.Empty();
|
||||
|
||||
// Close device handle
|
||||
if (Device->NativeDeviceHandle)
|
||||
{
|
||||
delete static_cast<BluetoothLEDevice*>(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<BluetoothLEDevice*>(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<GattDeviceService*>(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<uint8> 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<uint8>& Data)
|
||||
{
|
||||
#if PLATFORM_WINDOWS
|
||||
if (!Device || SI >= Device->NativeGattServices.Num()) return false;
|
||||
|
||||
TArray<uint8> DataCopy = Data;
|
||||
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI, DataCopy]()
|
||||
{
|
||||
try
|
||||
{
|
||||
GattDeviceService* Svc = static_cast<GattDeviceService*>(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<GattDeviceService*>(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<uint8> 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<GattDeviceService*>(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<winrt::event_token**>(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<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 { 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<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
|
||||
{
|
||||
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<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
|
||||
{
|
||||
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
|
||||
@ -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<uint8> 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 = "<noname>";
|
||||
|
||||
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<FPS_ServiceItem> 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<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);
|
||||
};
|
||||
@ -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();
|
||||
};
|
||||
@ -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<UPS_BLE_Device*> 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<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);
|
||||
};
|
||||
@ -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<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;
|
||||
|
||||
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<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);
|
||||
};
|
||||
122
Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLETypes.h
Normal file
122
Unreal/Plugins/PS_Win_BLE/Source/PS_Win_BLE/Public/PS_BLETypes.h
Normal file
@ -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<FPS_CharacteristicItem> 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<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);
|
||||
Loading…
x
Reference in New Issue
Block a user