@@ -134,3 +134,94 @@ def test_read_after_close_raises_typed_error(stub_device):
134134 recv .close ()
135135 with pytest .raises (HackRFDeviceError , match = "closed" ):
136136 recv .read (16 )
137+
138+
139+ # ---------------------------------------------------------------------------
140+ # Odd-length chunks must not swap I and Q.
141+ #
142+ # A pipe is free to return fewer bytes than read1() asked for, so a raw chunk
143+ # can end in the middle of an I/Q pair. decode_iq drops an odd trailing byte;
144+ # if blocks() handed it such a chunk directly, that one dropped byte would
145+ # swap I and Q for every sample afterwards -- a capture whose power stats look
146+ # healthy while the phase is garbage. capture()'s own stream path carries the
147+ # odd byte forward; blocks() did not. These pin the carry on blocks(), and that
148+ # read() and blocks() share one carry so they can be mixed.
149+ # ---------------------------------------------------------------------------
150+
151+ def _odd_chunk_receiver (stub_device , chunks ):
152+ """A PersistentReceiver whose raw stream is exactly `chunks`."""
153+ h = _streaming_device (stub_device , nbytes = 16 )
154+ rx = h .open_receiver (100e6 , 8e6 )
155+ rx ._gen = iter (chunks ) # bypass the child; feed raw bytes directly
156+ rx ._ensure_open = lambda : rx ._gen
157+ return rx
158+
159+
160+ def test_blocks_carries_odd_byte_instead_of_swapping_iq (stub_device ):
161+ # ramp: I = 0,2,4,.. Q = 1,3,5,.. so a correct decode has I even, Q odd
162+ n = 1000
163+ raw = bytes (i % 256 for i in range (2 * n ))
164+ # split into deliberately odd-sized chunks (7, 13, 1, 3, ...)
165+ sizes = [7 , 13 , 1 , 3 , 101 , 9 ]
166+ chunks , pos = [], 0
167+ while pos < len (raw ):
168+ for s in sizes :
169+ chunks .append (raw [pos :pos + s ])
170+ pos += s
171+ if pos >= len (raw ):
172+ break
173+ chunks = [c for c in chunks if c ]
174+ assert any (len (c ) % 2 for c in chunks ), "test needs odd chunks"
175+
176+ rx = _odd_chunk_receiver (stub_device , chunks )
177+ got = np .concatenate (list (rx .blocks ()))
178+ assert len (got ) == n
179+ # decode_iq divides by 128 -> undo to compare against the raw ramp
180+ i = np .rint (got .real * 128 ).astype (int ) % 256
181+ q = np .rint (got .imag * 128 ).astype (int ) % 256
182+ expect = np .arange (2 * n ) % 256
183+ # int8 wraps at 128, so compare modulo 256 on the signed values
184+ assert np .array_equal (i % 256 , expect [0 ::2 ])
185+ assert np .array_equal (q % 256 , expect [1 ::2 ])
186+ assert rx .total_samples == n
187+
188+
189+ def test_blocks_and_read_share_one_carry (stub_device ):
190+ n = 64
191+ raw = bytes (i % 256 for i in range (2 * n ))
192+ # first chunk odd, so blocks() leaves one byte in the carry; then read()
193+ # must pick it up and stay pair-aligned.
194+ chunks = [raw [:5 ], raw [5 :40 ], raw [40 :]]
195+ rx = _odd_chunk_receiver (stub_device , chunks )
196+ gen = rx .blocks ()
197+ first = next (gen ) # consumes raw[:5] -> 2 samples, 1 byte carried
198+ assert len (first ) == 2
199+ assert rx ._carry == raw [4 :5 ]
200+ rest = rx .read (n - 2 )
201+ got = np .concatenate ([first , rest ])
202+ assert len (got ) == n
203+ i = np .rint (got .real * 128 ).astype (int ) % 256
204+ assert np .array_equal (i , (np .arange (2 * n ) % 256 )[0 ::2 ])
205+
206+
207+ def test_blocks_through_a_real_pipe_with_odd_writes (stub_device ):
208+ """Same contract, but the odd boundaries come from an actual child process
209+ writing and flushing odd-sized pieces into the stdout pipe -- the path a
210+ downstream IQSource adapter (sdr_dsp's HackRFCapture) streams through.
211+ On the pre-fix code this swapped I and Q from the first odd piece on."""
212+ n = 200_000
213+ raw = bytes (i % 256 for i in range (2 * n ))
214+ h = stub_device (transfer = dict (emit_bytes = raw , idle = False ,
215+ emit_chunk_sizes = [7 , 65537 , 3 , 131071 , 1 ]))
216+ got = []
217+ with h .open_receiver (100e6 , 8e6 ) as rx :
218+ for blk in rx .blocks ():
219+ got .append (blk )
220+ if sum (len (g ) for g in got ) >= n :
221+ break
222+ x = np .concatenate (got )[:n ]
223+ i = np .rint (x .real * 128 ).astype (int ) % 256
224+ q = np .rint (x .imag * 128 ).astype (int ) % 256
225+ expect = np .arange (2 * n ) % 256
226+ assert np .array_equal (i , expect [0 ::2 ])
227+ assert np .array_equal (q , expect [1 ::2 ])
0 commit comments