BSOne.SFC/Tocsg.Lib/VCL/Tocsg.Bluetooth.pas

668 lines
19 KiB
Plaintext

{*******************************************************}
{ }
{ Tocsg.Bluetooth }
{ }
{ Copyright (C) 2022 kku }
{ }
{*******************************************************}
unit Tocsg.Bluetooth;
interface
uses
EM.jwabluetoothapis, Tocsg.Obj, Tocsg.Thread, System.Classes,
System.SysUtils, Winapi.Windows, System.Generics.Collections;
const
BT_DEVICE_CLASS_MISC = $0;
BT_DEVICE_CLASS_COMPUTER = $1;
BT_DEVICE_CLASS_PHONE = $2;
BT_DEVICE_CLASS_LANACCESSPOINT = $3;
BT_DEVICE_CLASS_AV = $4;
BT_DEVICE_CLASS_PERIPHERAL = $5;
BT_DEVICE_CLASS_IMAGING = $6;
BT_DEVICE_CLASS_UNCLASSIFIED = $1F;
type
PBtDevEnt = ^TBtDevEnt;
TBtDevEnt = record
sAddress: String;
dtLastSeen,
dtLastUsed: TDateTime;
dInfo: BLUETOOTH_DEVICE_INFO;
end;
TBluetoothDevice = class(TTgObject)
protected
BTDeviceList_: TList<PBtDevEnt>;
procedure OnBTDeviceNotify(Sender: TObject; const Item: PBtDevEnt;
Action: TCollectionNotification);
function GetCount: Integer;
function GetBTDeviceByIndex(nIndex: Integer): PBtDevEnt;
public
Constructor Create;
Destructor Destroy; override;
function RefreshBTDevice(pbWorkStop: PBoolean = nil): Boolean;
property Count: Integer read GetCount;
property Items[nIndex: Integer]: PBtDevEnt read GetBTDeviceByIndex; default;
end;
PBtRdiEnt = ^TBtRdiEnt;
TBtRdiEnt = record
sAddress: String;
dInfo: BLUETOOTH_RADIO_INFO;
end;
TBluetoothRadio = class(TTgObject)
protected
BTRadioList_: TList<PBtRdiEnt>;
procedure OnBTRadioNotify(Sender: TObject; const Item: PBtRdiEnt;
Action: TCollectionNotification);
function GetCount: Integer;
function GetBTRadioByIndex(nIndex: Integer): PBtRdiEnt;
public
Constructor Create;
Destructor Destroy; override;
function RefreshBTRadio: Boolean;
property Count: Integer read GetCount;
property Items[nIndex: Integer]: PBtRdiEnt read GetBTRadioByIndex; default;
end;
TBTChangeState = (csDetection, csConnected, csRemembered, csAuthenticated, csLastSeen, csLastUsed);
TBTChangeStates = set of TBTChangeState;
TBtDevChangeNotify = procedure(pEnt: PBtDevEnt; csBT: TBTChangeStates; var bPrevent: Boolean) of object;
TThdBtDevNotify = class(TTgThread)
private
bSync_: Boolean;
BTDevice_: TBluetoothDevice;
DcBTDevice_: TDictionary<String,PBtDevEnt>;
procedure OnBTDeviceNotify(Sender: TObject; const Item: PBtDevEnt;
Action: TCollectionNotification);
protected
evChangeBTDevice_: TBtDevChangeNotify;
pBTEntry_: PBtDevEnt;
csBT_: TBTChangeStates;
bPreventBtDevs_: Boolean;
function GetBTDeviceState(aEnt: PBtDevEnt): TBTChangeStates;
procedure Execute; override;
procedure ProcessBTDeviceNotify;
public
Constructor Create(bSync: Boolean = false);
Destructor Destroy; override;
procedure ResetBTDevice;
property OnChangeBTDevice: TBtDevChangeNotify write evChangeBTDevice_;
property PreventBtDevs: Boolean write bPreventBtDevs_;
end;
procedure BtDevTypeToStr(dwClassOfDevice: DWORD; var sMajor, sMinor: String);
function SetBtDevsEnable(bVal: Boolean): Integer;
implementation
uses
System.DateUtils, Tocsg.DateTime, Tocsg.Strings, Tocsg.Driver, Tocsg.Convert,
Tocsg.Exception;
function SetBtDevsEnable(bVal: Boolean): Integer;
var
hDev: HDEVINFO;
sdd: TSPDevInfoData;
i: Integer;
dwBufSize,
dwStatus, dwProblem,
dwPropertyRegDataType: DWORD;
pBuf: Pointer;
begin
Result := 0;
try
hDev := SetupDiGetClassDevs(@GUID_DEVCLASS_BLUETOOTH, nil, 0, DIGCF_PRESENT);
if hDev = INVALID_HANDLE_VALUE then
exit;
pBuf := nil;
try
ZeroMemory(@sdd, SizeOf(sdd));
sdd.cbSize := SizeOf(sdd);
i := 0;
while SetupDiEnumDeviceInfo(hDev, i, sdd) do
begin
dwBufSize := 0;
if pBuf <> nil then
begin
FreeMem(pBuf);
pBuf := nil;
end;
while not SetupDiGetDeviceRegistryProperty(hDev, sdd,
SPDRP_HARDWAREID, dwPropertyRegDataType, pBuf, dwBufSize, dwBufSize) do
begin
if GetLastError = ERROR_INSUFFICIENT_BUFFER then
begin
if pBuf <> nil then
FreeMem(pBuf);
pBuf := AllocMem(dwBufSize);
end else break;
end;
if pBuf <> nil then
begin
if (CompareText('BTH\MS_BTHLE', String(PChar(pBuf))) <> 0) and
(CompareText('BTH\MS_BTHBRB', String(PChar(pBuf))) <> 0) then
begin
dwStatus := 0;
dwProblem := 0;
if CM_Get_DevNode_Status(dwStatus, dwProblem, sdd.DevInst, 0) = CR_SUCCESS then
begin
// var bDisabled: Boolean := (((dwStatus and DN_HAS_PROBLEM) = 0) and (dwProblem = CM_PROB_DISABLED));
// if bVal = bDisabled then
begin
var PropChangeParams: TSPPropChangeParams;
ZeroMemory(@PropChangeParams, SizeOf(PropChangeParams));
PropChangeParams.ClassInstallHeader.cbSize := SizeOf(TSPClassInstallHeader);
PropChangeParams.ClassInstallHeader.InstallFunction := DIF_PROPERTYCHANGE;
PropChangeParams.Scope := DICS_FLAG_GLOBAL;
PropChangeParams.StateChange := BooleanToInt(bVal, DICS_ENABLE, DICS_DISABLE);
if SetupDiSetClassInstallParams(hDev, @sdd, PSPClassInstallHeader(@PropChangeParams), SizeOf(PropChangeParams)) then
begin
// 64bit OS 에서는 64bit 프로그램 에서만 실행 가능하다 22_0621 13:59:21 kku
if SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, hDev, @sdd) then
Inc(Result);
end;
end;
end;
end;
end;
Inc(i);
end;
finally
SetupDiDestroyDeviceInfoList(hDev);
if pBuf <> nil then
FreeMem(pBuf);
end;
except
on E: Exception do
ETgException.TraceException(E, 'Fail .. SetBtDevsEnable()');
end;
end;
{ TBluetoothDevice }
Constructor TBluetoothDevice.Create;
begin
Inherited Create;
BTDeviceList_ := TList<PBtDevEnt>.Create;
BTDeviceList_.OnNotify := OnBTDeviceNotify;
end;
Destructor TBluetoothDevice.Destroy;
begin
FreeAndNil(BTDeviceList_);
Inherited;
end;
procedure TBluetoothDevice.OnBTDeviceNotify(Sender: TObject; const Item: PBtDevEnt;
Action: TCollectionNotification);
begin
case Action of
cnAdded: ;
cnRemoved: Dispose(Item);
cnExtracted: ;
end;
end;
function TBluetoothDevice.GetCount: Integer;
begin
Result := BTDeviceList_.Count;
end;
function TBluetoothDevice.GetBTDeviceByIndex(nIndex: Integer): PBtDevEnt;
begin
Result := nil;
if (nIndex >= 0) and (nIndex < BTDeviceList_.Count) then
Result := BTDeviceList_[nIndex];
end;
function TBluetoothDevice.RefreshBTDevice(pbWorkStop: PBoolean = nil): Boolean;
var
hFind: HBLUETOOTH_DEVICE_FIND;
BtDevSchParam: BLUETOOTH_DEVICE_SEARCH_PARAMS;
BtDevInfo: BLUETOOTH_DEVICE_INFO;
pInfo: PBtDevEnt;
begin
Result := false;
BTDeviceList_.Clear;
ZeroMemory(@BtDevSchParam, SizeOf(BtDevSchParam));
BtDevSchParam.dwSize := SizeOf(BtDevSchParam);
BtDevSchParam.fReturnAuthenticated := true;
BtDevSchParam.fReturnRemembered := true;
BtDevSchParam.fReturnUnknown := true;
BtDevSchParam.fReturnConnected := true;
// BtDevSchParam.fIssueInquiry := true;
// BtDevSchParam.cTimeoutMultiplier := 10;
ZeroMemory(@BtDevInfo, SizeOf(BtDevInfo));
BtDevInfo.dwSize := SizeOf(BtDevInfo);
hFind := BluetoothFindFirstDevice(BtDevSchParam, BtDevInfo);
try
if hFind <> 0 then
begin
repeat
New(pInfo);
ZeroMemory(pInfo, SizeOf(TBtDevEnt));
BluetoothUpdateDeviceRecord(BtDevInfo);
pInfo.dInfo := BtDevInfo;
pInfo.sAddress := Format('%.2x:%.2x:%.2x:%.2x:%.2x:%.2x',
[BtDevInfo.Address.rgBytes[5], BtDevInfo.Address.rgBytes[4],
BtDevInfo.Address.rgBytes[3], BtDevInfo.Address.rgBytes[2],
BtDevInfo.Address.rgBytes[1], BtDevInfo.Address.rgBytes[0]]);
pInfo.dtLastSeen := ConvSystemTimeToDateTime_Local(BtDevInfo.stLastSeen);
try
if BtDevInfo.stLastUsed.wYear <> 0 then
pInfo.dtLastUsed := ConvSystemTimeToDateTime_Local(BtDevInfo.stLastUsed)
else
pInfo.dtLastUsed := 0;
except
pInfo.dtLastUsed := 0;
end;
BTDeviceList_.Add(pInfo);
if (pbWorkStop <> nil) and (pbWorkStop^ = true) then
exit;
until (hFind <> 0) and not BluetoothFindNextDevice(hFind, BtDevInfo);
Result := true;
end;
finally
if hFind <> 0 then
begin
BluetoothFindDeviceClose(hFind);
end;
end;
end;
{ TBluetoothRadio }
Constructor TBluetoothRadio.Create;
begin
BTRadioList_ := TList<PBtRdiEnt>.Create;
BTRadioList_.OnNotify := OnBTRadioNotify;
end;
Destructor TBluetoothRadio.Destroy;
begin
FreeAndNil(BTRadioList_);
Inherited;
end;
procedure TBluetoothRadio.OnBTRadioNotify(Sender: TObject; const Item: PBtRdiEnt;
Action: TCollectionNotification);
begin
if Action = cnRemoved then
Dispose(Item);
end;
function TBluetoothRadio.GetCount: Integer;
begin
Result := BTRadioList_.Count;
end;
function TBluetoothRadio.GetBTRadioByIndex(nIndex: Integer): PBtRdiEnt;
begin
Result := nil;
if (nIndex >= 0) and (nIndex < BTRadioList_.Count) then
Result := BTRadioList_[nIndex];
end;
function TBluetoothRadio.RefreshBTRadio: Boolean;
var
hFind: HBLUETOOTH_RADIO_FIND;
hRadio: THandle;
BtRadiFindParam: BLUETOOTH_FIND_RADIO_PARAMS;
BtRadiInfo: BLUETOOTH_RADIO_INFO;
pInfo: PBtRdiEnt;
begin
Result := false;
BTRadioList_.Clear;
ZeroMemory(@BtRadiFindParam, SizeOf(BtRadiFindParam));
BtRadiFindParam.dwSize := SizeOf(BtRadiFindParam);
ZeroMemory(@BtRadiInfo, SizeOf(BtRadiInfo));
BtRadiInfo.dwSize := SizeOf(BtRadiInfo);
hRadio := 0;
hFind := BluetoothFindFirstRadio(@BtRadiFindParam, hRadio);
try
if hFind <> 0 then
begin
repeat
if BluetoothGetRadioInfo(hRadio, BtRadiInfo) = 0 then
begin
New(pInfo);
ZeroMemory(pInfo, SizeOf(TBtDevEnt));
pInfo.dInfo := BtRadiInfo;
pInfo.sAddress := Format('%.2x:%.2x:%.2x:%.2x:%.2x:%.2x',
[BtRadiInfo.Address.rgBytes[5], BtRadiInfo.Address.rgBytes[4],
BtRadiInfo.Address.rgBytes[3], BtRadiInfo.Address.rgBytes[2],
BtRadiInfo.Address.rgBytes[1], BtRadiInfo.Address.rgBytes[0]]);
BTRadioList_.Add(pInfo);
end;
until (hFind <> 0) and not BluetoothFindNextRadio(hFind, hRadio);
Result := true;
end;
finally
if hFind <> 0 then
begin
BluetoothFindRadioClose(hFind);
end;
end;
end;
{ TThdBtDevNotify }
Constructor TThdBtDevNotify.Create(bSync: Boolean = false);
begin
Inherited Create;
bSync_ := bSync;
DcBTDevice_ := TDictionary<String,PBtDevEnt>.Create;
DcBTDevice_.OnValueNotify := OnBTDeviceNotify;
BTDevice_ := TBluetoothDevice.Create;
bPreventBtDevs_ := false;
end;
Destructor TThdBtDevNotify.Destroy;
begin
Inherited;
FreeAndNil(BTDevice_);
FreeAndNil(DcBTDevice_);
end;
procedure TThdBtDevNotify.OnBTDeviceNotify(Sender: TObject; const Item: PBtDevEnt;
Action: TCollectionNotification);
begin
case Action of
cnAdded: ;
cnRemoved: Dispose(Item);
cnExtracted: ;
end;
end;
procedure TThdBtDevNotify.ResetBTDevice;
begin
Lock;
try
DcBTDevice_.Clear;
finally
Unlock;
end;
end;
procedure TThdBtDevNotify.ProcessBTDeviceNotify;
var
bPrevent: Boolean;
begin
if Assigned(evChangeBTDevice_) then
begin
evChangeBTDevice_(pBTEntry_, csBT_, bPrevent);
if bPrevent then
bPreventBtDevs_ := true;
end;
end;
function TThdBtDevNotify.GetBTDeviceState(aEnt: PBtDevEnt): TBTChangeStates;
var
pEntry: PBtDevEnt;
begin
Result := [];
Lock;
try
if DcBTDevice_.ContainsKey(aEnt.sAddress) then
pEntry := DcBTDevice_[aEnt.sAddress]
else
pEntry := nil;
finally
Unlock;
end;
if pEntry <> nil then
begin
if pEntry.dInfo.fConnected <> aEnt.dInfo.fConnected then
Include(Result, csConnected);
if pEntry.dInfo.fRemembered <> aEnt.dInfo.fRemembered then
Include(Result, csRemembered);
if pEntry.dInfo.fAuthenticated <> aEnt.dInfo.fAuthenticated then
Include(Result, csAuthenticated);
// LastSeen은 BluetoothFindFirstDevice 이거 돌릴때마다 변경 12_1102 17:37 kku
// if not SameDateTime(pEntry.dtLastSeen, aEnt.dtLastSeen) then
// Include(Result, csLastSeen);
if not SameDateTime(pEntry.dtLastUsed, aEnt.dtLastUsed) then
Include(Result, csLastUsed);
pEntry^ := aEnt^;
end else begin
New(pEntry);
pEntry^ := aEnt^;
Lock;
try
DcBTDevice_.Add(pEntry.sAddress, pEntry);
finally
Unlock;
end;
Include(Result, csDetection);
// if pEntry.dInfo.fConnected then
// Include(Result, csConnected);
// if pEntry.dInfo.fRemembered then
// Include(Result, csRemembered);
// if pEntry.dInfo.fAuthenticated then
// Include(Result, csAuthenticated);
end;
end;
procedure TThdBtDevNotify.Execute;
var
i: Integer;
dwPvTick: DWORD;
begin
dwPvTick := 0;
while not Terminated and not GetWorkStop do
begin
if BTDevice_.RefreshBTDevice(@bWorkStop_) then
for i := 0 to BTDevice_.Count - 1 do
begin
if bWorkStop_ then
break;
pBTEntry_ := BTDevice_[i];
csBT_ := GetBTDeviceState(pBTEntry_);
if csBT_ <> [] then
begin
if bSync_ then
Synchronize(ProcessBTDeviceNotify)
else
ProcessBTDeviceNotify;
end;
end
else Sleep(1000);
if bPreventBtDevs_ then
begin
bPreventBtDevs_ := false;
SetBtDevsEnable(false);
dwPvTick := GetTickCount;
end;
if (dwPvTick <> 0) and ((GetTickCount - dwPvTick) >= 4000) then
begin
dwPvTick := 0;
SetBtDevsEnable(true);
end;
Sleep(1000);
end;
end;
// 디바이스 타입 구하기 = 개노가닥 2010-12-16 kku
// 참고 : http://slexy.org/view/s2vpOPLzA7
procedure BtDevTypeToStr(dwClassOfDevice: DWORD; var sMajor, sMinor: String);
var
ucMajor,
ucMinor: BYTE;
begin
ucMajor := BYTE((dwClassOfDevice and $0000FF00) shr 8);
ucMinor := BYTE(dwClassOfDevice and $000000FF);
case ucMajor of
BT_DEVICE_CLASS_MISC : sMajor := 'Misc';
BT_DEVICE_CLASS_COMPUTER : sMajor := 'Computer';
BT_DEVICE_CLASS_PHONE : sMajor := 'Phone';
BT_DEVICE_CLASS_LANACCESSPOINT : sMajor := 'LanAccessPoint';
BT_DEVICE_CLASS_AV : sMajor := 'Audio/Video';
BT_DEVICE_CLASS_PERIPHERAL : sMajor := 'Peripheral';
BT_DEVICE_CLASS_IMAGING : sMajor := 'Imaging';
BT_DEVICE_CLASS_UNCLASSIFIED : sMajor := 'Unclassified';
end;
case (ucMajor and $0F) of
0 : sMinor := 'None';
1 :
case ((ucMinor and $0F) shr 2) of
0 : sMinor := 'Uncategorised';
1 : sMinor := 'Desktop';
2 : sMinor := 'Server';
3 : sMinor := 'Laptop';
4 : sMinor := 'Handheld';
5 : sMinor := 'Palm';
6 : sMinor := 'Wearable';
end;
2 :
case ((ucMinor and $0F) shr 2) of
0 : sMinor := 'Uncategorised';
1 : sMinor := 'Mobile';
2 : sMinor := 'Cordless';
3 : sMinor := 'Smart phone';
4 : sMinor := 'Wired modem or voice gateway';
5 : sMinor := 'Common ISDN access';
6 : sMinor := 'Sim card reader';
end;
3 :
begin
if ((ucMinor and $0F) shr 2) = 0 then
sMinor := 'Uncategorised'
else
case (((ucMinor and $0F) shr 2) div 8) of
0 : sMinor := 'Fully available';
1 : sMinor := '1-17%% available';
2 : sMinor := '17-33%% utilised';
3 : sMinor := '33-50%% utilised';
4 : sMinor := '50-67%% utilised';
5 : sMinor := '67-83%% utilised';
6 : sMinor := '83-99%% utilised';
7 : sMinor := 'No service available';
end;
end;
4 :
case ((ucMinor and $0F) shr 2) of
0 : sMinor := 'Uncategorised';
1 : sMinor := 'Device conforms to the Headset profile';
2 : sMinor := 'Hands-free';
3 : sMinor := 'Reserved';
4 : sMinor := 'Microphone';
5 : sMinor := 'Loudspeaker';
6 : sMinor := 'Headphones';
7 : sMinor := 'Portable audio';
8 : sMinor := 'Car audio';
9 : sMinor := 'Set-top box';
10 : sMinor := 'HiFi audio device';
11 : sMinor := 'VCR';
12 : sMinor := 'Video camera';
13 : sMinor := 'Camcorder';
14 : sMinor := 'Video monitor';
15 : sMinor := 'Video display and loudspeaker';
16 : sMinor := 'Video conferencing';
17 : sMinor := 'Reserved';
18 : sMinor := 'Gaming/toy';
end;
5 :
begin
case (((ucMinor and $0F) shr 2) and 48) of
16 : sMinor := 'Keyboard';
32 : sMinor := 'Pointing device';
48 :
begin
sMinor := 'Combo keyboard/pointing device';
sMinor := sMinor + ' - ';
case (((ucMinor and $0F) shr 2) and 15) of
1 : sMinor := sMinor + 'Joystick';
2 : sMinor := sMinor + 'Gamepad';
3 : sMinor := sMinor + 'Remote control';
4 : sMinor := sMinor + 'Sensing device';
5 : sMinor := sMinor + 'Digitiser tablet';
6 : sMinor := sMinor + 'Card reader';
else
sMinor := sMinor + 'Reserved';
end;
end;
end;
end;
6 :
if (((ucMinor and $0F) shr 2) and 4) <> 0 then
sMinor := 'Display'
else if (((ucMinor and $0F) shr 2) and 8) <> 0 then
sMinor := 'Camera'
else if (((ucMinor and $0F) shr 2) and 16) <> 0 then
sMinor := 'Scanner'
else if (((ucMinor and $0F) shr 2) and 32) <> 0 then
sMinor := 'Printer';
7 :
case ((ucMinor and $0F) shr 2) of
1 : sMinor := 'Wrist watch';
2 : sMinor := 'Pager';
3 : sMinor := 'Jacket';
4 : sMinor := 'Helmet';
5 : sMinor := 'Glasses';
end;
8 :
case ((ucMinor and $0F) shr 2) of
1 : sMinor := 'Robot';
2 : sMinor := 'Vehicle';
3 : sMinor := 'Doll/action figure';
4 : sMinor := 'Controller';
5 : sMinor := 'Game';
end;
end;
if sMinor = '' then
sMinor := 'None';
end;
end.