sensor-sdk 0.0.4__tar.gz → 0.0.5__tar.gz

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.

Potentially problematic release.


This version of sensor-sdk might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sensor-sdk
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Python sdk for Synchroni
5
5
  Home-page: https://github.com/oymotion/SynchroniSDKPython
6
6
  Author: Martin Ye
@@ -68,6 +68,12 @@ success = SensorControllerInstance.startScan(6000)
68
68
 
69
69
  returns true if start scan success, periodInMS means onDeviceCallback will be called every periodInMS
70
70
 
71
+ Use `def scan(period_in_ms: int) -> list[BLEDevice]` to scan once time
72
+
73
+ ```python
74
+ bleDevices = SensorControllerInstance.scan(6000)
75
+ ```
76
+
71
77
  ### 3. Stop scan
72
78
 
73
79
  Use `def stopScan() -> None` to stop scan
@@ -122,7 +128,7 @@ sensorProfiles = SensorControllerInstance.getConnectedSensors()
122
128
 
123
129
  ### 9. Get Connected BLE Devices
124
130
 
125
- Use `def getConnectedDevices() -> list[SensorProfile]` to get connected BLE Devices.
131
+ Use `def getConnectedDevices() -> list[BLEDevice]` to get connected BLE Devices.
126
132
 
127
133
  ```python
128
134
  bleDevices = SensorControllerInstance.getConnectedDevices()
@@ -340,3 +346,9 @@ batteryPower = sensorProfile.getBatteryLevel()
340
346
  ```
341
347
 
342
348
  Please check console.py in examples directory
349
+
350
+ ## Async methods
351
+
352
+ all methods start with async is async methods, they has same params and return result as sync methods.
353
+
354
+ Please check async_console.py in examples directory
@@ -57,6 +57,12 @@ success = SensorControllerInstance.startScan(6000)
57
57
 
58
58
  returns true if start scan success, periodInMS means onDeviceCallback will be called every periodInMS
59
59
 
60
+ Use `def scan(period_in_ms: int) -> list[BLEDevice]` to scan once time
61
+
62
+ ```python
63
+ bleDevices = SensorControllerInstance.scan(6000)
64
+ ```
65
+
60
66
  ### 3. Stop scan
61
67
 
62
68
  Use `def stopScan() -> None` to stop scan
@@ -111,7 +117,7 @@ sensorProfiles = SensorControllerInstance.getConnectedSensors()
111
117
 
112
118
  ### 9. Get Connected BLE Devices
113
119
 
114
- Use `def getConnectedDevices() -> list[SensorProfile]` to get connected BLE Devices.
120
+ Use `def getConnectedDevices() -> list[BLEDevice]` to get connected BLE Devices.
115
121
 
116
122
  ```python
117
123
  bleDevices = SensorControllerInstance.getConnectedDevices()
@@ -329,3 +335,9 @@ batteryPower = sensorProfile.getBatteryLevel()
329
335
  ```
330
336
 
331
337
  Please check console.py in examples directory
338
+
339
+ ## Async methods
340
+
341
+ all methods start with async is async methods, they has same params and return result as sync methods.
342
+
343
+ Please check async_console.py in examples directory
@@ -16,6 +16,8 @@ from bleak import (
16
16
  BleakGATTCharacteristic,
17
17
  )
18
18
 
19
+ from sensor import utils
20
+
19
21
 
20
22
  @dataclass
21
23
  class Characteristic:
@@ -146,15 +148,15 @@ class DataSubscription(IntEnum):
146
148
  LOG = (0x00000800,)
147
149
 
148
150
  DNF_EEG = (0x00010000,)
149
-
151
+
150
152
  DNF_ECG = (0x00020000,)
151
-
153
+
152
154
  DNF_IMPEDANCE = (0x00040000,)
153
-
155
+
154
156
  DNF_IMU = (0x00080000,)
155
-
157
+
156
158
  DNF_ADS = (0x00100000,)
157
-
159
+
158
160
  DNF_BRTH = (0x00200000,)
159
161
 
160
162
  DNF_CONCAT_BLE = (0x80000000,)
@@ -181,8 +183,8 @@ class DataType(IntEnum):
181
183
 
182
184
  class SampleResolution(IntEnum):
183
185
  BITS_8 = (8,)
184
- BITS_12 = 12,
185
- BITS_16 = 16,
186
+ BITS_12 = (12,)
187
+ BITS_16 = (16,)
186
188
  BITS_24 = 24
187
189
 
188
190
 
@@ -215,6 +217,7 @@ class EmgRawDataConfig:
215
217
  )
216
218
  return cls(fs, channel_mask, batch_len, resolution)
217
219
 
220
+
218
221
  @dataclass
219
222
  class EegRawDataConfig:
220
223
  fs: SamplingRate = 0
@@ -240,6 +243,7 @@ class EegRawDataConfig:
240
243
  )
241
244
  return cls(fs, channel_mask, batch_len, resolution, K)
242
245
 
246
+
243
247
  @dataclass
244
248
  class EegRawDataCap:
245
249
  fs: SamplingRate = 0
@@ -262,7 +266,8 @@ class EegRawDataCap:
262
266
  data,
263
267
  )
264
268
  return cls(fs, channel_count, batch_len, resolution)
265
-
269
+
270
+
266
271
  @dataclass
267
272
  class EcgRawDataConfig:
268
273
  fs: SamplingRate = SamplingRate.HZ_250
@@ -287,7 +292,8 @@ class EcgRawDataConfig:
287
292
  data,
288
293
  )
289
294
  return cls(fs, channel_mask, batch_len, resolution, K)
290
-
295
+
296
+
291
297
  @dataclass
292
298
  class ImuRawDataConfig:
293
299
  channel_count: int = 0
@@ -295,6 +301,7 @@ class ImuRawDataConfig:
295
301
  batch_len: int = 0
296
302
  accK: float = 0
297
303
  gyroK: float = 0
304
+
298
305
  def to_bytes(self) -> bytes:
299
306
  body = b""
300
307
  body += struct.pack("<i", self.channel_count)
@@ -311,7 +318,8 @@ class ImuRawDataConfig:
311
318
  data,
312
319
  )
313
320
  return cls(channel_count, fs, batch_len, accK, gyroK)
314
-
321
+
322
+
315
323
  @dataclass
316
324
  class BrthRawDataConfig:
317
325
  fs: SamplingRate = 0
@@ -336,7 +344,8 @@ class BrthRawDataConfig:
336
344
  data,
337
345
  )
338
346
  return cls(fs, channel_mask, batch_len, resolution, K)
339
-
347
+
348
+
340
349
  @dataclass
341
350
  class Request:
342
351
  cmd: Command
@@ -361,7 +370,9 @@ class Response:
361
370
 
362
371
 
363
372
  class GForce:
364
- def __init__(self, device: BLEDevice, cmd_char: str, data_char: str, isUniversalStream: bool):
373
+ def __init__(
374
+ self, device: BLEDevice, cmd_char: str, data_char: str, isUniversalStream: bool
375
+ ):
365
376
  self.device_name = ""
366
377
  self.client = None
367
378
  self.cmd_char = cmd_char
@@ -371,21 +382,33 @@ class GForce:
371
382
  self._num_channels = 8
372
383
  self._device = device
373
384
  self._is_universal_stream = isUniversalStream
374
- self._raw_data_buf:queue.Queue[bytes] = None
385
+ self._raw_data_buf: queue.Queue[bytes] = None
375
386
  self.packet_id = 0
376
387
  self.data_packet = []
377
388
 
378
389
  async def connect(self, disconnect_cb, buf: queue.Queue[bytes]):
379
390
  client = BleakClient(self._device, disconnected_callback=disconnect_cb)
380
- await client.connect()
381
-
382
391
  self.client = client
383
392
  self.device_name = self._device.name
384
393
  self._raw_data_buf = buf
385
- if (not self._is_universal_stream):
386
- await client.start_notify(self.cmd_char,self._on_cmd_response)
394
+
395
+ try:
396
+ await asyncio.wait_for(client.connect(), utils._TIMEOUT)
397
+ except Exception as e:
398
+ return
399
+
400
+ if not client.is_connected:
401
+ return
402
+ if not self._is_universal_stream:
403
+ await asyncio.wait_for(
404
+ client.start_notify(self.cmd_char, self._on_cmd_response),
405
+ utils._TIMEOUT,
406
+ )
387
407
  else:
388
- await client.start_notify(self.data_char,self._on_universal_response)
408
+ await asyncio.wait_for(
409
+ client.start_notify(self.data_char, self._on_universal_response),
410
+ utils._TIMEOUT,
411
+ )
389
412
 
390
413
  def _on_data_response(self, q: Queue[bytes], bs: bytearray):
391
414
  bs = bytes(bs)
@@ -537,6 +560,7 @@ class GForce:
537
560
  num_channels = 6
538
561
 
539
562
  return emg_gesture_data.reshape(-1, num_channels)
563
+
540
564
  def _on_universal_response(self, _: BleakGATTCharacteristic, bs: bytearray):
541
565
  self._raw_data_buf.put_nowait(bytes(bs))
542
566
 
@@ -675,7 +699,6 @@ class GForce:
675
699
  )
676
700
  )
677
701
 
678
-
679
702
  async def system_reset(self):
680
703
  await self._send_request(
681
704
  Request(
@@ -684,11 +707,8 @@ class GForce:
684
707
  )
685
708
  )
686
709
 
687
-
688
710
  async def set_motor(self, switchStatus):
689
- body = [
690
- switchStatus == True
691
- ]
711
+ body = [switchStatus == True]
692
712
  body = bytes(body)
693
713
  ret = await self._send_request(
694
714
  Request(
@@ -699,9 +719,7 @@ class GForce:
699
719
  )
700
720
 
701
721
  async def set_led(self, switchStatus):
702
- body = [
703
- switchStatus == True
704
- ]
722
+ body = [switchStatus == True]
705
723
  body = bytes(body)
706
724
  ret = await self._send_request(
707
725
  Request(
@@ -712,9 +730,7 @@ class GForce:
712
730
  )
713
731
 
714
732
  async def set_log_level(self, logLevel):
715
- body = [
716
- 0xFF & logLevel
717
- ]
733
+ body = [0xFF & logLevel]
718
734
  body = bytes(body)
719
735
  ret = await self._send_request(
720
736
  Request(
@@ -724,7 +740,6 @@ class GForce:
724
740
  )
725
741
  )
726
742
 
727
-
728
743
  async def set_emg_raw_data_config(self, cfg=EmgRawDataConfig()):
729
744
  body = cfg.to_bytes()
730
745
  await self._send_request(
@@ -757,7 +772,7 @@ class GForce:
757
772
  )
758
773
  )
759
774
  return EmgRawDataConfig.from_bytes(buf)
760
-
775
+
761
776
  async def get_eeg_raw_data_config(self) -> EegRawDataConfig:
762
777
  buf = await self._send_request(
763
778
  Request(
@@ -766,7 +781,7 @@ class GForce:
766
781
  )
767
782
  )
768
783
  return EegRawDataConfig.from_bytes(buf)
769
-
784
+
770
785
  async def get_eeg_raw_data_cap(self) -> EegRawDataCap:
771
786
  buf = await self._send_request(
772
787
  Request(
@@ -784,7 +799,7 @@ class GForce:
784
799
  )
785
800
  )
786
801
  return EcgRawDataConfig.from_bytes(buf)
787
-
802
+
788
803
  async def get_imu_raw_data_config(self) -> ImuRawDataConfig:
789
804
  buf = await self._send_request(
790
805
  Request(
@@ -792,8 +807,8 @@ class GForce:
792
807
  has_res=True,
793
808
  )
794
809
  )
795
- return ImuRawDataConfig.from_bytes(buf)
796
-
810
+ return ImuRawDataConfig.from_bytes(buf)
811
+
797
812
  async def get_brth_raw_data_config(self) -> BrthRawDataConfig:
798
813
  buf = await self._send_request(
799
814
  Request(
@@ -801,8 +816,8 @@ class GForce:
801
816
  has_res=True,
802
817
  )
803
818
  )
804
- return BrthRawDataConfig.from_bytes(buf)
805
-
819
+ return BrthRawDataConfig.from_bytes(buf)
820
+
806
821
  async def set_subscription(self, subscription: DataSubscription):
807
822
  body = [
808
823
  0xFF & subscription,
@@ -819,20 +834,27 @@ class GForce:
819
834
  )
820
835
  )
821
836
 
822
- async def start_streaming(self, q:queue.Queue):
823
- await self.client.start_notify(
824
- self.data_char,
825
- lambda _, data: self._on_data_response(q, data),
837
+ async def start_streaming(self, q: queue.Queue):
838
+ await asyncio.wait_for(
839
+ self.client.start_notify(
840
+ self.data_char,
841
+ lambda _, data: self._on_data_response(q, data),
842
+ ),
843
+ utils._TIMEOUT,
826
844
  )
827
845
 
828
846
  async def stop_streaming(self):
829
847
  exceptions = []
830
848
  try:
831
- await self.set_subscription(DataSubscription.OFF)
849
+ await asyncio.wait_for(
850
+ self.set_subscription(DataSubscription.OFF), utils._TIMEOUT
851
+ )
832
852
  except Exception as e:
833
853
  exceptions.append(e)
834
854
  try:
835
- await self.client.stop_notify(self.data_char)
855
+ await asyncio.wait_for(
856
+ self.client.stop_notify(self.data_char), utils._TIMEOUT
857
+ )
836
858
  except Exception as e:
837
859
  exceptions.append(e)
838
860
 
@@ -841,7 +863,10 @@ class GForce:
841
863
 
842
864
  async def disconnect(self):
843
865
  with suppress(asyncio.CancelledError):
844
- await self.client.disconnect()
866
+ try:
867
+ await asyncio.wait_for(self.client.disconnect(), utils._TIMEOUT)
868
+ except Exception as e:
869
+ pass
845
870
 
846
871
  def _get_response_channel(self, cmd: Command) -> Queue:
847
872
  q = Queue()
@@ -856,9 +881,11 @@ class GForce:
856
881
  bs = bytes([req.cmd])
857
882
  if req.body is not None:
858
883
  bs += req.body
859
- await self.client.write_gatt_char(self.cmd_char, bs)
884
+ await asyncio.wait_for(
885
+ self.client.write_gatt_char(self.cmd_char, bs), utils._TIMEOUT
886
+ )
860
887
 
861
888
  if not req.has_res:
862
889
  return None
863
890
 
864
- return await asyncio.wait_for(q.get(), 3)
891
+ return await asyncio.wait_for(q.get(), utils._TIMEOUT)
@@ -5,10 +5,11 @@ from typing import Callable, Dict, List, Optional, Tuple
5
5
  import bleak
6
6
 
7
7
  from sensor import sensor_profile
8
+ from sensor import utils
8
9
  from sensor.sensor_profile import DeviceStateEx, SensorProfile
9
10
  import asyncio
10
11
 
11
- from sensor.utils import start_loop, sync_timer, timer
12
+ from sensor.utils import async_timer, start_loop, sync_timer, timer
12
13
  from bleak import (
13
14
  BleakScanner,
14
15
  AdvertisementData,
@@ -69,6 +70,7 @@ class SensorController:
69
70
  self._gforce_event_loop.close()
70
71
 
71
72
  def terminate(self):
73
+ utils._terminated = True
72
74
  for sensor in self._sensor_profiles.values():
73
75
  if (
74
76
  sensor.deviceState == DeviceStateEx.Connected
@@ -133,32 +135,80 @@ class SensorController:
133
135
  """
134
136
  self._device_callback = callback
135
137
 
136
- async def _device_scan_callback(
138
+ def _process_ble_devices(
137
139
  self, found_devices: Dict[str, Tuple[bleak.BLEDevice, AdvertisementData]]
138
- ):
140
+ ) -> List[sensor_profile.BLEDevice]:
141
+ devices: List[sensor_profile.BLEDevice] = list()
142
+ deviceMap: Dict[str, SensorProfile] = self._sensor_profiles.copy()
143
+ for uuid in found_devices:
144
+ device = found_devices[uuid][0]
145
+ if device.name == None:
146
+ continue
147
+ adv = found_devices[uuid][1]
148
+ if SERVICE_GUID in adv.service_uuids:
149
+ mac = None
150
+ if adv.service_data.get(SERVICE_GUID) != None:
151
+ bytes_val = adv.service_data[SERVICE_GUID]
152
+ mac = ":".join(f"{byte:02X}" for byte in bytes_val)
153
+ elif adv.service_data.get(RFSTAR_SERVICE_GUID) != None:
154
+ bytes_val = adv.service_data[RFSTAR_SERVICE_GUID]
155
+ mac = ":".join(f"{byte:02X}" for byte in reversed(bytes_val))
156
+
157
+ if mac == None:
158
+ continue
159
+ if deviceMap.get(mac) != None:
160
+ devices.append(self._sensor_profiles[mac].BLEDevice)
161
+ else:
162
+ newSensor = SensorProfile(device, adv, mac, self._gforce_event_loop)
163
+ deviceMap[mac] = newSensor
164
+ devices.append(newSensor.BLEDevice)
165
+
166
+ self._sensor_profiles = deviceMap
167
+ return devices
168
+
169
+ async def _async_scan(self, period):
170
+ self._is_scanning = True
171
+ found_devices = await self._scanner.discover(
172
+ timeout=period / 1000, return_adv=True
173
+ )
174
+ self._is_scanning = False
175
+ return self._process_ble_devices(found_devices)
176
+
177
+ def scan(self, period) -> List[sensor_profile.BLEDevice]:
178
+ """
179
+ 扫描一段时间后返回BLEDevice列表。
180
+
181
+ :param periodInMs (int): 扫描时长(毫秒)
182
+
183
+ :return: List[sensor_profile.BLEDevice]: BLEDevice列表
184
+ """
185
+ return sync_timer(self._gforce_event_loop, 0, self._async_scan(period))
186
+
187
+ async def asyncScan(self, period) -> List[sensor_profile.BLEDevice]:
188
+ """
189
+ 扫描一段时间后返回BLEDevice列表。
190
+
191
+ :param periodInMs (int): 扫描时长(毫秒)
192
+
193
+ :return: List[sensor_profile.BLEDevice]: BLEDevice列表
194
+ """
195
+ return await async_timer(self._gforce_event_loop, 0, self._async_scan(period))
196
+
197
+ async def _device_scan_callback(self, devices: List[sensor_profile.BLEDevice]):
139
198
  if self._device_callback:
140
- devices: List[sensor_profile.BLEDevice] = list()
141
- deviceMap: Dict[str, SensorProfile] = self._sensor_profiles.copy()
142
- for mac in found_devices:
143
- device = found_devices[mac][0]
144
- adv = found_devices[mac][1]
145
- if SERVICE_GUID in adv.service_uuids:
146
- if deviceMap.get(mac) != None:
147
- devices.append(self._sensor_profiles[mac].BLEDevice)
148
- else:
149
- newSensor = SensorProfile(device, adv, self._gforce_event_loop)
150
- deviceMap[mac] = newSensor
151
- devices.append(newSensor.BLEDevice)
152
- self._sensor_profiles = deviceMap
153
- self._device_callback(devices)
199
+ try:
200
+ self._device_callback(devices)
201
+ except Exception as e:
202
+ print(e)
154
203
 
155
204
  if self._is_scanning:
156
205
  timer(self._gforce_event_loop, 0, self._startScan())
157
206
 
158
207
  async def _startScan(self) -> bool:
159
- devices = await self._scanner.discover(
208
+ found_devices = await self._scanner.discover(
160
209
  timeout=self._device_callback_period / 1000, return_adv=True
161
210
  )
211
+ devices = self._process_ble_devices(found_devices)
162
212
  timer(self._event_loop, 0, self._device_scan_callback(devices))
163
213
 
164
214
  def startScan(self, periodInMs: int) -> bool:
@@ -1,6 +1,8 @@
1
1
  import asyncio
2
2
  from collections import deque
3
+ import platform
3
4
  from queue import Queue
5
+ import struct
4
6
  from typing import Deque, List
5
7
 
6
8
  from sensor.gforce import DataSubscription, GForce
@@ -42,6 +44,7 @@ class SensorProfileDataCtx:
42
44
  self.deviceMac = deviceMac
43
45
  self._device_info: DeviceInfo = None
44
46
 
47
+ self._is_initing = False
45
48
  self._is_running = True
46
49
  self._is_data_transfering = False
47
50
  self.isUniversalStream: bool = gForce._is_universal_stream
@@ -79,7 +82,9 @@ class SensorProfileDataCtx:
79
82
  return self._is_data_transfering
80
83
 
81
84
  def hasInit(self):
82
- return self.featureMap != 0 and self.notifyDataFlag != 0
85
+ return (
86
+ not self._is_initing and self.featureMap != 0 and self.notifyDataFlag != 0
87
+ )
83
88
 
84
89
  def hasEMG(self):
85
90
  return (self.featureMap & FeatureMaps.GFD_FEAT_EMG.value) != 0
@@ -198,32 +203,44 @@ class SensorProfileDataCtx:
198
203
 
199
204
  async def fetchDeviceInfo(self) -> DeviceInfo:
200
205
  info = DeviceInfo()
201
- info.MTUSize = self.gForce.client.mtu_size
206
+ if platform.system() != "Linux":
207
+ info.MTUSize = self.gForce.client.mtu_size
208
+ else:
209
+ info.MTUSize = 0
210
+ print("get_device_name")
202
211
  info.DeviceName = await self.gForce.get_device_name()
212
+ print("get_model_number")
203
213
  info.ModelName = await self.gForce.get_model_number()
214
+ print("get_hardware_revision")
204
215
  info.HardwareVersion = await self.gForce.get_hardware_revision()
216
+ print("get_firmware_revision")
205
217
  info.FirmwareVersion = await self.gForce.get_firmware_revision()
206
218
  return info
207
219
 
208
220
  async def init(self, packageCount: int) -> bool:
221
+ if self._is_initing:
222
+ return False
209
223
  try:
224
+ self._is_initing = True
210
225
  info = await self.fetchDeviceInfo()
211
-
212
226
  await self.initDataTransfer(True)
213
-
214
227
  if self.hasImpedance():
215
228
  self.notifyDataFlag |= DataSubscription.DNF_IMPEDANCE
216
229
 
217
230
  if self.hasEEG():
231
+ print("initEEG")
218
232
  info.EegChannelCount = await self.initEEG(packageCount)
219
233
 
220
234
  if self.hasECG():
235
+ print("initECG")
221
236
  info.EcgChannelCount = await self.initECG(packageCount)
222
237
 
223
238
  if self.hasBrth():
239
+ print("initBrth")
224
240
  info.BrthChannelCount = await self.initBrth(packageCount)
225
241
 
226
242
  if self.hasIMU():
243
+ print("initIMU")
227
244
  imuChannelCount = await self.initIMU(packageCount)
228
245
  info.AccChannelCount = imuChannelCount
229
246
  info.GyroChannelCount = imuChannelCount
@@ -233,9 +250,11 @@ class SensorProfileDataCtx:
233
250
  if not self.isUniversalStream:
234
251
  await self.initDataTransfer(False)
235
252
 
253
+ self._is_initing = False
236
254
  return True
237
255
  except Exception as e:
238
256
  print(e)
257
+ self._is_initing = False
239
258
  return False
240
259
 
241
260
  async def start_streaming(self) -> bool:
@@ -263,16 +282,16 @@ class SensorProfileDataCtx:
263
282
  await self.gForce.set_subscription(0)
264
283
  return True
265
284
 
266
- def process_data(self, buf: Queue[SensorData]):
285
+ def process_data(self, buf: Queue[SensorData], sensor):
267
286
  try:
268
287
  data: bytes = self._rawDataBuffer.get_nowait()
269
288
  except Exception as e:
270
289
  return
271
290
 
272
- self._processDataPackage(data, buf)
291
+ self._processDataPackage(data, buf, sensor)
273
292
  self._rawDataBuffer.task_done()
274
293
 
275
- def _processDataPackage(self, data: bytes, buf: Queue[SensorData]):
294
+ def _processDataPackage(self, data: bytes, buf: Queue[SensorData], sensor):
276
295
  v = data[0]
277
296
  if v == DataType.NTF_IMPEDANCE:
278
297
  offset = 1
@@ -283,15 +302,14 @@ class SensorProfileDataCtx:
283
302
  saturationData = []
284
303
 
285
304
  dataCount = (len(data) - 3) // 4 // 2
305
+
286
306
  for index in range(dataCount):
287
- impedance_bytes = data[offset : offset + 4]
288
- impedance = int.from_bytes(impedance_bytes, byteorder="little")
307
+ impedance = struct.unpack_from("<f", data, offset)[0]
289
308
  offset += 4
290
309
  impedanceData.append(impedance)
291
310
 
292
311
  for index in range(dataCount):
293
- saturation_bytes = data[offset : offset + 4]
294
- saturation = int.from_bytes(saturation_bytes, byteorder="little")
312
+ saturation = struct.unpack_from("<f", data, offset)[0]
295
313
  offset += 4
296
314
  saturationData.append(saturation / 10) # firmware value range 0 - 1000
297
315
 
@@ -300,27 +318,27 @@ class SensorProfileDataCtx:
300
318
 
301
319
  elif v == DataType.NTF_EEG:
302
320
  sensor_data = self.sensorDatas[SensorDataType.DATA_TYPE_EEG]
303
- if self.checkReadSamples(data, sensor_data, 3, 0):
321
+ if self.checkReadSamples(sensor, data, sensor_data, 3, 0):
304
322
  self.sendSensorData(sensor_data, buf)
305
323
  elif v == DataType.NTF_ECG:
306
324
  sensor_data = self.sensorDatas[SensorDataType.DATA_TYPE_ECG]
307
- if self.checkReadSamples(data, sensor_data, 3, 0):
325
+ if self.checkReadSamples(sensor, data, sensor_data, 3, 0):
308
326
  self.sendSensorData(sensor_data, buf)
309
327
  elif v == DataType.NTF_BRTH:
310
328
  sensor_data = self.sensorDatas[SensorDataType.DATA_TYPE_BRTH]
311
- if self.checkReadSamples(data, sensor_data, 3, 0):
329
+ if self.checkReadSamples(sensor, data, sensor_data, 3, 0):
312
330
  self.sendSensorData(sensor_data, buf)
313
331
  elif v == DataType.NTF_IMU:
314
332
  sensor_data_acc = self.sensorDatas[SensorDataType.DATA_TYPE_ACC]
315
- if self.checkReadSamples(data, sensor_data_acc, 3, 6):
333
+ if self.checkReadSamples(sensor, data, sensor_data_acc, 3, 6):
316
334
  self.sendSensorData(sensor_data_acc, buf)
317
335
 
318
336
  sensor_data_gyro = self.sensorDatas[SensorDataType.DATA_TYPE_GYRO]
319
- if self.checkReadSamples(data, sensor_data_gyro, 9, 6):
337
+ if self.checkReadSamples(sensor, data, sensor_data_gyro, 9, 6):
320
338
  self.sendSensorData(sensor_data_gyro, buf)
321
339
 
322
340
  def checkReadSamples(
323
- self, data: bytes, sensorData: SensorData, dataOffset: int, dataGap: int
341
+ self, sensor, data: bytes, sensorData: SensorData, dataOffset: int, dataGap: int
324
342
  ):
325
343
  offset = 1
326
344
  v = data[0]
@@ -343,9 +361,23 @@ class SensorProfileDataCtx:
343
361
  lostSampleCount = sensorData.packageSampleCount * (
344
362
  deltaPackageIndex - 1
345
363
  )
346
- print(
347
- f"lost dataType {sensorData.dataType} -> data {sensorData.deviceMac} {lostSampleCount}"
364
+ lostLog = (
365
+ "MSG|LOST SAMPLE|MAC|"
366
+ + str(sensorData.deviceMac)
367
+ + "|TYPE|"
368
+ + str(sensorData.dataType)
369
+ + "|COUNT|"
370
+ + str(lostSampleCount)
348
371
  )
372
+ # print(lostLog)
373
+ if sensor._event_loop != None and sensor._on_error_callback != None:
374
+ try:
375
+ sensor._event_loop.call_soon_threadsafe(
376
+ sensor._on_error_callback, sensor, lostLog
377
+ )
378
+ except Exception as e:
379
+ pass
380
+
349
381
  self.readSamples(data, sensorData, 0, dataGap, lostSampleCount)
350
382
  if newPackageIndex == 0:
351
383
  sensorData.lastPackageIndex = 65535
@@ -356,7 +388,8 @@ class SensorProfileDataCtx:
356
388
  self.readSamples(data, sensorData, dataOffset, dataGap, 0)
357
389
  sensorData.lastPackageIndex = newPackageIndex
358
390
  sensorData.lastPackageCounter += 1
359
- except Exception:
391
+ except Exception as e:
392
+ print(e)
360
393
  return False
361
394
  return True
362
395
 
@@ -392,12 +425,16 @@ class SensorProfileDataCtx:
392
425
  samples = channelSamples[channelIndex]
393
426
  impedance = 0.0
394
427
  saturation = 0.0
428
+
395
429
  if sensorData.dataType == DataType.NTF_ECG:
396
430
  impedanceChannelIndex = self.sensorDatas[
397
431
  SensorDataType.DATA_TYPE_EEG
398
432
  ].channelCount
399
- impedance = _impedanceData[impedanceChannelIndex]
400
- saturation = _saturationData[impedanceChannelIndex]
433
+
434
+ if impedanceChannelIndex < len(_impedanceData):
435
+ impedance = _impedanceData[impedanceChannelIndex]
436
+ saturation = _saturationData[impedanceChannelIndex]
437
+
401
438
  impedanceChannelIndex += 1
402
439
 
403
440
  dataItem = Sample()
@@ -805,14 +842,14 @@ class SensorProfileDataCtx:
805
842
  data_package = bytes(
806
843
  self._concatDataBuffer[index + 2 : index + 2 + n]
807
844
  )
808
- self._processDataPackage(data_package, buf)
845
+ self._processDataPackage(data_package, buf, sensor)
809
846
  while self._is_running and self.isDataTransfering:
810
847
  sensorData: SensorData = None
811
848
  try:
812
849
  sensorData = buf.get_nowait()
813
850
  except Exception as e:
814
851
  break
815
- if sensorData != None and callback != None:
852
+ if loop != None and sensorData != None and callback != None:
816
853
  try:
817
854
  loop.call_soon_threadsafe(
818
855
  callback, sensor, sensorData
@@ -841,9 +878,10 @@ class SensorProfileDataCtx:
841
878
  data_package = bytes(
842
879
  self._concatDataBuffer[index + 2 : index + 2 + n]
843
880
  )
844
- loop.call_soon_threadsafe(
845
- self.gForce._on_cmd_response, None, data_package
846
- )
881
+ if loop != None:
882
+ loop.call_soon_threadsafe(
883
+ self.gForce._on_cmd_response, None, data_package
884
+ )
847
885
  last_cut = index = index + 2 + n
848
886
  else:
849
887
  index += 1
@@ -13,6 +13,7 @@ from bleak import (
13
13
  )
14
14
 
15
15
  import sensor
16
+ from sensor import utils
16
17
  from sensor.gforce import GForce
17
18
  from sensor.sensor_data import DataType, Sample, SensorData
18
19
  import asyncio
@@ -20,7 +21,7 @@ import threading
20
21
 
21
22
  from sensor.sensor_data_context import SensorProfileDataCtx
22
23
  from sensor.sensor_device import BLEDevice, DeviceInfo, DeviceStateEx
23
- from sensor.utils import start_loop, sync_timer, timer
24
+ from sensor.utils import async_timer, start_loop, sync_timer, timer
24
25
  from contextlib import suppress
25
26
  from dataclasses import dataclass
26
27
 
@@ -44,6 +45,7 @@ class SensorProfile:
44
45
  self,
45
46
  device: bleak.BLEDevice,
46
47
  adv: bleak.AdvertisementData,
48
+ mac: str,
47
49
  gforce_event_loop: asyncio.AbstractEventLoop,
48
50
  ):
49
51
  """
@@ -52,7 +54,7 @@ class SensorProfile:
52
54
  :param device (BLEDevice): 蓝牙设备对象,包含设备的名称、地址和信号强度等信息。
53
55
  """
54
56
  self._detail_device = device
55
- self._device = BLEDevice(device.name, device.address, adv.rssi)
57
+ self._device = BLEDevice(device.name, mac, adv.rssi)
56
58
  self._device_state = DeviceStateEx.Disconnected
57
59
  self._on_state_changed: Callable[["SensorProfile", DeviceStateEx], None] = None
58
60
  self._on_error_callback: Callable[["SensorProfile", str], None] = None
@@ -85,6 +87,7 @@ class SensorProfile:
85
87
  try:
86
88
  self._data_event_loop.stop()
87
89
  self._data_event_loop.close()
90
+ self._data_event_loop = None
88
91
  self._data_event_thread.join()
89
92
  except Exception as e:
90
93
  pass
@@ -92,6 +95,7 @@ class SensorProfile:
92
95
  try:
93
96
  self._event_loop.stop()
94
97
  self._event_loop.close()
98
+ self._event_loop = None
95
99
  self._event_thread.join()
96
100
  except Exception as e:
97
101
  pass
@@ -108,7 +112,7 @@ class SensorProfile:
108
112
  def _set_device_state(self, newState: DeviceStateEx):
109
113
  if self._device_state != newState:
110
114
  self._device_state = newState
111
- if self._on_state_changed != None:
115
+ if self._event_loop != None and self._on_state_changed != None:
112
116
  try:
113
117
  self._event_loop.call_soon_threadsafe(
114
118
  self._on_state_changed, self, newState
@@ -223,43 +227,6 @@ class SensorProfile:
223
227
  self._on_power_changed = callback
224
228
 
225
229
  async def _connect(self) -> bool:
226
- if (
227
- self.deviceState == DeviceStateEx.Connected
228
- or self.deviceState == DeviceStateEx.Ready
229
- ):
230
- return True
231
- self._set_device_state(DeviceStateEx.Connecting)
232
-
233
- def handle_disconnect(_: BleakClient):
234
- self.stopDataNotification()
235
- self._data_ctx.close()
236
- time.sleep(1)
237
- self._data_buffer.queue.clear()
238
- self._data_ctx = None
239
- self._gforce = None
240
- self._set_device_state(DeviceStateEx.Disconnected)
241
- pass
242
-
243
- await self._gforce.connect(handle_disconnect, self._raw_data_buf)
244
-
245
- if self._gforce.client.is_connected:
246
- self._set_device_state(DeviceStateEx.Connected)
247
- if self._gforce.client.mtu_size >= 80:
248
- self._set_device_state(DeviceStateEx.Ready)
249
- else:
250
- self.disconnect()
251
- else:
252
- self._set_device_state(DeviceStateEx.Disconnected)
253
-
254
- return True
255
-
256
- def connect(self) -> bool:
257
- """
258
- 连接传感器。
259
-
260
- :return: bool: 如果连接成功,返回 True;否则返回 False。
261
-
262
- """
263
230
  if self._event_thread == None:
264
231
  self._event_loop = asyncio.new_event_loop()
265
232
  self._event_thread = threading.Thread(
@@ -304,12 +271,58 @@ class SensorProfile:
304
271
  self._data_ctx = SensorProfileDataCtx(
305
272
  self._gforce, self._device.Address, self._raw_data_buf
306
273
  )
307
- if self._data_ctx.isUniversalStream:
308
- timer(self._data_event_loop, 0, self._process_universal_data())
274
+ if self._data_ctx.isUniversalStream:
275
+ timer(self._data_event_loop, 0, self._process_universal_data())
276
+
277
+ if (
278
+ self.deviceState == DeviceStateEx.Connected
279
+ or self.deviceState == DeviceStateEx.Ready
280
+ ):
281
+ return True
282
+ self._set_device_state(DeviceStateEx.Connecting)
309
283
 
284
+ def handle_disconnect(_: BleakClient):
285
+ self._data_ctx.close()
286
+ time.sleep(0.1)
287
+ self._data_buffer.queue.clear()
288
+ self._data_ctx = None
289
+ self._gforce = None
290
+ self._set_device_state(DeviceStateEx.Disconnected)
291
+ pass
292
+
293
+ await self._gforce.connect(handle_disconnect, self._raw_data_buf)
294
+
295
+ if self._gforce.client.is_connected:
296
+ self._set_device_state(DeviceStateEx.Connected)
297
+ self._set_device_state(DeviceStateEx.Ready)
298
+ # if self._gforce.client.mtu_size >= 80:
299
+ # self._set_device_state(DeviceStateEx.Ready)
300
+ # else:
301
+ # self.disconnect()
302
+ else:
303
+ self._set_device_state(DeviceStateEx.Disconnected)
304
+
305
+ return True
306
+
307
+ def connect(self) -> bool:
308
+ """
309
+ 连接传感器。
310
+
311
+ :return: bool: 如果连接成功,返回 True;否则返回 False。
312
+
313
+ """
310
314
  result = sync_timer(self._gforce_event_loop, 0, self._connect())
311
315
  return result
312
316
 
317
+ async def asyncConnect(self) -> bool:
318
+ """
319
+ 连接传感器。
320
+
321
+ :return: bool: 如果连接成功,返回 True;否则返回 False。
322
+
323
+ """
324
+ return await async_timer(self._gforce_event_loop, 0, self._connect())
325
+
313
326
  async def _waitForDisconnect(self) -> bool:
314
327
  while self.deviceState != DeviceStateEx.Disconnected:
315
328
  asyncio.sleep(1)
@@ -325,7 +338,7 @@ class SensorProfile:
325
338
  return False
326
339
  self._set_device_state(DeviceStateEx.Disconnecting)
327
340
  await self._gforce.disconnect()
328
- await asyncio.wait_for(self._waitForDisconnect(), 5)
341
+ await asyncio.wait_for(self._waitForDisconnect(), utils._TIMEOUT)
329
342
  return True
330
343
 
331
344
  def disconnect(self) -> bool:
@@ -337,16 +350,29 @@ class SensorProfile:
337
350
  """
338
351
  return sync_timer(self._gforce_event_loop, 0, self._disconnect())
339
352
 
353
+ async def asyncDisconnect(self) -> bool:
354
+ """
355
+ 断开传感器连接。
356
+
357
+ :return: bool: 如果断开连接成功,返回 True;否则返回 False。
358
+
359
+ """
360
+ return await async_timer(self._gforce_event_loop, 0, self._disconnect())
361
+
340
362
  async def _process_data(self):
341
363
  while self._data_ctx._is_running and self._data_ctx.isDataTransfering:
342
- self._data_ctx.process_data(self._data_buffer)
364
+ self._data_ctx.process_data(self._data_buffer, self)
343
365
  while self._data_ctx._is_running and self._data_ctx.isDataTransfering:
344
366
  sensorData: SensorData = None
345
367
  try:
346
368
  sensorData = self._data_buffer.get_nowait()
347
369
  except Exception as e:
348
370
  break
349
- if sensorData != None and self._on_data_callback != None:
371
+ if (
372
+ self._event_loop != None
373
+ and sensorData != None
374
+ and self._on_data_callback != None
375
+ ):
350
376
  try:
351
377
  self._event_loop.call_soon_threadsafe(
352
378
  self._on_data_callback, self, sensorData
@@ -361,20 +387,6 @@ class SensorProfile:
361
387
  )
362
388
 
363
389
  async def _startDataNotification(self) -> bool:
364
- result = await self._data_ctx.start_streaming()
365
- self._data_buffer.queue.clear()
366
- self._data_ctx.clear()
367
- if not self._data_ctx.isUniversalStream:
368
- timer(self._data_event_loop, 0, self._process_data())
369
- return result
370
-
371
- def startDataNotification(self) -> bool:
372
- """
373
- 开始数据通知。
374
-
375
- :return: bool: 如果开始数据通知成功,返回 True;否则返回 False。
376
-
377
- """
378
390
  if self.deviceState != DeviceStateEx.Ready:
379
391
  return False
380
392
  if self._data_ctx == None:
@@ -394,19 +406,34 @@ class SensorProfile:
394
406
  self._data_event_thread.name = self.BLEDevice.Name + " data"
395
407
  self._data_event_thread.start()
396
408
 
397
- return sync_timer(self._gforce_event_loop, 0, self._startDataNotification())
409
+ result = await self._data_ctx.start_streaming()
410
+ self._data_buffer.queue.clear()
411
+ self._data_ctx.clear()
412
+ if not self._data_ctx.isUniversalStream:
413
+ timer(self._data_event_loop, 0, self._process_data())
414
+ return result
398
415
 
399
- async def _stopDataNotification(self) -> bool:
416
+ def startDataNotification(self) -> bool:
417
+ """
418
+ 开始数据通知。
400
419
 
401
- return not await self._data_ctx.stop_streaming()
420
+ :return: bool: 如果开始数据通知成功,返回 True;否则返回 False。
402
421
 
403
- def stopDataNotification(self) -> bool:
404
422
  """
405
- 停止数据通知。
423
+ return sync_timer(self._gforce_event_loop, 0, self._startDataNotification())
406
424
 
407
- :return: bool: 如果停止数据通知成功,返回 True;否则返回 False。
425
+ async def asyncStartDataNotification(self) -> bool:
426
+ """
427
+ 开始数据通知。
428
+
429
+ :return: bool: 如果开始数据通知成功,返回 True;否则返回 False。
408
430
 
409
431
  """
432
+ return await async_timer(
433
+ self._gforce_event_loop, 0, self._startDataNotification()
434
+ )
435
+
436
+ async def _stopDataNotification(self) -> bool:
410
437
  if self.deviceState != DeviceStateEx.Ready:
411
438
  return False
412
439
  if self._data_ctx == None:
@@ -417,12 +444,32 @@ class SensorProfile:
417
444
  if not self._data_ctx.isDataTransfering:
418
445
  return True
419
446
 
447
+ return not await self._data_ctx.stop_streaming()
448
+
449
+ def stopDataNotification(self) -> bool:
450
+ """
451
+ 停止数据通知。
452
+
453
+ :return: bool: 如果停止数据通知成功,返回 True;否则返回 False。
454
+
455
+ """
420
456
  return sync_timer(self._gforce_event_loop, 0, self._stopDataNotification())
421
457
 
458
+ async def asyncStopDataNotification(self) -> bool:
459
+ """
460
+ 停止数据通知。
461
+
462
+ :return: bool: 如果停止数据通知成功,返回 True;否则返回 False。
463
+
464
+ """
465
+ return await async_timer(
466
+ self._gforce_event_loop, 0, self._stopDataNotification()
467
+ )
468
+
422
469
  async def _refresh_power(self):
423
470
  self._power = await self._gforce.get_battery_level()
424
471
 
425
- if self._on_power_changed != None:
472
+ if self._event_loop != None and self._on_power_changed != None:
426
473
  try:
427
474
  self._event_loop.call_soon_threadsafe(
428
475
  self._on_power_changed, self, self._power
@@ -469,6 +516,26 @@ class SensorProfile:
469
516
  self._gforce_event_loop,
470
517
  0,
471
518
  self._init(packageSampleCount, powerRefreshInterval),
519
+ 120,
520
+ )
521
+
522
+ async def asyncInit(
523
+ self, packageSampleCount: int, powerRefreshInterval: int
524
+ ) -> bool:
525
+ """
526
+ 初始化数据采集。
527
+
528
+ :param packageSampleCount (int): 数据包中的样本数量。
529
+ :param powerRefreshInterval (int): 电量刷新间隔。
530
+
531
+ :return: bool: 初始化结果。True 表示成功,False 表示失败。
532
+
533
+ """
534
+ return await async_timer(
535
+ self._gforce_event_loop,
536
+ 0,
537
+ self._init(packageSampleCount, powerRefreshInterval),
538
+ 120,
472
539
  )
473
540
 
474
541
  def getBatteryLevel(self) -> int:
@@ -502,3 +569,15 @@ class SensorProfile:
502
569
 
503
570
  """
504
571
  return ""
572
+
573
+ async def AsyncSetParam(self, key: str, value: str) -> str:
574
+ """
575
+ 设置传感器的参数。
576
+
577
+ :param key (str): 参数的键。
578
+ :param value (str): 参数的值。
579
+
580
+ :return: str: 设置参数的结果。
581
+
582
+ """
583
+ return ""
@@ -0,0 +1,72 @@
1
+ import asyncio
2
+ import platform
3
+ import queue
4
+ import signal
5
+ import time
6
+
7
+ _terminated = False
8
+ _TIMEOUT = 10
9
+
10
+
11
+ async def delay(_time: float, function) -> any:
12
+ if _time > 0:
13
+ await asyncio.sleep(_time)
14
+ return await function
15
+
16
+
17
+ def timer(_loop: asyncio.AbstractEventLoop, _delay: float, function):
18
+ if _loop == None:
19
+ return
20
+ try:
21
+ asyncio.run_coroutine_threadsafe(delay(_delay, function), _loop)
22
+ except Exception as e:
23
+ print(e)
24
+ pass
25
+
26
+
27
+ def sync_timer(
28
+ _loop: asyncio.AbstractEventLoop, _delay: float, function, _timeout=_TIMEOUT
29
+ ) -> any:
30
+ if _loop == None:
31
+ return
32
+
33
+ try:
34
+ f = asyncio.run_coroutine_threadsafe(
35
+ asyncio.wait_for(delay(_delay, function), _delay + _timeout), _loop
36
+ )
37
+ return f.result(timeout=_timeout)
38
+ except Exception as e:
39
+ print(e)
40
+ pass
41
+
42
+
43
+ async def async_timer(
44
+ _loop: asyncio.AbstractEventLoop, _delay: float, function, _timeout=_TIMEOUT
45
+ ) -> any:
46
+ if _loop == None:
47
+ return
48
+
49
+ try:
50
+ f = asyncio.run_coroutine_threadsafe(
51
+ asyncio.wait_for(delay(_delay, function), _delay + _timeout), _loop
52
+ )
53
+ except Exception as e:
54
+ print(e)
55
+ pass
56
+
57
+ while not _terminated and not f.done():
58
+ await asyncio.sleep(0.1)
59
+
60
+ try:
61
+ if not f.cancelled():
62
+ return f.result()
63
+ except Exception as e:
64
+ print(e)
65
+ return
66
+
67
+
68
+ def start_loop(loop: asyncio.BaseEventLoop):
69
+ if platform.system() == "Darwin":
70
+ asyncio.get_running_loop = asyncio.get_event_loop
71
+ asyncio.set_event_loop(loop)
72
+ loop.run_forever()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sensor-sdk
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Python sdk for Synchroni
5
5
  Home-page: https://github.com/oymotion/SynchroniSDKPython
6
6
  Author: Martin Ye
@@ -68,6 +68,12 @@ success = SensorControllerInstance.startScan(6000)
68
68
 
69
69
  returns true if start scan success, periodInMS means onDeviceCallback will be called every periodInMS
70
70
 
71
+ Use `def scan(period_in_ms: int) -> list[BLEDevice]` to scan once time
72
+
73
+ ```python
74
+ bleDevices = SensorControllerInstance.scan(6000)
75
+ ```
76
+
71
77
  ### 3. Stop scan
72
78
 
73
79
  Use `def stopScan() -> None` to stop scan
@@ -122,7 +128,7 @@ sensorProfiles = SensorControllerInstance.getConnectedSensors()
122
128
 
123
129
  ### 9. Get Connected BLE Devices
124
130
 
125
- Use `def getConnectedDevices() -> list[SensorProfile]` to get connected BLE Devices.
131
+ Use `def getConnectedDevices() -> list[BLEDevice]` to get connected BLE Devices.
126
132
 
127
133
  ```python
128
134
  bleDevices = SensorControllerInstance.getConnectedDevices()
@@ -340,3 +346,9 @@ batteryPower = sensorProfile.getBatteryLevel()
340
346
  ```
341
347
 
342
348
  Please check console.py in examples directory
349
+
350
+ ## Async methods
351
+
352
+ all methods start with async is async methods, they has same params and return result as sync methods.
353
+
354
+ Please check async_console.py in examples directory
@@ -8,7 +8,7 @@ with open(os.path.join(this_directory, "README.md"), "r", encoding="utf-8") as f
8
8
 
9
9
  setup(
10
10
  name="sensor-sdk",
11
- version="0.0.4",
11
+ version="0.0.5",
12
12
  description="Python sdk for Synchroni",
13
13
  long_description=long_description,
14
14
  long_description_content_type="text/markdown",
@@ -1,30 +0,0 @@
1
- import asyncio
2
- import signal
3
-
4
-
5
- async def delay(time: float, function) -> any:
6
- if time > 0:
7
- await asyncio.sleep(time)
8
- return await function
9
-
10
-
11
- def timer(_loop: asyncio.AbstractEventLoop, time: float, function):
12
- try:
13
- asyncio.run_coroutine_threadsafe(delay(time, function), _loop)
14
- except Exception as e:
15
- print(e)
16
- pass
17
-
18
-
19
- def sync_timer(_loop: asyncio.AbstractEventLoop, time: float, function) -> any:
20
- try:
21
- f = asyncio.run_coroutine_threadsafe(delay(time, function), _loop)
22
- return f.result()
23
- except Exception as e:
24
- print(e)
25
- pass
26
-
27
-
28
- def start_loop(loop: asyncio.BaseEventLoop):
29
- asyncio.set_event_loop(loop)
30
- loop.run_forever()
File without changes
File without changes