synchroni-sensor-sdk 2.2.0__tar.gz → 2.2.1__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.
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/PKG-INFO +5 -3
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/README.md +4 -2
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/pyproject.toml +1 -1
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/__init__.py +46 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/base.py +58 -5
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/buffer.py +11 -1
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/convert.py +6 -2
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/data_context.py +658 -184
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/driver.py +23 -15
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/parsing_models.py +2 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/protocol.py +95 -17
- synchroni_sensor_sdk-2.2.1/synchroni_sensor_sdk/async_api/driver/ingress.py +108 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/backend.py +328 -21
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/macos.py +7 -10
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/rtk_firmware.py +7 -2
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/usb_common.py +62 -31
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/windows.py +66 -21
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/multi_adapter/controller.py +15 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/filter.py +18 -4
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/managed_usb.py +18 -6
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/registry.py +13 -1
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/sensor.py +120 -7
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/sensor_hub.py +139 -30
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/bluetooth.py +9 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/data.py +8 -2
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/device.py +153 -27
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/sync_api/sensor_hub.py +22 -3
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/LICENSE.txt +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/.gitignore +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/2gforce.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/__init__.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/bleak_host.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/bleak_no_ack_patch.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/bleak_process.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/exceptions.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/fb/DataType.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/fb/Sample.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/fb/SensorData.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/fb/__init__.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/gforce.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/py.typed +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sdk_log.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_controller.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_data.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_data_context.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_data_pool.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_device.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_profile.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_utils.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/winrt_high_throughput.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/__init__.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/__init__.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/bleak_no_ack_patch.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/constants.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/crc_utils.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/device_info.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/winrt_high_throughput.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/__init__.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/firmware.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/inventory.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/windows_claim.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/winusb_installer.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/multi_adapter/__init__.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/__init__.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/base.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/continuous.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/system_bleak.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/driver.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/exceptions.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/logging_config.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/params.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/py.typed +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/sync_api/__init__.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/sync_api/_bridge.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/sync_api/runtime.py +0 -0
- {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/sync_api/sensor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synchroni-sensor-sdk
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: SDK for interfacing with Synchroni BLE devices
|
|
5
5
|
License-File: LICENSE.txt
|
|
6
6
|
Author: it@synchroni.co
|
|
@@ -86,7 +86,9 @@ poetry run python examples/v2_cli.py clean -y
|
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
Useful env vars: `SYNCHRONI_WINUSB_INSTALLER`, `SYNCHRONI_SDK_ASSETS_MANIFEST_URL`,
|
|
89
|
-
`SYNCHRONI_RTK_FIRMWARE_AUTO=
|
|
89
|
+
`SYNCHRONI_RTK_FIRMWARE_AUTO=1` (opt in to Realtek host firmware downloads;
|
|
90
|
+
disabled by default). Otherwise provide local firmware through
|
|
91
|
+
`BUMBLE_RTK_FIRMWARE_DIR`.
|
|
90
92
|
|
|
91
93
|
## Quick start (v2)
|
|
92
94
|
|
|
@@ -199,7 +201,7 @@ The sync API runs BLE I/O on a **background event loop** thread (`EventLoopRunne
|
|
|
199
201
|
| Thread | Sync callbacks run on a **thread-pool worker** via `asyncio.to_thread` — not the main thread and not the BLE loop thread itself |
|
|
200
202
|
| Main thread | **Not blocked** by callbacks while it is free (e.g. sleeping or in your own loop) |
|
|
201
203
|
| Ordering | Dispatch for a given callback type is sequential: each invocation is awaited before the next |
|
|
202
|
-
| Throughput |
|
|
204
|
+
| Throughput | A full data buffer (default 64 packets) rejects new data and reports a scientific delivery fault; already accepted packets drain before the final error callback. Start a new stream explicitly to recover. |
|
|
203
205
|
|
|
204
206
|
**Do not call blocking hub/sensor methods from a sync callback** (`connect`, `disconnect`, `start_streaming`, `stop_streaming`, etc.). Those schedule work on the same background loop the dispatch task is waiting on and will **deadlock**.
|
|
205
207
|
|
|
@@ -66,7 +66,9 @@ poetry run python examples/v2_cli.py clean -y
|
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
Useful env vars: `SYNCHRONI_WINUSB_INSTALLER`, `SYNCHRONI_SDK_ASSETS_MANIFEST_URL`,
|
|
69
|
-
`SYNCHRONI_RTK_FIRMWARE_AUTO=
|
|
69
|
+
`SYNCHRONI_RTK_FIRMWARE_AUTO=1` (opt in to Realtek host firmware downloads;
|
|
70
|
+
disabled by default). Otherwise provide local firmware through
|
|
71
|
+
`BUMBLE_RTK_FIRMWARE_DIR`.
|
|
70
72
|
|
|
71
73
|
## Quick start (v2)
|
|
72
74
|
|
|
@@ -179,7 +181,7 @@ The sync API runs BLE I/O on a **background event loop** thread (`EventLoopRunne
|
|
|
179
181
|
| Thread | Sync callbacks run on a **thread-pool worker** via `asyncio.to_thread` — not the main thread and not the BLE loop thread itself |
|
|
180
182
|
| Main thread | **Not blocked** by callbacks while it is free (e.g. sleeping or in your own loop) |
|
|
181
183
|
| Ordering | Dispatch for a given callback type is sequential: each invocation is awaited before the next |
|
|
182
|
-
| Throughput |
|
|
184
|
+
| Throughput | A full data buffer (default 64 packets) rejects new data and reports a scientific delivery fault; already accepted packets drain before the final error callback. Start a new stream explicitly to recover. |
|
|
183
185
|
|
|
184
186
|
**Do not call blocking hub/sensor methods from a sync callback** (`connect`, `disconnect`, `start_streaming`, `stop_streaming`, etc.). Those schedule work on the same background loop the dispatch task is waiting on and will **deadlock**.
|
|
185
187
|
|
|
@@ -22,6 +22,30 @@ from synchroni_sensor_sdk.core.bluetooth import (
|
|
|
22
22
|
ClaimResult,
|
|
23
23
|
SensorRoute,
|
|
24
24
|
)
|
|
25
|
+
from synchroni_sensor_sdk.core.device import (
|
|
26
|
+
BREATHE_PRODUCT_SPECIFICATION,
|
|
27
|
+
GFORCE_OCT_PRODUCT_SPECIFICATION,
|
|
28
|
+
GFORCE_PRO_PRODUCT_SPECIFICATION,
|
|
29
|
+
NATIVE_DEVICE_PROFILES,
|
|
30
|
+
NURA_NEO_PRODUCT_SPECIFICATION,
|
|
31
|
+
NURA_OCTO_PRODUCT_SPECIFICATION,
|
|
32
|
+
NURA_PENTO_PRODUCT_SPECIFICATION,
|
|
33
|
+
NURA_PRODUCT_SPECIFICATION,
|
|
34
|
+
NURA_TRIO_PRODUCT_SPECIFICATION,
|
|
35
|
+
NURA_UNO_PRODUCT_SPECIFICATION,
|
|
36
|
+
OB3000_PRODUCT_SPECIFICATION,
|
|
37
|
+
ORION_A_PRODUCT_SPECIFICATION,
|
|
38
|
+
ORION_B_PRODUCT_SPECIFICATION,
|
|
39
|
+
ORION_C_PRODUCT_SPECIFICATION,
|
|
40
|
+
ORION_PRODUCT_SPECIFICATION,
|
|
41
|
+
OYWW1000_PRODUCT_SPECIFICATION,
|
|
42
|
+
NativeDeviceProfile,
|
|
43
|
+
ProductSpecification,
|
|
44
|
+
SignalAcquisitionMode,
|
|
45
|
+
native_device_profile,
|
|
46
|
+
native_device_profile_by_id,
|
|
47
|
+
published_product_specification,
|
|
48
|
+
)
|
|
25
49
|
from synchroni_sensor_sdk.core.exceptions import (
|
|
26
50
|
AdapterFirmwareError,
|
|
27
51
|
BluetoothAdapterBusyError,
|
|
@@ -64,6 +88,7 @@ __all__ = [
|
|
|
64
88
|
"BluetoothAdapterClaimRequiredError",
|
|
65
89
|
"BluetoothAdapterNotFoundError",
|
|
66
90
|
"BluetoothCapability",
|
|
91
|
+
"BREATHE_PRODUCT_SPECIFICATION",
|
|
67
92
|
"ClaimFailedError",
|
|
68
93
|
"ClaimResult",
|
|
69
94
|
"clear_winusb_installer_cache",
|
|
@@ -74,9 +99,27 @@ __all__ = [
|
|
|
74
99
|
"DataContextStopStreamingError",
|
|
75
100
|
"DataNotificationInProgressError",
|
|
76
101
|
"ensure_winusb_installer",
|
|
102
|
+
"GFORCE_OCT_PRODUCT_SPECIFICATION",
|
|
103
|
+
"GFORCE_PRO_PRODUCT_SPECIFICATION",
|
|
77
104
|
"InvalidDeviceServiceError",
|
|
78
105
|
"ManagedUsbUnavailableError",
|
|
79
106
|
"MultiAdapterDisabledError",
|
|
107
|
+
"NATIVE_DEVICE_PROFILES",
|
|
108
|
+
"NURA_NEO_PRODUCT_SPECIFICATION",
|
|
109
|
+
"NURA_OCTO_PRODUCT_SPECIFICATION",
|
|
110
|
+
"NURA_PENTO_PRODUCT_SPECIFICATION",
|
|
111
|
+
"NURA_PRODUCT_SPECIFICATION",
|
|
112
|
+
"NURA_TRIO_PRODUCT_SPECIFICATION",
|
|
113
|
+
"NURA_UNO_PRODUCT_SPECIFICATION",
|
|
114
|
+
"NativeDeviceProfile",
|
|
115
|
+
"ORION_A_PRODUCT_SPECIFICATION",
|
|
116
|
+
"ORION_B_PRODUCT_SPECIFICATION",
|
|
117
|
+
"ORION_C_PRODUCT_SPECIFICATION",
|
|
118
|
+
"ORION_PRODUCT_SPECIFICATION",
|
|
119
|
+
"OYWW1000_PRODUCT_SPECIFICATION",
|
|
120
|
+
"OB3000_PRODUCT_SPECIFICATION",
|
|
121
|
+
"ProductSpecification",
|
|
122
|
+
"SignalAcquisitionMode",
|
|
80
123
|
"SYSTEM_DEFAULT_ADAPTER_ID",
|
|
81
124
|
"ScanResult",
|
|
82
125
|
"SensorError",
|
|
@@ -90,6 +133,9 @@ __all__ = [
|
|
|
90
133
|
"StopDataNotificationError",
|
|
91
134
|
"WindowsClaimUnavailableError",
|
|
92
135
|
"configure_logging",
|
|
136
|
+
"native_device_profile",
|
|
137
|
+
"native_device_profile_by_id",
|
|
138
|
+
"published_product_specification",
|
|
93
139
|
"sensor_hub",
|
|
94
140
|
"winusb_installer_cache_dir",
|
|
95
141
|
]
|
|
@@ -16,7 +16,7 @@ class Driver(ABC):
|
|
|
16
16
|
|
|
17
17
|
All methods are async and must be awaited from the caller's event loop.
|
|
18
18
|
|
|
19
|
-
Sensor data uses a bounded
|
|
19
|
+
Sensor data uses a bounded buffer that faults on overflow; power, state, and error
|
|
20
20
|
events use unbounded asyncio queues.
|
|
21
21
|
|
|
22
22
|
**Publishing sensor data**
|
|
@@ -40,7 +40,11 @@ class Driver(ABC):
|
|
|
40
40
|
self._logger = logging.getLogger(__name__)
|
|
41
41
|
self._address = address
|
|
42
42
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
43
|
-
self._data_buffer: DropOldestBuffer[SensorData] = DropOldestBuffer(data_buffer_maxsize)
|
|
43
|
+
self._data_buffer: DropOldestBuffer[SensorData] = DropOldestBuffer(data_buffer_maxsize, reject_on_full=True)
|
|
44
|
+
self._scientific_delivery_fault: str | None = None
|
|
45
|
+
self._accepted_data_sequence = 0
|
|
46
|
+
self._delivery_generation = 0
|
|
47
|
+
self._scientific_fault_boundary: tuple[int, int] | None = None
|
|
44
48
|
self._power_queue: asyncio.Queue[int] = asyncio.Queue()
|
|
45
49
|
self._state_queue: asyncio.Queue[DeviceState] = asyncio.Queue()
|
|
46
50
|
self._error_queue: asyncio.Queue[str] = asyncio.Queue()
|
|
@@ -66,13 +70,62 @@ class Driver(ABC):
|
|
|
66
70
|
|
|
67
71
|
async def publish_data(self, data: SensorData) -> None:
|
|
68
72
|
"""
|
|
69
|
-
Enqueue sensor data,
|
|
73
|
+
Enqueue sensor data, reporting a scientific-delivery fault if full.
|
|
70
74
|
|
|
71
75
|
Use when the caller is already on the driver's event loop. For BLE
|
|
72
76
|
notification handlers that may run on a platform thread, use
|
|
73
77
|
:meth:`schedule_publish_data` instead.
|
|
74
78
|
"""
|
|
75
|
-
|
|
79
|
+
self.publish_data_on_loop(data)
|
|
80
|
+
|
|
81
|
+
def report_scientific_delivery_fault(self, reason: str) -> None:
|
|
82
|
+
"""Latch one fault on the driver loop; recovery requires an explicit new stream."""
|
|
83
|
+
if self._scientific_delivery_fault is not None:
|
|
84
|
+
return
|
|
85
|
+
self._scientific_delivery_fault = reason
|
|
86
|
+
self._scientific_fault_boundary = (self._delivery_generation, self._accepted_data_sequence)
|
|
87
|
+
self._publish_error_on_loop(
|
|
88
|
+
f"SDK_SCIENTIFIC_DELIVERY_FAULT|delivery_generation={self._delivery_generation}"
|
|
89
|
+
f"|accepted_sequence={self._accepted_data_sequence}|{reason}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def scientific_fault_boundary(self) -> tuple[int, int] | None:
|
|
94
|
+
"""Generation and last public packet accepted by this driver's bounded buffer."""
|
|
95
|
+
return self._scientific_fault_boundary
|
|
96
|
+
|
|
97
|
+
def reset_scientific_delivery(self) -> None:
|
|
98
|
+
"""Reset only when starting a new stream, never from the parser watchdog."""
|
|
99
|
+
if self._data_buffer.qsize:
|
|
100
|
+
raise RuntimeError("Previous sensor stream still has undelivered data")
|
|
101
|
+
self._data_buffer.clear_on_loop()
|
|
102
|
+
self._scientific_delivery_fault = None
|
|
103
|
+
self._scientific_fault_boundary = None
|
|
104
|
+
self._delivery_generation += 1
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def pending_data_packets(self) -> int:
|
|
108
|
+
return self._data_buffer.qsize
|
|
109
|
+
|
|
110
|
+
def publish_data_on_loop(self, data: SensorData) -> None:
|
|
111
|
+
"""Nonblocking parser handoff without a deferred callback for each packet."""
|
|
112
|
+
if self._scientific_delivery_fault is not None:
|
|
113
|
+
return
|
|
114
|
+
if self._data_buffer.closed:
|
|
115
|
+
self.report_scientific_delivery_fault("stage=parsed_buffer_closed")
|
|
116
|
+
return
|
|
117
|
+
try:
|
|
118
|
+
self._data_buffer.publish_on_loop(data)
|
|
119
|
+
self._accepted_data_sequence += 1
|
|
120
|
+
# publish_on_loop cannot yield; fill the accepted packet's boundary
|
|
121
|
+
# before its consumer can run. Rejected packets receive no sequence.
|
|
122
|
+
data.delivery_sequence = self._accepted_data_sequence
|
|
123
|
+
data.delivery_generation = self._delivery_generation
|
|
124
|
+
except BufferError:
|
|
125
|
+
self.report_scientific_delivery_fault(
|
|
126
|
+
f"stage=parsed_buffer|capacity={self._data_buffer.maxsize}"
|
|
127
|
+
f"|packet_counter={data.last_package_counter}|received_monotonic_ns={data.received_monotonic_ns}"
|
|
128
|
+
)
|
|
76
129
|
|
|
77
130
|
def schedule_publish_data(self, data: SensorData) -> None:
|
|
78
131
|
"""
|
|
@@ -92,7 +145,7 @@ class Driver(ABC):
|
|
|
92
145
|
"""
|
|
93
146
|
if self._loop is None:
|
|
94
147
|
raise RuntimeError("Driver event loop is not bound; call connect() first")
|
|
95
|
-
self._loop.call_soon_threadsafe(self.
|
|
148
|
+
self._loop.call_soon_threadsafe(self.publish_data_on_loop, data)
|
|
96
149
|
|
|
97
150
|
def _publish_power_on_loop(self, level: int) -> None:
|
|
98
151
|
self._power_queue.put_nowait(level)
|
|
@@ -23,11 +23,12 @@ class DropOldestBuffer(Generic[T]):
|
|
|
23
23
|
the loop thread.
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
-
def __init__(self, maxsize: int = DEFAULT_DATA_BUFFER_MAXSIZE) -> None:
|
|
26
|
+
def __init__(self, maxsize: int = DEFAULT_DATA_BUFFER_MAXSIZE, *, reject_on_full: bool = False) -> None:
|
|
27
27
|
self._logger = logging.getLogger(__name__)
|
|
28
28
|
if maxsize < 1:
|
|
29
29
|
raise ValueError("maxsize must be >= 1")
|
|
30
30
|
self._maxsize = maxsize
|
|
31
|
+
self._reject_on_full = reject_on_full
|
|
31
32
|
self._buffer: deque[T] = deque(maxlen=maxsize)
|
|
32
33
|
self._lock = asyncio.Lock()
|
|
33
34
|
self._event = asyncio.Event()
|
|
@@ -64,6 +65,8 @@ class DropOldestBuffer(Generic[T]):
|
|
|
64
65
|
return
|
|
65
66
|
if len(self._buffer) == self._maxsize:
|
|
66
67
|
self.dropped += 1
|
|
68
|
+
if self._reject_on_full:
|
|
69
|
+
raise BufferError("Parsed sensor data buffer is full")
|
|
67
70
|
self._buffer.append(item)
|
|
68
71
|
self._event.set()
|
|
69
72
|
|
|
@@ -72,8 +75,15 @@ class DropOldestBuffer(Generic[T]):
|
|
|
72
75
|
return
|
|
73
76
|
if len(self._buffer) == self._maxsize:
|
|
74
77
|
self.dropped += 1
|
|
78
|
+
if self._reject_on_full:
|
|
79
|
+
raise BufferError("Parsed sensor data buffer is full")
|
|
75
80
|
self._buffer.append(item)
|
|
76
81
|
|
|
82
|
+
def clear_on_loop(self) -> None:
|
|
83
|
+
"""Discard the previous stream's tail only at an explicit stream-start boundary."""
|
|
84
|
+
self._buffer.clear()
|
|
85
|
+
self._event.clear()
|
|
86
|
+
|
|
77
87
|
async def close(self) -> None:
|
|
78
88
|
async with self._lock:
|
|
79
89
|
self._closed = True
|
|
@@ -16,8 +16,11 @@ from synchroni_sensor_sdk.core.data import NtfDataType, Sample, SensorData
|
|
|
16
16
|
def _sample_to_public(sample: ParseSample) -> Sample:
|
|
17
17
|
return Sample(
|
|
18
18
|
raw_data=sample.rawData,
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
# Converted biosignal values and impedance are intentionally kept as
|
|
20
|
+
# floats. Casting here silently quantizes scientific data before it
|
|
21
|
+
# reaches application callbacks.
|
|
22
|
+
data=float(sample.data),
|
|
23
|
+
impedance=float(sample.impedance),
|
|
21
24
|
saturation=float(sample.saturation),
|
|
22
25
|
sample_index=sample.sampleIndex,
|
|
23
26
|
is_lost=sample.isLost,
|
|
@@ -42,4 +45,5 @@ def sensor_data_to_public(packet: ParseSensorData) -> SensorData:
|
|
|
42
45
|
min_package_sample_count=packet.minPackageSampleCount,
|
|
43
46
|
K=packet.K,
|
|
44
47
|
lost_package_count=getattr(packet, "lostPackageCount", 0),
|
|
48
|
+
received_monotonic_ns=max(0, int(getattr(packet, "receivedMonotonicNs", 0))),
|
|
45
49
|
)
|