PS_Win_BLE : fix erreurs de compilation WinRT

- Suppression des 'using namespace Windows::*' au scope global
  (conflit avec le namespace Windows d'Unreal via AllowWindowsPlatformTypes)
- Remplacement par un macro PS_BLE_WINRT_NS avec aliases locaux
  (WinBT, WinAdv, WinGAP, WinStr) utilisés dans chaque fonction
- Ajout de FPS_BLEDeviceHandle et FPS_GattServiceHandle : wrappers
  heap-alloués pour les types WinRT qui suppriment operator new
- Suppression warning C4265 (dtor non-virtual interne aux headers WinRT)
- Plugin charge sans erreur dans UE 5.5

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-02-18 16:46:01 +01:00
parent d593bbd9fd
commit 113eddef46
6 changed files with 204 additions and 150 deletions

View File

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

View File

@ -49,6 +49,13 @@ public class PS_Win_BLE : ModuleRules
"runtimeobject.lib" // RoInitialize / WinRT activation "runtimeobject.lib" // RoInitialize / WinRT activation
}); });
// C++/WinRT headers (winrt/Windows.Devices.Bluetooth.h, etc.)
// Located in the Windows SDK — we pick the version used by UE5.5
string WinSDKDir = "C:/Program Files (x86)/Windows Kits/10";
string WinSDKVersion = "10.0.22621.0";
PrivateIncludePaths.Add(Path.Combine(WinSDKDir, "Include", WinSDKVersion, "cppwinrt"));
PrivateIncludePaths.Add(Path.Combine(WinSDKDir, "Include", WinSDKVersion, "winrt"));
// Allow WinRT headers in C++ code // Allow WinRT headers in C++ code
bEnableExceptions = true; bEnableExceptions = true;
} }

View File

@ -4,18 +4,6 @@
#include "PS_BLEModule.h" #include "PS_BLEModule.h"
#include "PS_BLEManager.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()

View File

@ -8,15 +8,19 @@
#include "Modules/ModuleManager.h" #include "Modules/ModuleManager.h"
// ─── WinRT includes (Windows only) ─────────────────────────────────────────── // ─── WinRT includes (Windows only) ───────────────────────────────────────────
// NOTE: AllowWindowsPlatformTypes must wrap the WinRT headers.
// We do NOT place any "using namespace" at file scope because Unreal also
// defines a ::Windows namespace (via AllowWindowsPlatformTypes) and that would
// cause "ambiguous symbol" errors. All WinRT types are fully qualified with
// winrt:: inside each function body.
#if PLATFORM_WINDOWS #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/AllowWindowsPlatformTypes.h"
#include "Windows/AllowWindowsPlatformAtomics.h" #include "Windows/AllowWindowsPlatformAtomics.h"
#pragma warning(push) #pragma warning(push)
#pragma warning(disable: 4668 4946 5204 5220) #pragma warning(disable: 4265 4668 4946 5204 5220) // 4265: WinRT internal non-virtual dtor (harmless)
#include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h> #include <winrt/Windows.Foundation.Collections.h>
@ -30,22 +34,38 @@
#include "Windows/HideWindowsPlatformAtomics.h" #include "Windows/HideWindowsPlatformAtomics.h"
#include "Windows/HideWindowsPlatformTypes.h" #include "Windows/HideWindowsPlatformTypes.h"
using namespace winrt; // ─── Convenient namespace aliases used ONLY inside function bodies ────────────
using namespace Windows::Devices::Bluetooth; // (Defined as macros so we can paste them at the top of each #if PLATFORM_WINDOWS block)
using namespace Windows::Devices::Bluetooth::Advertisement; #define PS_BLE_WINRT_NS \
using namespace Windows::Devices::Bluetooth::GenericAttributeProfile; namespace WinBT = winrt::Windows::Devices::Bluetooth; \
using namespace Windows::Foundation; namespace WinAdv = winrt::Windows::Devices::Bluetooth::Advertisement; \
using namespace Windows::Foundation::Collections; namespace WinGAP = winrt::Windows::Devices::Bluetooth::GenericAttributeProfile; \
using namespace Windows::Storage::Streams; namespace WinFnd = winrt::Windows::Foundation; \
namespace WinStr = winrt::Windows::Storage::Streams;
// ─── Scanner state (allocated on heap so WinRT types don't leak into header) ─ // ─── Scanner state (heap-allocated so WinRT types don't appear in the header) ─
struct FPS_ScannerState struct FPS_ScannerState
{ {
BluetoothLEAdvertisementWatcher Watcher{ nullptr }; winrt::Windows::Devices::Bluetooth::Advertisement::BluetoothLEAdvertisementWatcher Watcher{ nullptr };
winrt::event_token ReceivedToken; winrt::event_token ReceivedToken;
winrt::event_token StoppedToken; winrt::event_token StoppedToken;
}; };
// ─── WinRT object wrappers ────────────────────────────────────────────────────
// WinRT types delete operator new — we wrap them in plain structs so they can
// live on the heap via regular new/delete (stored as void* in UObject headers).
struct FPS_BLEDeviceHandle
{
winrt::Windows::Devices::Bluetooth::BluetoothLEDevice Device{ nullptr };
winrt::event_token ConnectionStatusToken;
};
struct FPS_GattServiceHandle
{
winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattDeviceService Service{ nullptr };
};
#endif // PLATFORM_WINDOWS #endif // PLATFORM_WINDOWS
#define LOCTEXT_NAMESPACE "PS_Win_BLE" #define LOCTEXT_NAMESPACE "PS_Win_BLE"
@ -112,12 +132,14 @@ UPS_BLE_Manager* UPS_BLE_Module::GetBLEManager(const UPS_BLE_Module* Mod)
void UPS_BLE_Module::ScannerCleanup() void UPS_BLE_Module::ScannerCleanup()
{ {
#if PLATFORM_WINDOWS #if PLATFORM_WINDOWS
PS_BLE_WINRT_NS
if (ScannerHandle) if (ScannerHandle)
{ {
FPS_ScannerState* State = static_cast<FPS_ScannerState*>(ScannerHandle); FPS_ScannerState* State = static_cast<FPS_ScannerState*>(ScannerHandle);
try try
{ {
if (State->Watcher && State->Watcher.Status() == BluetoothLEAdvertisementWatcherStatus::Started) if (State->Watcher &&
State->Watcher.Status() == WinAdv::BluetoothLEAdvertisementWatcherStatus::Started)
{ {
State->Watcher.Stop(); State->Watcher.Stop();
} }
@ -132,24 +154,24 @@ void UPS_BLE_Module::ScannerCleanup()
bool UPS_BLE_Module::StartDiscoveryLive(UPS_BLE_Manager* Ref, int32 DurationMs, const FString& Filter) bool UPS_BLE_Module::StartDiscoveryLive(UPS_BLE_Manager* Ref, int32 DurationMs, const FString& Filter)
{ {
#if PLATFORM_WINDOWS #if PLATFORM_WINDOWS
PS_BLE_WINRT_NS
if (!Ref) return false; if (!Ref) return false;
ScannerCleanup(); ScannerCleanup();
FPS_ScannerState* State = new FPS_ScannerState(); FPS_ScannerState* State = new FPS_ScannerState();
State->Watcher = BluetoothLEAdvertisementWatcher(); State->Watcher = WinAdv::BluetoothLEAdvertisementWatcher();
State->Watcher.ScanningMode(BluetoothLEScanningMode::Active); State->Watcher.ScanningMode(WinAdv::BluetoothLEScanningMode::Active);
ScannerHandle = State; ScannerHandle = State;
// Capture filter and manager ref
FString FilterCopy = Filter; FString FilterCopy = Filter;
UPS_BLE_Manager* MgrRef = Ref; UPS_BLE_Manager* MgrRef = Ref;
State->ReceivedToken = State->Watcher.Received( State->ReceivedToken = State->Watcher.Received(
[MgrRef, FilterCopy](BluetoothLEAdvertisementWatcher const&, BluetoothLEAdvertisementReceivedEventArgs const& Args) [MgrRef, FilterCopy](WinAdv::BluetoothLEAdvertisementWatcher const&,
WinAdv::BluetoothLEAdvertisementReceivedEventArgs const& Args)
{ {
FString Name = Args.Advertisement().LocalName().c_str(); FString Name = Args.Advertisement().LocalName().c_str();
// Apply name filter (comma-separated substrings)
if (!FilterCopy.IsEmpty()) if (!FilterCopy.IsEmpty())
{ {
TArray<FString> Parts; TArray<FString> Parts;
@ -157,11 +179,7 @@ bool UPS_BLE_Module::StartDiscoveryLive(UPS_BLE_Manager* Ref, int32 DurationMs,
bool bMatch = false; bool bMatch = false;
for (const FString& Part : Parts) for (const FString& Part : Parts)
{ {
if (Name.Contains(Part.TrimStartAndEnd())) if (Name.Contains(Part.TrimStartAndEnd())) { bMatch = true; break; }
{
bMatch = true;
break;
}
} }
if (!bMatch) return; if (!bMatch) return;
} }
@ -170,12 +188,12 @@ bool UPS_BLE_Module::StartDiscoveryLive(UPS_BLE_Manager* Ref, int32 DurationMs,
Rec.ID = Args.BluetoothAddress(); Rec.ID = Args.BluetoothAddress();
Rec.RSSI = Args.RawSignalStrengthInDBm(); Rec.RSSI = Args.RawSignalStrengthInDBm();
Rec.Name = Name; Rec.Name = Name;
DispatchDeviceDiscovered(MgrRef, Rec); DispatchDeviceDiscovered(MgrRef, Rec);
}); });
State->StoppedToken = State->Watcher.Stopped( State->StoppedToken = State->Watcher.Stopped(
[MgrRef](BluetoothLEAdvertisementWatcher const&, BluetoothLEAdvertisementWatcherStoppedEventArgs const&) [MgrRef](WinAdv::BluetoothLEAdvertisementWatcher const&,
WinAdv::BluetoothLEAdvertisementWatcherStoppedEventArgs const&)
{ {
TArray<FPS_DeviceRecord> Empty; TArray<FPS_DeviceRecord> Empty;
DispatchDiscoveryEnd(MgrRef, Empty); DispatchDiscoveryEnd(MgrRef, Empty);
@ -183,7 +201,6 @@ bool UPS_BLE_Module::StartDiscoveryLive(UPS_BLE_Manager* Ref, int32 DurationMs,
State->Watcher.Start(); State->Watcher.Start();
// Auto-stop after DurationMs
if (DurationMs > 0) if (DurationMs > 0)
{ {
int32 Ms = DurationMs; int32 Ms = DurationMs;
@ -203,28 +220,28 @@ bool UPS_BLE_Module::StartDiscoveryLive(UPS_BLE_Manager* Ref, int32 DurationMs,
bool UPS_BLE_Module::StartDiscoveryInBackground(UPS_BLE_Manager* Ref, int32 DurationMs, const FString& Filter) bool UPS_BLE_Module::StartDiscoveryInBackground(UPS_BLE_Manager* Ref, int32 DurationMs, const FString& Filter)
{ {
#if PLATFORM_WINDOWS #if PLATFORM_WINDOWS
PS_BLE_WINRT_NS
if (!Ref) return false; if (!Ref) return false;
ScannerCleanup(); ScannerCleanup();
// Collect all devices during scan, fire DiscoveryEnd at the end
TSharedPtr<TArray<FPS_DeviceRecord>, ESPMode::ThreadSafe> Collected = TSharedPtr<TArray<FPS_DeviceRecord>, ESPMode::ThreadSafe> Collected =
MakeShared<TArray<FPS_DeviceRecord>, ESPMode::ThreadSafe>(); MakeShared<TArray<FPS_DeviceRecord>, ESPMode::ThreadSafe>();
FPS_ScannerState* State = new FPS_ScannerState(); FPS_ScannerState* State = new FPS_ScannerState();
State->Watcher = BluetoothLEAdvertisementWatcher(); State->Watcher = WinAdv::BluetoothLEAdvertisementWatcher();
State->Watcher.ScanningMode(BluetoothLEScanningMode::Active); State->Watcher.ScanningMode(WinAdv::BluetoothLEScanningMode::Active);
ScannerHandle = State; ScannerHandle = State;
FString FilterCopy = Filter; FString FilterCopy = Filter;
UPS_BLE_Manager* MgrRef = Ref; UPS_BLE_Manager* MgrRef = Ref;
State->ReceivedToken = State->Watcher.Received( State->ReceivedToken = State->Watcher.Received(
[MgrRef, FilterCopy, Collected](BluetoothLEAdvertisementWatcher const&, BluetoothLEAdvertisementReceivedEventArgs const& Args) [MgrRef, FilterCopy, Collected](WinAdv::BluetoothLEAdvertisementWatcher const&,
WinAdv::BluetoothLEAdvertisementReceivedEventArgs const& Args)
{ {
FString Name = Args.Advertisement().LocalName().c_str(); FString Name = Args.Advertisement().LocalName().c_str();
// Dedup by address
uint64 Addr = Args.BluetoothAddress(); uint64 Addr = Args.BluetoothAddress();
for (const FPS_DeviceRecord& R : *Collected) for (const FPS_DeviceRecord& R : *Collected)
{ {
if (R.ID == Addr) return; if (R.ID == Addr) return;
@ -250,7 +267,8 @@ bool UPS_BLE_Module::StartDiscoveryInBackground(UPS_BLE_Manager* Ref, int32 Dura
}); });
State->StoppedToken = State->Watcher.Stopped( State->StoppedToken = State->Watcher.Stopped(
[MgrRef, Collected](BluetoothLEAdvertisementWatcher const&, BluetoothLEAdvertisementWatcherStoppedEventArgs const&) [MgrRef, Collected](WinAdv::BluetoothLEAdvertisementWatcher const&,
WinAdv::BluetoothLEAdvertisementWatcherStoppedEventArgs const&)
{ {
DispatchDiscoveryEnd(MgrRef, *Collected); DispatchDiscoveryEnd(MgrRef, *Collected);
}); });
@ -276,12 +294,14 @@ bool UPS_BLE_Module::StartDiscoveryInBackground(UPS_BLE_Manager* Ref, int32 Dura
bool UPS_BLE_Module::StopDiscovery() bool UPS_BLE_Module::StopDiscovery()
{ {
#if PLATFORM_WINDOWS #if PLATFORM_WINDOWS
PS_BLE_WINRT_NS
if (ScannerHandle) if (ScannerHandle)
{ {
FPS_ScannerState* State = static_cast<FPS_ScannerState*>(ScannerHandle); FPS_ScannerState* State = static_cast<FPS_ScannerState*>(ScannerHandle);
try try
{ {
if (State->Watcher && State->Watcher.Status() == BluetoothLEAdvertisementWatcherStatus::Started) if (State->Watcher &&
State->Watcher.Status() == WinAdv::BluetoothLEAdvertisementWatcherStatus::Started)
{ {
State->Watcher.Stop(); State->Watcher.Stop();
} }
@ -303,39 +323,33 @@ bool UPS_BLE_Module::ConnectDevice(UPS_BLE_Device* Device)
if (!Device) return false; if (!Device) return false;
uint64 Addr = Device->DeviceID; uint64 Addr = Device->DeviceID;
UPS_BLE_Module* ModRef = this;
// WinRT async connect + service discovery on background thread AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, Addr]()
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [ModRef, Device, Addr]()
{ {
PS_BLE_WINRT_NS
try try
{ {
// FromBluetoothAddressAsync is the standard WinRT connect path auto BLEDev = WinBT::BluetoothLEDevice::FromBluetoothAddressAsync(Addr).get();
auto Op = BluetoothLEDevice::FromBluetoothAddressAsync(Addr);
BluetoothLEDevice BLEDev = Op.get();
if (!BLEDev) if (!BLEDev)
{ {
DispatchDeviceDisconnected(Device); DispatchDeviceDisconnected(Device);
return; return;
} }
// Store native handle (AddRef via IUnknown kept alive by winrt wrapper) FPS_BLEDeviceHandle* Handle = new FPS_BLEDeviceHandle();
Device->NativeDeviceHandle = new winrt::Windows::Devices::Bluetooth::BluetoothLEDevice(BLEDev); Handle->Device = BLEDev;
Handle->ConnectionStatusToken = BLEDev.ConnectionStatusChanged(
// Subscribe to connection-status change [Device](WinBT::BluetoothLEDevice const& Dev, winrt::Windows::Foundation::IInspectable const&)
BLEDev.ConnectionStatusChanged(
[Device](BluetoothLEDevice const& Dev, IInspectable const&)
{ {
if (Dev.ConnectionStatus() == BluetoothConnectionStatus::Disconnected) if (Dev.ConnectionStatus() == WinBT::BluetoothConnectionStatus::Disconnected)
{ {
DispatchDeviceDisconnected(Device); DispatchDeviceDisconnected(Device);
} }
}); });
Device->NativeDeviceHandle = Handle;
// Discover GATT services auto SvcResult = BLEDev.GetGattServicesAsync(WinBT::BluetoothCacheMode::Uncached).get();
auto SvcResult = BLEDev.GetGattServicesAsync(BluetoothCacheMode::Uncached).get(); if (SvcResult.Status() != WinGAP::GattCommunicationStatus::Success)
if (SvcResult.Status() != GattCommunicationStatus::Success)
{ {
DispatchDeviceDisconnected(Device); DispatchDeviceDisconnected(Device);
return; return;
@ -348,16 +362,17 @@ bool UPS_BLE_Module::ConnectDevice(UPS_BLE_Device* Device)
for (uint32_t si = 0; si < Services.Size(); si++) for (uint32_t si = 0; si < Services.Size(); si++)
{ {
auto Svc = Services.GetAt(si); auto Svc = Services.GetAt(si);
Device->NativeGattServices.Add(new GattDeviceService(Svc)); FPS_GattServiceHandle* SvcHandle = new FPS_GattServiceHandle();
SvcHandle->Service = Svc;
Device->NativeGattServices.Add(SvcHandle);
FPS_ServiceItem SvcItem; FPS_ServiceItem SvcItem;
GUID g = Svc.Uuid(); GUID g = Svc.Uuid();
SvcItem.ServiceUUID = UPS_BLE_Device::GUIDToString(&g); SvcItem.ServiceUUID = UPS_BLE_Device::GUIDToString(&g);
SvcItem.ServiceName = SvcItem.ServiceUUID; // WinRT doesn't give a friendly name SvcItem.ServiceName = SvcItem.ServiceUUID;
// Discover characteristics auto CharResult = SvcHandle->Service.GetCharacteristicsAsync(WinBT::BluetoothCacheMode::Uncached).get();
auto CharResult = Svc.GetCharacteristicsAsync(BluetoothCacheMode::Uncached).get(); if (CharResult.Status() == WinGAP::GattCommunicationStatus::Success)
if (CharResult.Status() == GattCommunicationStatus::Success)
{ {
auto Chars = CharResult.Characteristics(); auto Chars = CharResult.Characteristics();
for (uint32_t ci = 0; ci < Chars.Size(); ci++) for (uint32_t ci = 0; ci < Chars.Size(); ci++)
@ -368,17 +383,17 @@ bool UPS_BLE_Module::ConnectDevice(UPS_BLE_Device* Device)
ChItem.CharacteristicUUID = UPS_BLE_Device::GUIDToString(&cg); ChItem.CharacteristicUUID = UPS_BLE_Device::GUIDToString(&cg);
ChItem.CharacteristicName = ChItem.CharacteristicUUID; ChItem.CharacteristicName = ChItem.CharacteristicUUID;
// Map WinRT properties to our descriptor bits
auto Props = Ch.CharacteristicProperties(); auto Props = Ch.CharacteristicProperties();
uint8 Desc = 0; uint8 Desc = 0;
if ((Props & GattCharacteristicProperties::Broadcast) != GattCharacteristicProperties::None) Desc |= 0x01; using GP = WinGAP::GattCharacteristicProperties;
if ((Props & GattCharacteristicProperties::ExtendedProperties) != GattCharacteristicProperties::None) Desc |= 0x02; if ((Props & GP::Broadcast) != GP::None) Desc |= 0x01;
if ((Props & GattCharacteristicProperties::Notify) != GattCharacteristicProperties::None) Desc |= 0x04; if ((Props & GP::ExtendedProperties) != GP::None) Desc |= 0x02;
if ((Props & GattCharacteristicProperties::Indicate) != GattCharacteristicProperties::None) Desc |= 0x08; if ((Props & GP::Notify) != GP::None) Desc |= 0x04;
if ((Props & GattCharacteristicProperties::Read) != GattCharacteristicProperties::None) Desc |= 0x10; if ((Props & GP::Indicate) != GP::None) Desc |= 0x08;
if ((Props & GattCharacteristicProperties::Write) != GattCharacteristicProperties::None) Desc |= 0x20; if ((Props & GP::Read) != GP::None) Desc |= 0x10;
if ((Props & GattCharacteristicProperties::WriteWithoutResponse)!= GattCharacteristicProperties::None) Desc |= 0x40; if ((Props & GP::Write) != GP::None) Desc |= 0x20;
if ((Props & GattCharacteristicProperties::AuthenticatedSignedWrites)!= GattCharacteristicProperties::None) Desc |= 0x80; if ((Props & GP::WriteWithoutResponse) != GP::None) Desc |= 0x40;
if ((Props & GP::AuthenticatedSignedWrites) != GP::None) Desc |= 0x80;
ChItem.Descriptor = Desc; ChItem.Descriptor = Desc;
SvcItem.Characteristics.Add(ChItem); SvcItem.Characteristics.Add(ChItem);
@ -405,22 +420,18 @@ bool UPS_BLE_Module::ConnectDevice(UPS_BLE_Device* Device)
bool UPS_BLE_Module::DisconnectDevice(UPS_BLE_Device* Device) bool UPS_BLE_Module::DisconnectDevice(UPS_BLE_Device* Device)
{ {
#if PLATFORM_WINDOWS #if PLATFORM_WINDOWS
PS_BLE_WINRT_NS
if (!Device) return false; if (!Device) return false;
// Close native GATT service handles
for (void* SvcPtr : Device->NativeGattServices) for (void* SvcPtr : Device->NativeGattServices)
{ {
if (SvcPtr) if (SvcPtr) delete static_cast<FPS_GattServiceHandle*>(SvcPtr);
{
delete static_cast<GattDeviceService*>(SvcPtr);
}
} }
Device->NativeGattServices.Empty(); Device->NativeGattServices.Empty();
// Close device handle
if (Device->NativeDeviceHandle) if (Device->NativeDeviceHandle)
{ {
delete static_cast<BluetoothLEDevice*>(Device->NativeDeviceHandle); delete static_cast<FPS_BLEDeviceHandle*>(Device->NativeDeviceHandle);
Device->NativeDeviceHandle = nullptr; Device->NativeDeviceHandle = nullptr;
} }
return true; return true;
@ -432,11 +443,12 @@ bool UPS_BLE_Module::DisconnectDevice(UPS_BLE_Device* Device)
bool UPS_BLE_Module::IsDeviceConnected(UPS_BLE_Device* Device) bool UPS_BLE_Module::IsDeviceConnected(UPS_BLE_Device* Device)
{ {
#if PLATFORM_WINDOWS #if PLATFORM_WINDOWS
PS_BLE_WINRT_NS
if (!Device || !Device->NativeDeviceHandle) return false; if (!Device || !Device->NativeDeviceHandle) return false;
try try
{ {
BluetoothLEDevice* BLEDev = static_cast<BluetoothLEDevice*>(Device->NativeDeviceHandle); auto* Handle = static_cast<FPS_BLEDeviceHandle*>(Device->NativeDeviceHandle);
return BLEDev->ConnectionStatus() == BluetoothConnectionStatus::Connected; return Handle->Device.ConnectionStatus() == WinBT::BluetoothConnectionStatus::Connected;
} }
catch (...) {} catch (...) {}
#endif #endif
@ -450,26 +462,27 @@ bool UPS_BLE_Module::IsDeviceConnected(UPS_BLE_Device* Device)
bool UPS_BLE_Module::ReadCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8 CI) bool UPS_BLE_Module::ReadCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8 CI)
{ {
#if PLATFORM_WINDOWS #if PLATFORM_WINDOWS
if (!Device || SI >= Device->NativeGattServices.Num()) return false; if (!Device || SI >= (uint8)Device->NativeGattServices.Num()) return false;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI]() AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI]()
{ {
PS_BLE_WINRT_NS
try try
{ {
GattDeviceService* Svc = static_cast<GattDeviceService*>(Device->NativeGattServices[SI]); auto* SvcH = static_cast<FPS_GattServiceHandle*>(Device->NativeGattServices[SI]);
auto Chars = Svc->GetCharacteristicsAsync(BluetoothCacheMode::Cached).get(); auto Chars = SvcH->Service.GetCharacteristicsAsync(WinBT::BluetoothCacheMode::Cached).get();
if (Chars.Status() != GattCommunicationStatus::Success) return; if (Chars.Status() != WinGAP::GattCommunicationStatus::Success) return;
auto Ch = Chars.Characteristics().GetAt(CI); auto Ch = Chars.Characteristics().GetAt(CI);
auto Result = Ch.ReadValueAsync(BluetoothCacheMode::Uncached).get(); auto Result = Ch.ReadValueAsync(WinBT::BluetoothCacheMode::Uncached).get();
EPS_GATTStatus Status = (Result.Status() == GattCommunicationStatus::Success) EPS_GATTStatus Status = (Result.Status() == WinGAP::GattCommunicationStatus::Success)
? EPS_GATTStatus::Success : EPS_GATTStatus::Failure; ? EPS_GATTStatus::Success : EPS_GATTStatus::Failure;
TArray<uint8> Data; TArray<uint8> Data;
if (Result.Status() == GattCommunicationStatus::Success) if (Result.Status() == WinGAP::GattCommunicationStatus::Success)
{ {
auto Reader = DataReader::FromBuffer(Result.Value()); auto Reader = WinStr::DataReader::FromBuffer(Result.Value());
Data.SetNumUninitialized(Reader.UnconsumedBufferLength()); Data.SetNumUninitialized(Reader.UnconsumedBufferLength());
for (uint8& B : Data) B = Reader.ReadByte(); for (uint8& B : Data) B = Reader.ReadByte();
} }
@ -487,26 +500,26 @@ bool UPS_BLE_Module::ReadCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8
bool UPS_BLE_Module::WriteCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8 CI, const TArray<uint8>& Data) bool UPS_BLE_Module::WriteCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8 CI, const TArray<uint8>& Data)
{ {
#if PLATFORM_WINDOWS #if PLATFORM_WINDOWS
if (!Device || SI >= Device->NativeGattServices.Num()) return false; if (!Device || SI >= (uint8)Device->NativeGattServices.Num()) return false;
TArray<uint8> DataCopy = Data; TArray<uint8> DataCopy = Data;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI, DataCopy]() AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI, DataCopy]()
{ {
PS_BLE_WINRT_NS
try try
{ {
GattDeviceService* Svc = static_cast<GattDeviceService*>(Device->NativeGattServices[SI]); auto* SvcH = static_cast<FPS_GattServiceHandle*>(Device->NativeGattServices[SI]);
auto Chars = Svc->GetCharacteristicsAsync(BluetoothCacheMode::Cached).get(); auto Chars = SvcH->Service.GetCharacteristicsAsync(WinBT::BluetoothCacheMode::Cached).get();
if (Chars.Status() != GattCommunicationStatus::Success) return; if (Chars.Status() != WinGAP::GattCommunicationStatus::Success) return;
auto Ch = Chars.Characteristics().GetAt(CI); auto Ch = Chars.Characteristics().GetAt(CI);
auto Writer = WinStr::DataWriter();
auto Writer = DataWriter();
for (uint8 B : DataCopy) Writer.WriteByte(B); for (uint8 B : DataCopy) Writer.WriteByte(B);
auto Status = Ch.WriteValueAsync(Writer.DetachBuffer(), auto WriteStatus = Ch.WriteValueAsync(Writer.DetachBuffer(),
GattWriteOption::WriteWithResponse).get(); WinGAP::GattWriteOption::WriteWithResponse).get();
EPS_GATTStatus GattStatus = (Status == GattCommunicationStatus::Success) EPS_GATTStatus GattStatus = (WriteStatus == WinGAP::GattCommunicationStatus::Success)
? EPS_GATTStatus::Success : EPS_GATTStatus::Failure; ? EPS_GATTStatus::Success : EPS_GATTStatus::Failure;
DispatchWrite(Device, SI, CI, GattStatus); DispatchWrite(Device, SI, CI, GattStatus);
@ -522,39 +535,39 @@ bool UPS_BLE_Module::WriteCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8
bool UPS_BLE_Module::SubscribeCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8 CI) bool UPS_BLE_Module::SubscribeCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8 CI)
{ {
#if PLATFORM_WINDOWS #if PLATFORM_WINDOWS
if (!Device || SI >= Device->NativeGattServices.Num()) return false; if (!Device || SI >= (uint8)Device->NativeGattServices.Num()) return false;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI]() AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI]()
{ {
PS_BLE_WINRT_NS
try try
{ {
GattDeviceService* Svc = static_cast<GattDeviceService*>(Device->NativeGattServices[SI]); auto* SvcH = static_cast<FPS_GattServiceHandle*>(Device->NativeGattServices[SI]);
auto Chars = Svc->GetCharacteristicsAsync(BluetoothCacheMode::Cached).get(); auto Chars = SvcH->Service.GetCharacteristicsAsync(WinBT::BluetoothCacheMode::Cached).get();
if (Chars.Status() != GattCommunicationStatus::Success) return; if (Chars.Status() != WinGAP::GattCommunicationStatus::Success) return;
auto Ch = Chars.Characteristics().GetAt(CI); auto Ch = Chars.Characteristics().GetAt(CI);
// Write CCCD to enable notifications
auto WriteStatus = Ch.WriteClientCharacteristicConfigurationDescriptorAsync( auto WriteStatus = Ch.WriteClientCharacteristicConfigurationDescriptorAsync(
GattClientCharacteristicConfigurationDescriptorValue::Notify).get(); WinGAP::GattClientCharacteristicConfigurationDescriptorValue::Notify).get();
EPS_GATTStatus GattStatus = (WriteStatus == GattCommunicationStatus::Success) EPS_GATTStatus GattStatus = (WriteStatus == WinGAP::GattCommunicationStatus::Success)
? EPS_GATTStatus::Success : EPS_GATTStatus::Failure; ? EPS_GATTStatus::Success : EPS_GATTStatus::Failure;
if (WriteStatus == GattCommunicationStatus::Success) if (WriteStatus == WinGAP::GattCommunicationStatus::Success)
{ {
// Register value-changed callback
auto Token = Ch.ValueChanged( auto Token = Ch.ValueChanged(
[Device, SI, CI](GattCharacteristic const&, GattValueChangedEventArgs const& Args) [Device, SI, CI](WinGAP::GattCharacteristic const&,
WinGAP::GattValueChangedEventArgs const& Args)
{ {
auto Reader = DataReader::FromBuffer(Args.CharacteristicValue()); PS_BLE_WINRT_NS
auto Reader = WinStr::DataReader::FromBuffer(Args.CharacteristicValue());
TArray<uint8> Data; TArray<uint8> Data;
Data.SetNumUninitialized(Reader.UnconsumedBufferLength()); Data.SetNumUninitialized(Reader.UnconsumedBufferLength());
for (uint8& B : Data) B = Reader.ReadByte(); for (uint8& B : Data) B = Reader.ReadByte();
DispatchNotify(Device, SI, CI, EPS_GATTStatus::Success, MoveTemp(Data)); DispatchNotify(Device, SI, CI, EPS_GATTStatus::Success, MoveTemp(Data));
}); });
// Store token (key = packed SI<<8|CI)
uint64 Key = ((uint64)SI << 8) | CI; uint64 Key = ((uint64)SI << 8) | CI;
Device->NotifyTokens.Add(Key, new winrt::event_token(Token)); Device->NotifyTokens.Add(Key, new winrt::event_token(Token));
} }
@ -572,19 +585,19 @@ bool UPS_BLE_Module::SubscribeCharacteristic(UPS_BLE_Device* Device, uint8 SI, u
bool UPS_BLE_Module::UnsubscribeCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8 CI) bool UPS_BLE_Module::UnsubscribeCharacteristic(UPS_BLE_Device* Device, uint8 SI, uint8 CI)
{ {
#if PLATFORM_WINDOWS #if PLATFORM_WINDOWS
if (!Device || SI >= Device->NativeGattServices.Num()) return false; if (!Device || SI >= (uint8)Device->NativeGattServices.Num()) return false;
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI]() AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [Device, SI, CI]()
{ {
PS_BLE_WINRT_NS
try try
{ {
GattDeviceService* Svc = static_cast<GattDeviceService*>(Device->NativeGattServices[SI]); auto* SvcH = static_cast<FPS_GattServiceHandle*>(Device->NativeGattServices[SI]);
auto Chars = Svc->GetCharacteristicsAsync(BluetoothCacheMode::Cached).get(); auto Chars = SvcH->Service.GetCharacteristicsAsync(WinBT::BluetoothCacheMode::Cached).get();
if (Chars.Status() != GattCommunicationStatus::Success) return; if (Chars.Status() != WinGAP::GattCommunicationStatus::Success) return;
auto Ch = Chars.Characteristics().GetAt(CI); auto Ch = Chars.Characteristics().GetAt(CI);
// Remove event token
uint64 Key = ((uint64)SI << 8) | CI; uint64 Key = ((uint64)SI << 8) | CI;
if (winrt::event_token** TokenPtr = reinterpret_cast<winrt::event_token**>(Device->NotifyTokens.Find(Key))) if (winrt::event_token** TokenPtr = reinterpret_cast<winrt::event_token**>(Device->NotifyTokens.Find(Key)))
{ {
@ -593,11 +606,10 @@ bool UPS_BLE_Module::UnsubscribeCharacteristic(UPS_BLE_Device* Device, uint8 SI,
Device->NotifyTokens.Remove(Key); Device->NotifyTokens.Remove(Key);
} }
// Write CCCD to disable notifications
auto WriteStatus = Ch.WriteClientCharacteristicConfigurationDescriptorAsync( auto WriteStatus = Ch.WriteClientCharacteristicConfigurationDescriptorAsync(
GattClientCharacteristicConfigurationDescriptorValue::None).get(); WinGAP::GattClientCharacteristicConfigurationDescriptorValue::None).get();
EPS_GATTStatus GattStatus = (WriteStatus == GattCommunicationStatus::Success) EPS_GATTStatus GattStatus = (WriteStatus == WinGAP::GattCommunicationStatus::Success)
? EPS_GATTStatus::Success : EPS_GATTStatus::Failure; ? EPS_GATTStatus::Success : EPS_GATTStatus::Failure;
DispatchUnsubscribe(Device, SI, CI, GattStatus); DispatchUnsubscribe(Device, SI, CI, GattStatus);
@ -633,22 +645,55 @@ void UPS_BLE_Module::DispatchDiscoveryEnd(UPS_BLE_Manager* Mgr, const TArray<FPS
void UPS_BLE_Module::DispatchDeviceConnected(UPS_BLE_Device* Dev) void UPS_BLE_Module::DispatchDeviceConnected(UPS_BLE_Device* Dev)
{ {
if (!Dev) return; if (!Dev) return;
if (IsInGameThread()) { Dev->RefToManager->JustConnectedDevice(Dev); Dev->OnConnect.Broadcast(Dev, Dev->ActiveServices); } if (IsInGameThread())
else { AsyncTask(ENamedThreads::GameThread, [Dev]() { Dev->RefToManager->JustConnectedDevice(Dev); Dev->OnConnect.Broadcast(Dev, Dev->ActiveServices); }); } {
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) void UPS_BLE_Module::DispatchDeviceDisconnected(UPS_BLE_Device* Dev)
{ {
if (!Dev) return; if (!Dev) return;
if (IsInGameThread()) { Dev->RefToManager->JustDisconnectedDevice(Dev); Dev->OnDisconnect.Broadcast(Dev); } if (IsInGameThread())
else { AsyncTask(ENamedThreads::GameThread, [Dev]() { Dev->RefToManager->JustDisconnectedDevice(Dev); Dev->OnDisconnect.Broadcast(Dev); }); } {
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) void UPS_BLE_Module::DispatchServicesDiscovered(UPS_BLE_Device* Dev)
{ {
if (!Dev) return; if (!Dev) return;
if (IsInGameThread()) { Dev->RefToManager->JustDiscoveredServices(Dev); Dev->OnServicesDiscovered.Broadcast(Dev, Dev->ActiveServices); } if (IsInGameThread())
else { AsyncTask(ENamedThreads::GameThread, [Dev]() { Dev->RefToManager->JustDiscoveredServices(Dev); Dev->OnServicesDiscovered.Broadcast(Dev, Dev->ActiveServices); }); } {
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) void UPS_BLE_Module::DispatchRead(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_GATTStatus Status, TArray<uint8> Data)
@ -657,14 +702,16 @@ void UPS_BLE_Module::DispatchRead(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_G
if (IsInGameThread()) if (IsInGameThread())
{ {
if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num()) 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); Dev->OnRead.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID,
Dev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID, Data);
} }
else else
{ {
AsyncTask(ENamedThreads::GameThread, [Dev, SI, CI, Status, Data]() AsyncTask(ENamedThreads::GameThread, [Dev, SI, CI, Status, Data]()
{ {
if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num()) 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); Dev->OnRead.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID,
Dev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID, Data);
}); });
} }
} }
@ -675,14 +722,16 @@ void UPS_BLE_Module::DispatchNotify(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS
if (IsInGameThread()) if (IsInGameThread())
{ {
if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num()) 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); Dev->OnNotify.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID,
Dev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID, Data);
} }
else else
{ {
AsyncTask(ENamedThreads::GameThread, [Dev, SI, CI, Status, Data]() AsyncTask(ENamedThreads::GameThread, [Dev, SI, CI, Status, Data]()
{ {
if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num()) 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); Dev->OnNotify.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID,
Dev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID, Data);
}); });
} }
} }
@ -693,7 +742,8 @@ void UPS_BLE_Module::DispatchWrite(UPS_BLE_Device* Dev, uint8 SI, uint8 CI, EPS_
auto Fire = [Dev, SI, CI, Status]() auto Fire = [Dev, SI, CI, Status]()
{ {
if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num()) 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); Dev->OnWrite.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID,
Dev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID);
}; };
if (IsInGameThread()) Fire(); if (IsInGameThread()) Fire();
else AsyncTask(ENamedThreads::GameThread, Fire); else AsyncTask(ENamedThreads::GameThread, Fire);
@ -707,7 +757,8 @@ void UPS_BLE_Module::DispatchSubscribe(UPS_BLE_Device* Dev, uint8 SI, uint8 CI,
if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num()) if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num())
{ {
Dev->ActiveServices[SI].Characteristics[CI].subscribed = true; Dev->ActiveServices[SI].Characteristics[CI].subscribed = true;
Dev->OnSubscribe.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID, Dev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID); Dev->OnSubscribe.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID,
Dev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID);
} }
}; };
if (IsInGameThread()) Fire(); if (IsInGameThread()) Fire();
@ -722,7 +773,8 @@ void UPS_BLE_Module::DispatchUnsubscribe(UPS_BLE_Device* Dev, uint8 SI, uint8 CI
if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num()) if (SI < Dev->ActiveServices.Num() && CI < Dev->ActiveServices[SI].Characteristics.Num())
{ {
Dev->ActiveServices[SI].Characteristics[CI].subscribed = false; Dev->ActiveServices[SI].Characteristics[CI].subscribed = false;
Dev->OnUnsubscribe.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID, Dev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID); Dev->OnUnsubscribe.Broadcast(Status, Dev, Dev->ActiveServices[SI].ServiceUUID,
Dev->ActiveServices[SI].Characteristics[CI].CharacteristicUUID);
} }
}; };
if (IsInGameThread()) Fire(); if (IsInGameThread()) Fire();

View File

@ -9,15 +9,6 @@
class UPS_BLE_Device; class UPS_BLE_Device;
class UPS_BLE_Manager; 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 ─────────────────────────────────────────────────────────────────── // ─── Module ───────────────────────────────────────────────────────────────────
class UPS_BLE_Module : public IModuleInterface class UPS_BLE_Module : public IModuleInterface
@ -48,9 +39,9 @@ public:
bool UnsubscribeCharacteristic(UPS_BLE_Device* Device, uint8 ServiceIndex, uint8 CharIndex); bool UnsubscribeCharacteristic(UPS_BLE_Device* Device, uint8 ServiceIndex, uint8 CharIndex);
UPS_BLE_Manager* LocalBLEManager = nullptr; UPS_BLE_Manager* LocalBLEManager = nullptr;
bool bInitialized = false;
private: private:
bool bInitialized = false;
// WinRT scanner (opaque handle — implementation in .cpp using WinRT types) // WinRT scanner (opaque handle — implementation in .cpp using WinRT types)
void* ScannerHandle = nullptr; void* ScannerHandle = nullptr;

View File

@ -97,6 +97,18 @@ public:
UPROPERTY(BlueprintReadOnly, Category = "PS BLE") TArray<FPS_CharacteristicItem> Characteristics; UPROPERTY(BlueprintReadOnly, Category = "PS BLE") TArray<FPS_CharacteristicItem> Characteristics;
}; };
// ─── INTERNAL DEVICE RECORD (forward-declared here so Manager can use it) ────
// This is a plain C++ struct (no UCLASS/USTRUCT) used internally between
// the module and the manager — not exposed to Blueprint.
struct FPS_DeviceRecord
{
uint64 ID = 0;
int64 RSSI = 0;
FString Name;
void* NativeDeviceRef = nullptr;
};
// ─── FORWARD DECLARATIONS ──────────────────────────────────────────────────── // ─── FORWARD DECLARATIONS ────────────────────────────────────────────────────
class UPS_BLE_Device; class UPS_BLE_Device;