RTL-SDR IQ sample reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

rtlsdr: split test.ml into test_rtlsdr.ml + runner (merlint E620)

Also replace Format.fprintf with Fmt.pf.

+422 -418
+1 -418
test/test.ml
··· 1 - let pi = Float.pi 2 - 3 - let complex_testable = 4 - Alcotest.testable 5 - (fun ppf (c : Dsp.Complex.t) -> Format.fprintf ppf "(%f + %fi)" c.re c.im) 6 - (fun (a : Dsp.Complex.t) (b : Dsp.Complex.t) -> 7 - Float.abs (a.re -. b.re) < 1e-6 && Float.abs (a.im -. b.im) < 1e-6) 8 - 9 - let approx_complex_testable eps = 10 - Alcotest.testable 11 - (fun ppf (c : Dsp.Complex.t) -> Format.fprintf ppf "(%f + %fi)" c.re c.im) 12 - (fun (a : Dsp.Complex.t) (b : Dsp.Complex.t) -> 13 - Float.abs (a.re -. b.re) < eps && Float.abs (a.im -. b.im) < eps) 14 - 15 - (* Test 1: of_bytes roundtrip with known values *) 16 - let test_of_bytes_roundtrip () = 17 - (* (127, 127) -> approximately (0, 0) -- actually maps to 18 - (127 - 127.5)/127.5 = -0.003921... for both *) 19 - (* (0, 0) -> (-1, -1) *) 20 - (* (255, 255) -> (+1, +1) *) 21 - let data = Bytes.create 6 in 22 - Bytes.set data 0 (Char.chr 127); 23 - Bytes.set data 1 (Char.chr 127); 24 - Bytes.set data 2 (Char.chr 0); 25 - Bytes.set data 3 (Char.chr 0); 26 - Bytes.set data 4 (Char.chr 255); 27 - Bytes.set data 5 (Char.chr 255); 28 - let src = Rtlsdr.of_bytes data in 29 - let samples = Rtlsdr.read_all src in 30 - Alcotest.(check int) "3 samples" 3 (Array.length samples); 31 - (* (127,127): (127-127.5)/127.5 = -0.00392156... *) 32 - let near_zero = -0.5 /. 127.5 in 33 - let tc = approx_complex_testable 1e-6 in 34 - Alcotest.check tc "(127,127) -> ~0+0j" 35 - (Dsp.Complex.make near_zero near_zero) 36 - samples.(0); 37 - Alcotest.check tc "(0,0) -> -1-1j" 38 - (Dsp.Complex.make (-1.0) (-1.0)) 39 - samples.(1); 40 - Alcotest.check tc "(255,255) -> +1+1j" (Dsp.Complex.make 1.0 1.0) samples.(2) 41 - 42 - (* Test 2: generate_tone produces correct frequency *) 43 - let test_generate_tone () = 44 - let sample_rate = 1000.0 in 45 - let freq = 100.0 in 46 - let duration = 0.01 in 47 - let samples = Rtlsdr.generate_tone ~sample_rate ~freq ~duration in 48 - (* 10 samples at 1000 Hz sample rate for 0.01s *) 49 - Alcotest.(check int) "sample count" 10 (Array.length samples); 50 - (* Check phase rotation: each sample should advance by 51 - 2*pi*freq/sample_rate = 2*pi*100/1000 = 0.2*pi radians *) 52 - let phase_step = 2.0 *. pi *. freq /. sample_rate in 53 - let tc = approx_complex_testable 1e-9 in 54 - Array.iteri 55 - (fun i s -> 56 - let expected_angle = phase_step *. Float.of_int i in 57 - let expected = 58 - Dsp.Complex.make (Float.cos expected_angle) (Float.sin expected_angle) 59 - in 60 - Alcotest.check tc (Fmt.str "sample %d" i) expected s) 61 - samples 62 - 63 - (* Test 3: generate_bpsk changes phase on bit transitions *) 64 - let test_generate_bpsk () = 65 - let sample_rate = 1000.0 in 66 - let symbol_rate = 100.0 in 67 - let carrier_freq = 0.0 in 68 - (* Use carrier_freq=0 so the signal is just +1 or -1 on I axis *) 69 - let bits = Bytes.of_string "\x00\x01\x00" in 70 - let samples = 71 - Rtlsdr.generate_bpsk ~sample_rate ~symbol_rate ~bits ~carrier_freq 72 - in 73 - let sps = 10 in 74 - (* samples per symbol = 1000/100 *) 75 - Alcotest.(check int) "total samples" 30 (Array.length samples); 76 - (* Bit 0 (value 0): no phase offset, so cos(0)=1.0 for all samples *) 77 - let tc = approx_complex_testable 1e-9 in 78 - for i = 0 to sps - 1 do 79 - Alcotest.check tc 80 - (Fmt.str "bit0 sample %d" i) 81 - (Dsp.Complex.make 1.0 0.0) samples.(i) 82 - done; 83 - (* Bit 1 (value 1): pi phase offset, so cos(pi)=-1.0 *) 84 - for i = sps to (2 * sps) - 1 do 85 - Alcotest.check tc 86 - (Fmt.str "bit1 sample %d" i) 87 - (Dsp.Complex.make (-1.0) 0.0) 88 - samples.(i) 89 - done; 90 - (* Bit 2 (value 0): back to no offset, cos(0)=1.0 *) 91 - for i = 2 * sps to (3 * sps) - 1 do 92 - Alcotest.check tc 93 - (Fmt.str "bit2 sample %d" i) 94 - (Dsp.Complex.make 1.0 0.0) samples.(i) 95 - done 96 - 97 - (* Test 4: sample_count matches byte length / 2 *) 98 - let test_sample_count () = 99 - let data = Bytes.create 100 in 100 - let src = Rtlsdr.of_bytes data in 101 - Alcotest.(check int) "50 samples from 100 bytes" 50 (Rtlsdr.sample_count src); 102 - let data2 = Bytes.create 0 in 103 - let src2 = Rtlsdr.of_bytes data2 in 104 - Alcotest.(check int) "0 samples from 0 bytes" 0 (Rtlsdr.sample_count src2); 105 - (* Odd number of bytes: last byte is ignored *) 106 - let data3 = Bytes.create 7 in 107 - let src3 = Rtlsdr.of_bytes data3 in 108 - Alcotest.(check int) "3 samples from 7 bytes" 3 (Rtlsdr.sample_count src3) 109 - 110 - (* Test 5: read returns correct number of samples *) 111 - let test_read_count () = 112 - let data = Bytes.create 20 in 113 - let src = Rtlsdr.of_bytes data in 114 - (* Read 3 samples *) 115 - let s1 = Rtlsdr.read src 3 in 116 - Alcotest.(check int) "read 3" 3 (Array.length s1); 117 - (* Read 5 more *) 118 - let s2 = Rtlsdr.read src 5 in 119 - Alcotest.(check int) "read 5" 5 (Array.length s2); 120 - (* Only 2 remain, ask for 10 *) 121 - let s3 = Rtlsdr.read src 10 in 122 - Alcotest.(check int) "read remaining 2" 2 (Array.length s3); 123 - (* No more samples *) 124 - let s4 = Rtlsdr.read src 5 in 125 - Alcotest.(check int) "read at EOF" 0 (Array.length s4) 126 - 127 - (* Test 6: metadata attach/retrieve roundtrip *) 128 - let test_metadata () = 129 - let data = Bytes.create 10 in 130 - let src = Rtlsdr.of_bytes data in 131 - Alcotest.(check bool) 132 - "no metadata initially" true 133 - (Option.is_none (Rtlsdr.metadata src)); 134 - let meta : Rtlsdr.metadata = 135 - { center_freq = 433.92e6; sample_rate = 2.4e6; gain = 40.0 } 136 - in 137 - let src2 = Rtlsdr.with_metadata meta src in 138 - let retrieved = Rtlsdr.metadata src2 in 139 - Alcotest.(check bool) "has metadata" true (Option.is_some retrieved); 140 - let m = Option.get retrieved in 141 - Alcotest.(check (float 0.0)) "center_freq" 433.92e6 m.center_freq; 142 - Alcotest.(check (float 0.0)) "sample_rate" 2.4e6 m.sample_rate; 143 - Alcotest.(check (float 0.0)) "gain" 40.0 m.gain 144 - 145 - (* -- Tone generation: DSP textbook reference vectors -- *) 146 - 147 - (* fs/4 tone: phase advances pi/2 per sample → (1,0),(0,1),(-1,0),(0,-1),... *) 148 - let test_tone_fs_over_4 () = 149 - let samples = 150 - Rtlsdr.generate_tone ~sample_rate:1.0 ~freq:0.25 ~duration:4.0 151 - in 152 - Alcotest.(check int) "4 samples" 4 (Array.length samples); 153 - let tc = approx_complex_testable 1e-9 in 154 - Alcotest.check tc "sample[0] = 1+0j" (Dsp.Complex.make 1.0 0.0) samples.(0); 155 - Alcotest.check tc "sample[1] = 0+1j" (Dsp.Complex.make 0.0 1.0) samples.(1); 156 - Alcotest.check tc "sample[2] = -1+0j" 157 - (Dsp.Complex.make (-1.0) 0.0) 158 - samples.(2); 159 - Alcotest.check tc "sample[3] = 0-1j" (Dsp.Complex.make 0.0 (-1.0)) samples.(3) 160 - 161 - (* DC tone: freq=0 → all samples are (1, 0) *) 162 - let test_tone_dc () = 163 - let samples = 164 - Rtlsdr.generate_tone ~sample_rate:1000.0 ~freq:0.0 ~duration:0.01 165 - in 166 - Alcotest.(check int) "10 samples" 10 (Array.length samples); 167 - let tc = approx_complex_testable 1e-9 in 168 - Array.iteri 169 - (fun i s -> 170 - Alcotest.check tc (Fmt.str "DC sample %d" i) (Dsp.Complex.make 1.0 0.0) s) 171 - samples 172 - 173 - (* Tone amplitude: |sample| ≈ 1.0 for all samples *) 174 - let test_tone_unit_magnitude () = 175 - let samples = 176 - Rtlsdr.generate_tone ~sample_rate:8000.0 ~freq:1234.5 ~duration:0.01 177 - in 178 - Array.iteri 179 - (fun i (s : Dsp.Complex.t) -> 180 - let mag = Dsp.Complex.norm s in 181 - if Float.abs (mag -. 1.0) > 1e-9 then 182 - Alcotest.failf "sample %d magnitude = %f, expected 1.0" i mag) 183 - samples 184 - 185 - (* Nyquist/2 (fs/2): phase advances by pi per sample → alternating ±1 *) 186 - let test_tone_nyquist_half () = 187 - let samples = Rtlsdr.generate_tone ~sample_rate:1.0 ~freq:0.5 ~duration:4.0 in 188 - Alcotest.(check int) "4 samples" 4 (Array.length samples); 189 - let tc = approx_complex_testable 1e-9 in 190 - (* freq=0.5*fs: angle = 2*pi*0.5*i = pi*i 191 - i=0: (1,0), i=1: (-1,~0), i=2: (1,~0), i=3: (-1,~0) *) 192 - Alcotest.check tc "sample[0]" (Dsp.Complex.make 1.0 0.0) samples.(0); 193 - Alcotest.check tc "sample[1]" (Dsp.Complex.make (-1.0) 0.0) samples.(1); 194 - Alcotest.check tc "sample[2]" (Dsp.Complex.make 1.0 0.0) samples.(2); 195 - Alcotest.check tc "sample[3]" (Dsp.Complex.make (-1.0) 0.0) samples.(3) 196 - 197 - (* -- BPSK generation: communications reference vectors -- *) 198 - 199 - (* BPSK bits [1, 0]: first symbol phase π (negative), second phase 0 (positive) *) 200 - let test_bpsk_bit_polarity () = 201 - let bits = Bytes.of_string "\x01\x00" in 202 - let sps = 8 in 203 - let samples = 204 - Rtlsdr.generate_bpsk ~sample_rate:8000.0 ~symbol_rate:1000.0 ~bits 205 - ~carrier_freq:0.0 206 - in 207 - Alcotest.(check int) "total samples" (2 * sps) (Array.length samples); 208 - let tc = approx_complex_testable 1e-9 in 209 - (* bit=1 → phase π → cos(π)=-1 *) 210 - Alcotest.check tc "bit=1 center sample" 211 - (Dsp.Complex.make (-1.0) 0.0) 212 - samples.(sps / 2); 213 - (* bit=0 → phase 0 → cos(0)=+1 *) 214 - Alcotest.check tc "bit=0 center sample" (Dsp.Complex.make 1.0 0.0) 215 - samples.(sps + (sps / 2)) 216 - 217 - (* Verify transitions happen at symbol boundaries *) 218 - let test_bpsk_transitions () = 219 - let bits = Bytes.of_string "\x00\x01\x00" in 220 - let sps = 4 in 221 - let samples = 222 - Rtlsdr.generate_bpsk ~sample_rate:4000.0 ~symbol_rate:1000.0 ~bits 223 - ~carrier_freq:0.0 224 - in 225 - Alcotest.(check int) "total samples" 12 (Array.length samples); 226 - let tc = approx_complex_testable 1e-9 in 227 - (* Last sample of symbol 0 (bit=0): positive *) 228 - Alcotest.check tc "end of sym0" (Dsp.Complex.make 1.0 0.0) samples.(sps - 1); 229 - (* First sample of symbol 1 (bit=1): negative *) 230 - Alcotest.check tc "start of sym1" (Dsp.Complex.make (-1.0) 0.0) samples.(sps); 231 - (* Last sample of symbol 1 (bit=1): negative *) 232 - Alcotest.check tc "end of sym1" 233 - (Dsp.Complex.make (-1.0) 0.0) 234 - samples.((2 * sps) - 1); 235 - (* First sample of symbol 2 (bit=0): positive *) 236 - Alcotest.check tc "start of sym2" (Dsp.Complex.make 1.0 0.0) samples.(2 * sps) 237 - 238 - (* Verify sps samples per symbol *) 239 - let test_bpsk_sps () = 240 - let bits = Bytes.of_string "\x01\x00\x01\x01\x00" in 241 - let sps = 10 in 242 - let samples = 243 - Rtlsdr.generate_bpsk ~sample_rate:10000.0 ~symbol_rate:1000.0 ~bits 244 - ~carrier_freq:0.0 245 - in 246 - Alcotest.(check int) "total = nbits * sps" (5 * sps) (Array.length samples) 247 - 248 - (* -- IQ byte format: RTL-SDR hardware spec -- *) 249 - 250 - (* RTL-SDR: unsigned 8-bit I,Q interleaved. Center is 127/128. *) 251 - let test_iq_byte_format () = 252 - let data = Bytes.create 4 in 253 - Bytes.set data 0 (Char.chr 127); 254 - Bytes.set data 1 (Char.chr 128); 255 - Bytes.set data 2 (Char.chr 0); 256 - Bytes.set data 3 (Char.chr 255); 257 - let src = Rtlsdr.of_bytes data in 258 - let samples = Rtlsdr.read_all src in 259 - Alcotest.(check int) "2 IQ samples" 2 (Array.length samples); 260 - let tc = approx_complex_testable 0.01 in 261 - (* (127,128): I=(127-127.5)/127.5=-0.00392, Q=(128-127.5)/127.5=+0.00392 *) 262 - Alcotest.check tc "sample 0 near DC" 263 - (Dsp.Complex.make (-0.5 /. 127.5) (0.5 /. 127.5)) 264 - samples.(0); 265 - (* (0,255): I=-1.0, Q=+1.0 *) 266 - Alcotest.check tc "sample 1 = (-1, +1)" 267 - (Dsp.Complex.make (-1.0) 1.0) 268 - samples.(1) 269 - 270 - (* Large buffer: 10000 random bytes → of_bytes → 5000 samples *) 271 - let test_iq_large_buffer () = 272 - let n_bytes = 10000 in 273 - let data = Bytes.init n_bytes (fun _ -> Char.chr (Random.int 256)) in 274 - let src = Rtlsdr.of_bytes data in 275 - Alcotest.(check int) "5000 samples" 5000 (Rtlsdr.sample_count src); 276 - let samples = Rtlsdr.read_all src in 277 - Alcotest.(check int) "read_all returns 5000" 5000 (Array.length samples); 278 - (* Verify all values in [-1,+1] range *) 279 - Array.iteri 280 - (fun i (s : Dsp.Complex.t) -> 281 - if s.re < -1.0 || s.re > 1.0 then 282 - Alcotest.failf "sample %d re=%f out of range" i s.re; 283 - if s.im < -1.0 || s.im > 1.0 then 284 - Alcotest.failf "sample %d im=%f out of range" i s.im) 285 - samples 286 - 287 - (* -- Source operations -- *) 288 - 289 - (* of_bytes → read all → verify matches input *) 290 - let test_source_read_all_matches () = 291 - let data = Bytes.create 6 in 292 - Bytes.set data 0 (Char.chr 0); 293 - Bytes.set data 1 (Char.chr 255); 294 - Bytes.set data 2 (Char.chr 128); 295 - Bytes.set data 3 (Char.chr 128); 296 - Bytes.set data 4 (Char.chr 64); 297 - Bytes.set data 5 (Char.chr 192); 298 - let src = Rtlsdr.of_bytes data in 299 - let samples = Rtlsdr.read_all src in 300 - Alcotest.(check int) "3 samples" 3 (Array.length samples); 301 - let tc = approx_complex_testable 1e-6 in 302 - Alcotest.check tc "sample 0" (Dsp.Complex.make (-1.0) 1.0) samples.(0); 303 - Alcotest.check tc "sample 1" 304 - (Dsp.Complex.make (0.5 /. 127.5) (0.5 /. 127.5)) 305 - samples.(1); 306 - Alcotest.check tc "sample 2" 307 - (Dsp.Complex.make ((64.0 -. 127.5) /. 127.5) ((192.0 -. 127.5) /. 127.5)) 308 - samples.(2) 309 - 310 - (* Sequential reads: read(10) + read(10) = read_all for 20-sample source *) 311 - let test_sequential_reads () = 312 - let data = Bytes.init 40 (fun i -> Char.chr (i mod 256)) in 313 - let src1 = Rtlsdr.of_bytes data in 314 - let all = Rtlsdr.read_all src1 in 315 - let src2 = Rtlsdr.of_bytes data in 316 - let first = Rtlsdr.read src2 10 in 317 - let second = Rtlsdr.read src2 10 in 318 - Alcotest.(check int) "first batch" 10 (Array.length first); 319 - Alcotest.(check int) "second batch" 10 (Array.length second); 320 - let tc = approx_complex_testable 1e-9 in 321 - for i = 0 to 9 do 322 - Alcotest.check tc (Fmt.str "first[%d]" i) all.(i) first.(i) 323 - done; 324 - for i = 0 to 9 do 325 - Alcotest.check tc (Fmt.str "second[%d]" i) all.(10 + i) second.(i) 326 - done 327 - 328 - (* Read after exhaustion → empty *) 329 - let test_read_after_exhaustion () = 330 - let data = Bytes.create 4 in 331 - let src = Rtlsdr.of_bytes data in 332 - let _ = Rtlsdr.read_all src in 333 - let after = Rtlsdr.read src 10 in 334 - Alcotest.(check int) "empty after exhaustion" 0 (Array.length after); 335 - let after2 = Rtlsdr.read_all src in 336 - Alcotest.(check int) "read_all after exhaustion" 0 (Array.length after2) 337 - 338 - (* sample_count accuracy *) 339 - let test_sample_count_accuracy () = 340 - let check n_bytes expected = 341 - let data = Bytes.create n_bytes in 342 - let src = Rtlsdr.of_bytes data in 343 - Alcotest.(check int) 344 - (Fmt.str "%d bytes -> %d samples" n_bytes expected) 345 - expected (Rtlsdr.sample_count src) 346 - in 347 - check 0 0; 348 - check 1 0; 349 - check 2 1; 350 - check 3 1; 351 - check 100 50; 352 - check 255 127; 353 - check 10000 5000 354 - 355 - (* -- Metadata -- *) 356 - 357 - (* Attach metadata → read back → verify all fields match *) 358 - let test_metadata_roundtrip () = 359 - let data = Bytes.create 10 in 360 - let src = Rtlsdr.of_bytes data in 361 - let meta : Rtlsdr.metadata = 362 - { center_freq = 145.8e6; sample_rate = 2.048e6; gain = 49.6 } 363 - in 364 - let src2 = Rtlsdr.with_metadata meta src in 365 - let m = Option.get (Rtlsdr.metadata src2) in 366 - Alcotest.(check (float 0.0)) "center_freq" 145.8e6 m.center_freq; 367 - Alcotest.(check (float 0.0)) "sample_rate" 2.048e6 m.sample_rate; 368 - Alcotest.(check (float 0.0)) "gain" 49.6 m.gain 369 - 370 - (* Default metadata: None *) 371 - let test_metadata_default () = 372 - let data = Bytes.create 10 in 373 - let src = Rtlsdr.of_bytes data in 374 - Alcotest.(check bool) 375 - "no metadata by default" true 376 - (Option.is_none (Rtlsdr.metadata src)) 377 - 378 - let () = 379 - Alcotest.run "rtlsdr" 380 - [ 381 - ( "iq", 382 - [ 383 - Alcotest.test_case "of_bytes roundtrip" `Quick test_of_bytes_roundtrip; 384 - Alcotest.test_case "sample_count" `Quick test_sample_count; 385 - Alcotest.test_case "read count" `Quick test_read_count; 386 - Alcotest.test_case "metadata roundtrip" `Quick test_metadata; 387 - Alcotest.test_case "IQ byte format" `Quick test_iq_byte_format; 388 - Alcotest.test_case "IQ large buffer" `Quick test_iq_large_buffer; 389 - ] ); 390 - ( "signal", 391 - [ 392 - Alcotest.test_case "generate_tone" `Quick test_generate_tone; 393 - Alcotest.test_case "generate_bpsk" `Quick test_generate_bpsk; 394 - Alcotest.test_case "tone fs/4" `Quick test_tone_fs_over_4; 395 - Alcotest.test_case "tone DC" `Quick test_tone_dc; 396 - Alcotest.test_case "tone unit magnitude" `Quick 397 - test_tone_unit_magnitude; 398 - Alcotest.test_case "tone Nyquist/2" `Quick test_tone_nyquist_half; 399 - Alcotest.test_case "BPSK bit polarity" `Quick test_bpsk_bit_polarity; 400 - Alcotest.test_case "BPSK transitions" `Quick test_bpsk_transitions; 401 - Alcotest.test_case "BPSK sps" `Quick test_bpsk_sps; 402 - ] ); 403 - ( "source", 404 - [ 405 - Alcotest.test_case "read_all matches" `Quick 406 - test_source_read_all_matches; 407 - Alcotest.test_case "sequential reads" `Quick test_sequential_reads; 408 - Alcotest.test_case "read after exhaustion" `Quick 409 - test_read_after_exhaustion; 410 - Alcotest.test_case "sample_count accuracy" `Quick 411 - test_sample_count_accuracy; 412 - ] ); 413 - ( "metadata", 414 - [ 415 - Alcotest.test_case "metadata roundtrip" `Quick test_metadata_roundtrip; 416 - Alcotest.test_case "metadata default" `Quick test_metadata_default; 417 - ] ); 418 - ] 1 + let () = Alcotest.run "rtlsdr" Test_rtlsdr.suites
+417
test/test_rtlsdr.ml
··· 1 + let pi = Float.pi 2 + 3 + let complex_testable = 4 + Alcotest.testable 5 + (fun ppf (c : Dsp.Complex.t) -> Fmt.pf ppf "(%f + %fi)" c.re c.im) 6 + (fun (a : Dsp.Complex.t) (b : Dsp.Complex.t) -> 7 + Float.abs (a.re -. b.re) < 1e-6 && Float.abs (a.im -. b.im) < 1e-6) 8 + 9 + let approx_complex_testable eps = 10 + Alcotest.testable 11 + (fun ppf (c : Dsp.Complex.t) -> Fmt.pf ppf "(%f + %fi)" c.re c.im) 12 + (fun (a : Dsp.Complex.t) (b : Dsp.Complex.t) -> 13 + Float.abs (a.re -. b.re) < eps && Float.abs (a.im -. b.im) < eps) 14 + 15 + (* Test 1: of_bytes roundtrip with known values *) 16 + let test_of_bytes_roundtrip () = 17 + (* (127, 127) -> approximately (0, 0) -- actually maps to 18 + (127 - 127.5)/127.5 = -0.003921... for both *) 19 + (* (0, 0) -> (-1, -1) *) 20 + (* (255, 255) -> (+1, +1) *) 21 + let data = Bytes.create 6 in 22 + Bytes.set data 0 (Char.chr 127); 23 + Bytes.set data 1 (Char.chr 127); 24 + Bytes.set data 2 (Char.chr 0); 25 + Bytes.set data 3 (Char.chr 0); 26 + Bytes.set data 4 (Char.chr 255); 27 + Bytes.set data 5 (Char.chr 255); 28 + let src = Rtlsdr.of_bytes data in 29 + let samples = Rtlsdr.read_all src in 30 + Alcotest.(check int) "3 samples" 3 (Array.length samples); 31 + (* (127,127): (127-127.5)/127.5 = -0.00392156... *) 32 + let near_zero = -0.5 /. 127.5 in 33 + let tc = approx_complex_testable 1e-6 in 34 + Alcotest.check tc "(127,127) -> ~0+0j" 35 + (Dsp.Complex.make near_zero near_zero) 36 + samples.(0); 37 + Alcotest.check tc "(0,0) -> -1-1j" 38 + (Dsp.Complex.make (-1.0) (-1.0)) 39 + samples.(1); 40 + Alcotest.check tc "(255,255) -> +1+1j" (Dsp.Complex.make 1.0 1.0) samples.(2) 41 + 42 + (* Test 2: generate_tone produces correct frequency *) 43 + let test_generate_tone () = 44 + let sample_rate = 1000.0 in 45 + let freq = 100.0 in 46 + let duration = 0.01 in 47 + let samples = Rtlsdr.generate_tone ~sample_rate ~freq ~duration in 48 + (* 10 samples at 1000 Hz sample rate for 0.01s *) 49 + Alcotest.(check int) "sample count" 10 (Array.length samples); 50 + (* Check phase rotation: each sample should advance by 51 + 2*pi*freq/sample_rate = 2*pi*100/1000 = 0.2*pi radians *) 52 + let phase_step = 2.0 *. pi *. freq /. sample_rate in 53 + let tc = approx_complex_testable 1e-9 in 54 + Array.iteri 55 + (fun i s -> 56 + let expected_angle = phase_step *. Float.of_int i in 57 + let expected = 58 + Dsp.Complex.make (Float.cos expected_angle) (Float.sin expected_angle) 59 + in 60 + Alcotest.check tc (Fmt.str "sample %d" i) expected s) 61 + samples 62 + 63 + (* Test 3: generate_bpsk changes phase on bit transitions *) 64 + let test_generate_bpsk () = 65 + let sample_rate = 1000.0 in 66 + let symbol_rate = 100.0 in 67 + let carrier_freq = 0.0 in 68 + (* Use carrier_freq=0 so the signal is just +1 or -1 on I axis *) 69 + let bits = Bytes.of_string "\x00\x01\x00" in 70 + let samples = 71 + Rtlsdr.generate_bpsk ~sample_rate ~symbol_rate ~bits ~carrier_freq 72 + in 73 + let sps = 10 in 74 + (* samples per symbol = 1000/100 *) 75 + Alcotest.(check int) "total samples" 30 (Array.length samples); 76 + (* Bit 0 (value 0): no phase offset, so cos(0)=1.0 for all samples *) 77 + let tc = approx_complex_testable 1e-9 in 78 + for i = 0 to sps - 1 do 79 + Alcotest.check tc 80 + (Fmt.str "bit0 sample %d" i) 81 + (Dsp.Complex.make 1.0 0.0) samples.(i) 82 + done; 83 + (* Bit 1 (value 1): pi phase offset, so cos(pi)=-1.0 *) 84 + for i = sps to (2 * sps) - 1 do 85 + Alcotest.check tc 86 + (Fmt.str "bit1 sample %d" i) 87 + (Dsp.Complex.make (-1.0) 0.0) 88 + samples.(i) 89 + done; 90 + (* Bit 2 (value 0): back to no offset, cos(0)=1.0 *) 91 + for i = 2 * sps to (3 * sps) - 1 do 92 + Alcotest.check tc 93 + (Fmt.str "bit2 sample %d" i) 94 + (Dsp.Complex.make 1.0 0.0) samples.(i) 95 + done 96 + 97 + (* Test 4: sample_count matches byte length / 2 *) 98 + let test_sample_count () = 99 + let data = Bytes.create 100 in 100 + let src = Rtlsdr.of_bytes data in 101 + Alcotest.(check int) "50 samples from 100 bytes" 50 (Rtlsdr.sample_count src); 102 + let data2 = Bytes.create 0 in 103 + let src2 = Rtlsdr.of_bytes data2 in 104 + Alcotest.(check int) "0 samples from 0 bytes" 0 (Rtlsdr.sample_count src2); 105 + (* Odd number of bytes: last byte is ignored *) 106 + let data3 = Bytes.create 7 in 107 + let src3 = Rtlsdr.of_bytes data3 in 108 + Alcotest.(check int) "3 samples from 7 bytes" 3 (Rtlsdr.sample_count src3) 109 + 110 + (* Test 5: read returns correct number of samples *) 111 + let test_read_count () = 112 + let data = Bytes.create 20 in 113 + let src = Rtlsdr.of_bytes data in 114 + (* Read 3 samples *) 115 + let s1 = Rtlsdr.read src 3 in 116 + Alcotest.(check int) "read 3" 3 (Array.length s1); 117 + (* Read 5 more *) 118 + let s2 = Rtlsdr.read src 5 in 119 + Alcotest.(check int) "read 5" 5 (Array.length s2); 120 + (* Only 2 remain, ask for 10 *) 121 + let s3 = Rtlsdr.read src 10 in 122 + Alcotest.(check int) "read remaining 2" 2 (Array.length s3); 123 + (* No more samples *) 124 + let s4 = Rtlsdr.read src 5 in 125 + Alcotest.(check int) "read at EOF" 0 (Array.length s4) 126 + 127 + (* Test 6: metadata attach/retrieve roundtrip *) 128 + let test_metadata () = 129 + let data = Bytes.create 10 in 130 + let src = Rtlsdr.of_bytes data in 131 + Alcotest.(check bool) 132 + "no metadata initially" true 133 + (Option.is_none (Rtlsdr.metadata src)); 134 + let meta : Rtlsdr.metadata = 135 + { center_freq = 433.92e6; sample_rate = 2.4e6; gain = 40.0 } 136 + in 137 + let src2 = Rtlsdr.with_metadata meta src in 138 + let retrieved = Rtlsdr.metadata src2 in 139 + Alcotest.(check bool) "has metadata" true (Option.is_some retrieved); 140 + let m = Option.get retrieved in 141 + Alcotest.(check (float 0.0)) "center_freq" 433.92e6 m.center_freq; 142 + Alcotest.(check (float 0.0)) "sample_rate" 2.4e6 m.sample_rate; 143 + Alcotest.(check (float 0.0)) "gain" 40.0 m.gain 144 + 145 + (* -- Tone generation: DSP textbook reference vectors -- *) 146 + 147 + (* fs/4 tone: phase advances pi/2 per sample → (1,0),(0,1),(-1,0),(0,-1),... *) 148 + let test_tone_fs_over_4 () = 149 + let samples = 150 + Rtlsdr.generate_tone ~sample_rate:1.0 ~freq:0.25 ~duration:4.0 151 + in 152 + Alcotest.(check int) "4 samples" 4 (Array.length samples); 153 + let tc = approx_complex_testable 1e-9 in 154 + Alcotest.check tc "sample[0] = 1+0j" (Dsp.Complex.make 1.0 0.0) samples.(0); 155 + Alcotest.check tc "sample[1] = 0+1j" (Dsp.Complex.make 0.0 1.0) samples.(1); 156 + Alcotest.check tc "sample[2] = -1+0j" 157 + (Dsp.Complex.make (-1.0) 0.0) 158 + samples.(2); 159 + Alcotest.check tc "sample[3] = 0-1j" (Dsp.Complex.make 0.0 (-1.0)) samples.(3) 160 + 161 + (* DC tone: freq=0 → all samples are (1, 0) *) 162 + let test_tone_dc () = 163 + let samples = 164 + Rtlsdr.generate_tone ~sample_rate:1000.0 ~freq:0.0 ~duration:0.01 165 + in 166 + Alcotest.(check int) "10 samples" 10 (Array.length samples); 167 + let tc = approx_complex_testable 1e-9 in 168 + Array.iteri 169 + (fun i s -> 170 + Alcotest.check tc (Fmt.str "DC sample %d" i) (Dsp.Complex.make 1.0 0.0) s) 171 + samples 172 + 173 + (* Tone amplitude: |sample| ≈ 1.0 for all samples *) 174 + let test_tone_unit_magnitude () = 175 + let samples = 176 + Rtlsdr.generate_tone ~sample_rate:8000.0 ~freq:1234.5 ~duration:0.01 177 + in 178 + Array.iteri 179 + (fun i (s : Dsp.Complex.t) -> 180 + let mag = Dsp.Complex.norm s in 181 + if Float.abs (mag -. 1.0) > 1e-9 then 182 + Alcotest.failf "sample %d magnitude = %f, expected 1.0" i mag) 183 + samples 184 + 185 + (* Nyquist/2 (fs/2): phase advances by pi per sample → alternating ±1 *) 186 + let test_tone_nyquist_half () = 187 + let samples = Rtlsdr.generate_tone ~sample_rate:1.0 ~freq:0.5 ~duration:4.0 in 188 + Alcotest.(check int) "4 samples" 4 (Array.length samples); 189 + let tc = approx_complex_testable 1e-9 in 190 + (* freq=0.5*fs: angle = 2*pi*0.5*i = pi*i 191 + i=0: (1,0), i=1: (-1,~0), i=2: (1,~0), i=3: (-1,~0) *) 192 + Alcotest.check tc "sample[0]" (Dsp.Complex.make 1.0 0.0) samples.(0); 193 + Alcotest.check tc "sample[1]" (Dsp.Complex.make (-1.0) 0.0) samples.(1); 194 + Alcotest.check tc "sample[2]" (Dsp.Complex.make 1.0 0.0) samples.(2); 195 + Alcotest.check tc "sample[3]" (Dsp.Complex.make (-1.0) 0.0) samples.(3) 196 + 197 + (* -- BPSK generation: communications reference vectors -- *) 198 + 199 + (* BPSK bits [1, 0]: first symbol phase π (negative), second phase 0 (positive) *) 200 + let test_bpsk_bit_polarity () = 201 + let bits = Bytes.of_string "\x01\x00" in 202 + let sps = 8 in 203 + let samples = 204 + Rtlsdr.generate_bpsk ~sample_rate:8000.0 ~symbol_rate:1000.0 ~bits 205 + ~carrier_freq:0.0 206 + in 207 + Alcotest.(check int) "total samples" (2 * sps) (Array.length samples); 208 + let tc = approx_complex_testable 1e-9 in 209 + (* bit=1 → phase π → cos(π)=-1 *) 210 + Alcotest.check tc "bit=1 center sample" 211 + (Dsp.Complex.make (-1.0) 0.0) 212 + samples.(sps / 2); 213 + (* bit=0 → phase 0 → cos(0)=+1 *) 214 + Alcotest.check tc "bit=0 center sample" (Dsp.Complex.make 1.0 0.0) 215 + samples.(sps + (sps / 2)) 216 + 217 + (* Verify transitions happen at symbol boundaries *) 218 + let test_bpsk_transitions () = 219 + let bits = Bytes.of_string "\x00\x01\x00" in 220 + let sps = 4 in 221 + let samples = 222 + Rtlsdr.generate_bpsk ~sample_rate:4000.0 ~symbol_rate:1000.0 ~bits 223 + ~carrier_freq:0.0 224 + in 225 + Alcotest.(check int) "total samples" 12 (Array.length samples); 226 + let tc = approx_complex_testable 1e-9 in 227 + (* Last sample of symbol 0 (bit=0): positive *) 228 + Alcotest.check tc "end of sym0" (Dsp.Complex.make 1.0 0.0) samples.(sps - 1); 229 + (* First sample of symbol 1 (bit=1): negative *) 230 + Alcotest.check tc "start of sym1" (Dsp.Complex.make (-1.0) 0.0) samples.(sps); 231 + (* Last sample of symbol 1 (bit=1): negative *) 232 + Alcotest.check tc "end of sym1" 233 + (Dsp.Complex.make (-1.0) 0.0) 234 + samples.((2 * sps) - 1); 235 + (* First sample of symbol 2 (bit=0): positive *) 236 + Alcotest.check tc "start of sym2" (Dsp.Complex.make 1.0 0.0) samples.(2 * sps) 237 + 238 + (* Verify sps samples per symbol *) 239 + let test_bpsk_sps () = 240 + let bits = Bytes.of_string "\x01\x00\x01\x01\x00" in 241 + let sps = 10 in 242 + let samples = 243 + Rtlsdr.generate_bpsk ~sample_rate:10000.0 ~symbol_rate:1000.0 ~bits 244 + ~carrier_freq:0.0 245 + in 246 + Alcotest.(check int) "total = nbits * sps" (5 * sps) (Array.length samples) 247 + 248 + (* -- IQ byte format: RTL-SDR hardware spec -- *) 249 + 250 + (* RTL-SDR: unsigned 8-bit I,Q interleaved. Center is 127/128. *) 251 + let test_iq_byte_format () = 252 + let data = Bytes.create 4 in 253 + Bytes.set data 0 (Char.chr 127); 254 + Bytes.set data 1 (Char.chr 128); 255 + Bytes.set data 2 (Char.chr 0); 256 + Bytes.set data 3 (Char.chr 255); 257 + let src = Rtlsdr.of_bytes data in 258 + let samples = Rtlsdr.read_all src in 259 + Alcotest.(check int) "2 IQ samples" 2 (Array.length samples); 260 + let tc = approx_complex_testable 0.01 in 261 + (* (127,128): I=(127-127.5)/127.5=-0.00392, Q=(128-127.5)/127.5=+0.00392 *) 262 + Alcotest.check tc "sample 0 near DC" 263 + (Dsp.Complex.make (-0.5 /. 127.5) (0.5 /. 127.5)) 264 + samples.(0); 265 + (* (0,255): I=-1.0, Q=+1.0 *) 266 + Alcotest.check tc "sample 1 = (-1, +1)" 267 + (Dsp.Complex.make (-1.0) 1.0) 268 + samples.(1) 269 + 270 + (* Large buffer: 10000 random bytes → of_bytes → 5000 samples *) 271 + let test_iq_large_buffer () = 272 + let n_bytes = 10000 in 273 + let data = Bytes.init n_bytes (fun _ -> Char.chr (Random.int 256)) in 274 + let src = Rtlsdr.of_bytes data in 275 + Alcotest.(check int) "5000 samples" 5000 (Rtlsdr.sample_count src); 276 + let samples = Rtlsdr.read_all src in 277 + Alcotest.(check int) "read_all returns 5000" 5000 (Array.length samples); 278 + (* Verify all values in [-1,+1] range *) 279 + Array.iteri 280 + (fun i (s : Dsp.Complex.t) -> 281 + if s.re < -1.0 || s.re > 1.0 then 282 + Alcotest.failf "sample %d re=%f out of range" i s.re; 283 + if s.im < -1.0 || s.im > 1.0 then 284 + Alcotest.failf "sample %d im=%f out of range" i s.im) 285 + samples 286 + 287 + (* -- Source operations -- *) 288 + 289 + (* of_bytes → read all → verify matches input *) 290 + let test_source_read_all_matches () = 291 + let data = Bytes.create 6 in 292 + Bytes.set data 0 (Char.chr 0); 293 + Bytes.set data 1 (Char.chr 255); 294 + Bytes.set data 2 (Char.chr 128); 295 + Bytes.set data 3 (Char.chr 128); 296 + Bytes.set data 4 (Char.chr 64); 297 + Bytes.set data 5 (Char.chr 192); 298 + let src = Rtlsdr.of_bytes data in 299 + let samples = Rtlsdr.read_all src in 300 + Alcotest.(check int) "3 samples" 3 (Array.length samples); 301 + let tc = approx_complex_testable 1e-6 in 302 + Alcotest.check tc "sample 0" (Dsp.Complex.make (-1.0) 1.0) samples.(0); 303 + Alcotest.check tc "sample 1" 304 + (Dsp.Complex.make (0.5 /. 127.5) (0.5 /. 127.5)) 305 + samples.(1); 306 + Alcotest.check tc "sample 2" 307 + (Dsp.Complex.make ((64.0 -. 127.5) /. 127.5) ((192.0 -. 127.5) /. 127.5)) 308 + samples.(2) 309 + 310 + (* Sequential reads: read(10) + read(10) = read_all for 20-sample source *) 311 + let test_sequential_reads () = 312 + let data = Bytes.init 40 (fun i -> Char.chr (i mod 256)) in 313 + let src1 = Rtlsdr.of_bytes data in 314 + let all = Rtlsdr.read_all src1 in 315 + let src2 = Rtlsdr.of_bytes data in 316 + let first = Rtlsdr.read src2 10 in 317 + let second = Rtlsdr.read src2 10 in 318 + Alcotest.(check int) "first batch" 10 (Array.length first); 319 + Alcotest.(check int) "second batch" 10 (Array.length second); 320 + let tc = approx_complex_testable 1e-9 in 321 + for i = 0 to 9 do 322 + Alcotest.check tc (Fmt.str "first[%d]" i) all.(i) first.(i) 323 + done; 324 + for i = 0 to 9 do 325 + Alcotest.check tc (Fmt.str "second[%d]" i) all.(10 + i) second.(i) 326 + done 327 + 328 + (* Read after exhaustion → empty *) 329 + let test_read_after_exhaustion () = 330 + let data = Bytes.create 4 in 331 + let src = Rtlsdr.of_bytes data in 332 + let _ = Rtlsdr.read_all src in 333 + let after = Rtlsdr.read src 10 in 334 + Alcotest.(check int) "empty after exhaustion" 0 (Array.length after); 335 + let after2 = Rtlsdr.read_all src in 336 + Alcotest.(check int) "read_all after exhaustion" 0 (Array.length after2) 337 + 338 + (* sample_count accuracy *) 339 + let test_sample_count_accuracy () = 340 + let check n_bytes expected = 341 + let data = Bytes.create n_bytes in 342 + let src = Rtlsdr.of_bytes data in 343 + Alcotest.(check int) 344 + (Fmt.str "%d bytes -> %d samples" n_bytes expected) 345 + expected (Rtlsdr.sample_count src) 346 + in 347 + check 0 0; 348 + check 1 0; 349 + check 2 1; 350 + check 3 1; 351 + check 100 50; 352 + check 255 127; 353 + check 10000 5000 354 + 355 + (* -- Metadata -- *) 356 + 357 + (* Attach metadata → read back → verify all fields match *) 358 + let test_metadata_roundtrip () = 359 + let data = Bytes.create 10 in 360 + let src = Rtlsdr.of_bytes data in 361 + let meta : Rtlsdr.metadata = 362 + { center_freq = 145.8e6; sample_rate = 2.048e6; gain = 49.6 } 363 + in 364 + let src2 = Rtlsdr.with_metadata meta src in 365 + let m = Option.get (Rtlsdr.metadata src2) in 366 + Alcotest.(check (float 0.0)) "center_freq" 145.8e6 m.center_freq; 367 + Alcotest.(check (float 0.0)) "sample_rate" 2.048e6 m.sample_rate; 368 + Alcotest.(check (float 0.0)) "gain" 49.6 m.gain 369 + 370 + (* Default metadata: None *) 371 + let test_metadata_default () = 372 + let data = Bytes.create 10 in 373 + let src = Rtlsdr.of_bytes data in 374 + Alcotest.(check bool) 375 + "no metadata by default" true 376 + (Option.is_none (Rtlsdr.metadata src)) 377 + 378 + let suites = 379 + [ 380 + ( "iq", 381 + [ 382 + Alcotest.test_case "of_bytes roundtrip" `Quick test_of_bytes_roundtrip; 383 + Alcotest.test_case "sample_count" `Quick test_sample_count; 384 + Alcotest.test_case "read count" `Quick test_read_count; 385 + Alcotest.test_case "metadata roundtrip" `Quick test_metadata; 386 + Alcotest.test_case "IQ byte format" `Quick test_iq_byte_format; 387 + Alcotest.test_case "IQ large buffer" `Quick test_iq_large_buffer; 388 + ] ); 389 + ( "signal", 390 + [ 391 + Alcotest.test_case "generate_tone" `Quick test_generate_tone; 392 + Alcotest.test_case "generate_bpsk" `Quick test_generate_bpsk; 393 + Alcotest.test_case "tone fs/4" `Quick test_tone_fs_over_4; 394 + Alcotest.test_case "tone DC" `Quick test_tone_dc; 395 + Alcotest.test_case "tone unit magnitude" `Quick 396 + test_tone_unit_magnitude; 397 + Alcotest.test_case "tone Nyquist/2" `Quick test_tone_nyquist_half; 398 + Alcotest.test_case "BPSK bit polarity" `Quick test_bpsk_bit_polarity; 399 + Alcotest.test_case "BPSK transitions" `Quick test_bpsk_transitions; 400 + Alcotest.test_case "BPSK sps" `Quick test_bpsk_sps; 401 + ] ); 402 + ( "source", 403 + [ 404 + Alcotest.test_case "read_all matches" `Quick 405 + test_source_read_all_matches; 406 + Alcotest.test_case "sequential reads" `Quick test_sequential_reads; 407 + Alcotest.test_case "read after exhaustion" `Quick 408 + test_read_after_exhaustion; 409 + Alcotest.test_case "sample_count accuracy" `Quick 410 + test_sample_count_accuracy; 411 + ] ); 412 + ( "metadata", 413 + [ 414 + Alcotest.test_case "metadata roundtrip" `Quick test_metadata_roundtrip; 415 + Alcotest.test_case "metadata default" `Quick test_metadata_default; 416 + ] ); 417 + ]
+4
test/test_rtlsdr.mli
··· 1 + (** RTL-SDR tests. *) 2 + 3 + val suites : (string * unit Alcotest.test_case list) list 4 + (** [suites] is the list of RTL-SDR test suites. *)