diff --git a/.editorconfig b/.editorconfig
index b6355c4..29df28b 100755
--- a/.editorconfig
+++ b/.editorconfig
@@ -11,7 +11,10 @@ trim_trailing_whitespace = true
[*.java]
ij_java_imports_layout = $*, |, javax.**, java.**, |, *
-[*.gradle]
+[*.swift]
+indent_size = 4
+
+[{*.gradle,*.kt,*.kts}]
indent_size = 4
[*.md]
diff --git a/EvvaAbrevvaCapacitor.podspec b/EvvaAbrevvaCapacitor.podspec
index 8d23584..8a3531e 100644
--- a/EvvaAbrevvaCapacitor.podspec
+++ b/EvvaAbrevvaCapacitor.podspec
@@ -21,5 +21,5 @@ TODO: Add long description of the pod here.
s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
s.dependency 'Capacitor'
- s.dependency 'AbrevvaSDK', '~> 1.1.0'
+ s.dependency 'AbrevvaSDK', '~> 3.0.1'
end
diff --git a/README.md b/README.md
index 032ac99..998845c 100644
--- a/README.md
+++ b/README.md
@@ -54,46 +54,130 @@ npx cap sync
import { AbrevvaBLEClient, ScanResult } from "@evva/abrevva-capacitor";
class ExampleClass {
- private results: ScanResult[];
+ private devices: BleDevice[];
async startScan(event: any) {
- this.results = [];
-
- await AbrevvaBLEClient.requestLEScan({ timeout: 5_000 }, (result: ScanResult) => {
- this.results.push(result);
+ this.devices = [];
+
+ await AbrevvaBLEClient.initialize()
+ await AbrevvaBLEClient.startScan({ timeout: 5_000 }, (device: BleDevice) => {
+ this.devices.push(device);
+ }, (success: boolean) => {
+ console.log(`Scan started, success: ${success}`);
+ }, (success: boolean) => {
+ console.log(`Scan stopped, success: ${success}`);
});
}
}
```
+### Read EVVA component advertisement
+
+Get the EVVA advertisement data from a scanned EVVA component.
+
+```typescript
+const ad = device.advertisementData
+console.log(ad?.rssi)
+console.log(ad?.isConnectable)
+
+const md = ad?.manufacturerData
+console.log(md?.batteryStatus)
+console.log(md?.isOnline)
+console.log(md?.officeModeEnabled)
+console.log(md?.officeModeActive)
+// ...
+```
+
+There are several properties that can be accessed from the advertisement.
+
+```typescript
+export interface BleDeviceAdvertisementData {
+ rssi?: number;
+ isConnectable?: boolean;
+ manufacturerData?: BleDeviceManufacturerData;
+}
+
+export interface BleDeviceManufacturerData {
+ companyIdentifier?: string;
+ version?: number;
+ componentType?: "handle" | "escutcheon" | "cylinder" | "wallreader" | "emzy" | "iobox" | "unknown";
+ mainFirmwareVersionMajor?: number;
+ mainFirmwareVersionMinor?: number;
+ mainFirmwareVersionPatch?: number;
+ componentHAL?: string;
+ batteryStatus?: "battery-full" | "battery-empty";
+ mainConstructionMode?: boolean;
+ subConstructionMode?: boolean;
+ isOnline?: boolean;
+ officeModeEnabled?: boolean;
+ twoFactorRequired?: boolean;
+ officeModeActive?: boolean;
+ identifier?: string;
+ subFirmwareVersionMajor?: number;
+ subFirmwareVersionMinor?: number;
+ subFirmwareVersionPatch?: number;
+ subComponentIdentifier?: string;
+}
+```
+
### Localize EVVA component
-With the signalize method you can localize EVVA components. On a successful signalization the component will emit a melody indicating its location.
+With the signalize method you can localize scanned EVVA components. On a successful signalization the component will emit a melody indicating its location.
```typescript
const success = await AbrevvaBLEClient.signalize('deviceId');
```
-### Perform disengage on EVVA components
+### Disengage EVVA components
For the component disengage you have to provide access credentials to the EVVA component. Those are generally acquired in the form of access media metadata from the Xesar software.
```typescript
const status = await AbrevvaBLEClient.disengage(
+ 'deviceId',
'mobileId',
'mobileDeviceKey',
'mobileGroupId',
- 'mobileAccessData',
+ 'mediumAccessData',
false,
);
```
+There are several access status types upon attempting the component disengage.
+
+```typescript
+export enum DisengageStatusType {
+ /// Component
+ Authorized = "AUTHORIZED",
+ AuthorizedPermanentEngage = "AUTHORIZED_PERMANENT_ENGAGE",
+ AuthorizedPermanentDisengage = "AUTHORIZED_PERMANENT_DISENGAGE",
+ AuthorizedBatteryLow = "AUTHORIZED_BATTERY_LOW",
+ AuthorizedOffline = "AUTHORIZED_OFFLINE",
+ Unauthorized = "UNAUTHORIZED",
+ UnauthorizedOffline = "UNAUTHORIZED_OFFLINE",
+ SignalLocalization = "SIGNAL_LOCALIZATION",
+ MediumDefectOnline = "MEDIUM_DEFECT_ONLINE",
+ MediumBlacklisted = "MEDIUM_BLACKLISTED",
+ Error = "ERROR",
+
+ /// Interface
+ UnableToConnect = "UNABLE_TO_CONNECT",
+ UnableToSetNotifications = "UNABLE_TO_SET_NOTIFICATIONS",
+ UnableToReadChallenge = "UNABLE_TO_READ_CHALLENGE",
+ UnableToWriteMDF = "UNABLE_TO_WRITE_MDF",
+ AccessCipherError = "ACCESS_CIPHER_ERROR",
+ BleAdapterDisabled = "BLE_ADAPTER_DISABLED",
+ UnknownDevice = "UNKNOWN_DEVICE",
+ UnknownStatusCode = "UNKNOWN_STATUS_CODE",
+ Timeout = "TIMEOUT",
+}
+```
+
## API
* [Interfaces](#interfaces)
-* [Enums](#enums)
@@ -105,29 +189,31 @@ const status = await AbrevvaBLEClient.disengage(
#### AbrevvaBLEInterface
-| Method | Signature |
-| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **initialize** | (options?: InitializeOptions \| undefined) => Promise<void> |
-| **isEnabled** | () => Promise<BooleanResult> |
-| **isLocationEnabled** | () => Promise<BooleanResult> |
-| **startEnabledNotifications** | () => Promise<void> |
-| **stopEnabledNotifications** | () => Promise<void> |
-| **openLocationSettings** | () => Promise<void> |
-| **openBluetoothSettings** | () => Promise<void> |
-| **openAppSettings** | () => Promise<void> |
-| **requestLEScan** | (options?: RequestBleDeviceOptions \| undefined) => Promise<void> |
-| **stopLEScan** | () => Promise<void> |
-| **addListener** | (eventName: "onEnabledChanged", listenerFunc: (result: BooleanResult) => void) => PluginListenerHandle |
-| **addListener** | (eventName: string, listenerFunc: (event: ReadResult) => void) => PluginListenerHandle |
-| **addListener** | (eventName: "onScanResult", listenerFunc: (result: ScanResultInternal) => void) => PluginListenerHandle |
-| **connect** | (options: DeviceIdOptions & TimeoutOptions) => Promise<void> |
-| **disconnect** | (options: DeviceIdOptions) => Promise<void> |
-| **read** | (options: ReadOptions & TimeoutOptions) => Promise<ReadResult> |
-| **write** | (options: WriteOptions & TimeoutOptions) => Promise<void> |
-| **signalize** | (options: SignalizeOptions) => Promise<void> |
-| **disengage** | (options: DisengageOptions) => Promise<StringResult> |
-| **startNotifications** | (options: ReadOptions) => Promise<void> |
-| **stopNotifications** | (options: ReadOptions) => Promise<void> |
+| Method | Signature |
+| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **initialize** | (options?: InitializeOptions \| undefined) => Promise<void> |
+| **isEnabled** | () => Promise<BooleanResult> |
+| **isLocationEnabled** | () => Promise<BooleanResult> |
+| **startEnabledNotifications** | () => Promise<void> |
+| **stopEnabledNotifications** | () => Promise<void> |
+| **openLocationSettings** | () => Promise<void> |
+| **openBluetoothSettings** | () => Promise<void> |
+| **openAppSettings** | () => Promise<void> |
+| **startScan** | (options?: BleScannerOptions \| undefined) => Promise<void> |
+| **stopScan** | () => Promise<void> |
+| **addListener** | (eventName: "onEnabledChanged", listenerFunc: (result: BooleanResult) => void) => PluginListenerHandle |
+| **addListener** | (eventName: string, listenerFunc: (event: ReadResult) => void) => PluginListenerHandle |
+| **addListener** | (eventName: "onScanResult", listenerFunc: (result: BleDevice) => void) => PluginListenerHandle |
+| **addListener** | (eventName: "onScanStart", listenerFunc: (success: BooleanResult) => void) => PluginListenerHandle |
+| **addListener** | (eventName: "onScanStop", listenerFunc: (success: BooleanResult) => void) => PluginListenerHandle |
+| **connect** | (options: DeviceIdOptions & TimeoutOptions) => Promise<void> |
+| **disconnect** | (options: DeviceIdOptions) => Promise<void> |
+| **read** | (options: ReadOptions & TimeoutOptions) => Promise<ReadResult> |
+| **write** | (options: WriteOptions & TimeoutOptions) => Promise<void> |
+| **signalize** | (options: SignalizeOptions) => Promise<void> |
+| **disengage** | (options: DisengageOptions) => Promise<StringResult> |
+| **startNotifications** | (options: ReadOptions) => Promise<void> |
+| **stopNotifications** | (options: ReadOptions) => Promise<void> |
#### InitializeOptions
@@ -144,17 +230,13 @@ const status = await AbrevvaBLEClient.disengage(
| **`value`** | boolean
|
-#### RequestBleDeviceOptions
+#### BleScannerOptions
-| Prop | Type |
-| ---------------------- | --------------------------------------------- |
-| **`services`** | string[]
|
-| **`name`** | string
|
-| **`namePrefix`** | string
|
-| **`optionalServices`** | string[]
|
-| **`allowDuplicates`** | boolean
|
-| **`scanMode`** | ScanMode
|
-| **`timeout`** | number
|
+| Prop | Type |
+| --------------------- | -------------------- |
+| **`macFilter`** | string
|
+| **`allowDuplicates`** | boolean
|
+| **`timeout`** | number
|
#### PluginListenerHandle
@@ -171,27 +253,47 @@ const status = await AbrevvaBLEClient.disengage(
| **`value`** | string
|
-#### ScanResultInternal
-
-| Prop | Type |
-| ---------------------- | ----------------------------------------------- |
-| **`device`** | BleDevice
|
-| **`localName`** | string
|
-| **`rssi`** | number
|
-| **`txPower`** | number
|
-| **`manufacturerData`** | { [key: string]: T; }
|
-| **`serviceData`** | { [key: string]: T; }
|
-| **`uuids`** | string[]
|
-| **`rawAdvertisement`** | T
|
-
-
#### BleDevice
-| Prop | Type |
-| -------------- | --------------------- |
-| **`deviceId`** | string
|
-| **`name`** | string
|
-| **`uuids`** | string[]
|
+| Prop | Type |
+| ----------------------- | --------------------------------------------------------------------------------- |
+| **`deviceId`** | string
|
+| **`name`** | string
|
+| **`advertisementData`** | BleDeviceAdvertisementData
|
+
+
+#### BleDeviceAdvertisementData
+
+| Prop | Type |
+| ---------------------- | ------------------------------------------------------------------------------- |
+| **`rssi`** | number
|
+| **`isConnectable`** | boolean
|
+| **`manufacturerData`** | BleDeviceManufacturerData
|
+
+
+#### BleDeviceManufacturerData
+
+| Prop | Type |
+| ------------------------------ | ----------------------------------------------------------------------------------------------------- |
+| **`companyIdentifier`** | string
|
+| **`version`** | number
|
+| **`componentType`** | 'handle' \| 'escutcheon' \| 'cylinder' \| 'wallreader' \| 'emzy' \| 'iobox' \| 'unknown'
|
+| **`mainFirmwareVersionMajor`** | number
|
+| **`mainFirmwareVersionMinor`** | number
|
+| **`mainFirmwareVersionPatch`** | number
|
+| **`componentHAL`** | string
|
+| **`batteryStatus`** | 'battery-full' \| 'battery-empty'
|
+| **`mainConstructionMode`** | boolean
|
+| **`subConstructionMode`** | boolean
|
+| **`isOnline`** | boolean
|
+| **`officeModeEnabled`** | boolean
|
+| **`twoFactorRequired`** | boolean
|
+| **`officeModeActive`** | boolean
|
+| **`identifier`** | string
|
+| **`subFirmwareVersionMajor`** | number
|
+| **`subFirmwareVersionMinor`** | number
|
+| **`subFirmwareVersionPatch`** | number
|
+| **`subComponentIdentifier`** | string
|
#### DeviceIdOptions
@@ -249,7 +351,7 @@ const status = await AbrevvaBLEClient.disengage(
| **`mobileId`** | string
|
| **`mobileDeviceKey`** | string
|
| **`mobileGroupId`** | string
|
-| **`mobileAccessData`** | string
|
+| **`mediumAccessData`** | string
|
| **`isPermanentRelease`** | boolean
|
@@ -270,16 +372,4 @@ const status = await AbrevvaBLEClient.disengage(
| **random** | (options: { numBytes: number; }) => Promise<{ value: string; }> |
| **derive** | (options: { key: string; salt: string; info: string; length: number; }) => Promise<{ value: string; }> |
-
-### Enums
-
-
-#### ScanMode
-
-| Members | Value |
-| --------------------------- | -------------- |
-| **`SCAN_MODE_LOW_POWER`** | 0
|
-| **`SCAN_MODE_BALANCED`** | 1
|
-| **`SCAN_MODE_LOW_LATENCY`** | 2
|
-
diff --git a/android/build.gradle b/android/build.gradle
index 46c5a8a..cd17235 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -70,7 +70,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.core:core-ktx:$coreKtx"
- implementation group: "com.evva.xesar", name: "abrevva-sdk-android", version: "1.0.21"
+ implementation group: "com.evva.xesar", name: "abrevva-sdk-android", version: "3.0.1"
testImplementation "junit:junit:$junitVersion"
diff --git a/android/src/main/java/com/evva/xesar/abrevva/plugins/capacitor/AbrevvaPluginBLE.kt b/android/src/main/java/com/evva/xesar/abrevva/plugins/capacitor/AbrevvaPluginBLE.kt
index ec38dab..f022445 100644
--- a/android/src/main/java/com/evva/xesar/abrevva/plugins/capacitor/AbrevvaPluginBLE.kt
+++ b/android/src/main/java/com/evva/xesar/abrevva/plugins/capacitor/AbrevvaPluginBLE.kt
@@ -6,10 +6,11 @@ import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.annotation.RequiresPermission
+import com.evva.xesar.abrevva.ble.BleDevice
import com.evva.xesar.abrevva.ble.BleManager
+import com.evva.xesar.abrevva.ble.BleWriteType
import com.evva.xesar.abrevva.util.bytesToString
import com.evva.xesar.abrevva.util.stringToBytes
-import com.getcapacitor.JSArray
import com.getcapacitor.JSObject
import com.getcapacitor.Logger
import com.getcapacitor.PermissionState
@@ -18,7 +19,9 @@ import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import com.getcapacitor.annotation.PermissionCallback
-import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanResult
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
import java.util.UUID
@CapacitorPlugin(
@@ -194,43 +197,34 @@ class AbrevvaPluginBLE : Plugin() {
}
@PluginMethod
- fun requestLEScan(call: PluginCall) {
+ fun startScan(call: PluginCall) {
+ val macFilter = call.getString("macFilter", null)
+ val allowDuplicates = call.getBoolean("allowDuplicates", false)
val timeout = call.getFloat("timeout", 15000.0F)!!.toLong()
- this.manager.startScan({ success ->
- if (success) {
- call.resolve()
- } else {
- call.reject("requestLEScan(): failed to start")
- }
- }, { result ->
- Logger.debug(tag, "Found device: ${result.device.address}")
-
- val scanResult = getScanResultFromNordic(result)
- try {
- notifyListeners("onScanResult", scanResult)
- } catch (e: java.util.ConcurrentModificationException) {
- Logger.error(tag, "requestLEScan()", e)
- }
- }, { address ->
- try {
- notifyListeners("connected|${address}", null)
- } catch (e: java.util.ConcurrentModificationException) {
- Logger.error(tag, "onConnect()", e)
- }
- }, { address ->
+ this.manager.startScan({ device ->
+ Logger.debug(tag, "onScanResult(): device found: ${device.address}")
+ val bleDevice = getBleDeviceData(device)
try {
- notifyListeners("disconnected|${address}", null)
- } catch (e: java.util.ConcurrentModificationException) {
- Logger.error(tag, "onDisconnect()", e)
+ notifyListeners("onScanResult", bleDevice)
+ } catch (e: Exception) {
+ Logger.error(tag, "onScanResult()", e)
}
- },
- timeout
- )
+ }, { success ->
+ val data = JSObject()
+ data.put("value", success)
+ notifyListeners("onScanStart", data)
+ call.resolve()
+ }, { success ->
+ val data = JSObject()
+ data.put("value", success)
+ notifyListeners("onScanStop", data)
+ call.resolve()
+ }, macFilter, allowDuplicates, timeout)
}
@PluginMethod
- fun stopLEScan(call: PluginCall) {
+ fun stopScan(call: PluginCall) {
manager.stopScan()
call.resolve()
}
@@ -240,13 +234,27 @@ class AbrevvaPluginBLE : Plugin() {
fun connect(call: PluginCall) {
val deviceId = call.getString("deviceId", "")!!
val timeout = call.getFloat("timeout", 16000.0F)!!.toLong()
+ val device = manager.getBleDevice(deviceId) ?: run {
+ return call.reject("connect(): device not found")
+ }
- manager.connect(deviceId, { success ->
+ manager.connect(device, { success ->
if (success) {
+ try {
+ notifyListeners("connected|${deviceId}", null)
+ } catch (e: java.util.ConcurrentModificationException) {
+ Logger.error(tag, "onConnect()", e)
+ }
call.resolve()
} else {
call.reject("connect(): failed to connect after $timeout ms")
}
+ }, {
+ try {
+ notifyListeners("disconnected|${deviceId}", null)
+ } catch (e: java.util.ConcurrentModificationException) {
+ Logger.error(tag, "onConnect()", e)
+ }
}, timeout)
}
@@ -254,9 +262,17 @@ class AbrevvaPluginBLE : Plugin() {
@RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT")
fun disconnect(call: PluginCall) {
val deviceId = call.getString("deviceId", "")!!
+ val device = manager.getBleDevice(deviceId) ?: run {
+ return call.reject("disconnect(): device not found")
+ }
- manager.disconnect(deviceId) { success ->
+ manager.disconnect(device) { success ->
if (success) {
+ try {
+ notifyListeners("disconnected|${deviceId}", null)
+ } catch (e: java.util.ConcurrentModificationException) {
+ Logger.error(tag, "onDisconnect()", e)
+ }
call.resolve()
} else {
call.reject("disconnect(): failed to disconnect")
@@ -266,55 +282,70 @@ class AbrevvaPluginBLE : Plugin() {
@PluginMethod
@RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT")
+ @OptIn(DelicateCoroutinesApi::class)
fun read(call: PluginCall) {
val deviceId = call.getString("deviceId", "")!!
val timeout = call.getFloat("timeout", 10000.0F)!!.toLong()
- val characteristic = getCharacteristic(call)
- ?: return call.reject("read(): bad characteristic")
+ val characteristic = getCharacteristic(call) ?: run {
+ return call.reject("read(): bad characteristic")
+ }
+ val device = manager.getBleDevice(deviceId) ?: run {
+ return call.reject("read(): device not found")
+ }
- manager.read(deviceId, characteristic.first, characteristic.second, { success, data ->
- if (success) {
+ GlobalScope.launch {
+ val data = device.read(characteristic.first, characteristic.second, timeout)
+ if (data != null) {
val ret = JSObject()
- ret.put("value", bytesToString(data!!))
+ ret.put("value", bytesToString(data))
call.resolve(ret)
} else {
call.reject("read(): failed to read from device")
}
- }, timeout)
+ }
}
@PluginMethod
@RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT")
+ @OptIn(DelicateCoroutinesApi::class)
fun write(call: PluginCall) {
val deviceId = call.getString("deviceId", "")!!
val timeout = call.getFloat("timeout", 10000.0F)!!.toLong()
- val characteristic =
- getCharacteristic(call) ?: return call.reject("read(): bad characteristic")
- val value =
- call.getString("value", null) ?: return call.reject("write(): missing value for write")
-
- manager.write(
- deviceId,
- characteristic.first,
- characteristic.second,
- stringToBytes(value),
- { success ->
- if (success) {
- call.resolve()
- } else {
- call.reject("write(): failed to write to device")
- }
- },
- timeout
- )
+ val characteristic = getCharacteristic(call) ?: run {
+ return call.reject("write(): bad characteristic")
+ }
+ val value = call.getString("value", null) ?: run {
+ return call.reject("write(): missing value for write")
+ }
+ val device = manager.getBleDevice(deviceId) ?: run {
+ return call.reject("write(): device not found")
+ }
+
+ GlobalScope.launch {
+ val success = device.write(
+ characteristic.first,
+ characteristic.second,
+ stringToBytes(value),
+ BleWriteType.NO_RESPONSE,
+ timeout
+ )
+ if (success) {
+ call.resolve()
+ } else {
+ call.reject("write(): failed to write to device")
+ }
+ }
}
@PluginMethod
@RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT")
fun signalize(call: PluginCall) {
val deviceId = call.getString("deviceId", "")!!
+ val device = manager.getBleDevice(deviceId) ?: run {
+ return call.reject("signalize(): device not found")
+ }
- manager.signalize(deviceId) { success ->
+ manager.signalize(device) { success ->
if (success) {
call.resolve()
} else {
@@ -330,15 +361,18 @@ class AbrevvaPluginBLE : Plugin() {
val mobileId = call.getString("mobileId", "")!!
val mobileDeviceKey = call.getString("mobileDeviceKey", "")!!
val mobileGroupId = call.getString("mobileGroupId", "")!!
- val mobileAccessData = call.getString("mobileAccessData", "")!!
+ val mediumAccessData = call.getString("mediumAccessData", "")!!
val isPermanentRelease = call.getBoolean("isPermanentRelease", false)!!
+ val device = manager.getBleDevice(deviceId) ?: run {
+ return call.reject("disengage(): device not found")
+ }
manager.disengage(
- deviceId,
+ device,
mobileId,
mobileDeviceKey,
mobileGroupId,
- mobileAccessData,
+ mediumAccessData,
isPermanentRelease
) { status ->
val result = JSObject()
@@ -350,50 +384,53 @@ class AbrevvaPluginBLE : Plugin() {
@PluginMethod
@RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT")
+ @OptIn(DelicateCoroutinesApi::class)
fun startNotifications(call: PluginCall) {
val deviceId = call.getString("deviceId", "")!!
- val characteristic =
- getCharacteristic(call)
- ?: return call.reject("startNotifications(): bad characteristic")
-
- manager.startNotifications(
- deviceId,
- characteristic.first,
- characteristic.second,
- { success ->
- if (success) {
- call.resolve()
- } else {
- call.reject("startNotifications(): failed to set notifications")
- }
- }, { data ->
- val key =
- "notification|${deviceId}|${(characteristic.first)}|${(characteristic.second)}"
-
- val ret = JSObject()
- ret.put("value", bytesToString(data))
+ val characteristic = getCharacteristic(call) ?: run {
+ return call.reject("startNotifications(): bad characteristic")
+ }
+ val device = manager.getBleDevice(deviceId) ?: run {
+ return call.reject("startNotifications(): device not found")
+ }
- try {
- notifyListeners(key, ret)
- } catch (e: java.util.ConcurrentModificationException) {
- Logger.error(tag, "startNotifications()", e)
- }
- })
+ GlobalScope.launch {
+ val success = device.setNotifications(characteristic.first,
+ characteristic.second, { data ->
+ val key =
+ "notification|${deviceId}|${(characteristic.first)}|${(characteristic.second)}"
+
+ val ret = JSObject()
+ ret.put("value", bytesToString(data))
+
+ try {
+ notifyListeners(key, ret)
+ } catch (e: java.util.ConcurrentModificationException) {
+ Logger.error(tag, "startNotifications()", e)
+ }
+ })
+ if (success) {
+ call.resolve()
+ } else {
+ call.reject("startNotifications(): failed to set notifications")
+ }
+ }
}
@PluginMethod
@RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT")
+ @OptIn(DelicateCoroutinesApi::class)
fun stopNotifications(call: PluginCall) {
val deviceId = call.getString("deviceId", "")!!
- val characteristic =
- getCharacteristic(call)
- ?: return call.reject("stopNotifications(): bad characteristic")
-
- manager.stopNotifications(
- deviceId,
- characteristic.first,
- characteristic.second
- ) { success ->
+ val characteristic = getCharacteristic(call) ?: run {
+ return call.reject("stopNotifications(): bad characteristic")
+ }
+ val device = manager.getBleDevice(deviceId) ?: run {
+ return call.reject("stopNotifications(): device not found")
+ }
+
+ GlobalScope.launch {
+ val success = device.stopNotifications(characteristic.first, characteristic.second)
if (success) {
call.resolve()
} else {
@@ -436,80 +473,76 @@ class AbrevvaPluginBLE : Plugin() {
return Pair(serviceUUID, characteristicUUID)
}
- private fun getBleDeviceFromNordic(result: BleScanResult): JSObject {
- val bleDevice = JSObject()
-
- bleDevice.put("deviceId", result.device.address)
-
- if (result.device.hasName) {
- bleDevice.put("name", result.device.name)
- }
-
- val uuids = JSArray()
- result.data?.scanRecord?.serviceUuids?.forEach { uuid -> uuids.put(uuid.toString()) }
-
- if (uuids.length() > 0) {
- bleDevice.put("uuids", uuids)
- }
-
- return bleDevice
- }
-
- @OptIn(ExperimentalStdlibApi::class)
- private fun getScanResultFromNordic(result: BleScanResult): JSObject {
- val scanResult = JSObject()
- val bleDevice = getBleDeviceFromNordic(result)
-
- scanResult.put("device", bleDevice)
-
- if (result.device.hasName) {
- scanResult.put("localName", result.device.name)
- }
- if (result.data?.rssi != null) {
- scanResult.put("rssi", result.data!!.rssi)
- }
- if (result.data?.txPower != null) {
- scanResult.put("txPower", result.data!!.txPower)
- } else {
- scanResult.put("txPower", 127)
- }
-
- val manufacturerData = JSObject()
-
- val scanRecordBytes = result.data?.scanRecord?.bytes
- if (scanRecordBytes != null) {
- try {
- // Extract EVVA manufacturer-id
- val keyHex = scanRecordBytes.getByte(6)?.toHexString() + scanRecordBytes.getByte(5)
- ?.toHexString()
- val keyDec = keyHex.toInt(16)
-
- // Slice out manufacturer data
- val bytes = scanRecordBytes.copyOfRange(7, scanRecordBytes.size)
-
- manufacturerData.put(keyDec.toString(), bytesToString(bytes.value))
- } catch (e: Exception) {
- Logger.warn("getScanResultFromNordic(): invalid manufacturer data")
+ private fun getBleDeviceData(device: BleDevice): JSObject {
+ val bleDeviceData = JSObject()
+
+ bleDeviceData.put("deviceId", device.address)
+ bleDeviceData.put("name", device.localName)
+
+ val advertisementData = JSObject()
+ device.advertisementData?.let {
+ advertisementData.put("rssi", it.rssi)
+ advertisementData.put("isConnectable", it.isConnectable)
+
+ val manufacturerData = JSObject()
+ it.manufacturerData?.let { data ->
+ manufacturerData.put("companyIdentifier", data.companyIdentifier.toInt())
+ manufacturerData.put("version", data.version.toInt())
+ manufacturerData.put(
+ "componentType",
+ when (data.componentType.toInt()) {
+ 98 -> "escutcheon"
+ 100 -> "handle"
+ 105 -> "iobox"
+ 109 -> "emzy"
+ 119 -> "wallreader"
+ 122 -> "cylinder"
+ else -> "unknown"
+ }
+ )
+ manufacturerData.put(
+ "mainFirmwareVersionMajor",
+ data.mainFirmwareVersionMajor.toInt()
+ )
+ manufacturerData.put(
+ "mainFirmwareVersionMinor",
+ data.mainFirmwareVersionMinor.toInt()
+ )
+ manufacturerData.put(
+ "mainFirmwareVersionPatch",
+ data.mainFirmwareVersionPatch.toInt()
+ )
+ manufacturerData.put("componentHAL", data.componentHAL)
+ manufacturerData.put(
+ "batteryStatus",
+ if (data.batteryStatus) "battery-full" else "battery-empty"
+ )
+ manufacturerData.put("mainConstructionMode", data.mainConstructionMode)
+ manufacturerData.put("subConstructionMode", data.subConstructionMode)
+ manufacturerData.put("isOnline", data.isOnline)
+ manufacturerData.put("officeModeEnabled", data.officeModeEnabled)
+ manufacturerData.put("twoFactorRequired", data.twoFactorRequired)
+ manufacturerData.put("officeModeActive", data.officeModeActive)
+ manufacturerData.put("reservedBits", data.reservedBits)
+ manufacturerData.put("identifier", data.identifier)
+ manufacturerData.put(
+ "subFirmwareVersionMajor",
+ data.subFirmwareVersionMajor?.toInt()
+ )
+ manufacturerData.put(
+ "subFirmwareVersionMinor",
+ data.subFirmwareVersionMinor?.toInt()
+ )
+ manufacturerData.put(
+ "subFirmwareVersionPatch",
+ data.subFirmwareVersionPatch?.toInt()
+ )
+ manufacturerData.put("subComponentIdentifier", data.subComponentIdentifier)
}
+ advertisementData.put("manufacturerData", manufacturerData)
}
+ bleDeviceData.put("advertisementData", advertisementData)
- scanResult.put("manufacturerData", manufacturerData)
-
- val serviceDataObject = JSObject()
- val serviceData = result.data?.scanRecord?.serviceData
- serviceData?.forEach {
- serviceDataObject.put(it.key.toString(), bytesToString(it.value.value))
- }
- scanResult.put("serviceData", serviceDataObject)
-
- val uuids = JSArray()
- result.data?.scanRecord?.serviceUuids?.forEach { uuid -> uuids.put(uuid.toString()) }
- scanResult.put("uuids", uuids)
- scanResult.put(
- "rawAdvertisement",
- result.data?.scanRecord?.bytes?.toString()
- )
-
- return scanResult
+ return bleDeviceData
}
}
diff --git a/android/src/main/java/com/evva/xesar/abrevva/plugins/capacitor/AbrevvaPluginCrypto.kt b/android/src/main/java/com/evva/xesar/abrevva/plugins/capacitor/AbrevvaPluginCrypto.kt
index 33fd88b..59ee5fb 100644
--- a/android/src/main/java/com/evva/xesar/abrevva/plugins/capacitor/AbrevvaPluginCrypto.kt
+++ b/android/src/main/java/com/evva/xesar/abrevva/plugins/capacitor/AbrevvaPluginCrypto.kt
@@ -29,7 +29,6 @@ private enum class CryptoError {
DecryptInvalidArgumentError,
DecryptEmptyResultError,
DecryptCryptoError,
- DecryptFileReadError,
DecryptFileCryptoError,
DecryptFileInvalidArgumentError,
DecryptFileFromURLNetworkError,
diff --git a/ios/Plugin/ble/AbrevvaPluginBLE.m b/ios/Plugin/ble/AbrevvaPluginBLE.m
index 730493d..be21f77 100644
--- a/ios/Plugin/ble/AbrevvaPluginBLE.m
+++ b/ios/Plugin/ble/AbrevvaPluginBLE.m
@@ -10,8 +10,8 @@
CAP_PLUGIN_METHOD(openLocationSettings, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(openBluetoothSettings, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(openAppSettings, CAPPluginReturnPromise);
- CAP_PLUGIN_METHOD(requestLEScan, CAPPluginReturnPromise);
- CAP_PLUGIN_METHOD(stopLEScan, CAPPluginReturnPromise);
+ CAP_PLUGIN_METHOD(startScan, CAPPluginReturnPromise);
+ CAP_PLUGIN_METHOD(stopScan, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(connect, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(disconnect, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(read, CAPPluginReturnPromise);
diff --git a/ios/Plugin/ble/AbrevvaPluginBLE.swift b/ios/Plugin/ble/AbrevvaPluginBLE.swift
index 11b0dbe..2946dd2 100644
--- a/ios/Plugin/ble/AbrevvaPluginBLE.swift
+++ b/ios/Plugin/ble/AbrevvaPluginBLE.swift
@@ -79,40 +79,34 @@ public class AbrevvaPluginBLE: CAPPlugin {
}
@objc
- func requestLEScan(_ call: CAPPluginCall) {
+ func startScan(_ call: CAPPluginCall) {
guard let bleManager = self.getBleManager(call) else { return }
- let name = call.getString("name")
- let namePrefix = call.getString("namePrefix")
- let allowDuplicates = call.getBool("allowDuplicates", false)
+ let macFilter = call.getString("macFilter")
+ let allowDuplicates = call.getBool("allowDuplicates") ?? false
let timeout = call.getDouble("timeout").map { Int($0) } ?? nil
bleManager.startScan(
- name,
- namePrefix,
- allowDuplicates,
- { success in
- if success {
- call.resolve()
- } else {
- call.reject("requestLEScan(): failed to start")
- }
- }, { device, advertisementData, rssi in
+ { device in
self.bleDeviceMap[device.getAddress()] = device
- let data = self.getScanResultDict(device, advertisementData, rssi)
- self.notifyListeners("onScanResult", data: data)
+ let data = self.getAdvertismentData(device)
+ self.notifyListeners("onScanResult", data: data as [String: Any])
},
- { address in
- self.notifyListeners("connected|\(address)", data: nil)
+ { error in
+ self.notifyListeners("onScanStart", data: ["value": error == nil])
+ call.resolve()
},
- { address in
- self.notifyListeners("disconnected|\(address)", data: nil)
+ { error in
+ self.notifyListeners("onScanStop", data: ["value": error == nil])
+ call.resolve()
},
+ macFilter,
+ allowDuplicates,
timeout
)
}
@objc
- func stopLEScan(_ call: CAPPluginCall) {
+ func stopScan(_ call: CAPPluginCall) {
guard let bleManager = self.getBleManager(call) else { return }
bleManager.stopScan()
call.resolve()
@@ -123,9 +117,12 @@ public class AbrevvaPluginBLE: CAPPlugin {
guard self.getBleManager(call) != nil else { return }
guard let device = self.getDevice(call, checkConnection: false) else { return }
let timeout = call.getDouble("timeout").map { Int($0) } ?? nil
-
Task {
- let success = await self.bleManager!.connect(device, timeout)
+ let success = await self.bleManager!.connect(device, { address in
+ self.notifyListeners("disconnected|\(address)", data: nil)
+ },
+ timeout)
+
if success {
call.resolve()
} else {
@@ -216,7 +213,7 @@ public class AbrevvaPluginBLE: CAPPlugin {
let mobileID = call.getString("mobileId") ?? ""
let mobileDeviceKey = call.getString("mobileDeviceKey") ?? ""
let mobileGroupID = call.getString("mobileGroupId") ?? ""
- let mobileAccessData = call.getString("mobileAccessData") ?? ""
+ let mediumAccessData = call.getString("mediumAccessData") ?? ""
let isPermanentRelease = call.getBool("isPermanentRelease") ?? false
let timeout = call.getDouble("timeout").map { Int($0) } ?? nil
@@ -226,7 +223,7 @@ public class AbrevvaPluginBLE: CAPPlugin {
mobileID,
mobileDeviceKey,
mobileGroupID,
- mobileAccessData,
+ mediumAccessData,
isPermanentRelease,
timeout
)
@@ -328,67 +325,71 @@ public class AbrevvaPluginBLE: CAPPlugin {
return (serviceUUID, characteristicUUID)
}
- private func getBleDeviceDict(_ device: BleDevice) -> [String: String] {
- var bleDevice = [
- "deviceId": device.getAddress()
- ]
- if device.getName() != nil {
- bleDevice["name"] = device.getName()
- }
- return bleDevice
- }
+ private func getAdvertismentData(
+ _ device: BleDevice
+ ) -> [String: Any?] {
- private func getScanResultDict(
- _ device: BleDevice,
- _ advertisementData: [String: Any],
- _ rssi: NSNumber
- ) -> [String: Any] {
- var data = [
- "device": self.getBleDeviceDict(device),
- "rssi": rssi,
- "txPower": advertisementData[CBAdvertisementDataTxPowerLevelKey] ?? 127,
- "uuids": (advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] ?? []).map { uuid -> String in
- return CBUUIDToString(uuid)
- }
+ var bleDeviceData: [String: Any?] = [
+ "deviceId": device.getAddress(),
+ "name": device.getName()
]
- let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String
- if localName != nil {
- data["localName"] = localName
+ var advertismentData: [String: Any?] = [
+ "rssi": device.advertisementData?.rssi
+ ]
+ if let isConnectable = device.advertisementData?.isConnectable {
+ advertismentData["isConnectable"] = isConnectable
}
- let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
- if manufacturerData != nil {
- data["manufacturerData"] = self.getManufacturerDataDict(data: manufacturerData!)
+ guard let mfData = device.advertisementData?.manufacturerData else {
+ bleDeviceData ["advertisementData"] = advertismentData
+ return bleDeviceData
}
- let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data]
- if serviceData != nil {
- data["serviceData"] = self.getServiceDataDict(data: serviceData!)
- }
- return data
- }
+ var manufacturerData: [String: Any?] = [
+ "companyIdentifier": mfData.companyIdentifier,
+ "version": mfData.version,
+ "mainFirmwareVersionMajor": mfData.mainFirmwareVersionMajor,
+ "mainFirmwareVersionMinor": mfData.mainFirmwareVersionMinor,
+ "mainFirmwareVersionPatch": mfData.mainFirmwareVersionPatch,
+ "componentHAL": mfData.componentHAL,
+ "batteryStatus": mfData.batteryStatus ? "battery-full" : "battery-empty",
+ "mainConstructionMode": mfData.mainConstructionMode,
+ "subConstructionMode": mfData.subConstructionMode,
+ "isOnline": mfData.isOnline,
+ "officeModeEnabled": mfData.officeModeEnabled,
+ "twoFactorRequired": mfData.twoFactorRequired,
+ "officeModeActive": mfData.officeModeActive,
+ "identifier": mfData.identifier,
+ "subFirmwareVersionMajor": mfData.subFirmwareVersionMajor,
+ "subFirmwareVersionMinor": mfData.subFirmwareVersionMinor,
+ "subFirmwareVersionPatch": mfData.subFirmwareVersionPatch,
+ "subComponentIdentifier": mfData.subComponentIdentifier,
+ "componentType": getComponentType(mfData.componentType)
+ ]
- private func getManufacturerDataDict(data: Data) -> [String: String] {
- var company = 0
- var rest = ""
- for (index, byte) in data.enumerated() {
- if index == 0 {
- company += Int(byte)
- } else if index == 1 {
- company += Int(byte) * 256
- } else {
- rest += String(format: "%02hhx ", byte)
- }
- }
- return [String(company): rest]
+ advertismentData["manufacturerData"] = manufacturerData
+ bleDeviceData["advertisementData"] = advertismentData
+
+ return bleDeviceData
}
- private func getServiceDataDict(data: [CBUUID: Data]) -> [String: String] {
- var result: [String: String] = [:]
- for (key, value) in data {
- result[CBUUIDToString(key)] = dataToString(value)
+ private func getComponentType(_ componentType: UInt8) -> String {
+ switch componentType {
+ case 98:
+ "escutcheon"
+ case 100:
+ "handle"
+ case 105:
+ "iobox"
+ case 109:
+ "emzy"
+ case 119:
+ "wallreader"
+ case 122:
+ "cylinder"
+ default:
+ "unkown"
}
- return result
}
}
diff --git a/ios/Podfile b/ios/Podfile
index 8552ffd..81118e7 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -13,7 +13,7 @@ end
target 'Plugin' do
capacitor_pods
- pod 'AbrevvaSDK'
+ pod 'AbrevvaSDK', '~> 3.0.1'
end
target 'PluginTests' do
diff --git a/package-lock.json b/package-lock.json
index cfef710..48e6d0d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5640,9 +5640,9 @@
}
},
"node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
diff --git a/src/plugins/ble/client.spec.ts b/src/plugins/ble/client.spec.ts
index 63db001..9f0e416 100644
--- a/src/plugins/ble/client.spec.ts
+++ b/src/plugins/ble/client.spec.ts
@@ -5,12 +5,14 @@ import { Capacitor } from "@capacitor/core";
import type { AbrevvaBLEClientInterface } from "./client";
import { AbrevvaBLEClient } from "./client";
import { hexStringToDataView, numbersToDataView } from "./conversion";
-import type { BleDevice } from "./definitions";
+import { BleDevice, DisengageStatusType } from "./definitions";
import { AbrevvaBLE } from "./plugin";
interface AbrevvaBLEClientWithPrivate extends AbrevvaBLEClientInterface {
eventListeners: Map;
- scanListener: PluginListenerHandle | null;
+ scanResultListener: PluginListenerHandle | null;
+ scanStartListener: PluginListenerHandle | null;
+ scanStopListener: PluginListenerHandle | null;
}
jest.mock("@capacitor/core", () => {
@@ -30,8 +32,8 @@ jest.mock("./plugin", () => {
startEnabledNotifications: jest.fn(),
stopEnabledNotifications: jest.fn(),
requestDevice: jest.fn(),
- requestLEScan: jest.fn(),
- stopLEScan: jest.fn(),
+ startScan: jest.fn(),
+ stopScan: jest.fn(),
connect: jest.fn(),
createBond: jest.fn(),
isBonded: jest.fn(),
@@ -125,30 +127,41 @@ describe("AbrevvaBLEClient", () => {
).toBeUndefined();
});
- it("should run requestLEScan", async () => {
- const mockCallback = jest.fn();
- const mockScanListener = {
+ it("should run startScan", async () => {
+ const mockScanResultCallback = jest.fn();
+ const mockScanStartCallback = jest.fn();
+ const mockScanStopCallback = jest.fn();
+
+ const mockScanResultListener = {
remove: jest.fn(),
};
- (AbrevvaBLE.addListener as jest.Mock).mockReturnValue(mockScanListener);
- await AbrevvaBLEClient.requestLEScan({}, mockCallback);
+
+ (AbrevvaBLE.addListener as jest.Mock).mockReturnValue(mockScanResultListener);
+ await AbrevvaBLEClient.startScan({}, mockScanResultCallback, mockScanStartCallback, mockScanStopCallback);
+
expect(AbrevvaBLE.addListener).toHaveBeenCalledWith("onScanResult", expect.any(Function));
- expect((AbrevvaBLEClient as unknown as AbrevvaBLEClientWithPrivate).scanListener).toBe(mockScanListener);
- expect(AbrevvaBLE.requestLEScan).toHaveBeenCalledTimes(1);
+ expect(AbrevvaBLE.addListener).toHaveBeenCalledWith("onScanStart", expect.any(Function));
+ expect(AbrevvaBLE.addListener).toHaveBeenCalledWith("onScanStop", expect.any(Function));
+
+ expect((AbrevvaBLEClient as unknown as AbrevvaBLEClientWithPrivate).scanResultListener).toBe(
+ mockScanResultListener,
+ );
+
+ expect(AbrevvaBLE.startScan).toHaveBeenCalledTimes(1);
});
- it("should run stopLEScan", async () => {
+ it("should run stopScan", async () => {
const mockCallback = jest.fn();
const mockScanListener = {
remove: jest.fn(),
};
(AbrevvaBLE.addListener as jest.Mock).mockReturnValue(mockScanListener);
- await AbrevvaBLEClient.requestLEScan({}, mockCallback);
- expect((AbrevvaBLEClient as unknown as AbrevvaBLEClientWithPrivate).scanListener).toBe(mockScanListener);
- await AbrevvaBLEClient.stopLEScan();
+ await AbrevvaBLEClient.startScan({}, mockCallback);
+ expect((AbrevvaBLEClient as unknown as AbrevvaBLEClientWithPrivate).scanResultListener).toBe(mockScanListener);
+ await AbrevvaBLEClient.stopScan();
expect(mockScanListener.remove).toHaveBeenCalledTimes(1);
- expect((AbrevvaBLEClient as unknown as AbrevvaBLEClientWithPrivate).scanListener).toBe(null);
- expect(AbrevvaBLE.stopLEScan).toHaveBeenCalledTimes(1);
+ expect((AbrevvaBLEClient as unknown as AbrevvaBLEClientWithPrivate).scanResultListener).toBe(null);
+ expect(AbrevvaBLE.stopScan).toHaveBeenCalledTimes(1);
});
it("should run connect without disconnect callback", async () => {
@@ -261,7 +274,7 @@ describe("AbrevvaBLEClient", () => {
expect(Capacitor.getPlatform()).toBe("android");
(AbrevvaBLE.disengage as jest.Mock).mockReturnValue({
- value: "ACCESS_STATUS_AUTHORIZED",
+ value: DisengageStatusType.Authorized,
});
const result = await AbrevvaBLEClient.disengage(mockDevice.deviceId, "", "", "", "", false);
expect(AbrevvaBLE.disengage).toHaveBeenCalledWith({
@@ -269,10 +282,10 @@ describe("AbrevvaBLEClient", () => {
mobileId: "",
mobileDeviceKey: "",
mobileGroupId: "",
- mobileAccessData: "",
+ mediumAccessData: "",
isPermanentRelease: false,
});
- expect(result).toEqual("ACCESS_STATUS_AUTHORIZED");
+ expect(result).toEqual(DisengageStatusType.Authorized);
});
it("should run startNotifications", async () => {
diff --git a/src/plugins/ble/client.ts b/src/plugins/ble/client.ts
index 157f038..944114a 100644
--- a/src/plugins/ble/client.ts
+++ b/src/plugins/ble/client.ts
@@ -1,14 +1,14 @@
import type { PluginListenerHandle } from "@capacitor/core";
import { dataViewToHexString, hexStringToDataView } from "./conversion";
-import type {
+import {
+ BleDevice,
+ BleScannerOptions,
Data,
+ DisengageStatusType,
InitializeOptions,
- RequestBleDeviceOptions,
- TimeoutOptions,
ReadResult,
- ScanResult,
- ScanResultInternal,
+ TimeoutOptions,
} from "./definitions";
import { AbrevvaBLE } from "./plugin";
import { getQueue } from "./queue";
@@ -23,8 +23,13 @@ export interface AbrevvaBLEClientInterface {
openLocationSettings(): Promise;
openBluetoothSettings(): Promise;
openAppSettings(): Promise;
- requestLEScan(options: RequestBleDeviceOptions, callback: (result: ScanResult) => void): Promise;
- stopLEScan(): Promise;
+ startScan(
+ options: BleScannerOptions,
+ onScanResult: (result: BleDevice) => void,
+ onScanStart?: (success: boolean) => void,
+ onScanStop?: (success: boolean) => void,
+ ): Promise;
+ stopScan(): Promise;
connect(deviceId: string, onDisconnect?: (deviceId: string) => void, options?: TimeoutOptions): Promise;
disconnect(deviceId: string): Promise;
read(deviceId: string, service: string, characteristic: string, options?: TimeoutOptions): Promise;
@@ -41,7 +46,7 @@ export interface AbrevvaBLEClientInterface {
mobileId: string,
mobileDeviceKey: string,
mobileGroupId: string,
- mobileAccessData: string,
+ mediumAccessData: string,
isPermanentRelease: boolean,
): Promise;
startNotifications(
@@ -54,7 +59,10 @@ export interface AbrevvaBLEClientInterface {
}
class AbrevvaBLEClientClass implements AbrevvaBLEClientInterface {
- private scanListener: PluginListenerHandle | null = null;
+ private scanResultListener: PluginListenerHandle | null = null;
+ private scanStartListener: PluginListenerHandle | null = null;
+ private scanStopListener: PluginListenerHandle | null = null;
+
private eventListeners = new Map();
private queue = getQueue(true);
@@ -117,30 +125,40 @@ class AbrevvaBLEClientClass implements AbrevvaBLEClientInterface {
});
}
- async requestLEScan(options: RequestBleDeviceOptions, callback: (result: ScanResult) => void): Promise {
- options = this.validateRequestBleDeviceOptions(options);
+ async startScan(
+ options: BleScannerOptions,
+ onScanResult: (result: BleDevice) => void,
+ onScanStart?: (success: boolean) => void,
+ onScanStop?: (success: boolean) => void,
+ ): Promise {
await this.queue(async () => {
- await this.scanListener?.remove();
- this.scanListener = AbrevvaBLE.addListener("onScanResult", (resultInternal: ScanResultInternal) => {
- const result: ScanResult = {
- ...resultInternal,
- manufacturerData: this.convertObject(resultInternal.manufacturerData),
- serviceData: this.convertObject(resultInternal.serviceData),
- rawAdvertisement: resultInternal.rawAdvertisement
- ? this.convertValue(resultInternal.rawAdvertisement)
- : undefined,
- };
- callback(result);
+ await this.scanResultListener?.remove();
+ this.scanResultListener = AbrevvaBLE.addListener("onScanResult", (device: BleDevice) => {
+ onScanResult(device);
});
- await AbrevvaBLE.requestLEScan(options);
+ if (onScanStart) {
+ await this.scanStartListener?.remove();
+ this.scanStartListener = AbrevvaBLE.addListener("onScanStart", (result) => {
+ onScanStart(result.value);
+ this.scanStartListener?.remove();
+ });
+ }
+ if (onScanStop) {
+ await this.scanStopListener?.remove();
+ this.scanStopListener = AbrevvaBLE.addListener("onScanStop", (result) => {
+ onScanStop(result.value);
+ this.scanStopListener?.remove();
+ });
+ }
+ await AbrevvaBLE.startScan(options);
});
}
- async stopLEScan(): Promise {
+ async stopScan(): Promise {
await this.queue(async () => {
- await this.scanListener?.remove();
- this.scanListener = null;
- await AbrevvaBLE.stopLEScan();
+ await this.scanResultListener?.remove();
+ this.scanResultListener = null;
+ await AbrevvaBLE.stopScan();
});
}
@@ -213,11 +231,11 @@ class AbrevvaBLEClientClass implements AbrevvaBLEClientInterface {
mobileId: string,
mobileDeviceKey: string,
mobileGroupId: string,
- mobileAccessData: string,
+ mediumAccessData: string,
isPermanentRelease: boolean,
onConnect?: (address: string) => void,
onDisconnect?: (address: string) => void,
- ): Promise {
+ ): Promise {
return await this.queue(async () => {
if (onConnect) {
await this.eventListeners.get(`connected|${deviceId}`)?.remove();
@@ -238,16 +256,24 @@ class AbrevvaBLEClientClass implements AbrevvaBLEClientInterface {
);
}
- const result = await AbrevvaBLE.disengage({
- deviceId,
- mobileId,
- mobileDeviceKey,
- mobileGroupId,
- mobileAccessData,
- isPermanentRelease,
- });
+ const status = (
+ await AbrevvaBLE.disengage({
+ deviceId,
+ mobileId,
+ mobileDeviceKey,
+ mobileGroupId,
+ mediumAccessData,
+ isPermanentRelease,
+ })
+ ).value;
- return result.value;
+ let result: DisengageStatusType;
+ if (Object.values(DisengageStatusType).some((val: string) => val === status)) {
+ result = status;
+ } else {
+ result = DisengageStatusType.Error;
+ }
+ return result;
});
}
@@ -289,16 +315,6 @@ class AbrevvaBLEClientClass implements AbrevvaBLEClientInterface {
});
}
- private validateRequestBleDeviceOptions(options: RequestBleDeviceOptions): RequestBleDeviceOptions {
- if (options.services) {
- options.services = options.services.map(validateUUID);
- }
- if (options.optionalServices) {
- options.optionalServices = options.optionalServices.map(validateUUID);
- }
- return options;
- }
-
private convertValue(value?: Data): DataView {
if (typeof value === "string") {
return hexStringToDataView(value);
@@ -307,17 +323,6 @@ class AbrevvaBLEClientClass implements AbrevvaBLEClientInterface {
}
return value;
}
-
- private convertObject(obj?: { [key: string]: Data }): { [key: string]: DataView } | undefined {
- if (obj === undefined) {
- return undefined;
- }
- const result: { [key: string]: DataView } = {};
- for (const key of Object.keys(obj)) {
- result[key] = this.convertValue(obj[key]);
- }
- return result;
- }
}
export const AbrevvaBLEClient = new AbrevvaBLEClientClass();
diff --git a/src/plugins/ble/definitions.ts b/src/plugins/ble/definitions.ts
index 307415a..7e56377 100644
--- a/src/plugins/ble/definitions.ts
+++ b/src/plugins/ble/definitions.ts
@@ -1,27 +1,11 @@
import type { PluginListenerHandle } from "@capacitor/core";
+export type Data = DataView | string;
+
export interface InitializeOptions {
androidNeverForLocation?: boolean;
}
-export enum ScanMode {
- SCAN_MODE_LOW_POWER = 0,
- SCAN_MODE_BALANCED = 1,
- SCAN_MODE_LOW_LATENCY = 2,
-}
-
-export interface RequestBleDeviceOptions {
- services?: string[];
- name?: string;
- namePrefix?: string;
- optionalServices?: string[];
- allowDuplicates?: boolean;
- scanMode?: ScanMode;
- timeout?: number;
-}
-
-export type Data = DataView | string;
-
export interface BooleanResult {
value: boolean;
}
@@ -38,32 +22,44 @@ export interface TimeoutOptions {
timeout?: number;
}
-export interface BleDevice {
- deviceId: string;
- name?: string;
- uuids?: string[];
+export interface BleScannerOptions {
+ macFilter?: string;
+ allowDuplicates?: boolean;
+ timeout?: number;
}
-export interface ScanResult {
- device: BleDevice;
- localName?: string;
+export interface BleDeviceAdvertisementData {
rssi?: number;
- txPower?: number;
- manufacturerData?: { [key: string]: DataView };
- serviceData?: { [key: string]: DataView };
- uuids?: string[];
- rawAdvertisement?: DataView;
+ isConnectable?: boolean;
+ manufacturerData?: BleDeviceManufacturerData;
+}
+
+export interface BleDeviceManufacturerData {
+ companyIdentifier?: string;
+ version?: number;
+ componentType?: "handle" | "escutcheon" | "cylinder" | "wallreader" | "emzy" | "iobox" | "unknown";
+ mainFirmwareVersionMajor?: number;
+ mainFirmwareVersionMinor?: number;
+ mainFirmwareVersionPatch?: number;
+ componentHAL?: string;
+ batteryStatus?: "battery-full" | "battery-empty";
+ mainConstructionMode?: boolean;
+ subConstructionMode?: boolean;
+ isOnline?: boolean;
+ officeModeEnabled?: boolean;
+ twoFactorRequired?: boolean;
+ officeModeActive?: boolean;
+ identifier?: string;
+ subFirmwareVersionMajor?: number;
+ subFirmwareVersionMinor?: number;
+ subFirmwareVersionPatch?: number;
+ subComponentIdentifier?: string;
}
-export interface ScanResultInternal {
- device: BleDevice;
- localName?: string;
- rssi?: number;
- txPower?: number;
- manufacturerData?: { [key: string]: T };
- serviceData?: { [key: string]: T };
- uuids?: string[];
- rawAdvertisement?: T;
+export interface BleDevice {
+ deviceId: string;
+ name?: string;
+ advertisementData?: BleDeviceAdvertisementData;
}
export interface ReadOptions {
@@ -92,10 +88,36 @@ export interface DisengageOptions {
mobileId: string;
mobileDeviceKey: string;
mobileGroupId: string;
- mobileAccessData: string;
+ mediumAccessData: string;
isPermanentRelease: boolean;
}
+export enum DisengageStatusType {
+ /// Component
+ Authorized = "AUTHORIZED",
+ AuthorizedPermanentEngage = "AUTHORIZED_PERMANENT_ENGAGE",
+ AuthorizedPermanentDisengage = "AUTHORIZED_PERMANENT_DISENGAGE",
+ AuthorizedBatteryLow = "AUTHORIZED_BATTERY_LOW",
+ AuthorizedOffline = "AUTHORIZED_OFFLINE",
+ Unauthorized = "UNAUTHORIZED",
+ UnauthorizedOffline = "UNAUTHORIZED_OFFLINE",
+ SignalLocalization = "SIGNAL_LOCALIZATION",
+ MediumDefectOnline = "MEDIUM_DEFECT_ONLINE",
+ MediumBlacklisted = "MEDIUM_BLACKLISTED",
+ Error = "ERROR",
+
+ /// Interface
+ UnableToConnect = "UNABLE_TO_CONNECT",
+ UnableToSetNotifications = "UNABLE_TO_SET_NOTIFICATIONS",
+ UnableToReadChallenge = "UNABLE_TO_READ_CHALLENGE",
+ UnableToWriteMDF = "UNABLE_TO_WRITE_MDF",
+ AccessCipherError = "ACCESS_CIPHER_ERROR",
+ BleAdapterDisabled = "BLE_ADAPTER_DISABLED",
+ UnknownDevice = "UNKNOWN_DEVICE",
+ UnknownStatusCode = "UNKNOWN_STATUS_CODE",
+ Timeout = "TIMEOUT",
+}
+
export interface AbrevvaBLEInterface {
initialize(options?: InitializeOptions): Promise;
isEnabled(): Promise;
@@ -105,11 +127,13 @@ export interface AbrevvaBLEInterface {
openLocationSettings(): Promise;
openBluetoothSettings(): Promise;
openAppSettings(): Promise;
- requestLEScan(options?: RequestBleDeviceOptions): Promise;
- stopLEScan(): Promise;
+ startScan(options?: BleScannerOptions): Promise;
+ stopScan(): Promise;
addListener(eventName: "onEnabledChanged", listenerFunc: (result: BooleanResult) => void): PluginListenerHandle;
addListener(eventName: string, listenerFunc: (event: ReadResult) => void): PluginListenerHandle;
- addListener(eventName: "onScanResult", listenerFunc: (result: ScanResultInternal) => void): PluginListenerHandle;
+ addListener(eventName: "onScanResult", listenerFunc: (result: BleDevice) => void): PluginListenerHandle;
+ addListener(eventName: "onScanStart", listenerFunc: (success: BooleanResult) => void): PluginListenerHandle;
+ addListener(eventName: "onScanStop", listenerFunc: (success: BooleanResult) => void): PluginListenerHandle;
connect(options: DeviceIdOptions & TimeoutOptions): Promise;
disconnect(options: DeviceIdOptions): Promise;
read(options: ReadOptions & TimeoutOptions): Promise;
diff --git a/test-app/package-lock.json b/test-app/package-lock.json
index cce29a7..aae26ee 100644
--- a/test-app/package-lock.json
+++ b/test-app/package-lock.json
@@ -64,7 +64,7 @@
},
"..": {
"name": "@evva/abrevva-capacitor",
- "version": "2.0.1",
+ "version": "3.0.2",
"license": "SEE LICENSE IN ",
"dependencies": {
"throat": "^6.0.2"
@@ -79,7 +79,7 @@
"@capacitor/ios": "^6.1.2",
"@ionic/swiftlint-config": "^1.1.2",
"@release-it/conventional-changelog": "^8.0.2",
- "@types/jest": "^29.5.13",
+ "@types/jest": "^29.5.14",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.7.4",
"auto-changelog": "^2.5.0",
@@ -93,6 +93,7 @@
"rimraf": "^6.0.1",
"rollup": "^3.29.4",
"swiftlint": "^1.0.2",
+ "ts-jest": "^29.2.5",
"typescript": "~5.3.3"
},
"peerDependencies": {
@@ -7111,9 +7112,9 @@
}
},
"node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
@@ -8395,9 +8396,9 @@
"dev": true
},
"node_modules/express": {
- "version": "4.21.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
- "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
"dependencies": {
"accepts": "~1.3.8",
@@ -8419,7 +8420,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -8434,6 +8435,10 @@
},
"engines": {
"node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/cookie": {
@@ -11647,9 +11652,9 @@
"dev": true
},
"node_modules/nanoid": {
- "version": "3.3.7",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
- "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
@@ -12863,9 +12868,9 @@
"dev": true
},
"node_modules/path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"dev": true
},
"node_modules/path-type": {
diff --git a/test-app/src/app/ble/ble.component.ts b/test-app/src/app/ble/ble.component.ts
index 35e0425..8105532 100644
--- a/test-app/src/app/ble/ble.component.ts
+++ b/test-app/src/app/ble/ble.component.ts
@@ -1,6 +1,6 @@
import { Component, OnInit } from "@angular/core";
import { ChangeDetectorRef } from "@angular/core";
-import { AbrevvaBLEClient, ScanResult } from "@evva/abrevva-capacitor";
+import { AbrevvaBLEClient, BleDevice } from "@evva/abrevva-capacitor";
@Component({
selector: "app-ble",
@@ -10,7 +10,7 @@ import { AbrevvaBLEClient, ScanResult } from "@evva/abrevva-capacitor";
export class BleComponent implements OnInit {
constructor(private readonly changeDetectorRef: ChangeDetectorRef) {}
- results: ScanResult[] = [];
+ results: BleDevice[] = [];
async ngOnInit() {
await AbrevvaBLEClient.initialize({ androidNeverForLocation: true });
@@ -20,9 +20,8 @@ export class BleComponent implements OnInit {
const timeout = 5_000;
this.results = [];
- await AbrevvaBLEClient.requestLEScan({ timeout: timeout }, (result: ScanResult) => {
+ await AbrevvaBLEClient.startScan({ timeout: timeout }, (result: BleDevice) => {
this.results.push(result);
- result.device.deviceId;
this.changeDetectorRef.detectChanges();
});
setTimeout(() => {
@@ -30,21 +29,14 @@ export class BleComponent implements OnInit {
}, timeout);
}
- async disengage(device: ScanResult) {
- await AbrevvaBLEClient.connect(
- device.device.deviceId,
- (device) => {
- console.log(`disconnected: ${device}`);
- },
- { timeout: 1_000 },
- );
-
+ async disengage(device: BleDevice) {
+ await AbrevvaBLEClient.stopScan();
await AbrevvaBLEClient.disengage(
- "deviveId",
+ device.deviceId,
"mobileId",
"mobileDeviceKey",
"mobileGroupId",
- "mobileAccessData",
+ "mediumAccessData",
true,
);
}