ka9q-python 3.19.0__tar.gz → 3.20.0__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 (113) hide show
  1. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/CHANGELOG.md +20 -0
  2. {ka9q_python-3.19.0/ka9q_python.egg-info → ka9q_python-3.20.0}/PKG-INFO +1 -1
  3. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/__init__.py +2 -1
  4. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/slot_clock.py +54 -2
  5. {ka9q_python-3.19.0 → ka9q_python-3.20.0/ka9q_python.egg-info}/PKG-INFO +1 -1
  6. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/pyproject.toml +1 -1
  7. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_slot_clock.py +45 -1
  8. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/LICENSE +0 -0
  9. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/MANIFEST.in +0 -0
  10. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/README.md +0 -0
  11. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/docs/API_REFERENCE.md +0 -0
  12. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/docs/ARCHITECTURE.md +0 -0
  13. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/docs/CLI_GUIDE.md +0 -0
  14. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/docs/GETTING_STARTED.md +0 -0
  15. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/docs/INSTALLATION.md +0 -0
  16. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/docs/MULTI_STREAM.md +0 -0
  17. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/docs/RECIPES.md +0 -0
  18. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/docs/REQUIREMENTS.md +0 -0
  19. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/docs/RTP_TIMING_SUPPORT.md +0 -0
  20. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/docs/SECURITY.md +0 -0
  21. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/docs/TESTING_GUIDE.md +0 -0
  22. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/docs/TUI_GUIDE.md +0 -0
  23. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/advanced_features_demo.py +0 -0
  24. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/channel_cleanup_example.py +0 -0
  25. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/codar_oceanography.py +0 -0
  26. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/diagnostics/diagnose_packets.py +0 -0
  27. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/diagnostics/repro_utc_bug.py +0 -0
  28. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/discover_example.py +0 -0
  29. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/grape_integration_example.py +0 -0
  30. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/hf_band_scanner.py +0 -0
  31. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/multi_stream_smoke.py +0 -0
  32. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/rtp_recorder_example.py +0 -0
  33. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/simple_am_radio.py +0 -0
  34. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/spectrum_example.py +0 -0
  35. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/stream_example.py +0 -0
  36. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/superdarn_recorder.py +0 -0
  37. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/test_channel_operations.py +0 -0
  38. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/test_improvements.py +0 -0
  39. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/test_timing_fields.py +0 -0
  40. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/tune.py +0 -0
  41. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/examples/tune_example.py +0 -0
  42. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/_multicast.py +0 -0
  43. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/addressing.py +0 -0
  44. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/cli.py +0 -0
  45. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/compat.py +0 -0
  46. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/control.py +0 -0
  47. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/discovery.py +0 -0
  48. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/exceptions.py +0 -0
  49. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/managed_stream.py +0 -0
  50. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/monitor.py +0 -0
  51. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/multi_stream.py +0 -0
  52. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/pps_calibrator.py +0 -0
  53. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/resequencer.py +0 -0
  54. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/rtp_recorder.py +0 -0
  55. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/spectrum_stream.py +0 -0
  56. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/status.py +0 -0
  57. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/status_listener.py +0 -0
  58. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/stream.py +0 -0
  59. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/stream_quality.py +0 -0
  60. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/tui.py +0 -0
  61. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/types.py +0 -0
  62. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q/utils.py +0 -0
  63. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q_python.egg-info/SOURCES.txt +0 -0
  64. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q_python.egg-info/dependency_links.txt +0 -0
  65. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q_python.egg-info/entry_points.txt +0 -0
  66. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q_python.egg-info/requires.txt +0 -0
  67. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q_python.egg-info/top_level.txt +0 -0
  68. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/ka9q_radio_compat +0 -0
  69. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/scripts/check_upstream_drift.py +0 -0
  70. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/scripts/sync_types.py +0 -0
  71. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/setup.cfg +0 -0
  72. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/__init__.py +0 -0
  73. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/conftest.py +0 -0
  74. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_addressing.py +0 -0
  75. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_channel_verification.py +0 -0
  76. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_client_id_destination.py +0 -0
  77. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_create_split_encoding.py +0 -0
  78. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_decode_description.py +0 -0
  79. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_decode_functions.py +0 -0
  80. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_encode_functions.py +0 -0
  81. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_encode_socket.py +0 -0
  82. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_ensure_channel_encoding.py +0 -0
  83. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_filter_edges.py +0 -0
  84. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_integration.py +0 -0
  85. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_iq_20khz_f32.py +0 -0
  86. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_lifetime.py +0 -0
  87. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_listen_multicast.py +0 -0
  88. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_managed_stream_recovery.py +0 -0
  89. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_monitor.py +0 -0
  90. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_multicast_helpers.py +0 -0
  91. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_multihomed.py +0 -0
  92. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_multistream_gap_storm.py +0 -0
  93. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_multistream_prune.py +0 -0
  94. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_native_discovery.py +0 -0
  95. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_parse_rtp_samples_iq.py +0 -0
  96. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_performance_fixes.py +0 -0
  97. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_protocol_compat.py +0 -0
  98. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_remove_channel.py +0 -0
  99. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_rtp_recorder.py +0 -0
  100. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_security_features.py +0 -0
  101. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_spectrum.py +0 -0
  102. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_ssrc_dest_unit.py +0 -0
  103. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_ssrc_encoding_unit.py +0 -0
  104. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_ssrc_radiod_host_unit.py +0 -0
  105. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_status_decoder.py +0 -0
  106. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_status_listener.py +0 -0
  107. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_ttl_warning.py +0 -0
  108. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_tune.py +0 -0
  109. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_tune_cli.py +0 -0
  110. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_tune_debug.py +0 -0
  111. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_tune_live.py +0 -0
  112. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_tune_method.py +0 -0
  113. {ka9q_python-3.19.0 → ka9q_python-3.20.0}/tests/test_upstream_drift.py +0 -0
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.20.0] - 2026-06-29
4
+
5
+ ### Added — `SlotClock` ambiguous-unwrap guard (fail loud, never silent)
6
+
7
+ - **`SlotClock.offset_of_rtp` now raises `SlotClockDesyncError`** when a wrapped
8
+ RTP timestamp is more than `2**30` samples (half the signed-32 window, ~24.8 h
9
+ @ 12 kHz / ~4.7 h @ 64 kHz IQ) from the unwrap high-water in either direction.
10
+ The high-water unwrap (3.18.0) keeps continuous streams correct past the
11
+ `2**31` window, but it only disambiguates while each step stays within
12
+ `±2**31` of the high-water. A *single* forward jump `>= 2**31` samples (a
13
+ `>49.7 h` stall-then-resume, a missed re-anchor, or a discontinuous timestamp)
14
+ aliased into a backward step — `advance()` then harvested **0 slots forever on
15
+ live RF**, the exact silent failure `SlotClock` exists to prevent.
16
+ - **`SlotClock.advance` catches the desync, logs an ERROR, drops the anchor, and
17
+ returns `[]`**, so the caller's normal `if not clk.anchored: clk.anchor(...)`
18
+ path re-establishes the grid on a fresh RTP reference instead of stalling. The
19
+ `2**30` margin never false-fires: continuous advancing steps a few hundred
20
+ samples per call.
21
+ - **`SlotClockDesyncError` is exported** from the package.
22
+
3
23
  ## [3.19.0] - 2026-06-28
4
24
 
5
25
  ### Removed / Changed — simplify how we look at radiod (defer to RTP)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ka9q-python
3
- Version: 3.19.0
3
+ Version: 3.20.0
4
4
  Summary: Python interface for ka9q-radio control and monitoring
5
5
  Author-email: Michael Hauan AC0G <ac0g@hauan.org>
6
6
  License: MIT
@@ -115,7 +115,7 @@ from .managed_stream import (
115
115
  StreamState,
116
116
  )
117
117
  from .multi_stream import MultiStream
118
- from .slot_clock import SlotClock, Slot, rtp_diff
118
+ from .slot_clock import SlotClock, Slot, SlotClockDesyncError, rtp_diff
119
119
  from .spectrum_stream import SpectrumStream
120
120
  from .status_listener import StatusListener, StatusListenerStats
121
121
 
@@ -165,6 +165,7 @@ __all__ = [
165
165
  # Slot timing (epoch-aligned, RTP-referenced)
166
166
  'SlotClock',
167
167
  'Slot',
168
+ 'SlotClockDesyncError',
168
169
  'rtp_diff',
169
170
 
170
171
  # Stream API (sample-oriented)
@@ -68,6 +68,28 @@ def rtp_diff(a: int, b: int) -> int:
68
68
  return d
69
69
 
70
70
 
71
+ # Largest single unwrap step (in samples) we trust from the high-water. The
72
+ # signed-32 unwrap is only unambiguous within +/-2**31 of the high-water; we
73
+ # refuse anything past the half-window (2**30 ~= 24.8 h @ 12 kHz, ~4.7 h @
74
+ # 64 kHz IQ) so the unwrap fails LOUD with a wide margin instead of silently
75
+ # aliasing a huge forward jump into a backward step (-> advance() harvests 0
76
+ # slots on live RF: the exact silent failure SlotClock exists to prevent).
77
+ # Continuous advancing steps by a few hundred samples per call, so this never
78
+ # false-fires in normal operation — only a >24.8 h stall, a missed re-anchor,
79
+ # or a bogus/discontinuous timestamp reaches it.
80
+ _SAFE_UNWRAP_SAMPLES = 2 ** 30
81
+
82
+
83
+ class SlotClockDesyncError(RuntimeError):
84
+ """A wrapped RTP timestamp is too far from the unwrap high-water to be
85
+ disambiguated (a >24.8 h gap, a missed re-anchor, or a discontinuous/bogus
86
+ timestamp). The grid can no longer be trusted from this timestamp alone;
87
+ the caller must re-anchor against a fresh RTP reference. ``advance``
88
+ catches this, drops the anchor, and returns no slots, so the caller's
89
+ normal ``if not clk.anchored: clk.anchor(...)`` path re-establishes the
90
+ grid rather than silently stalling at zero decodes."""
91
+
92
+
71
93
  @dataclass(frozen=True)
72
94
  class Slot:
73
95
  """One completed, epoch-aligned slot.
@@ -189,10 +211,26 @@ class SlotClock:
189
211
  within the last slot), which are always within one signed-32 window of
190
212
  the high-water — so the unwrap is unambiguous. Advances the high-water
191
213
  when a query is ahead of it; never rewinds it on a stale/past query.
214
+
215
+ Raises ``SlotClockDesyncError`` if the timestamp is more than
216
+ ``_SAFE_UNWRAP_SAMPLES`` (half the signed-32 window) from the
217
+ high-water in either direction: at that distance a wrapped 32-bit
218
+ value can no longer be told apart from a ~2**32-aliased one, so any
219
+ unwrap would be a guess. This converts the old silent alias (huge
220
+ forward jump read as a backward step -> 0 slots forever) into a loud,
221
+ recoverable signal.
192
222
  """
193
223
  assert self._anchor_rtp is not None
194
224
  ref_rtp = (self._anchor_rtp + self._latest_off) & 0xFFFFFFFF
195
- off = self._latest_off + rtp_diff(rtp_timestamp, ref_rtp)
225
+ delta = rtp_diff(rtp_timestamp, ref_rtp)
226
+ if abs(delta) > _SAFE_UNWRAP_SAMPLES:
227
+ raise SlotClockDesyncError(
228
+ f"rtp={rtp_timestamp} is {delta} samples from the unwrap "
229
+ f"high-water (offset {self._latest_off}); |delta| exceeds the "
230
+ f"safe window {_SAFE_UNWRAP_SAMPLES} — cannot unwrap "
231
+ f"unambiguously, re-anchor required"
232
+ )
233
+ off = self._latest_off + delta
196
234
  if off > self._latest_off:
197
235
  self._latest_off = off
198
236
  return off
@@ -209,7 +247,21 @@ class SlotClock:
209
247
  """
210
248
  if self._anchor_rtp is None or self._next_boundary_off is None:
211
249
  return []
212
- latest_off = self.offset_of_rtp(latest_rtp_timestamp)
250
+ try:
251
+ latest_off = self.offset_of_rtp(latest_rtp_timestamp)
252
+ except SlotClockDesyncError as exc:
253
+ # A wrapped timestamp this far from the high-water can't be
254
+ # unwrapped safely. Fail LOUD and drop the anchor so the caller's
255
+ # ``if not anchored: anchor()`` path re-establishes the grid on a
256
+ # fresh RTP reference — instead of silently harvesting 0 slots on
257
+ # live RF (the failure mode this whole class exists to prevent).
258
+ logger.error(
259
+ "SlotClock(cadence=%.3fs, sr=%d): %s; dropping anchor to force "
260
+ "a clean re-anchor (was at slot index %d)",
261
+ self.cadence_sec, self.sample_rate, exc, self._next_index,
262
+ )
263
+ self.reset()
264
+ return []
213
265
  out: List[Slot] = []
214
266
  while latest_off >= (
215
267
  self._next_boundary_off + self.cadence_samples + self.settle_samples
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ka9q-python
3
- Version: 3.19.0
3
+ Version: 3.20.0
4
4
  Summary: Python interface for ka9q-radio control and monitoring
5
5
  Author-email: Michael Hauan AC0G <ac0g@hauan.org>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ka9q-python"
7
- version = "3.19.0"
7
+ version = "3.20.0"
8
8
  description = "Python interface for ka9q-radio control and monitoring"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -3,7 +3,13 @@ import math
3
3
 
4
4
  import pytest
5
5
 
6
- from ka9q.slot_clock import SlotClock, Slot, rtp_diff
6
+ from ka9q.slot_clock import (
7
+ SlotClock,
8
+ Slot,
9
+ SlotClockDesyncError,
10
+ rtp_diff,
11
+ _SAFE_UNWRAP_SAMPLES,
12
+ )
7
13
 
8
14
 
9
15
  SR = 12000
@@ -136,6 +142,44 @@ def test_offset_of_rtp_unwraps_past_window():
136
142
  assert off > 2 ** 31 # we genuinely crossed the signed-32 boundary
137
143
 
138
144
 
145
+ def test_safe_unwrap_limit_allows_full_half_window_step():
146
+ """A single step of exactly the safe limit (half the signed-32 window) is
147
+ still unambiguous and must be accepted — the guard rejects only *past* it."""
148
+ clk = SlotClock(15.0, SR)
149
+ clk.anchor(rtp_timestamp=0, utc=900_000_000.0)
150
+ off = _SAFE_UNWRAP_SAMPLES
151
+ assert clk.offset_of_rtp(off & 0xFFFFFFFF) == off
152
+
153
+
154
+ def test_offset_of_rtp_raises_on_ambiguous_jump():
155
+ """A single unwrap step past the safe half-window can alias a huge forward
156
+ jump into a backward step; offset_of_rtp must raise rather than guess."""
157
+ clk = SlotClock(15.0, SR)
158
+ clk.anchor(rtp_timestamp=0, utc=900_000_000.0)
159
+ with pytest.raises(SlotClockDesyncError):
160
+ clk.offset_of_rtp((_SAFE_UNWRAP_SAMPLES + 1) & 0xFFFFFFFF)
161
+
162
+
163
+ def test_advance_big_jump_fails_loud_and_drops_anchor(caplog):
164
+ """Regression: a single advance() forward jump > 2**31 from the anchor used
165
+ to alias to a backward step and silently harvest 0 slots forever (live RF,
166
+ zero decodes). The guard must instead log loudly, drop the anchor, and
167
+ return [] so the caller re-anchors — turning a silent stall into recovery."""
168
+ clk = SlotClock(15.0, SR, settle_sec=1.5)
169
+ clk.anchor(rtp_timestamp=0, utc=900_000_000.0)
170
+ # 50 h in samples: 2,160,000,000 > 2**31 (2,147,483,648) in ONE call.
171
+ big = 50 * 3600 * SR & 0xFFFFFFFF
172
+ with caplog.at_level("ERROR"):
173
+ slots = clk.advance(big)
174
+ assert slots == [] # no garbage slots
175
+ assert not clk.anchored # anchor dropped -> caller must re-anchor
176
+ assert any("SlotClock" in r.message for r in caplog.records)
177
+ # and recovery works: re-anchor near the real leading edge, harvest normally
178
+ clk.anchor(rtp_timestamp=big, utc=900_180_000.0) # multiple of 15
179
+ out = clk.advance((big + 180000 + 24000 + 1) & 0xFFFFFFFF)
180
+ assert len(out) == 1 and out[0].index == 0
181
+
182
+
139
183
  def test_settle_delays_completion():
140
184
  clk = SlotClock(15.0, SR, settle_sec=2.0)
141
185
  clk.anchor(rtp_timestamp=0, utc=900_000_000.0) # 900000000 % 15 == 0 -> boundary0 at offset 0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes