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.
Files changed (76) hide show
  1. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/PKG-INFO +5 -3
  2. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/README.md +4 -2
  3. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/pyproject.toml +1 -1
  4. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/__init__.py +46 -0
  5. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/base.py +58 -5
  6. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/buffer.py +11 -1
  7. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/convert.py +6 -2
  8. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/data_context.py +658 -184
  9. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/driver.py +23 -15
  10. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/parsing_models.py +2 -0
  11. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/protocol.py +95 -17
  12. synchroni_sensor_sdk-2.2.1/synchroni_sensor_sdk/async_api/driver/ingress.py +108 -0
  13. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/backend.py +328 -21
  14. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/macos.py +7 -10
  15. {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
  16. {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
  17. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/windows.py +66 -21
  18. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/multi_adapter/controller.py +15 -0
  19. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/filter.py +18 -4
  20. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/managed_usb.py +18 -6
  21. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/registry.py +13 -1
  22. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/sensor.py +120 -7
  23. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/sensor_hub.py +139 -30
  24. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/bluetooth.py +9 -0
  25. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/data.py +8 -2
  26. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/device.py +153 -27
  27. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/sync_api/sensor_hub.py +22 -3
  28. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/LICENSE.txt +0 -0
  29. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/.gitignore +0 -0
  30. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/2gforce.py +0 -0
  31. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/__init__.py +0 -0
  32. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/bleak_host.py +0 -0
  33. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/bleak_no_ack_patch.py +0 -0
  34. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/bleak_process.py +0 -0
  35. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/exceptions.py +0 -0
  36. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/fb/DataType.py +0 -0
  37. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/fb/Sample.py +0 -0
  38. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/fb/SensorData.py +0 -0
  39. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/fb/__init__.py +0 -0
  40. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/gforce.py +0 -0
  41. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/py.typed +0 -0
  42. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sdk_log.py +0 -0
  43. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_controller.py +0 -0
  44. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_data.py +0 -0
  45. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_data_context.py +0 -0
  46. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_data_pool.py +0 -0
  47. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_device.py +0 -0
  48. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_profile.py +0 -0
  49. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/sensor_utils.py +0 -0
  50. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/sensor/winrt_high_throughput.py +0 -0
  51. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/__init__.py +0 -0
  52. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/__init__.py +0 -0
  53. {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
  54. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/constants.py +0 -0
  55. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/crc_utils.py +0 -0
  56. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/gforce/device_info.py +0 -0
  57. {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
  58. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/__init__.py +0 -0
  59. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/firmware.py +0 -0
  60. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/driver/managed_usb/inventory.py +0 -0
  61. {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
  62. {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
  63. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/multi_adapter/__init__.py +0 -0
  64. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/__init__.py +0 -0
  65. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/base.py +0 -0
  66. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/continuous.py +0 -0
  67. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/async_api/radio/system_bleak.py +0 -0
  68. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/driver.py +0 -0
  69. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/exceptions.py +0 -0
  70. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/logging_config.py +0 -0
  71. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/core/params.py +0 -0
  72. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/py.typed +0 -0
  73. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/sync_api/__init__.py +0 -0
  74. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/sync_api/_bridge.py +0 -0
  75. {synchroni_sensor_sdk-2.2.0 → synchroni_sensor_sdk-2.2.1}/synchroni_sensor_sdk/sync_api/runtime.py +0 -0
  76. {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.0
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=0` (disable Realtek host firmware auto-download).
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 | Slow handlers can cause the drop-oldest data buffer (default 64 packets) to discard data; that is buffer pressure, not main-thread blocking |
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=0` (disable Realtek host firmware auto-download).
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 | Slow handlers can cause the drop-oldest data buffer (default 64 packets) to discard data; that is buffer pressure, not main-thread blocking |
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "synchroni-sensor-sdk"
3
- version = "2.2.0"
3
+ version = "2.2.1"
4
4
  description = "SDK for interfacing with Synchroni BLE devices"
5
5
  url = "https://github.com/SynchroniSI/synchroni-sensor-sdk"
6
6
  authors = [
@@ -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 :class:`DropOldestBuffer`; power, state, and error
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, dropping the oldest packet when the buffer is full.
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
- await self._data_buffer.publish(data)
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._data_buffer.publish_on_loop, data)
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
- data=int(sample.data) if isinstance(sample.data, float) else sample.data,
20
- impedance=int(sample.impedance),
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
  )