RTL-SDR IQ sample reader
0
fork

Configure Feed

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

Add 8 new CCSDS/RFC protocol packages

- ocaml-rice: CCSDS 121.0-B lossless compression (Rice/Golomb)
- ocaml-udpcl: RFC 7122 UDP convergence layer for Bundle Protocol
- ocaml-erasure: CCSDS 131.5-B erasure correcting codes (GF(2^8))
- ocaml-short-ldpc: CCSDS 131.4-B short block-length LDPC
- ocaml-opm: CCSDS 502.0-B Orbit Parameter Message (KVN)
- ocaml-aem: CCSDS 504.0-B Attitude Ephemeris Message (KVN)
- ocaml-tdm: CCSDS 503.0-B Tracking Data Message (KVN)
- ocaml-rdm: CCSDS 508.1-B Re-entry Data Message (KVN)

+381
+22
fuzz/dune
··· 1 + (executable 2 + (name fuzz) 3 + (modules fuzz fuzz_rtlsdr) 4 + (libraries rtlsdr alcobar)) 5 + 6 + (rule 7 + (alias runtest) 8 + (enabled_if 9 + (<> %{profile} afl)) 10 + (deps fuzz.exe) 11 + (action 12 + (run %{exe:fuzz.exe}))) 13 + 14 + (rule 15 + (alias fuzz) 16 + (enabled_if 17 + (= %{profile} afl)) 18 + (deps fuzz.exe) 19 + (action 20 + (progn 21 + (run %{exe:fuzz.exe} --gen-corpus corpus) 22 + (run afl-fuzz -V 60 -i corpus -o _fuzz -- %{exe:fuzz.exe} @@))))
+1
fuzz/fuzz.ml
··· 1 + let () = Alcobar.run "rtlsdr" [ Fuzz_rtlsdr.suite ]
+94
fuzz/fuzz_rtlsdr.ml
··· 1 + (** Fuzz tests for RTL-SDR IQ processing. *) 2 + 3 + open Alcobar 4 + 5 + (** IQ conversion crash safety: random bytes -> of_bytes -> read_all -> no 6 + crash. Also verifies all sample values are in [-1, +1]. *) 7 + let test_iq_conversion_crash_safety input = 8 + let data = Bytes.of_string input in 9 + let src = Rtlsdr.of_bytes data in 10 + let samples = Rtlsdr.read_all src in 11 + let expected_count = String.length input / 2 in 12 + check_eq ~eq:( = ) ~pp:pp_int expected_count (Array.length samples); 13 + Array.iter 14 + (fun (s : Dsp.Complex.t) -> 15 + if s.re < -1.0 || s.re > 1.0 then 16 + failf "re=%a out of [-1,+1]" pp_float s.re; 17 + if s.im < -1.0 || s.im > 1.0 then 18 + failf "im=%a out of [-1,+1]" pp_float s.im) 19 + samples 20 + 21 + (** Tone generation: random frequency in [0, 0.5*fs], random n_samples -> 22 + generate_tone -> no crash, all magnitudes approx 1.0. *) 23 + let test_tone_generation freq_millionths n_samples = 24 + (* Map freq_millionths (0..999999) to [0.0, 0.5) of sample_rate *) 25 + let sample_rate = 1000.0 in 26 + let freq = 27 + Float.of_int (abs (freq_millionths mod 1_000_000)) 28 + /. 1_000_000.0 *. 0.5 *. sample_rate 29 + in 30 + let n = abs (n_samples mod 1000) + 1 in 31 + let duration = Float.of_int n /. sample_rate in 32 + let samples = Rtlsdr.generate_tone ~sample_rate ~freq ~duration in 33 + check_eq ~eq:( = ) ~pp:pp_int n (Array.length samples); 34 + Array.iteri 35 + (fun i (s : Dsp.Complex.t) -> 36 + let mag = Float.sqrt ((s.re *. s.re) +. (s.im *. s.im)) in 37 + if Float.abs (mag -. 1.0) > 1e-6 then 38 + failf "sample %d: magnitude=%a, expected 1.0" i pp_float mag) 39 + samples 40 + 41 + (** BPSK generation: random bits, random sps (2-16) -> generate_bpsk -> no 42 + crash, correct sample count. *) 43 + let test_bpsk_generation input sps_offset = 44 + let bits = Bytes.of_string input in 45 + let n_bits = Bytes.length bits in 46 + guard (n_bits > 0); 47 + let sps = abs (sps_offset mod 15) + 2 in 48 + (* 2..16 *) 49 + let sample_rate = 1000.0 *. Float.of_int sps in 50 + let symbol_rate = 1000.0 in 51 + let samples = 52 + Rtlsdr.generate_bpsk ~sample_rate ~symbol_rate ~bits ~carrier_freq:0.0 53 + in 54 + let expected = n_bits * sps in 55 + check_eq ~eq:( = ) ~pp:pp_int expected (Array.length samples) 56 + 57 + (** Sequential reads: random byte buffer -> of_bytes -> random read sizes -> no 58 + crash, total matches sample_count. *) 59 + let test_sequential_reads input read_sizes = 60 + let data = Bytes.of_string input in 61 + let src = Rtlsdr.of_bytes data in 62 + let expected_total = Rtlsdr.sample_count src in 63 + let total = ref 0 in 64 + List.iter 65 + (fun sz -> 66 + let n = (abs sz mod 100) + 1 in 67 + let chunk = Rtlsdr.read src n in 68 + total := !total + Array.length chunk) 69 + read_sizes; 70 + (* Drain any remaining *) 71 + let rest = Rtlsdr.read_all src in 72 + total := !total + Array.length rest; 73 + check_eq ~eq:( = ) ~pp:pp_int expected_total !total 74 + 75 + (** Source close: create -> close -> read -> should handle gracefully. *) 76 + let test_source_close input = 77 + let data = Bytes.of_string input in 78 + let src = Rtlsdr.of_bytes data in 79 + Rtlsdr.close src; 80 + (* Reading after close should not crash *) 81 + let _ = Rtlsdr.read src 10 in 82 + let _ = Rtlsdr.read_all src in 83 + () 84 + 85 + let suite = 86 + ( "rtlsdr", 87 + [ 88 + test_case "IQ conversion crash safety" [ bytes ] 89 + test_iq_conversion_crash_safety; 90 + test_case "tone generation" [ int; int ] test_tone_generation; 91 + test_case "BPSK generation" [ bytes; int ] test_bpsk_generation; 92 + test_case "sequential reads" [ bytes; list int ] test_sequential_reads; 93 + test_case "source close" [ bytes ] test_source_close; 94 + ] )
+4
fuzz/fuzz_rtlsdr.mli
··· 1 + (** Fuzz tests for {!Rtlsdr}. *) 2 + 3 + val suite : string * Alcobar.test_case list 4 + (** Test suite. *)
+260
test/test.ml
··· 142 142 Alcotest.(check (float 0.0)) "sample_rate" 2.4e6 m.sample_rate; 143 143 Alcotest.(check (float 0.0)) "gain" 40.0 m.gain 144 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 171 + (Printf.sprintf "DC sample %d" i) 172 + (Dsp.Complex.make 1.0 0.0) s) 173 + samples 174 + 175 + (* Tone amplitude: |sample| ≈ 1.0 for all samples *) 176 + let test_tone_unit_magnitude () = 177 + let samples = 178 + Rtlsdr.generate_tone ~sample_rate:8000.0 ~freq:1234.5 ~duration:0.01 179 + in 180 + Array.iteri 181 + (fun i (s : Dsp.Complex.t) -> 182 + let mag = Dsp.Complex.norm s in 183 + if Float.abs (mag -. 1.0) > 1e-9 then 184 + Alcotest.failf "sample %d magnitude = %f, expected 1.0" i mag) 185 + samples 186 + 187 + (* Nyquist/2 (fs/2): phase advances by pi per sample → alternating ±1 *) 188 + let test_tone_nyquist_half () = 189 + let samples = Rtlsdr.generate_tone ~sample_rate:1.0 ~freq:0.5 ~duration:4.0 in 190 + Alcotest.(check int) "4 samples" 4 (Array.length samples); 191 + let tc = approx_complex_testable 1e-9 in 192 + (* freq=0.5*fs: angle = 2*pi*0.5*i = pi*i 193 + i=0: (1,0), i=1: (-1,~0), i=2: (1,~0), i=3: (-1,~0) *) 194 + Alcotest.check tc "sample[0]" (Dsp.Complex.make 1.0 0.0) samples.(0); 195 + Alcotest.check tc "sample[1]" (Dsp.Complex.make (-1.0) 0.0) samples.(1); 196 + Alcotest.check tc "sample[2]" (Dsp.Complex.make 1.0 0.0) samples.(2); 197 + Alcotest.check tc "sample[3]" (Dsp.Complex.make (-1.0) 0.0) samples.(3) 198 + 199 + (* -- BPSK generation: communications reference vectors -- *) 200 + 201 + (* BPSK bits [1, 0]: first symbol phase π (negative), second phase 0 (positive) *) 202 + let test_bpsk_bit_polarity () = 203 + let bits = Bytes.of_string "\x01\x00" in 204 + let sps = 8 in 205 + let samples = 206 + Rtlsdr.generate_bpsk ~sample_rate:8000.0 ~symbol_rate:1000.0 ~bits 207 + ~carrier_freq:0.0 208 + in 209 + Alcotest.(check int) "total samples" (2 * sps) (Array.length samples); 210 + let tc = approx_complex_testable 1e-9 in 211 + (* bit=1 → phase π → cos(π)=-1 *) 212 + Alcotest.check tc "bit=1 center sample" 213 + (Dsp.Complex.make (-1.0) 0.0) 214 + samples.(sps / 2); 215 + (* bit=0 → phase 0 → cos(0)=+1 *) 216 + Alcotest.check tc "bit=0 center sample" (Dsp.Complex.make 1.0 0.0) 217 + samples.(sps + (sps / 2)) 218 + 219 + (* Verify transitions happen at symbol boundaries *) 220 + let test_bpsk_transitions () = 221 + let bits = Bytes.of_string "\x00\x01\x00" in 222 + let sps = 4 in 223 + let samples = 224 + Rtlsdr.generate_bpsk ~sample_rate:4000.0 ~symbol_rate:1000.0 ~bits 225 + ~carrier_freq:0.0 226 + in 227 + Alcotest.(check int) "total samples" 12 (Array.length samples); 228 + let tc = approx_complex_testable 1e-9 in 229 + (* Last sample of symbol 0 (bit=0): positive *) 230 + Alcotest.check tc "end of sym0" (Dsp.Complex.make 1.0 0.0) samples.(sps - 1); 231 + (* First sample of symbol 1 (bit=1): negative *) 232 + Alcotest.check tc "start of sym1" (Dsp.Complex.make (-1.0) 0.0) samples.(sps); 233 + (* Last sample of symbol 1 (bit=1): negative *) 234 + Alcotest.check tc "end of sym1" 235 + (Dsp.Complex.make (-1.0) 0.0) 236 + samples.((2 * sps) - 1); 237 + (* First sample of symbol 2 (bit=0): positive *) 238 + Alcotest.check tc "start of sym2" (Dsp.Complex.make 1.0 0.0) samples.(2 * sps) 239 + 240 + (* Verify sps samples per symbol *) 241 + let test_bpsk_sps () = 242 + let bits = Bytes.of_string "\x01\x00\x01\x01\x00" in 243 + let sps = 10 in 244 + let samples = 245 + Rtlsdr.generate_bpsk ~sample_rate:10000.0 ~symbol_rate:1000.0 ~bits 246 + ~carrier_freq:0.0 247 + in 248 + Alcotest.(check int) "total = nbits * sps" (5 * sps) (Array.length samples) 249 + 250 + (* -- IQ byte format: RTL-SDR hardware spec -- *) 251 + 252 + (* RTL-SDR: unsigned 8-bit I,Q interleaved. Center is 127/128. *) 253 + let test_iq_byte_format () = 254 + let data = Bytes.create 4 in 255 + Bytes.set data 0 (Char.chr 127); 256 + Bytes.set data 1 (Char.chr 128); 257 + Bytes.set data 2 (Char.chr 0); 258 + Bytes.set data 3 (Char.chr 255); 259 + let src = Rtlsdr.of_bytes data in 260 + let samples = Rtlsdr.read_all src in 261 + Alcotest.(check int) "2 IQ samples" 2 (Array.length samples); 262 + let tc = approx_complex_testable 0.01 in 263 + (* (127,128): I=(127-127.5)/127.5=-0.00392, Q=(128-127.5)/127.5=+0.00392 *) 264 + Alcotest.check tc "sample 0 near DC" 265 + (Dsp.Complex.make (-0.5 /. 127.5) (0.5 /. 127.5)) 266 + samples.(0); 267 + (* (0,255): I=-1.0, Q=+1.0 *) 268 + Alcotest.check tc "sample 1 = (-1, +1)" 269 + (Dsp.Complex.make (-1.0) 1.0) 270 + samples.(1) 271 + 272 + (* Large buffer: 10000 random bytes → of_bytes → 5000 samples *) 273 + let test_iq_large_buffer () = 274 + let n_bytes = 10000 in 275 + let data = Bytes.init n_bytes (fun _ -> Char.chr (Random.int 256)) in 276 + let src = Rtlsdr.of_bytes data in 277 + Alcotest.(check int) "5000 samples" 5000 (Rtlsdr.sample_count src); 278 + let samples = Rtlsdr.read_all src in 279 + Alcotest.(check int) "read_all returns 5000" 5000 (Array.length samples); 280 + (* Verify all values in [-1,+1] range *) 281 + Array.iteri 282 + (fun i (s : Dsp.Complex.t) -> 283 + if s.re < -1.0 || s.re > 1.0 then 284 + Alcotest.failf "sample %d re=%f out of range" i s.re; 285 + if s.im < -1.0 || s.im > 1.0 then 286 + Alcotest.failf "sample %d im=%f out of range" i s.im) 287 + samples 288 + 289 + (* -- Source operations -- *) 290 + 291 + (* of_bytes → read all → verify matches input *) 292 + let test_source_read_all_matches () = 293 + let data = Bytes.create 6 in 294 + Bytes.set data 0 (Char.chr 0); 295 + Bytes.set data 1 (Char.chr 255); 296 + Bytes.set data 2 (Char.chr 128); 297 + Bytes.set data 3 (Char.chr 128); 298 + Bytes.set data 4 (Char.chr 64); 299 + Bytes.set data 5 (Char.chr 192); 300 + let src = Rtlsdr.of_bytes data in 301 + let samples = Rtlsdr.read_all src in 302 + Alcotest.(check int) "3 samples" 3 (Array.length samples); 303 + let tc = approx_complex_testable 1e-6 in 304 + Alcotest.check tc "sample 0" (Dsp.Complex.make (-1.0) 1.0) samples.(0); 305 + Alcotest.check tc "sample 1" 306 + (Dsp.Complex.make (0.5 /. 127.5) (0.5 /. 127.5)) 307 + samples.(1); 308 + Alcotest.check tc "sample 2" 309 + (Dsp.Complex.make ((64.0 -. 127.5) /. 127.5) ((192.0 -. 127.5) /. 127.5)) 310 + samples.(2) 311 + 312 + (* Sequential reads: read(10) + read(10) = read_all for 20-sample source *) 313 + let test_sequential_reads () = 314 + let data = Bytes.init 40 (fun i -> Char.chr (i mod 256)) in 315 + let src1 = Rtlsdr.of_bytes data in 316 + let all = Rtlsdr.read_all src1 in 317 + let src2 = Rtlsdr.of_bytes data in 318 + let first = Rtlsdr.read src2 10 in 319 + let second = Rtlsdr.read src2 10 in 320 + Alcotest.(check int) "first batch" 10 (Array.length first); 321 + Alcotest.(check int) "second batch" 10 (Array.length second); 322 + let tc = approx_complex_testable 1e-9 in 323 + for i = 0 to 9 do 324 + Alcotest.check tc (Printf.sprintf "first[%d]" i) all.(i) first.(i) 325 + done; 326 + for i = 0 to 9 do 327 + Alcotest.check tc (Printf.sprintf "second[%d]" i) all.(10 + i) second.(i) 328 + done 329 + 330 + (* Read after exhaustion → empty *) 331 + let test_read_after_exhaustion () = 332 + let data = Bytes.create 4 in 333 + let src = Rtlsdr.of_bytes data in 334 + let _ = Rtlsdr.read_all src in 335 + let after = Rtlsdr.read src 10 in 336 + Alcotest.(check int) "empty after exhaustion" 0 (Array.length after); 337 + let after2 = Rtlsdr.read_all src in 338 + Alcotest.(check int) "read_all after exhaustion" 0 (Array.length after2) 339 + 340 + (* sample_count accuracy *) 341 + let test_sample_count_accuracy () = 342 + let check n_bytes expected = 343 + let data = Bytes.create n_bytes in 344 + let src = Rtlsdr.of_bytes data in 345 + Alcotest.(check int) 346 + (Printf.sprintf "%d bytes -> %d samples" n_bytes expected) 347 + expected (Rtlsdr.sample_count src) 348 + in 349 + check 0 0; 350 + check 1 0; 351 + check 2 1; 352 + check 3 1; 353 + check 100 50; 354 + check 255 127; 355 + check 10000 5000 356 + 357 + (* -- Metadata -- *) 358 + 359 + (* Attach metadata → read back → verify all fields match *) 360 + let test_metadata_roundtrip () = 361 + let data = Bytes.create 10 in 362 + let src = Rtlsdr.of_bytes data in 363 + let meta : Rtlsdr.metadata = 364 + { center_freq = 145.8e6; sample_rate = 2.048e6; gain = 49.6 } 365 + in 366 + let src2 = Rtlsdr.with_metadata meta src in 367 + let m = Option.get (Rtlsdr.metadata src2) in 368 + Alcotest.(check (float 0.0)) "center_freq" 145.8e6 m.center_freq; 369 + Alcotest.(check (float 0.0)) "sample_rate" 2.048e6 m.sample_rate; 370 + Alcotest.(check (float 0.0)) "gain" 49.6 m.gain 371 + 372 + (* Default metadata: None *) 373 + let test_metadata_default () = 374 + let data = Bytes.create 10 in 375 + let src = Rtlsdr.of_bytes data in 376 + Alcotest.(check bool) 377 + "no metadata by default" true 378 + (Option.is_none (Rtlsdr.metadata src)) 379 + 145 380 let () = 146 381 Alcotest.run "rtlsdr" 147 382 [ ··· 151 386 Alcotest.test_case "sample_count" `Quick test_sample_count; 152 387 Alcotest.test_case "read count" `Quick test_read_count; 153 388 Alcotest.test_case "metadata roundtrip" `Quick test_metadata; 389 + Alcotest.test_case "IQ byte format" `Quick test_iq_byte_format; 390 + Alcotest.test_case "IQ large buffer" `Quick test_iq_large_buffer; 154 391 ] ); 155 392 ( "signal", 156 393 [ 157 394 Alcotest.test_case "generate_tone" `Quick test_generate_tone; 158 395 Alcotest.test_case "generate_bpsk" `Quick test_generate_bpsk; 396 + Alcotest.test_case "tone fs/4" `Quick test_tone_fs_over_4; 397 + Alcotest.test_case "tone DC" `Quick test_tone_dc; 398 + Alcotest.test_case "tone unit magnitude" `Quick 399 + test_tone_unit_magnitude; 400 + Alcotest.test_case "tone Nyquist/2" `Quick test_tone_nyquist_half; 401 + Alcotest.test_case "BPSK bit polarity" `Quick test_bpsk_bit_polarity; 402 + Alcotest.test_case "BPSK transitions" `Quick test_bpsk_transitions; 403 + Alcotest.test_case "BPSK sps" `Quick test_bpsk_sps; 404 + ] ); 405 + ( "source", 406 + [ 407 + Alcotest.test_case "read_all matches" `Quick 408 + test_source_read_all_matches; 409 + Alcotest.test_case "sequential reads" `Quick test_sequential_reads; 410 + Alcotest.test_case "read after exhaustion" `Quick 411 + test_read_after_exhaustion; 412 + Alcotest.test_case "sample_count accuracy" `Quick 413 + test_sample_count_accuracy; 414 + ] ); 415 + ( "metadata", 416 + [ 417 + Alcotest.test_case "metadata roundtrip" `Quick test_metadata_roundtrip; 418 + Alcotest.test_case "metadata default" `Quick test_metadata_default; 159 419 ] ); 160 420 ]