@tempivo/sensor-beacon 0.3.3 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +4 -4
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/AndroidManifest.xml +2 -1
  4. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +1 -3
  5. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +11 -2
  6. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +27 -84
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +138 -0
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +53 -0
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +161 -74
  10. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +7 -8
  11. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +2 -10
  12. package/dist/decoder.d.ts +0 -2
  13. package/dist/decoder.js +0 -17
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/native-module.js +5 -1
  17. package/dist/qr.d.ts +3 -2
  18. package/dist/qr.js +8 -6
  19. package/dist/react-native.d.ts +24 -1
  20. package/dist/react-native.js +22 -1
  21. package/dist/session-types.d.ts +4 -2
  22. package/dist/tempivo-sensor-beacon.aar +0 -0
  23. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  24. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +10 -7
  25. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +5 -3
  26. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +0 -1
  27. package/ios/TempivoSensorBeaconModule.swift +137 -14
  28. package/package.json +1 -1
package/README.md CHANGED
@@ -151,8 +151,8 @@ Sticker QR is JSON. Parse with `parseSensorQrJson`.
151
151
  |------|------|---------|
152
152
  | `serial` | string | Device serial, 12 hex chars, no colons. From QR `sn`. |
153
153
  | `pin` | string | Sticker PIN. Required on `connect`. Also accepted as QR `resetCode`. |
154
- | `model` | string | Optional. Firmware from the last scan wins when present. |
155
- | `sessionType` | `modern` \| `legacy` | GATT path. Firmware 7+ is `modern`. Firmware 6.x is `legacy`. |
154
+ | `model` | string | Optional. Defaults to **HC7**. **HC5** is rejected. |
155
+ | `sessionType` | `modern` | Always modern GATT (HC7 / firmware 7+). |
156
156
  | `bluetoothMac` | string | BLE MAC with colons, derived from `serial`. |
157
157
 
158
158
  ## Scan result
@@ -172,7 +172,7 @@ Filter manufacturer data on company id **0x026C**. Active scanning is on so scan
172
172
 
173
173
  | Field | Meaning |
174
174
  |------|---------|
175
- | `firmware` | `major.minor.patch`, e.g. `7.3.4`. Firmware 7+ is modern GATT. |
175
+ | `firmware` | `major.minor.patch`, e.g. `7.3.4`. Partner GATT requires firmware 7+. |
176
176
  | `batteryOk` | Battery status flag. |
177
177
  | `encryptionEnabled` | Advertisement payload encryption flag. |
178
178
  | `cellularStatus` | `ble_only`, `cell_ok`, `no_server`, or `net_issue`. |
@@ -224,7 +224,7 @@ AAR after `npm run build:android` (from `packages/tempivo-sensor-beacon`):
224
224
  | `android/libs/tempivo-sensor-beacon.aar` | Expo / React Native autolinking (`android/build.gradle`) |
225
225
  | `dist/tempivo-sensor-beacon.aar` | Same file; handy for plain Android `files(...)` deps |
226
226
 
227
- Sources: `android-aar/library`. Request `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` (API 31+) or location on older APIs.
227
+ Sources: `android-aar/library`. Request `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, and on some OEMs **`ACCESS_FINE_LOCATION`** even on API 31+ (location on older APIs).
228
228
 
229
229
  **Scan:** temperature and other live values are in scan-response frame **`0x04`** (`telemetry.readings`). Frame **`0x03`** alone fills firmware / battery / cellular but leaves `readings` empty until an active scan delivers `0x04` (usually within a second or two). Keep scanning; do not stop on the first event.
230
230
 
@@ -4,14 +4,14 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'com.tempivo.sensor.beacon'
7
- version = '0.3.3'
7
+ version = '0.4.3'
8
8
 
9
9
  android {
10
10
  namespace "expo.modules.tempivosensorbeacon"
11
11
  defaultConfig {
12
12
  minSdk 26
13
13
  versionCode 1
14
- versionName "0.3.3"
14
+ versionName "0.4.3"
15
15
  }
16
16
  compileOptions {
17
17
  sourceCompatibility JavaVersion.VERSION_17
@@ -5,6 +5,7 @@
5
5
  android:name="android.permission.BLUETOOTH_SCAN"
6
6
  android:usesPermissionFlags="neverForLocation" />
7
7
  <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
8
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
8
+ <!-- Some OEMs (e.g. Samsung) still filter BLE scan results without location on API 31+. -->
9
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
9
10
  <uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
10
11
  </manifest>
@@ -181,9 +181,7 @@ final class SensorBeaconNative {
181
181
  payload.put("serial", qr.getSerial());
182
182
  payload.put("pin", qr.getPin());
183
183
  payload.put("model", qr.getModel());
184
- payload.put(
185
- "sessionType",
186
- qr.getSessionType() == TempivoSensorSession.SessionType.LEGACY ? "legacy" : "modern");
184
+ payload.put("sessionType", "modern");
187
185
  payload.put("bluetoothMac", qr.getBluetoothMac());
188
186
  return payload;
189
187
  }
@@ -8,6 +8,7 @@ import expo.modules.kotlin.Promise
8
8
  import expo.modules.kotlin.exception.CodedException
9
9
  import expo.modules.kotlin.modules.Module
10
10
  import expo.modules.kotlin.modules.ModuleDefinition
11
+ import kotlinx.coroutines.launch
11
12
 
12
13
  class TempivoSensorBeaconModule : Module() {
13
14
  private val native = SensorBeaconNative()
@@ -24,8 +25,11 @@ class TempivoSensorBeaconModule : Module() {
24
25
 
25
26
  AsyncFunction("startScan") {
26
27
  try {
28
+ val ctx = appContext
27
29
  native.startScan(requireContext()) { payload ->
28
- sendEvent("onDeviceFound", payload)
30
+ ctx.mainQueue.launch {
31
+ sendEvent("onDeviceFound", payload)
32
+ }
29
33
  }
30
34
  } catch (error: Throwable) {
31
35
  throw error.toSensorException()
@@ -101,7 +105,12 @@ class TempivoSensorBeaconModule : Module() {
101
105
 
102
106
  private fun blePermissions(): Array<String> {
103
107
  return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
104
- arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
108
+ arrayOf(
109
+ Manifest.permission.BLUETOOTH_SCAN,
110
+ Manifest.permission.BLUETOOTH_CONNECT,
111
+ // Some Samsung/OEM builds still filter scan results without location granted.
112
+ Manifest.permission.ACCESS_FINE_LOCATION,
113
+ )
105
114
  } else {
106
115
  arrayOf(
107
116
  Manifest.permission.ACCESS_FINE_LOCATION,
@@ -4,7 +4,6 @@ import android.app.Application
4
4
  import kotlinx.coroutines.delay
5
5
  import pl.efento.mobile.bluetooth.EfentoBluetooth
6
6
  import pl.efento.mobile.bluetooth.api.SensorConnection
7
- import pl.efento.mobile.bluetooth.api.SensorLegacyConnection
8
7
  import pl.efento.mobile.bluetooth.exception.InvalidResetCodeException
9
8
  import pl.efento.mobile.bluetooth.exception.UnsupportedCommandException
10
9
  import pl.efento.mobile.bluetooth.model.BluetoothMacAddress
@@ -50,103 +49,50 @@ internal object SensorBleRuntime {
50
49
  serial: String,
51
50
  bluetoothMac: String,
52
51
  pin: Int,
53
- legacy: Boolean,
54
52
  ): Session {
55
53
  initialize(application)
56
54
  val mac = BluetoothMacAddress(bluetoothMac)
57
- // PIN stays on the connection: set, trigger, calibration writes, and many other commands check it.
58
- return if (legacy) {
59
- val c =
60
- EfentoBluetooth.sensorLegacyConnection(
61
- deviceID = serial,
62
- bluetoothMacAddress = mac,
63
- resetCode = pin,
64
- encryptionKey = "",
65
- )
66
- c.connect()
67
- if (pin >= 0) c.resetCode = pin
68
- Session.Legacy(c)
69
- } else {
70
- val c =
71
- EfentoBluetooth.sensorConnection(
72
- deviceID = serial,
73
- bluetoothMacAddress = mac,
74
- resetCode = pin,
75
- encryptionKey = "",
76
- )
77
- c.connect()
78
- if (pin >= 0) c.resetCode = pin
79
- Session.Modern(c)
80
- }
55
+ val c =
56
+ EfentoBluetooth.sensorConnection(
57
+ deviceID = serial,
58
+ bluetoothMacAddress = mac,
59
+ resetCode = pin,
60
+ encryptionKey = "",
61
+ )
62
+ c.connect()
63
+ if (pin >= 0) c.resetCode = pin
64
+ return Session(c)
81
65
  }
82
66
 
83
67
  suspend fun disconnect(session: Session?) {
84
- when (session) {
85
- is Session.Modern -> runCatching { session.connection.disconnect() }
86
- is Session.Legacy -> runCatching { session.connection.disconnect() }
87
- null -> Unit
88
- }
68
+ session?.let { runCatching { it.connection.disconnect() } }
89
69
  }
90
70
 
91
- suspend fun readConfiguration(session: Session): SensorConfiguration {
92
- return when (session) {
93
- is Session.Modern -> modernGetConfiguration(session.connection)
94
- is Session.Legacy ->
95
- throw TempivoSensorException(
96
- TempivoSensorException.Code.UNSUPPORTED_COMMAND,
97
- "Configuration rules require a current-generation sensor session.",
98
- )
99
- }
100
- }
71
+ suspend fun readConfiguration(session: Session): SensorConfiguration =
72
+ modernGetConfiguration(session.connection)
101
73
 
102
74
  suspend fun writeConfiguration(session: Session, cfg: TempivoSensorConfiguration) {
103
75
  val (rules, calendars) = compileRules(cfg)
104
- when (session) {
105
- is Session.Modern -> {
106
- val base = modernGetConfiguration(session.connection)
107
- var next = base.copy(rules = rules.take(12), ruleCalendars = calendars.take(6))
108
- cfg.measurementIntervalMinutes?.let { minutes ->
109
- val baseSec = (minutes * 60.0).toInt().coerceAtLeast(1)
110
- next = next.copy(measurementPeriod = MeasurementPeriod(baseSec, 1))
111
- }
112
- cfg.transmissionIntervalSeconds?.let { sec ->
113
- next = next.copy(transmissionInterval = TransmissionInterval(sec))
114
- }
115
- session.connection.commands.setConfiguration(next) { }
116
- }
117
- is Session.Legacy ->
118
- throw TempivoSensorException(
119
- TempivoSensorException.Code.UNSUPPORTED_COMMAND,
120
- "Configuration rules require a current-generation sensor session.",
121
- )
76
+ val base = modernGetConfiguration(session.connection)
77
+ var next = base.copy(rules = rules.take(12), ruleCalendars = calendars.take(6))
78
+ cfg.measurementIntervalMinutes?.let { minutes ->
79
+ val baseSec = (minutes * 60.0).toInt().coerceAtLeast(1)
80
+ next = next.copy(measurementPeriod = MeasurementPeriod(baseSec, 1))
81
+ }
82
+ cfg.transmissionIntervalSeconds?.let { sec ->
83
+ next = next.copy(transmissionInterval = TransmissionInterval(sec))
122
84
  }
85
+ session.connection.commands.setConfiguration(next) { }
123
86
  }
124
87
 
125
88
  suspend fun triggerTransmission(session: Session): TempivoSensorSession.TriggerResult {
126
- return when (session) {
127
- is Session.Modern -> {
128
- session.connection.commands.triggerCommunicationWithServer { }
129
- TempivoSensorSession.TriggerResult(ok = true, supported = true)
130
- }
131
- is Session.Legacy -> {
132
- try {
133
- session.connection.cellularCommands.triggerCommunicationWithServer { }
134
- TempivoSensorSession.TriggerResult(ok = true, supported = true)
135
- } catch (e: UnsupportedCommandException) {
136
- TempivoSensorSession.TriggerResult(ok = false, supported = false)
137
- }
138
- }
139
- }
89
+ session.connection.commands.triggerCommunicationWithServer { }
90
+ return TempivoSensorSession.TriggerResult(ok = true, supported = true)
140
91
  }
141
92
 
142
93
  suspend fun readCalibration(session: Session): TempivoSensorCalibration {
143
- return when (session) {
144
- is Session.Modern -> {
145
- val ext = session.connection.commands.getExtendedConfiguration { }
146
- TempivoSensorCalibrationDecoder.decode(ext.laboratoryCalibrationTimestamp)
147
- }
148
- is Session.Legacy -> TempivoSensorCalibration(null, null)
149
- }
94
+ val ext = session.connection.commands.getExtendedConfiguration { }
95
+ return TempivoSensorCalibrationDecoder.decode(ext.laboratoryCalibrationTimestamp)
150
96
  }
151
97
 
152
98
  fun compileRules(cfg: TempivoSensorConfiguration): Pair<List<Rule>, List<RuleCalendar>> =
@@ -171,8 +117,5 @@ internal object SensorBleRuntime {
171
117
  throw last ?: TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Could not read configuration.")
172
118
  }
173
119
 
174
- sealed class Session {
175
- class Modern(val connection: SensorConnection) : Session()
176
- class Legacy(val connection: SensorLegacyConnection) : Session()
177
- }
120
+ class Session(val connection: SensorConnection)
178
121
  }
@@ -0,0 +1,138 @@
1
+ package com.tempivo.sensor.beacon
2
+
3
+ import pl.efento.mobile.bluetooth.measurement.Measurement
4
+ import pl.efento.mobile.bluetooth.model.Sensor
5
+ import kotlin.math.abs
6
+ import kotlin.math.floor
7
+
8
+ /** Advertisement telemetry from Efento SDK [Sensor] (same path as Tempivo Capacitor app). */
9
+ internal object SensorSdkScanTelemetry {
10
+ fun fromSensor(sensor: Sensor): TempivoSensorBeaconTelemetry {
11
+ val readings = buildReadings(sensor)
12
+ val summary = sensorAdvertisementSummary(sensor, readings.size)
13
+ val fw = sensor.softwareVersion
14
+ val lts = fw.lts ?: 0
15
+ val periodBase = sensor.measurementPeriod.base
16
+ val periodFactor = sensor.measurementPeriod.factor
17
+ val intervalSec =
18
+ if (periodBase > 0) {
19
+ periodBase * (if (periodFactor == 0) 1 else periodFactor)
20
+ } else {
21
+ null
22
+ }
23
+ val ts = sensor.measurementsTimestamp
24
+ return TempivoSensorBeaconTelemetry(
25
+ firmware = "${fw.major}.${fw.minor}.$lts",
26
+ batteryOk = sensor.batteryStatus.name.contains("OK", ignoreCase = true),
27
+ encryptionEnabled = sensor.encryptionStatus.name.contains("ENABLED", ignoreCase = true),
28
+ cellularStatus = resolveCellularStatus(sensor),
29
+ measurementCounter = sensor.counter.takeIf { it > 0 }?.toLong(),
30
+ readingTimestampUnix = ts.takeIf { it > 0 },
31
+ measurementIntervalSeconds = intervalSec,
32
+ readings = readings,
33
+ readingsCount = readings.size,
34
+ summary = summary,
35
+ )
36
+ }
37
+
38
+ fun deviceFromSensor(sensor: Sensor): TempivoSensorBeaconDevice {
39
+ val mac = sensor.bluetoothMacAddress.withColons
40
+ val serial = TempivoSensorBeaconDecoder.macKey(mac)
41
+ val telemetry = fromSensor(sensor)
42
+ return TempivoSensorBeaconDevice(
43
+ deviceId = sensor.id,
44
+ bluetoothMacAddress = mac,
45
+ serialNumber = serial,
46
+ rssi = sensor.rssi,
47
+ telemetry = telemetry,
48
+ summary = telemetry.summary ?: serial,
49
+ )
50
+ }
51
+
52
+ private fun sensorAdvertisementSummary(
53
+ sensor: Sensor,
54
+ readingsCount: Int,
55
+ ): String {
56
+ val parts = measurementSummaryParts(sensor)
57
+ if (parts.isNotEmpty()) return parts.take(4).joinToString(" · ")
58
+ val fw = sensor.softwareVersion
59
+ val lts = fw.lts ?: 0
60
+ return if (readingsCount == 0) {
61
+ "${fw.major}.${fw.minor}.$lts"
62
+ } else {
63
+ "${fw.major}.${fw.minor}.$lts"
64
+ }
65
+ }
66
+
67
+ private fun buildReadings(sensor: Sensor): List<TempivoSensorBeaconReading> {
68
+ val out = mutableListOf<TempivoSensorBeaconReading>()
69
+ for ((_, measurement) in sensor.measurements) {
70
+ if (!measurement.isValid) continue
71
+ out +=
72
+ TempivoSensorBeaconReading(
73
+ typeHex = measurementTypeHex(measurement),
74
+ raw24 = 0,
75
+ text = formatMeasurementText(measurement),
76
+ )
77
+ }
78
+ return out
79
+ }
80
+
81
+ private fun measurementTypeHex(m: Measurement<*>): String {
82
+ val typeName = m.type.toString()
83
+ return Regex("0x[0-9A-Fa-f]+").find(typeName)?.value ?: typeName
84
+ }
85
+
86
+ private fun formatMeasurementText(m: Measurement<*>): String {
87
+ val type = m.type.toString().substringAfterLast('.').replace('_', ' ')
88
+ val v = m.value
89
+ return when (v) {
90
+ is Double -> "$type ${formatNum(v)}"
91
+ is Float -> "$type ${formatNum(v.toDouble())}"
92
+ is Int -> "$type $v"
93
+ is Long -> "$type $v"
94
+ else -> "$type $v"
95
+ }
96
+ }
97
+
98
+ private fun formatNum(v: Double): String =
99
+ if (abs(v - floor(v)) < 0.0001) {
100
+ v.toInt().toString()
101
+ } else {
102
+ v.toString()
103
+ }
104
+
105
+ private fun measurementSummaryParts(sensor: Sensor): List<String> {
106
+ val out = mutableListOf<String>()
107
+ for ((_, measurement) in sensor.measurements) {
108
+ if (!measurement.isValid) continue
109
+ out.add(formatMeasurementText(measurement))
110
+ }
111
+ return out
112
+ }
113
+
114
+ private fun resolveCellularStatus(sensor: Sensor): String? {
115
+ sensor.cellularStatus?.let { return mapCellularStatus(it) }
116
+ return mapConnectivityToCellular(sensor.connectivityStatus)
117
+ }
118
+
119
+ private fun mapCellularStatus(status: Sensor.CellularStatus): String =
120
+ when (status) {
121
+ Sensor.CellularStatus.COMMUNICATION_WITH_SERVER_DISABLED -> "ble_only"
122
+ Sensor.CellularStatus.OPERATING_CORRECTLY -> "cell_ok"
123
+ Sensor.CellularStatus.CLOUD_PROBLEM -> "no_server"
124
+ Sensor.CellularStatus.NETWORK_PROBLEM -> "net_issue"
125
+ }
126
+
127
+ private fun mapConnectivityToCellular(status: Sensor.ConnectivityStatus): String? {
128
+ val n = status.name.uppercase()
129
+ return when {
130
+ n.contains("BLE_ONLY") || n.contains("BLUETOOTH_ONLY") -> "ble_only"
131
+ n.contains("SERVER") || n.contains("CLOUD") -> "no_server"
132
+ n.contains("NETWORK") || n.contains("NET") -> "net_issue"
133
+ n.contains("CELLULAR") && n.contains("OK") -> "cell_ok"
134
+ n.contains("WORKING") && n.contains("CORRECT") -> "cell_ok"
135
+ else -> null
136
+ }
137
+ }
138
+ }
@@ -1,5 +1,6 @@
1
1
  package com.tempivo.sensor.beacon
2
2
 
3
+ import android.bluetooth.le.ScanRecord
3
4
  import java.text.SimpleDateFormat
4
5
  import java.util.Date
5
6
  import java.util.Locale
@@ -116,6 +117,58 @@ object TempivoSensorBeaconDecoder {
116
117
  return out
117
118
  }
118
119
 
120
+ /**
121
+ * All Tempivo manufacturer payloads from a scan record.
122
+ *
123
+ * `getBytes()` may contain only the advertising `0x03` block while
124
+ * [ScanRecord.getManufacturerSpecificData] holds a **concatenated** `0x03` + `0x04` blob
125
+ * (Android merges both when they share company id `0x026C`). Always merge every source.
126
+ */
127
+ fun manufacturerPayloadsFromScanRecord(record: ScanRecord): List<ByteArray> {
128
+ val out = mutableListOf<ByteArray>()
129
+ fun addUnique(payloads: List<ByteArray>) {
130
+ for (payload in payloads) {
131
+ if (payload.isEmpty()) continue
132
+ for (part in expandConcatenatedManufacturerPayload(payload)) {
133
+ if (out.none { it.contentEquals(part) }) {
134
+ out += part
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ addUnique(manufacturerPayloadsFromScanRecordBytes(record.bytes))
141
+
142
+ val sparse = record.manufacturerSpecificData
143
+ if (sparse != null) {
144
+ for (i in 0 until sparse.size()) {
145
+ if (sparse.keyAt(i) == MANUFACTURER_COMPANY_ID) {
146
+ sparse.valueAt(i)?.let { addUnique(listOf(it)) }
147
+ }
148
+ }
149
+ }
150
+ record.getManufacturerSpecificData(MANUFACTURER_COMPANY_ID)?.let { addUnique(listOf(it)) }
151
+
152
+ return out
153
+ }
154
+
155
+ /**
156
+ * Android often returns one manufacturer blob with adv `0x03` immediately followed by
157
+ * scan-response `0x04`. Raw AD parsing may only surface the first block.
158
+ */
159
+ private fun expandConcatenatedManufacturerPayload(payload: ByteArray): List<ByteArray> {
160
+ if (payload.size > ADV_FRAME_03_LEN && (payload[0].toInt() and 0xFF) == 0x03) {
161
+ val tail = payload.copyOfRange(ADV_FRAME_03_LEN, payload.size)
162
+ if (tail.isNotEmpty() && (tail[0].toInt() and 0xFF) == 0x04) {
163
+ return listOf(
164
+ payload.copyOfRange(0, ADV_FRAME_03_LEN),
165
+ tail,
166
+ )
167
+ }
168
+ }
169
+ return listOf(payload)
170
+ }
171
+
119
172
  /**
120
173
  * @return true if any `0x03` / `0x04` frame was stored.
121
174
  */