Reputation: 103
For the last few days I have been unsuccessfully trying to understand the behavior of Delphi and its component for working with BLE. I'm not sure the problem is related to bluetooth or winrt, maybe i'm just missing something fundamental about delphi and multithreading.
In my application i create an instance of TBluetoothLE
, assign events for device discovery (OnDiscoverLEDevice
) and discovery end (OnEndDiscoverDevices
). Then i start discovery with BluetoothLE.DiscoverDevices(10000)
. As i understand, i can rely on that events are executed in main thread (and they really are, as i can see in System.Bluetooth.Components.pas
):
procedure TBluetoothLE.DoOnDiscoverLEDevice(const Sender: TObject; const ADevice: TBluetoothLEDevice; Rssi: Integer; const ScanResponse: TScanResponse);
begin
if FEnabled and Assigned(FOnDiscoverLEDevice) then
TThread.Synchronize(nil, procedure begin
if FEnabled and Assigned(FOnDiscoverLEDevice) then
FOnDiscoverLEDevice(Sender, ADevice, Rssi, ScanResponse);
end);
end;
procedure TBluetoothLE.DoOnEndDiscoverDevices(const Sender: TObject; const ADeviceList: TBluetoothLEDeviceList);
begin
if FEnabled and Assigned(FOnEndDiscoverDevices) then
TThread.Synchronize(nil, procedure begin
if FEnabled and Assigned(FOnEndDiscoverDevices) then
FOnEndDiscoverDevices(Sender, ADeviceList);
end);
end;
So, when i write code of handlers for discovery and its end, i can be sure that they will be executed only when application calls CheckSynchronize
somewhere in its main loop (or immediately, if TThread.Synchronize
is called in main thread). And handler execution can never be interrupted: when the programm entered my handler, it will safely (for the sake of visual components and application integrity) execute every line of the handler code.
But i stumbled across very strange behaviour, as if handlers execution is mixed up.
minimum, reproducible example
object MainForm: TMainForm
Left = 0
Top = 0
Caption = 'BLE Thread Issue'
ClientHeight = 316
ClientWidth = 444
Position = ScreenCenter
FormFactor.Width = 320
FormFactor.Height = 480
FormFactor.Devices = [Desktop]
OnCreate = FormCreate
DesignerMasterStyle = 0
object StartButton: TButton
Align = Top
Size.Width = 444.000000000000000000
Size.Height = 65.000000000000000000
Size.PlatformDefault = False
TabOrder = 1
Text = 'StartButton'
OnClick = StartButtonClick
end
object Memo: TMemo
Touch.InteractiveGestures = [Pan, LongTap, DoubleTap]
DataDetectorTypes = []
Align = Client
Size.Width = 444.000000000000000000
Size.Height = 251.000000000000000000
Size.PlatformDefault = False
TabOrder = 2
Viewport.Width = 440.000000000000000000
Viewport.Height = 247.000000000000000000
end
object BluetoothLE: TBluetoothLE
Enabled = True
OnDiscoverLEDevice = DeviceDiscovered
OnEndDiscoverDevices = DiscoveryFinished
Left = 24
Top = 8
end
end
unit MainUnit;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
System.Generics.Collections, System.RegularExpressions,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
System.Bluetooth, System.Bluetooth.Components, FMX.Controls.Presentation,
FMX.StdCtrls, FMX.Memo.Types, FMX.ScrollBox, FMX.Memo,
WinAPI.Winrt, WinAPI.WinRT.Utils;
type
TMainForm = class(TForm)
BluetoothLE: TBluetoothLE;
StartButton: TButton;
Memo: TMemo;
procedure DiscoveryFinished(const Sender: TObject; const ADeviceList: TBluetoothLEDeviceList);
procedure StartButtonClick(Sender: TObject);
procedure DeviceDiscovered(const Sender: TObject; const ADevice: TBluetoothLEDevice; Rssi: Integer; const ScanResponse: TScanResponse);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
FDiscoveredBluetoothLEDevices: TList<TBluetoothLEDevice>;
FStatusLabel: TLabel;
public
{ Public declarations }
end;
var
MainForm: TMainForm;
implementation
{$R *.fmx}
procedure TMainForm.DeviceDiscovered(const Sender: TObject; const ADevice: TBluetoothLEDevice; Rssi: Integer; const ScanResponse: TScanResponse);
var
LMark: Integer;
begin
LMark:=TThread.GetTickCount;
Memo.Lines.Add(Format('[%u] DeviceDiscovered_%u', [TThread.GetTickCount, LMark]));
if FStatusLabel=nil then Exit;
if not FDiscoveredBluetoothLEDevices.Contains(ADevice) then
if Pos(ADevice.DeviceName, '<my device significator>')<>0 then
FDiscoveredBluetoothLEDevices.Add(ADevice);
Memo.Lines.Add(Format('[%u] FStatusLabel_%u', [TThread.GetTickCount, LMark]));
FStatusLabel.Text:=FDiscoveredBluetoothLEDevices.Count.ToString;
end;
procedure TMainForm.DiscoveryFinished(const Sender: TObject; const ADeviceList: TBluetoothLEDeviceList);
begin
FreeAndNil(FStatusLabel);
Memo.Lines.Add(Format('[%u] DiscoveryFinished', [TThread.GetTickCount]));
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
TThread.NameThreadForDebugging('MainThread');
FDiscoveredBluetoothLEDevices:=TList<TBluetoothLEDevice>.Create;
end;
procedure TMainForm.StartButtonClick(Sender: TObject);
begin
FStatusLabel:=TLabel.Create(Self);
FStatusLabel.Align:=TAlignLayout.Bottom;
FStatusLabel.Parent:=Self;
Memo.Lines.Clear;
FDiscoveredBluetoothLEDevices.Clear;
BluetoothLE.DiscoverDevices(10000);
end;
end.
Sometimes everything works fine, but sometimes i got access violation in DeviceDiscovered
on FStatusLabel.Text:=FDiscoveredBluetoothLEDevices.Count.ToString;
because FStatusLabel is nil! I have no idea why is this possible, because FStatusLabel is checked against nil at the begining of the handler. The memo content is such:
Looks like as if the execution of DeviceDiscovered
was interrupted by DiscoveryFinished
, where TLabel was freed, and by the end of DeviceDiscovered
access violation happened. But isn't this insane? How could DiscoveryFinished
be fired without the programm finished processing current syncronized method (DeviceDiscovered
)?
UPDATE
The problem does not reproduce on VCL. Only on FMX.
UPDATE 2
Ok, it seems the mystery is unveiled. I have managed to get call stack of my application main thread:
Interestingly, that DiscoveryFinished
is executing while DeviceDiscovered
is upper in the stack! It seems that the reason for this is in CheckInitialized
call in blutooth device name getter (in the handler for DeviceDiscovered
i access this field). The getter is not simply returns stored string, but tries to perform some async operations:
procedure TWinRTBluetoothLEDevice.CheckInitialized;
var
LBLEDeviceAsyncOp: IAsyncOperation_1__IBluetoothLEDevice;
begin
if not Assigned(FBluetoothLEDevice) or FClosed then
begin
if FId = 0 then
begin
if TAsyncOperation<IAsyncOperation_1__IBluetoothLEDevice>.Wait(
TBluetoothLEDevice.Statics.FromBluetoothAddressAsync(FAddress), LBLEDeviceAsyncOp) = AsyncStatus.Completed then
begin
FBluetoothLEDevice := LBLEDeviceAsyncOp.GetResults;
FClosed := False;
if Assigned(FBluetoothLEDevice) then
begin
if DeviceName.IsEmpty then
FDeviceName := FBluetoothLEDevice.Name.ToString;
FConnectionStatusChangeDelegate := TConnectionStatusChangeEventHandler.Create(Self);
FBluetoothLEDevice.add_ConnectionStatusChanged(FConnectionStatusChangeDelegate);
end;
end;
end
else
if TAsyncOperation<IAsyncOperation_1__IBluetoothLEDevice>.Wait(
TBluetoothLEDevice.Statics.FromIdAsync(FId), LBLEDeviceAsyncOp) = AsyncStatus.Completed then
begin
FBluetoothLEDevice := LBLEDeviceAsyncOp.GetResults;
FClosed := False;
if DeviceName.IsEmpty then
FDeviceName := FBluetoothLEDevice.Name.ToString;
FConnectionStatusChangeDelegate := TConnectionStatusChangeEventHandler.Create(Self);
FBluetoothLEDevice.add_ConnectionStatusChanged(FConnectionStatusChangeDelegate);
end;
end;
end;
I dont quite understand these WinRT envelopes, but looks like they spawn threads, which at the some time (on destroing) call CheckSyncronize
. Since WinRT operation is async and original thread is running, DiscoveryFinished
gets into syncronization queue before that.
So, i should avoid accessing TBluetoothLEDevice.DeviceName
in event handlers. It reminds me traps of Application.ProcessMessages
...
Upvotes: 0
Views: 63