react-native-mytatva-rn-sdk 1.2.98 → 1.2.99
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.
- package/android/src/main/java/com/mytatvarnsdk/activity/ConnectSensorActivity.kt +152 -6
- package/android/src/main/res/layout/activity_connect_sensor.xml +117 -2
- package/ios/Database/KLTBluetoothManager.m +69 -14
- package/ios/TableViewCell/ImageTVC/ImageTVC.swift +147 -15
- package/ios/ViewControllers/ConnectToSensorViewController.swift +198 -14
- package/package.json +1 -1
|
@@ -14,6 +14,8 @@ import android.view.Gravity
|
|
|
14
14
|
import android.view.View
|
|
15
15
|
import android.view.ViewGroup
|
|
16
16
|
import android.view.Window
|
|
17
|
+
import android.widget.Toast
|
|
18
|
+
import ist.com.sdk.AlgorithmTools
|
|
17
19
|
import androidx.activity.OnBackPressedCallback
|
|
18
20
|
import androidx.activity.enableEdgeToEdge
|
|
19
21
|
import androidx.core.app.ActivityCompat
|
|
@@ -21,10 +23,13 @@ import androidx.core.content.ContextCompat
|
|
|
21
23
|
import androidx.core.graphics.drawable.toDrawable
|
|
22
24
|
import androidx.core.view.ViewCompat
|
|
23
25
|
import androidx.core.view.WindowInsetsCompat
|
|
26
|
+
import cgmblelib.Enum.enumUnBind
|
|
27
|
+
import cgmblelib.ble.gattcallback.GattTool
|
|
24
28
|
import cgmblelib.ble.gattcallback.ManageGattCallback
|
|
25
29
|
import cgmblelib.ble.scancallback.ManageScanCallback
|
|
26
30
|
import cgmblelib.custom.livedata.scan.PocDeviceAndRssi
|
|
27
31
|
import cgmblelib.database.entity.PocDevice
|
|
32
|
+
import cgmblelib.database.repository.RepositoryDevice
|
|
28
33
|
import cgmblelib.utils.SharedPreferencesLibraryUtil
|
|
29
34
|
import com.bumptech.glide.Glide
|
|
30
35
|
import com.facebook.react.bridge.Arguments
|
|
@@ -59,6 +64,8 @@ class ConnectSensorActivity : BaseBleActivity() {
|
|
|
59
64
|
var transmitterDeviceInfo: PocDeviceAndRssi? = null
|
|
60
65
|
private var isForReconnect: Boolean = false
|
|
61
66
|
private val reactContext = CgmTrackyLibModule.mReactContext
|
|
67
|
+
private var isManualInputVisible: Boolean = false
|
|
68
|
+
private var suppressUnbindErrorForSensorRebind = false
|
|
62
69
|
|
|
63
70
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
64
71
|
super.onCreate(savedInstanceState)
|
|
@@ -133,6 +140,128 @@ class ConnectSensorActivity : BaseBleActivity() {
|
|
|
133
140
|
binding.commonButton.btnProceed.alpha = 0.5f
|
|
134
141
|
binding.clPermission.visibility = View.VISIBLE
|
|
135
142
|
}
|
|
143
|
+
|
|
144
|
+
binding.tvEnterIdManually.setOnClickListener {
|
|
145
|
+
toggleManualInput()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
binding.btnConnectManual.setOnClickListener {
|
|
149
|
+
connectSensorManually()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private fun toggleManualInput() {
|
|
154
|
+
isManualInputVisible = !isManualInputVisible
|
|
155
|
+
if (isManualInputVisible) {
|
|
156
|
+
binding.clPermission.visibility = View.GONE
|
|
157
|
+
binding.clManualInput.visibility = View.VISIBLE
|
|
158
|
+
binding.commonButton.root.visibility = View.GONE
|
|
159
|
+
binding.tvCantScanQr.text = "Scan the QR."
|
|
160
|
+
binding.tvEnterIdManually.text = "Scan QR"
|
|
161
|
+
} else {
|
|
162
|
+
binding.clManualInput.visibility = View.GONE
|
|
163
|
+
binding.clPermission.visibility = View.VISIBLE
|
|
164
|
+
binding.commonButton.root.visibility = View.VISIBLE
|
|
165
|
+
binding.tvCantScanQr.setText(R.string.txt_cant_scan_sensor_qr)
|
|
166
|
+
binding.tvEnterIdManually.setText(R.string.txt_enter_id_manually)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private fun connectSensorManually() {
|
|
171
|
+
val sensorId = binding.etSensorId.text.toString().trim().uppercase()
|
|
172
|
+
if (sensorId.isEmpty()) {
|
|
173
|
+
Toast.makeText(this, "Please enter Sensor ID", Toast.LENGTH_SHORT).show()
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
val device = transmitterDeviceInfo?.pocDevice
|
|
178
|
+
if (device == null) {
|
|
179
|
+
Toast.makeText(this, "Transmitter not found. Please restart.", Toast.LENGTH_SHORT).show()
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
val transmitterName = device.name
|
|
184
|
+
if (transmitterName.isNullOrEmpty()) {
|
|
185
|
+
Toast.makeText(this, "Transmitter not found. Please restart.", Toast.LENGTH_SHORT).show()
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
val decoded = try {
|
|
190
|
+
AlgorithmTools.getInstance().decodeCT(sensorId.toCharArray())
|
|
191
|
+
} catch (e: Exception) {
|
|
192
|
+
Log.e("ConnectSensor", "Manual entry decode failed: ${e.message}")
|
|
193
|
+
Toast.makeText(this, "Invalid Sensor ID format", Toast.LENGTH_SHORT).show()
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Match iOS manual entry: decoded CT code must yield usable algorithm coefficients.
|
|
198
|
+
if (decoded.k <= 0f && decoded.r <= 0f) {
|
|
199
|
+
Toast.makeText(this, getString(R.string.verification_fail), Toast.LENGTH_SHORT).show()
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
val prefsHelper = SharedPreferencesLibraryUtil(this)
|
|
204
|
+
prefsHelper.setQRInformation(
|
|
205
|
+
transmitterName,
|
|
206
|
+
sensorId,
|
|
207
|
+
decoded.k,
|
|
208
|
+
decoded.r,
|
|
209
|
+
sensorId
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
// Match QRActivity.showMessage: verify sensor/transmitter pairing before BLE bind.
|
|
213
|
+
val qrInformation = prefsHelper.qrInformation
|
|
214
|
+
val isEffective = qrInformation?.isEffective(transmitterName, decoded.r, decoded.k) ?: false
|
|
215
|
+
safeLogModuleEvent {
|
|
216
|
+
logModuleEvent(
|
|
217
|
+
where = "cgmJourney.cgm_manual_sensor_is_effective",
|
|
218
|
+
message = "CGM journey | screen=${javaClass.simpleName} | manualEntry isEffective=$isEffective",
|
|
219
|
+
level = SentryLogLevel.INFO,
|
|
220
|
+
jsonPayload = sensorId,
|
|
221
|
+
attributes = mapOf(
|
|
222
|
+
"journey_step" to "cgm_manual_sensor_is_effective",
|
|
223
|
+
"screen" to javaClass.simpleName,
|
|
224
|
+
"is_effective" to isEffective.toString(),
|
|
225
|
+
"transmitter_name" to transmitterName,
|
|
226
|
+
"sensor_id" to sensorId,
|
|
227
|
+
),
|
|
228
|
+
logsOnly = true,
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
if (!isEffective || qrInformation == null) {
|
|
232
|
+
Toast.makeText(this, getString(R.string.verification_fail), Toast.LENGTH_SHORT).show()
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
proceedToSensorBind(device)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private fun proceedToSensorBind(device: PocDevice) {
|
|
240
|
+
val lastConnectCgm = CgmTrackyLibModule.lastConnectCgm
|
|
241
|
+
if (lastConnectCgm.equals("Diasens", ignoreCase = true)) {
|
|
242
|
+
validateDiasensSensor(device)
|
|
243
|
+
} else {
|
|
244
|
+
bind(device)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* If the transmitter is already bound (e.g. from a prior failed manual attempt), the BLE stack
|
|
250
|
+
* takes the reset shortcut and skips check/init that programs K/R for the new sensor code.
|
|
251
|
+
* Force-clear the DB bind so the next connection runs the full sensor pairing flow.
|
|
252
|
+
*/
|
|
253
|
+
private fun clearExistingBindIfNeeded() {
|
|
254
|
+
try {
|
|
255
|
+
val existing = RepositoryDevice.getInstance(this).getLatestDeviceIoThread() ?: return
|
|
256
|
+
if (!existing.isBound) {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
suppressUnbindErrorForSensorRebind = true
|
|
260
|
+
GattTool.unBindInDatabaseIoThread(this, existing, enumUnBind.UNBIND_FORCE)
|
|
261
|
+
} catch (e: Exception) {
|
|
262
|
+
suppressUnbindErrorForSensorRebind = false
|
|
263
|
+
Log.w("ConnectSensor", "clearExistingBindIfNeeded: ${e.message}")
|
|
264
|
+
}
|
|
136
265
|
}
|
|
137
266
|
|
|
138
267
|
fun manageErrorState(showError: Boolean, errorStatus: String) {
|
|
@@ -185,10 +314,14 @@ class ConnectSensorActivity : BaseBleActivity() {
|
|
|
185
314
|
binding.btnRetry.btnProceed.setOnClickListener {
|
|
186
315
|
sendDataToRN("", "cgm_retry_connect_sensor_clicked")
|
|
187
316
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
317
|
+
if (isManualInputVisible) {
|
|
318
|
+
manageErrorState(false, "")
|
|
319
|
+
} else {
|
|
320
|
+
transmitterDeviceInfo?.let {
|
|
321
|
+
QRActivity.startQR(
|
|
322
|
+
this, it.pocDevice
|
|
323
|
+
)
|
|
324
|
+
}
|
|
192
325
|
}
|
|
193
326
|
}
|
|
194
327
|
|
|
@@ -533,8 +666,12 @@ class ConnectSensorActivity : BaseBleActivity() {
|
|
|
533
666
|
binding.btnRetry.btnProceed.setOnClickListener {
|
|
534
667
|
sendDataToRN("", "cgm_retry_connect_sensor_clicked")
|
|
535
668
|
|
|
536
|
-
|
|
537
|
-
|
|
669
|
+
if (isManualInputVisible) {
|
|
670
|
+
manageErrorState(false, "")
|
|
671
|
+
} else {
|
|
672
|
+
transmitterDeviceInfo?.let {
|
|
673
|
+
QRActivity.startQR(this, it.pocDevice)
|
|
674
|
+
}
|
|
538
675
|
}
|
|
539
676
|
}
|
|
540
677
|
|
|
@@ -554,6 +691,7 @@ class ConnectSensorActivity : BaseBleActivity() {
|
|
|
554
691
|
private fun bind(device: PocDevice) {
|
|
555
692
|
try {
|
|
556
693
|
if (device.address != null) {
|
|
694
|
+
clearExistingBindIfNeeded()
|
|
557
695
|
ManageGattCallback.getInstance().ConnectViaAddress(device.address)
|
|
558
696
|
ManageScanCallback.getInstance().stopLeScan()
|
|
559
697
|
ProgressManagement.getInstance()
|
|
@@ -686,12 +824,20 @@ class ConnectSensorActivity : BaseBleActivity() {
|
|
|
686
824
|
|
|
687
825
|
override fun unbindReset() {
|
|
688
826
|
super.unbindReset()
|
|
827
|
+
if (suppressUnbindErrorForSensorRebind) {
|
|
828
|
+
suppressUnbindErrorForSensorRebind = false
|
|
829
|
+
return
|
|
830
|
+
}
|
|
689
831
|
manageErrorState(true, "Unbinded")
|
|
690
832
|
|
|
691
833
|
}
|
|
692
834
|
|
|
693
835
|
override fun unbindCommand() {
|
|
694
836
|
super.unbindCommand()
|
|
837
|
+
if (suppressUnbindErrorForSensorRebind) {
|
|
838
|
+
suppressUnbindErrorForSensorRebind = false
|
|
839
|
+
return
|
|
840
|
+
}
|
|
695
841
|
manageErrorState(true, "Unbinded")
|
|
696
842
|
|
|
697
843
|
}
|
|
@@ -246,7 +246,11 @@
|
|
|
246
246
|
app:layout_constraintStart_toStartOf="parent"
|
|
247
247
|
app:layout_constraintTop_toBottomOf="@id/transmitterImages">
|
|
248
248
|
|
|
249
|
-
|
|
249
|
+
<LinearLayout
|
|
250
|
+
android:layout_width="match_parent"
|
|
251
|
+
android:layout_height="wrap_content"
|
|
252
|
+
android:orientation="vertical">
|
|
253
|
+
|
|
250
254
|
<androidx.constraintlayout.widget.ConstraintLayout
|
|
251
255
|
android:id="@+id/clPermission"
|
|
252
256
|
android:layout_width="match_parent"
|
|
@@ -289,7 +293,6 @@
|
|
|
289
293
|
app:layout_constraintTop_toBottomOf="@+id/tvDesc"
|
|
290
294
|
app:layout_constraintVertical_chainStyle="packed" />
|
|
291
295
|
|
|
292
|
-
|
|
293
296
|
<androidx.appcompat.widget.AppCompatImageView
|
|
294
297
|
android:id="@+id/ivProceedArrow"
|
|
295
298
|
android:layout_width="18dp"
|
|
@@ -303,9 +306,121 @@
|
|
|
303
306
|
app:layout_constraintTop_toTopOf="@+id/tvPermission"
|
|
304
307
|
app:tint="@color/color_green" />
|
|
305
308
|
|
|
309
|
+
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
310
|
+
|
|
311
|
+
<androidx.constraintlayout.widget.ConstraintLayout
|
|
312
|
+
android:id="@+id/clManualInput"
|
|
313
|
+
android:layout_width="match_parent"
|
|
314
|
+
android:layout_height="wrap_content"
|
|
315
|
+
android:layout_marginTop="12dp"
|
|
316
|
+
android:background="@drawable/bg_dark_black"
|
|
317
|
+
android:paddingHorizontal="20dp"
|
|
318
|
+
android:paddingVertical="20dp"
|
|
319
|
+
android:visibility="gone">
|
|
320
|
+
|
|
321
|
+
<EditText
|
|
322
|
+
android:id="@+id/etSensorId"
|
|
323
|
+
android:layout_width="0dp"
|
|
324
|
+
android:layout_height="52dp"
|
|
325
|
+
android:background="@drawable/bg_white_corner_10"
|
|
326
|
+
android:fontFamily="@font/roboto_regular"
|
|
327
|
+
android:hint="Enter Sensor ID"
|
|
328
|
+
android:importantForAutofill="no"
|
|
329
|
+
android:inputType="textCapCharacters"
|
|
330
|
+
android:paddingHorizontal="14dp"
|
|
331
|
+
android:textColor="#101828"
|
|
332
|
+
android:textColorHint="#98A2B3"
|
|
333
|
+
android:textSize="14sp"
|
|
334
|
+
app:layout_constraintEnd_toEndOf="parent"
|
|
335
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
336
|
+
app:layout_constraintTop_toTopOf="parent" />
|
|
337
|
+
|
|
338
|
+
<androidx.cardview.widget.CardView
|
|
339
|
+
android:id="@+id/btnConnectManual"
|
|
340
|
+
android:layout_width="0dp"
|
|
341
|
+
android:layout_height="54dp"
|
|
342
|
+
android:layout_marginTop="12dp"
|
|
343
|
+
app:cardBackgroundColor="#2A805A"
|
|
344
|
+
app:cardCornerRadius="12dp"
|
|
345
|
+
app:cardElevation="0dp"
|
|
346
|
+
app:layout_constraintEnd_toEndOf="parent"
|
|
347
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
348
|
+
app:layout_constraintTop_toBottomOf="@+id/etSensorId">
|
|
349
|
+
|
|
350
|
+
<androidx.constraintlayout.widget.ConstraintLayout
|
|
351
|
+
android:layout_width="match_parent"
|
|
352
|
+
android:layout_height="match_parent"
|
|
353
|
+
android:background="@drawable/bg_green_border_20">
|
|
354
|
+
|
|
355
|
+
<TextView
|
|
356
|
+
android:id="@+id/tvConnectManual"
|
|
357
|
+
android:layout_width="wrap_content"
|
|
358
|
+
android:layout_height="wrap_content"
|
|
359
|
+
android:fontFamily="@font/roboto_semibold"
|
|
360
|
+
android:text="Connect"
|
|
361
|
+
android:textColor="#FFFFFF"
|
|
362
|
+
android:textSize="16sp"
|
|
363
|
+
app:layout_constraintBottom_toBottomOf="parent"
|
|
364
|
+
app:layout_constraintEnd_toEndOf="parent"
|
|
365
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
366
|
+
app:layout_constraintTop_toTopOf="parent" />
|
|
367
|
+
|
|
368
|
+
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
369
|
+
|
|
370
|
+
</androidx.cardview.widget.CardView>
|
|
371
|
+
|
|
372
|
+
<TextView
|
|
373
|
+
android:id="@+id/tvManualEntryHint"
|
|
374
|
+
android:layout_width="0dp"
|
|
375
|
+
android:layout_height="wrap_content"
|
|
376
|
+
android:layout_marginTop="10dp"
|
|
377
|
+
android:fontFamily="@font/roboto_regular"
|
|
378
|
+
android:gravity="center"
|
|
379
|
+
android:text="Scan the QR with mobile camera to get a sensor ID."
|
|
380
|
+
android:textAlignment="center"
|
|
381
|
+
android:textColor="#98A2B3"
|
|
382
|
+
android:textSize="12sp"
|
|
383
|
+
app:layout_constraintBottom_toBottomOf="parent"
|
|
384
|
+
app:layout_constraintEnd_toEndOf="parent"
|
|
385
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
386
|
+
app:layout_constraintTop_toBottomOf="@+id/btnConnectManual" />
|
|
306
387
|
|
|
307
388
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
308
389
|
|
|
390
|
+
<LinearLayout
|
|
391
|
+
android:id="@+id/clManualEntry"
|
|
392
|
+
android:layout_width="match_parent"
|
|
393
|
+
android:layout_height="wrap_content"
|
|
394
|
+
android:layout_marginTop="12dp"
|
|
395
|
+
android:background="@drawable/bg_manual_entry_bar"
|
|
396
|
+
android:gravity="center"
|
|
397
|
+
android:orientation="horizontal"
|
|
398
|
+
android:paddingHorizontal="16dp"
|
|
399
|
+
android:paddingVertical="14dp">
|
|
400
|
+
|
|
401
|
+
<TextView
|
|
402
|
+
android:id="@+id/tvCantScanQr"
|
|
403
|
+
android:layout_width="wrap_content"
|
|
404
|
+
android:layout_height="wrap_content"
|
|
405
|
+
android:fontFamily="@font/roboto_regular"
|
|
406
|
+
android:text="@string/txt_cant_scan_sensor_qr"
|
|
407
|
+
android:textColor="#2D3282"
|
|
408
|
+
android:textSize="14sp" />
|
|
409
|
+
|
|
410
|
+
<TextView
|
|
411
|
+
android:id="@+id/tvEnterIdManually"
|
|
412
|
+
android:layout_width="wrap_content"
|
|
413
|
+
android:layout_height="wrap_content"
|
|
414
|
+
android:layout_marginStart="4dp"
|
|
415
|
+
android:fontFamily="@font/roboto_bold"
|
|
416
|
+
android:text="@string/txt_enter_id_manually"
|
|
417
|
+
android:textColor="@color/color_green"
|
|
418
|
+
android:textSize="14sp" />
|
|
419
|
+
|
|
420
|
+
</LinearLayout>
|
|
421
|
+
|
|
422
|
+
</LinearLayout>
|
|
423
|
+
|
|
309
424
|
</FrameLayout>
|
|
310
425
|
|
|
311
426
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
//
|
|
2
2
|
// KLTBluetoothManager.m
|
|
3
3
|
// KaiLiTe
|
|
4
4
|
//
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
// 蓝牙管理状态
|
|
29
29
|
@property (nonatomic, assign) CBManagerState centralManagerState;
|
|
30
30
|
|
|
31
|
+
- (BOOL)klt_safeWriteData:(NSData *)data withType:(CBCharacteristicWriteType)type;
|
|
32
|
+
|
|
31
33
|
@end
|
|
32
34
|
|
|
33
35
|
@implementation KLTBluetoothManager
|
|
@@ -71,6 +73,19 @@
|
|
|
71
73
|
return self;
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
/// NSTimer must not be invalidated synchronously from its own fired callback — that
|
|
77
|
+
/// can block the main thread for seconds (Sentry AppHang in -[KLTBluetoothManager endScan]).
|
|
78
|
+
- (void)invalidateConnectTimerSafely {
|
|
79
|
+
NSTimer *timer = _connectTimer;
|
|
80
|
+
_connectTimer = nil;
|
|
81
|
+
if (!timer) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
85
|
+
[timer invalidate];
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
+ (KLTBluetoothManager *)sharedManager {
|
|
75
90
|
static KLTBluetoothManager *bluetooth;
|
|
76
91
|
static dispatch_once_t onceToken;
|
|
@@ -81,6 +96,21 @@
|
|
|
81
96
|
return bluetooth;
|
|
82
97
|
}
|
|
83
98
|
|
|
99
|
+
- (BOOL)klt_safeWriteData:(NSData *)data withType:(CBCharacteristicWriteType)type {
|
|
100
|
+
if (!data || data.length == 0) {
|
|
101
|
+
return NO;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
CBPeripheral *peripheral = self.connectedPeripheral;
|
|
105
|
+
CBCharacteristic *characteristic = self.writeReadCharacteristic;
|
|
106
|
+
if (!peripheral || peripheral.state != CBPeripheralStateConnected || !characteristic) {
|
|
107
|
+
KLTLog(@"BLE write skipped - peripheral or write characteristic unavailable");
|
|
108
|
+
return NO;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
[peripheral writeValue:data forCharacteristic:characteristic type:type];
|
|
112
|
+
return YES;
|
|
113
|
+
}
|
|
84
114
|
|
|
85
115
|
#pragma mark - cbcenteralmanager delegate
|
|
86
116
|
|
|
@@ -476,7 +506,8 @@
|
|
|
476
506
|
Device *currentDevice = self.currentDevice;
|
|
477
507
|
if (KLTLocalSettingManager.shareInstance.canConnectOtherDevice) {
|
|
478
508
|
// 直接开始自检协议
|
|
479
|
-
[self
|
|
509
|
+
[self klt_safeWriteData:[ProtocalTools check_request:self.eDevice]
|
|
510
|
+
withType:CBCharacteristicWriteWithoutResponse];
|
|
480
511
|
} else {
|
|
481
512
|
// 二次重连 0x22快传批量数据
|
|
482
513
|
NSInteger maxGlucoseId = [[KLTDatabaseHandler shared] getLatestAndMaxGlucoseIdOfDevice:currentDevice];
|
|
@@ -515,7 +546,9 @@
|
|
|
515
546
|
}
|
|
516
547
|
|
|
517
548
|
NSData *data = [ProtocalTools pullGlucose_series_request:glucoseId + 1 andNumber:pullNums andEDevice:self.eDevice];
|
|
518
|
-
[self
|
|
549
|
+
if (![self klt_safeWriteData:data withType:CBCharacteristicWriteWithoutResponse]) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
519
552
|
self.status = BluetoothManagerStatusResendStart;
|
|
520
553
|
self.isInResend = YES;
|
|
521
554
|
}
|
|
@@ -532,7 +565,8 @@
|
|
|
532
565
|
[Notification_Center postNotificationName:@"DidHandleResendData" object:nil];
|
|
533
566
|
|
|
534
567
|
// CT3/CT4发送重置协议这条命令0x11,结束重传状态
|
|
535
|
-
[self
|
|
568
|
+
[self klt_safeWriteData:[ProtocalTools reset_request:self.eDevice]
|
|
569
|
+
withType:CBCharacteristicWriteWithoutResponse];
|
|
536
570
|
} else {
|
|
537
571
|
for (Glucose *glucose in glucoseList) {
|
|
538
572
|
ReceiveData *receive = [self handleCommonReceiveGluData:glucose commandText:@"22"];
|
|
@@ -553,7 +587,7 @@
|
|
|
553
587
|
pullNums = remain_points;
|
|
554
588
|
}
|
|
555
589
|
NSData *data = [ProtocalTools pullGlucose_series_request:maxGlucoseId + 1 andNumber:pullNums andEDevice:self.eDevice];
|
|
556
|
-
[self
|
|
590
|
+
[self klt_safeWriteData:data withType:CBCharacteristicWriteWithoutResponse];
|
|
557
591
|
}
|
|
558
592
|
}
|
|
559
593
|
}
|
|
@@ -595,8 +629,7 @@
|
|
|
595
629
|
[self.peripherals removeAllObjects];
|
|
596
630
|
self.connectedPeripheral = nil;
|
|
597
631
|
|
|
598
|
-
[
|
|
599
|
-
_connectTimer = nil;
|
|
632
|
+
[self invalidateConnectTimerSafely];
|
|
600
633
|
[_rescanTimer invalidate];
|
|
601
634
|
_rescanTimer = nil;
|
|
602
635
|
|
|
@@ -610,6 +643,14 @@
|
|
|
610
643
|
#pragma mark - other method
|
|
611
644
|
|
|
612
645
|
- (void)endScan {
|
|
646
|
+
// endScan is the _connectTimer target. Defer to the next run-loop turn so we never
|
|
647
|
+
// invalidate the firing timer or run stopScan/KVO inside the timer callback.
|
|
648
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
649
|
+
[self performEndScanAfterTimer];
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
- (void)performEndScanAfterTimer {
|
|
613
654
|
// 20s后扫描设备超时未连上再次尝试连接
|
|
614
655
|
if (self.currentDevice && BluetoothManagerStatusScanStart == self.status) {
|
|
615
656
|
for (CBPeripheral *per in self.peripherals) {
|
|
@@ -620,7 +661,7 @@
|
|
|
620
661
|
}
|
|
621
662
|
}
|
|
622
663
|
}
|
|
623
|
-
|
|
664
|
+
|
|
624
665
|
[self stopScan];
|
|
625
666
|
self.status = BluetoothManagerStatusTimeOut;
|
|
626
667
|
}
|
|
@@ -630,8 +671,7 @@
|
|
|
630
671
|
self.status = BluetoothManagerStatusScanStop;
|
|
631
672
|
[self.centralManager stopScan];
|
|
632
673
|
|
|
633
|
-
[
|
|
634
|
-
_connectTimer = nil;
|
|
674
|
+
[self invalidateConnectTimerSafely];
|
|
635
675
|
}
|
|
636
676
|
|
|
637
677
|
// 开始扫描
|
|
@@ -678,23 +718,38 @@
|
|
|
678
718
|
//}
|
|
679
719
|
|
|
680
720
|
- (void)startScan {
|
|
721
|
+
// Defer to the next main-queue run-loop turn so scanForPeripheralsWithServices is
|
|
722
|
+
// never invoked synchronously from an NSTimer callback (Sentry AppHang on iOS).
|
|
723
|
+
// CBCentralManager is bound to the main queue — this does not move BLE off main.
|
|
724
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
725
|
+
[self performStartScan];
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
- (void)performStartScan {
|
|
730
|
+
if (self.status == BluetoothManagerStatusScanStart) {
|
|
731
|
+
KLTLog(@"startScan skipped — scan already in progress");
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
|
|
681
735
|
self.connectedPeripheral = nil;
|
|
682
736
|
[self.peripherals removeAllObjects];
|
|
683
|
-
|
|
737
|
+
|
|
684
738
|
if (self.centralManager.state != CBManagerStatePoweredOn) {
|
|
685
739
|
NSLog(@"Bluetooth is not powered on. Aborting scan.");
|
|
686
740
|
// Do not change status here – just wait for BluetoothEnable notification
|
|
687
741
|
// The UI layer listens for BluetoothEnable and will start scan once BLE is ready.
|
|
688
742
|
return;
|
|
689
743
|
}
|
|
690
|
-
|
|
744
|
+
|
|
691
745
|
NSLog(@"Allen manager start scan %d", __LINE__);
|
|
692
746
|
self.status = BluetoothManagerStatusScanStart;
|
|
693
747
|
self.isBgForceConnectDevice = NO;
|
|
694
|
-
|
|
748
|
+
|
|
695
749
|
NSDictionary *scanOptions = @{ CBCentralManagerScanOptionAllowDuplicatesKey: @(YES) };
|
|
696
750
|
[self.centralManager scanForPeripheralsWithServices:nil options:scanOptions];
|
|
697
|
-
|
|
751
|
+
|
|
752
|
+
[self invalidateConnectTimerSafely];
|
|
698
753
|
_connectTimer = [NSTimer scheduledTimerWithTimeInterval:20.0
|
|
699
754
|
target:self
|
|
700
755
|
selector:@selector(endScan)
|
|
@@ -7,58 +7,191 @@
|
|
|
7
7
|
|
|
8
8
|
import UIKit
|
|
9
9
|
import AVFoundation
|
|
10
|
+
|
|
10
11
|
class ImageTVC: UITableViewCell {
|
|
11
12
|
|
|
12
13
|
@IBOutlet weak var bottomConstraint: NSLayoutConstraint!
|
|
13
14
|
@IBOutlet weak var displayImageView: UIImageView!
|
|
14
15
|
@IBOutlet weak var topConstraint: NSLayoutConstraint!
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
@IBOutlet var gifView: UIImageView!
|
|
17
18
|
@IBOutlet weak var cameraView: CustomView!
|
|
18
19
|
@IBOutlet weak var cameraTitleLabel: UILabel!
|
|
19
20
|
@IBOutlet weak var cameraDescLabel: UILabel!
|
|
20
|
-
|
|
21
|
+
|
|
21
22
|
@IBOutlet var ic_green_arrow: UIImageView!
|
|
22
23
|
var scannerView: QRScannerView!
|
|
24
|
+
|
|
25
|
+
var onConnectTapped: ((String) -> Void)?
|
|
26
|
+
var onTextFieldDidBeginEditing: (() -> Void)?
|
|
27
|
+
|
|
28
|
+
private var manualEntryView: UIView?
|
|
29
|
+
private var sensorIdTextField: UITextField?
|
|
30
|
+
private var isManualEntryVisible = false
|
|
31
|
+
|
|
32
|
+
var activeSensorIdTextField: UITextField? {
|
|
33
|
+
isManualEntryVisible ? sensorIdTextField : nil
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
override func awakeFromNib() {
|
|
24
37
|
super.awakeFromNib()
|
|
25
38
|
gifView.isHidden = true
|
|
26
39
|
ic_green_arrow.image = loadImage(named: "ic_green_arrow")
|
|
27
|
-
// Initialization code
|
|
28
40
|
cameraTitleLabel.text = "GoodFlip requires camera permission to scan the QR and connect with the sensor"
|
|
29
|
-
|
|
30
41
|
cameraTitleLabel.font = FontManager.font(ofSize: 14, weight: .medium)
|
|
31
|
-
|
|
32
42
|
cameraTitleLabel.textColor = .white
|
|
33
|
-
|
|
34
|
-
let first = "Provide Camera Permissions"
|
|
35
43
|
|
|
44
|
+
let first = "Provide Camera Permissions"
|
|
36
45
|
let attributedString = NSMutableAttributedString(string: first, attributes: [
|
|
37
46
|
.font: FontManager.font(ofSize: 16, weight: .semibold),
|
|
38
47
|
.foregroundColor: CustomColor.shared.lightGreen,
|
|
39
48
|
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
|
40
49
|
])
|
|
41
|
-
|
|
42
50
|
cameraDescLabel.attributedText = attributedString
|
|
51
|
+
|
|
52
|
+
setupManualEntryView()
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
override func setSelected(_ selected: Bool, animated: Bool) {
|
|
46
56
|
super.setSelected(selected, animated: animated)
|
|
57
|
+
}
|
|
47
58
|
|
|
48
|
-
|
|
59
|
+
override func prepareForReuse() {
|
|
60
|
+
super.prepareForReuse()
|
|
61
|
+
onConnectTapped = nil
|
|
62
|
+
onTextFieldDidBeginEditing = nil
|
|
63
|
+
scannerView?.stopScanning()
|
|
64
|
+
scannerView?.removeFromSuperview()
|
|
65
|
+
scannerView = nil
|
|
66
|
+
manualEntryView?.isHidden = true
|
|
67
|
+
sensorIdTextField?.text = nil
|
|
68
|
+
isManualEntryVisible = false
|
|
49
69
|
}
|
|
50
|
-
|
|
70
|
+
|
|
71
|
+
// MARK: - Manual Entry UI Setup
|
|
72
|
+
|
|
73
|
+
private func setupManualEntryView() {
|
|
74
|
+
let container = UIView()
|
|
75
|
+
container.translatesAutoresizingMaskIntoConstraints = false
|
|
76
|
+
container.backgroundColor = UIColor(red: 16/255, green: 24/255, blue: 40/255, alpha: 1)
|
|
77
|
+
container.layer.cornerRadius = 12
|
|
78
|
+
container.isHidden = true
|
|
79
|
+
contentView.addSubview(container)
|
|
80
|
+
|
|
81
|
+
let textField = UITextField()
|
|
82
|
+
textField.translatesAutoresizingMaskIntoConstraints = false
|
|
83
|
+
textField.placeholder = "Enter Sensor ID"
|
|
84
|
+
textField.autocapitalizationType = .allCharacters
|
|
85
|
+
textField.autocorrectionType = .no
|
|
86
|
+
textField.returnKeyType = .done
|
|
87
|
+
textField.font = FontManager.font(ofSize: 14, weight: .regular)
|
|
88
|
+
textField.textColor = UIColor(red: 16/255, green: 24/255, blue: 40/255, alpha: 1)
|
|
89
|
+
textField.backgroundColor = .white
|
|
90
|
+
textField.layer.cornerRadius = 8
|
|
91
|
+
textField.layer.masksToBounds = true
|
|
92
|
+
textField.delegate = self
|
|
93
|
+
let leftPad = UIView(frame: CGRect(x: 0, y: 0, width: 14, height: 1))
|
|
94
|
+
textField.leftView = leftPad
|
|
95
|
+
textField.leftViewMode = .always
|
|
96
|
+
textField.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 14, height: 1))
|
|
97
|
+
textField.rightViewMode = .always
|
|
98
|
+
container.addSubview(textField)
|
|
99
|
+
|
|
100
|
+
let connectButton = UIButton(type: .custom)
|
|
101
|
+
connectButton.translatesAutoresizingMaskIntoConstraints = false
|
|
102
|
+
connectButton.setTitle("Connect", for: .normal)
|
|
103
|
+
connectButton.setTitleColor(.white, for: .normal)
|
|
104
|
+
connectButton.titleLabel?.font = FontManager.font(ofSize: 16, weight: .semibold)
|
|
105
|
+
connectButton.backgroundColor = CustomColor.shared.lightGreen
|
|
106
|
+
connectButton.layer.cornerRadius = 12
|
|
107
|
+
connectButton.addTarget(self, action: #selector(connectButtonTapped), for: .touchUpInside)
|
|
108
|
+
container.addSubview(connectButton)
|
|
109
|
+
|
|
110
|
+
let hintLabel = UILabel()
|
|
111
|
+
hintLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
112
|
+
hintLabel.text = "Scan the QR with mobile camera to get a sensor ID."
|
|
113
|
+
hintLabel.font = FontManager.font(ofSize: 12, weight: .regular)
|
|
114
|
+
hintLabel.textColor = UIColor(white: 1, alpha: 0.55)
|
|
115
|
+
hintLabel.textAlignment = .center
|
|
116
|
+
hintLabel.numberOfLines = 0
|
|
117
|
+
container.addSubview(hintLabel)
|
|
118
|
+
|
|
119
|
+
NSLayoutConstraint.activate([
|
|
120
|
+
textField.topAnchor.constraint(equalTo: container.topAnchor, constant: 16),
|
|
121
|
+
textField.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
|
|
122
|
+
textField.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
|
|
123
|
+
textField.heightAnchor.constraint(equalToConstant: 48),
|
|
124
|
+
|
|
125
|
+
connectButton.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 12),
|
|
126
|
+
connectButton.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
|
|
127
|
+
connectButton.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
|
|
128
|
+
connectButton.heightAnchor.constraint(equalToConstant: 52),
|
|
129
|
+
|
|
130
|
+
hintLabel.topAnchor.constraint(equalTo: connectButton.bottomAnchor, constant: 10),
|
|
131
|
+
hintLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
|
|
132
|
+
hintLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
|
|
133
|
+
hintLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -16),
|
|
134
|
+
|
|
135
|
+
container.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
|
136
|
+
container.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
|
137
|
+
container.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
|
|
138
|
+
container.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -8),
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
manualEntryView = container
|
|
142
|
+
sensorIdTextField = textField
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// MARK: - Public API
|
|
146
|
+
|
|
147
|
+
func configureAsStaticImage() {
|
|
148
|
+
isManualEntryVisible = false
|
|
149
|
+
manualEntryView?.isHidden = true
|
|
150
|
+
cameraView.isHidden = true
|
|
151
|
+
scannerView?.stopScanning()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func configure(isManualEntry: Bool) {
|
|
155
|
+
isManualEntryVisible = isManualEntry
|
|
156
|
+
if isManualEntry {
|
|
157
|
+
cameraView.isHidden = true
|
|
158
|
+
manualEntryView?.isHidden = false
|
|
159
|
+
scannerView?.stopScanning()
|
|
160
|
+
} else {
|
|
161
|
+
cameraView.isHidden = false
|
|
162
|
+
manualEntryView?.isHidden = true
|
|
163
|
+
sensorIdTextField?.text = nil
|
|
164
|
+
sensorIdTextField?.resignFirstResponder()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
51
168
|
func setupQRView() {
|
|
52
169
|
scannerView = QRScannerView(frame: cameraView.bounds)
|
|
53
170
|
scannerView.onCodeDetected = { code in
|
|
54
171
|
print("Detected QR Code: \(code)")
|
|
55
|
-
// You can stop scanning or navigate
|
|
56
172
|
self.scannerView.stopScanning()
|
|
57
173
|
}
|
|
58
|
-
|
|
59
174
|
cameraView.addSubview(scannerView)
|
|
60
175
|
}
|
|
61
|
-
|
|
176
|
+
|
|
177
|
+
// MARK: - Actions
|
|
178
|
+
|
|
179
|
+
@objc private func connectButtonTapped() {
|
|
180
|
+
sensorIdTextField?.resignFirstResponder()
|
|
181
|
+
let sensorId = (sensorIdTextField?.text ?? "").trimmingCharacters(in: .whitespaces).uppercased()
|
|
182
|
+
onConnectTapped?(sensorId)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
extension ImageTVC: UITextFieldDelegate {
|
|
187
|
+
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
188
|
+
textField.resignFirstResponder()
|
|
189
|
+
return true
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func textFieldDidBeginEditing(_ textField: UITextField) {
|
|
193
|
+
onTextFieldDidBeginEditing?()
|
|
194
|
+
}
|
|
62
195
|
}
|
|
63
196
|
|
|
64
197
|
class QRScannerView: UIView {
|
|
@@ -90,7 +223,6 @@ class QRScannerView: UIView {
|
|
|
90
223
|
let metadataOutput = AVCaptureMetadataOutput()
|
|
91
224
|
if captureSession.canAddOutput(metadataOutput) {
|
|
92
225
|
captureSession.addOutput(metadataOutput)
|
|
93
|
-
|
|
94
226
|
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
|
95
227
|
metadataOutput.metadataObjectTypes = [.qr]
|
|
96
228
|
}
|
|
@@ -100,7 +232,7 @@ class QRScannerView: UIView {
|
|
|
100
232
|
previewLayer.videoGravity = .resizeAspectFill
|
|
101
233
|
layer.addSublayer(previewLayer)
|
|
102
234
|
|
|
103
|
-
|
|
235
|
+
captureSession.startRunning()
|
|
104
236
|
}
|
|
105
237
|
|
|
106
238
|
func stopScanning() {
|
|
@@ -26,6 +26,7 @@ class ConnectToSensorViewController: UIViewController {
|
|
|
26
26
|
case verticalLabel
|
|
27
27
|
case image
|
|
28
28
|
case camera
|
|
29
|
+
case manualEntryBar
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
enum enumSuccessTableRow: Int, CaseIterable {
|
|
@@ -52,19 +53,33 @@ class ConnectToSensorViewController: UIViewController {
|
|
|
52
53
|
var screenType: enumScreenType = .camera
|
|
53
54
|
var isCodeDetected = false
|
|
54
55
|
var isPermissionGiven: Bool = false
|
|
56
|
+
var isManualEntryMode: Bool = false
|
|
57
|
+
private var keyboardInset: CGFloat = 0
|
|
58
|
+
private var keyboardObserversRegistered = false
|
|
55
59
|
let manager = KLTBluetoothManager.shared()
|
|
56
60
|
let debouncer = EnumDebouncer<CGMConnectionStatus>()
|
|
57
61
|
var deviceVerificationErrorMessage: String? = nil
|
|
58
62
|
|
|
59
63
|
override func viewDidLoad() {
|
|
60
64
|
super.viewDidLoad()
|
|
61
|
-
// Do any additional setup after loading the view.
|
|
62
65
|
setupLayout()
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
override func viewWillAppear(_ animated: Bool) {
|
|
66
69
|
super.viewWillAppear(animated)
|
|
67
70
|
CustomTopViewSupportHelper.configureWhatsapp(customTopView: customTopView)
|
|
71
|
+
registerKeyboardObserversIfNeeded()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override func viewWillDisappear(_ animated: Bool) {
|
|
75
|
+
super.viewWillDisappear(animated)
|
|
76
|
+
removeKeyboardObservers()
|
|
77
|
+
resetKeyboardInsets()
|
|
78
|
+
view.endEditing(true)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
deinit {
|
|
82
|
+
removeKeyboardObservers()
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
func setupLayout() {
|
|
@@ -73,6 +88,7 @@ class ConnectToSensorViewController: UIViewController {
|
|
|
73
88
|
|
|
74
89
|
tableView.delegate = self
|
|
75
90
|
tableView.dataSource = self
|
|
91
|
+
tableView.keyboardDismissMode = .interactive
|
|
76
92
|
tableView.registerNibs([VerticalLabelsTVC.self,
|
|
77
93
|
SeparatorTVC.self,
|
|
78
94
|
ImageTVC.self,
|
|
@@ -82,6 +98,7 @@ class ConnectToSensorViewController: UIViewController {
|
|
|
82
98
|
NoteTVC.self,
|
|
83
99
|
PodDetailsTVC.self,
|
|
84
100
|
WatchVideoTVC.self])
|
|
101
|
+
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "ManualEntryBarCell")
|
|
85
102
|
tableView.reloadData()
|
|
86
103
|
contactSupport.isHidden = screenType != .error
|
|
87
104
|
contactSupport.labelText = "Contact Support"
|
|
@@ -242,7 +259,7 @@ class ConnectToSensorViewController: UIViewController {
|
|
|
242
259
|
let data = AlgorithmTools.decodeCT(value)
|
|
243
260
|
|
|
244
261
|
if data.k > 0 || data.r > 0 {
|
|
245
|
-
|
|
262
|
+
KLTLocalSettingManager.shareInstance().deviceInfoSensor = value
|
|
246
263
|
UserDefaults.standard.set(data.k, forKey: "algo_k")
|
|
247
264
|
UserDefaults.standard.set(data.r, forKey: "algo_r")
|
|
248
265
|
UserDefaults.standard.synchronize()
|
|
@@ -250,18 +267,22 @@ class ConnectToSensorViewController: UIViewController {
|
|
|
250
267
|
let lastConnectCgm = UserDefaults.standard.string(forKey: "lastConnectCgm") ?? ""
|
|
251
268
|
|
|
252
269
|
if lastConnectCgm.lowercased() == "diasens" {
|
|
253
|
-
// Diasens user - validate sensor via API before connecting
|
|
254
270
|
validateDiasensSensor(sensorId: value)
|
|
255
271
|
} else {
|
|
256
|
-
// Non-Diasens user - proceed directly
|
|
257
272
|
viewModel.debouncer.update(with: .connected)
|
|
258
273
|
self.showConfirmInsulinUser()
|
|
259
274
|
}
|
|
260
275
|
return
|
|
261
276
|
}
|
|
277
|
+
|
|
278
|
+
// decodeCT returned k=0 and r=0 — sensor QR is not effective for this transmitter
|
|
279
|
+
showSensorInvalidAlert(message: "This sensor is not compatible with your transmitter. Please verify the sensor QR.")
|
|
280
|
+
isCodeDetected = false
|
|
281
|
+
return
|
|
262
282
|
}
|
|
263
283
|
|
|
264
|
-
//
|
|
284
|
+
// Sensor ID format is invalid
|
|
285
|
+
showSensorInvalidAlert(message: "The scanned sensor ID is not valid. Please try again or enter the sensor ID manually.")
|
|
265
286
|
}
|
|
266
287
|
|
|
267
288
|
func validateDiasensSensor(sensorId: String) {
|
|
@@ -319,7 +340,99 @@ class ConnectToSensorViewController: UIViewController {
|
|
|
319
340
|
// Connect the transmitter
|
|
320
341
|
manager?.connectPeripheral(manager?.readyToConnectedPeripheral)
|
|
321
342
|
}
|
|
343
|
+
|
|
344
|
+
@objc func toggleManualEntry() {
|
|
345
|
+
isManualEntryMode.toggle()
|
|
346
|
+
if !isManualEntryMode {
|
|
347
|
+
resetKeyboardInsets()
|
|
348
|
+
view.endEditing(true)
|
|
349
|
+
}
|
|
350
|
+
tableView.reloadData()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private var manualEntryIndexPath: IndexPath {
|
|
354
|
+
IndexPath(row: enumTableRow.camera.rawValue, section: enumTableSection.details.rawValue)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private func registerKeyboardObserversIfNeeded() {
|
|
358
|
+
guard !keyboardObserversRegistered else { return }
|
|
359
|
+
keyboardObserversRegistered = true
|
|
360
|
+
NotificationCenter.default.addObserver(
|
|
361
|
+
self,
|
|
362
|
+
selector: #selector(keyboardWillShow(_:)),
|
|
363
|
+
name: UIResponder.keyboardWillShowNotification,
|
|
364
|
+
object: nil
|
|
365
|
+
)
|
|
366
|
+
NotificationCenter.default.addObserver(
|
|
367
|
+
self,
|
|
368
|
+
selector: #selector(keyboardWillHide(_:)),
|
|
369
|
+
name: UIResponder.keyboardWillHideNotification,
|
|
370
|
+
object: nil
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private func removeKeyboardObservers() {
|
|
375
|
+
guard keyboardObserversRegistered else { return }
|
|
376
|
+
keyboardObserversRegistered = false
|
|
377
|
+
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
|
|
378
|
+
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private func resetKeyboardInsets() {
|
|
382
|
+
keyboardInset = 0
|
|
383
|
+
tableView.contentInset.bottom = 0
|
|
384
|
+
tableView.verticalScrollIndicatorInsets.bottom = 0
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@objc private func keyboardWillShow(_ notification: Notification) {
|
|
388
|
+
guard isManualEntryMode,
|
|
389
|
+
let userInfo = notification.userInfo,
|
|
390
|
+
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let convertedFrame = view.convert(keyboardFrame, from: nil)
|
|
395
|
+
let inset = max(0, view.bounds.maxY - convertedFrame.minY - view.safeAreaInsets.bottom)
|
|
396
|
+
keyboardInset = inset
|
|
397
|
+
tableView.contentInset.bottom = inset
|
|
398
|
+
tableView.verticalScrollIndicatorInsets.bottom = inset
|
|
399
|
+
|
|
400
|
+
let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.25
|
|
401
|
+
UIView.animate(withDuration: duration) {
|
|
402
|
+
self.scrollManualEntryTextFieldAboveKeyboard(animated: false)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@objc private func keyboardWillHide(_ notification: Notification) {
|
|
407
|
+
guard let userInfo = notification.userInfo else {
|
|
408
|
+
resetKeyboardInsets()
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.25
|
|
412
|
+
UIView.animate(withDuration: duration) {
|
|
413
|
+
self.resetKeyboardInsets()
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private func scrollManualEntryTextFieldAboveKeyboard(animated: Bool) {
|
|
418
|
+
guard isManualEntryMode,
|
|
419
|
+
let cell = tableView.cellForRow(at: manualEntryIndexPath) as? ImageTVC,
|
|
420
|
+
let textField = cell.activeSensorIdTextField else {
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
let fieldRect = textField.convert(textField.bounds, to: tableView)
|
|
425
|
+
tableView.scrollRectToVisible(fieldRect.insetBy(dx: 0, dy: -12), animated: animated)
|
|
426
|
+
}
|
|
322
427
|
|
|
428
|
+
private func showSensorInvalidAlert(message: String) {
|
|
429
|
+
DispatchQueue.main.async {
|
|
430
|
+
let alert = UIAlertController(title: "Invalid Sensor", message: message, preferredStyle: .alert)
|
|
431
|
+
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
|
432
|
+
self.present(alert, animated: true)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
323
436
|
func showConfirmInsulinUser() {
|
|
324
437
|
let typeOfDiabetes = UserDefaults.standard.object(forKey: "profilePatientType") as? NSNumber
|
|
325
438
|
|
|
@@ -457,7 +570,9 @@ extension ConnectToSensorViewController: UITableViewDelegate, UITableViewDataSou
|
|
|
457
570
|
case .image:
|
|
458
571
|
return (UIScreen.main.bounds.width * 120 / 390) + 40
|
|
459
572
|
case .camera:
|
|
460
|
-
return 220
|
|
573
|
+
return isManualEntryMode ? 200 : 220
|
|
574
|
+
case .manualEntryBar:
|
|
575
|
+
return 60
|
|
461
576
|
default:
|
|
462
577
|
return UITableView.automaticDimension
|
|
463
578
|
}
|
|
@@ -491,17 +606,35 @@ extension ConnectToSensorViewController: UITableViewDelegate, UITableViewDataSou
|
|
|
491
606
|
return cell
|
|
492
607
|
case .image:
|
|
493
608
|
let cell: ImageTVC = tableView.dequeueReusableCell(for: indexPath)
|
|
609
|
+
cell.configureAsStaticImage()
|
|
494
610
|
cell.displayImageView.image = loadImage(named: "flip_the_reference")
|
|
495
611
|
cell.displayImageView.contentMode = .scaleAspectFill
|
|
496
612
|
cell.displayImageView.isHidden = false
|
|
497
|
-
cell.cameraView.isHidden = true
|
|
498
613
|
return cell
|
|
499
614
|
case .camera:
|
|
500
615
|
let cell: ImageTVC = tableView.dequeueReusableCell(for: indexPath)
|
|
501
616
|
cell.displayImageView.contentMode = .scaleAspectFill
|
|
502
617
|
cell.displayImageView.isHidden = true
|
|
503
|
-
cell.
|
|
504
|
-
if
|
|
618
|
+
cell.configure(isManualEntry: isManualEntryMode)
|
|
619
|
+
if isManualEntryMode {
|
|
620
|
+
cell.onTextFieldDidBeginEditing = { [weak self] in
|
|
621
|
+
self?.scrollManualEntryTextFieldAboveKeyboard(animated: true)
|
|
622
|
+
}
|
|
623
|
+
cell.onConnectTapped = { [weak self] sensorId in
|
|
624
|
+
guard let self = self else { return }
|
|
625
|
+
guard !sensorId.isEmpty else {
|
|
626
|
+
let alert = UIAlertController(
|
|
627
|
+
title: nil,
|
|
628
|
+
message: "Please enter a Sensor ID",
|
|
629
|
+
preferredStyle: .alert
|
|
630
|
+
)
|
|
631
|
+
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
|
632
|
+
self.present(alert, animated: true)
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
self.connectSensor(value: sensorId, controller: self)
|
|
636
|
+
}
|
|
637
|
+
} else if isPermissionGiven {
|
|
505
638
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
506
639
|
cell.setupQRView()
|
|
507
640
|
cell.scannerView?.onCodeDetected = { data in
|
|
@@ -513,6 +646,59 @@ extension ConnectToSensorViewController: UITableViewDelegate, UITableViewDataSou
|
|
|
513
646
|
}
|
|
514
647
|
}
|
|
515
648
|
return cell
|
|
649
|
+
case .manualEntryBar:
|
|
650
|
+
let cell = tableView.dequeueReusableCell(withIdentifier: "ManualEntryBarCell")!
|
|
651
|
+
cell.selectionStyle = .none
|
|
652
|
+
cell.backgroundColor = .clear
|
|
653
|
+
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
|
654
|
+
|
|
655
|
+
let bar = UIView()
|
|
656
|
+
bar.translatesAutoresizingMaskIntoConstraints = false
|
|
657
|
+
bar.backgroundColor = UIColor(red: 240/255, green: 244/255, blue: 255/255, alpha: 1)
|
|
658
|
+
bar.layer.cornerRadius = 10
|
|
659
|
+
bar.layer.borderWidth = 1
|
|
660
|
+
bar.layer.borderColor = UIColor(red: 220/255, green: 225/255, blue: 245/255, alpha: 1).cgColor
|
|
661
|
+
|
|
662
|
+
let prefixText = isManualEntryMode ? "Scan the QR." : "Can't scan the sensor QR?"
|
|
663
|
+
let linkText = isManualEntryMode ? " Scan QR" : " Enter ID Manually"
|
|
664
|
+
|
|
665
|
+
let prefixLabel = UILabel()
|
|
666
|
+
prefixLabel.text = prefixText
|
|
667
|
+
prefixLabel.font = FontManager.font(ofSize: 14, weight: .regular)
|
|
668
|
+
prefixLabel.textColor = UIColor(red: 45/255, green: 50/255, blue: 130/255, alpha: 1)
|
|
669
|
+
|
|
670
|
+
let linkLabel = UILabel()
|
|
671
|
+
let attr = NSMutableAttributedString(string: linkText, attributes: [
|
|
672
|
+
.font: FontManager.font(ofSize: 14, weight: .bold),
|
|
673
|
+
.foregroundColor: CustomColor.shared.lightGreen,
|
|
674
|
+
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
|
675
|
+
])
|
|
676
|
+
linkLabel.attributedText = attr
|
|
677
|
+
|
|
678
|
+
let stack = UIStackView(arrangedSubviews: [prefixLabel, linkLabel])
|
|
679
|
+
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
680
|
+
stack.axis = .horizontal
|
|
681
|
+
stack.alignment = .center
|
|
682
|
+
stack.spacing = 4
|
|
683
|
+
bar.addSubview(stack)
|
|
684
|
+
|
|
685
|
+
// Tap the entire bar so the hit area is large and reliable
|
|
686
|
+
bar.isUserInteractionEnabled = true
|
|
687
|
+
let tap = UITapGestureRecognizer(target: self, action: #selector(toggleManualEntry))
|
|
688
|
+
bar.addGestureRecognizer(tap)
|
|
689
|
+
|
|
690
|
+
cell.contentView.addSubview(bar)
|
|
691
|
+
NSLayoutConstraint.activate([
|
|
692
|
+
stack.centerXAnchor.constraint(equalTo: bar.centerXAnchor),
|
|
693
|
+
stack.centerYAnchor.constraint(equalTo: bar.centerYAnchor),
|
|
694
|
+
stack.leadingAnchor.constraint(greaterThanOrEqualTo: bar.leadingAnchor, constant: 12),
|
|
695
|
+
stack.trailingAnchor.constraint(lessThanOrEqualTo: bar.trailingAnchor, constant: -12),
|
|
696
|
+
bar.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 24),
|
|
697
|
+
bar.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -24),
|
|
698
|
+
bar.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 4),
|
|
699
|
+
bar.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -4),
|
|
700
|
+
])
|
|
701
|
+
return cell
|
|
516
702
|
}
|
|
517
703
|
case .success:
|
|
518
704
|
switch enumSuccessTableRow(rawValue: indexPath.row)! {
|
|
@@ -542,10 +728,9 @@ extension ConnectToSensorViewController: UITableViewDelegate, UITableViewDataSou
|
|
|
542
728
|
switch enumFailureTableRow(rawValue: indexPath.row)! {
|
|
543
729
|
case .failAnimation:
|
|
544
730
|
let cell: ImageTVC = tableView.dequeueReusableCell(for: indexPath)
|
|
731
|
+
cell.configureAsStaticImage()
|
|
545
732
|
cell.displayImageView.loadGif(named: "failure")
|
|
546
733
|
cell.bottomConstraint.constant = 11
|
|
547
|
-
// Hide camera view to prevent it from overlapping error content (cell reuse)
|
|
548
|
-
cell.cameraView.isHidden = true
|
|
549
734
|
cell.displayImageView.isHidden = false
|
|
550
735
|
// Scale down only for validation API error (65% of original size)
|
|
551
736
|
if deviceVerificationErrorMessage != nil {
|
|
@@ -577,11 +762,10 @@ extension ConnectToSensorViewController: UITableViewDelegate, UITableViewDataSou
|
|
|
577
762
|
return cell
|
|
578
763
|
case .image:
|
|
579
764
|
let cell: ImageTVC = tableView.dequeueReusableCell(for: indexPath)
|
|
765
|
+
cell.configureAsStaticImage()
|
|
580
766
|
cell.topConstraint.constant = 0
|
|
581
767
|
cell.displayImageView.image = loadImage(named: "flip_the_reference")
|
|
582
768
|
cell.displayImageView.contentMode = .scaleAspectFill
|
|
583
|
-
// Hide camera view to prevent it from overlapping error content (cell reuse)
|
|
584
|
-
cell.cameraView.isHidden = true
|
|
585
769
|
cell.displayImageView.isHidden = false
|
|
586
770
|
return cell
|
|
587
771
|
case .watchVideo:
|