Skip to content

Commit 1aab574

Browse files
committed
refactor second pass & add tests
1 parent 1139675 commit 1aab574

2 files changed

Lines changed: 134 additions & 53 deletions

File tree

sigmf/apps/convert_blue.py

Lines changed: 85 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import os
1919
import struct
2020
from datetime import datetime, timezone
21+
from pathlib import Path
22+
from typing import Optional
2123

2224
import numpy as np
2325

@@ -242,14 +244,16 @@ def parse_extended_header(file_path, hcb, endian="<"):
242244
return entries
243245

244246

245-
def parse_data_values(file_path, hcb, endianness):
247+
def parse_data_values(blue_path: Path, out_path: Path, hcb: dict, endianness: str):
246248
"""
247249
Parse key HCB values used for further processing.
248250
249251
Parameters
250252
----------
251-
file_path : str
253+
blue_path : Path
252254
Path to the Blue file.
255+
out_path : Path
256+
Path to output SigMF metadata file.
253257
hcb : dict
254258
Header Control Block dictionary.
255259
endianness : str
@@ -261,126 +265,155 @@ def parse_data_values(file_path, hcb, endianness):
261265
Parsed samples.
262266
"""
263267
log.info("parsing blue file data values")
264-
with open(file_path, "rb") as handle:
268+
with open(blue_path, "rb") as handle:
265269
data = handle.read(HEADER_SIZE_BYTES)
266270
if len(data) < HEADER_SIZE_BYTES:
267271
raise ValueError("Incomplete header")
268272
dtype = data[52:54].decode("utf-8") # eg 'CI', 'CF', 'SD'
269273
log.debug(f"data type: {dtype}")
270274

271-
272275
time_interval = np.frombuffer(data[264:272], dtype=np.float64)[0]
273276
if time_interval <= 0:
274277
raise ValueError(f"Invalid time interval: {time_interval}")
275278
sample_rate_hz = 1 / time_interval
276279
log.info(f"sample rate: {sample_rate_hz/1e6:.3f} MHz")
277280
extended_header_data_size = int.from_bytes(data[28:32], byteorder="little")
278-
file_size_bytes = os.path.getsize(file_path)
281+
file_size_bytes = os.path.getsize(blue_path)
279282
log.debug(f"file size: {file_size_bytes} bytes")
280283

281284
# Determine destination path for SigMF data file
282-
dest_path = file_path.rsplit(".", 1)[0]
285+
dest_path = out_path.with_suffix(".sigmf-data")
283286

284287
### complex data parsing
285288

286289
# complex 16-bit integer IQ data > ci16_le in SigMF
287290
if dtype == "CI":
288291
elem_size = np.dtype(np.int16).itemsize
289292
elem_count = (file_size_bytes - extended_header_data_size) // elem_size
290-
raw_samples = np.fromfile(file_path, dtype=np.int16, offset=HEADER_SIZE_BYTES, count=elem_count)
293+
raw_samples = np.fromfile(blue_path, dtype=np.int16, offset=HEADER_SIZE_BYTES, count=elem_count)
291294
# reassemble interleaved IQ samples
292295
samples = raw_samples[::2] + 1j * raw_samples[1::2] # convert to IQIQIQ...
293296
# normalize samples to -1.0 to +1.0 range
294297
samples = samples.astype(np.float32) / 32767.0
295298
# save out as SigMF IQ data file
296-
samples.tofile(f"{dest_path}.sigmf-data")
299+
samples.tofile(dest_path)
297300

298301
# complex 32-bit integer IQ data > ci32_le in SigMF
299302
elif dtype == "CL":
300303
elem_size = np.dtype(np.int32).itemsize
301304
elem_count = (file_size_bytes - extended_header_data_size) // elem_size
302-
raw_samples = np.fromfile(file_path, dtype=np.int32, offset=HEADER_SIZE_BYTES, count=elem_count)
305+
raw_samples = np.fromfile(blue_path, dtype=np.int32, offset=HEADER_SIZE_BYTES, count=elem_count)
303306
# reassemble interleaved IQ samples
304307
samples = raw_samples[::2] + 1j * raw_samples[1::2] # convert to IQIQIQ...
305308
# normalize samples to -1.0 to +1.0 range
306309
samples = samples.astype(np.float32) / 2147483647.0
307310
# save out as SigMF IQ data file
308-
samples.tofile(f"{dest_path}.sigmf-data")
311+
samples.tofile(dest_path)
309312

310313
# complex 32-bit float IQ data > cf32_le in SigMF
311314
elif dtype == "CF":
312315
# each complex sample is 8 bytes total (2 × float32), so np.complex64 is appropriate
313316
# no need to reassemble IQ — already complex
314317
elem_size = np.dtype(np.complex64).itemsize # will be 8 bytes
315318
elem_count = (file_size_bytes - extended_header_data_size) // elem_size
316-
samples = np.fromfile(file_path, dtype=np.complex64, offset=HEADER_SIZE_BYTES, count=elem_count)
319+
samples = np.fromfile(blue_path, dtype=np.complex64, offset=HEADER_SIZE_BYTES, count=elem_count)
317320
# save out as SigMF IQ data file
318-
samples.tofile(f"{dest_path}.sigmf-data")
321+
samples.tofile(dest_path)
319322

320323
### scalar data parsing
321324

322325
# scalar data parsing > ri8_le in SigMF
323326
elif dtype == "SB":
324327
elem_size = np.dtype(np.int8).itemsize
325328
elem_count = (file_size_bytes - extended_header_data_size) // elem_size
326-
samples = np.fromfile(file_path, dtype=np.int8, offset=HEADER_SIZE_BYTES, count=elem_count)
329+
samples = np.fromfile(blue_path, dtype=np.int8, offset=HEADER_SIZE_BYTES, count=elem_count)
327330
# normalize samples to -1.0 to +1.0 range
328331
samples = samples.astype(np.float32) / 127.0
329332
# save out as SigMF IQ data file
330-
samples.tofile(f"{dest_path}.sigmf-data")
333+
samples.tofile(dest_path)
331334

332335
# scalar data parsing > ri16_le in SigMF
333336
elif dtype == "SI":
334337
elem_size = np.dtype(np.int16).itemsize
335338
elem_count = (file_size_bytes - extended_header_data_size) // elem_size
336-
samples = np.fromfile(file_path, dtype=np.int16, offset=HEADER_SIZE_BYTES, count=elem_count)
339+
samples = np.fromfile(blue_path, dtype=np.int16, offset=HEADER_SIZE_BYTES, count=elem_count)
337340
# normalize samples to -1.0 to +1.0 range
338341
samples = samples / 32767.0
339342
# save out as SigMF IQ data file
340-
samples.tofile(f"{dest_path}.sigmf-data")
343+
samples.tofile(dest_path)
341344

342345
# scalar data parsing > ri32_le in SigMF
343346
elif dtype == "SL":
344347
elem_size = np.dtype(np.int32).itemsize
345348
elem_count = (file_size_bytes - extended_header_data_size) // elem_size
346-
samples = np.fromfile(file_path, dtype=np.int32, offset=HEADER_SIZE_BYTES, count=elem_count)
349+
samples = np.fromfile(blue_path, dtype=np.int32, offset=HEADER_SIZE_BYTES, count=elem_count)
347350
# normalize samples to -1.0 to +1.0 range
348351
samples = samples / 2147483647.0
349352
# save out as SigMF IQ data file
350-
samples.tofile(f"{dest_path}.sigmf-data")
353+
samples.tofile(dest_path)
351354

352355
# scalar data parsing > ri64_le in SigMF
353356
elif dtype == "SX":
354357
elem_size = np.dtype(np.int64).itemsize
355358
elem_count = (file_size_bytes - extended_header_data_size) // elem_size
356-
samples = np.fromfile(file_path, dtype=np.int64, offset=HEADER_SIZE_BYTES, count=elem_count)
359+
samples = np.fromfile(blue_path, dtype=np.int64, offset=HEADER_SIZE_BYTES, count=elem_count)
357360
# save out as SigMF IQ data file
358-
samples.tofile(f"{dest_path}.sigmf-data")
361+
samples.tofile(dest_path)
359362

360363
# scalar data parsing > rf32_le in SigMF
361364
elif dtype == "SF":
362365
elem_size = np.dtype(np.float32).itemsize
363366
elem_count = (file_size_bytes - extended_header_data_size) // elem_size
364-
samples = np.fromfile(file_path, dtype=np.float32, offset=HEADER_SIZE_BYTES, count=elem_count)
367+
samples = np.fromfile(blue_path, dtype=np.float32, offset=HEADER_SIZE_BYTES, count=elem_count)
365368
# save out as SigMF IQ data file
366-
samples.tofile(f"{dest_path}.sigmf-data")
369+
samples.tofile(dest_path)
367370

368371
# scalar data parsing > rf64_le in SigMF
369372
elif dtype == "SD":
370373
elem_size = np.dtype(np.float64).itemsize
371374
elem_count = (file_size_bytes - extended_header_data_size) // elem_size
372-
samples = np.fromfile(file_path, dtype=np.float64, offset=HEADER_SIZE_BYTES, count=elem_count)
375+
samples = np.fromfile(blue_path, dtype=np.float64, offset=HEADER_SIZE_BYTES, count=elem_count)
373376
# save out as SigMF IQ data file
374-
samples.astype(np.complex64).tofile(f"{dest_path}.sigmf-data")
377+
samples.astype(np.complex64).tofile(dest_path)
375378
else:
376379
raise ValueError(f"Unsupported data type: {dtype}")
377380

378-
# TODO: validate handling of scalar types - reshape per mathlab port shown here?
381+
# TODO: validate handling of scalar types - Reshape per mathlab port shown here?
382+
383+
"""
384+
# Save out as SigMF IQ data file
385+
if dtype in ("CI", "CL", "CF"):
386+
# Complex IQ data → save as cf32_le
387+
samples.astype(np.complex64).tofile(dest_path)
388+
else:
389+
# Scalar data → save in native dtype
390+
samples.tofile(dest_path)
391+
"""
392+
393+
# TODO: validate handling of scalar types - Reshape per mathlab port shown here?
394+
395+
"""
396+
fmt_size_char = self.hcb["format"][0]
397+
fmt_type_char = self.hcb["format"][1]
398+
399+
elementsPerSample = self.FormatSize(fmt_size_char)
400+
print('Elements Per Sample', elementsPerSample/1e6)
401+
elem_count_per_sample = (
402+
np.prod(elementsPerSample) if isinstance(elementsPerSample, (tuple, list)) else elementsPerSample
403+
)
404+
405+
dtype_str, elem_bytes = self.FormatType(fmt_type_char)
406+
bytesPerSample = int(elem_bytes) * int(elem_count_per_sample)
407+
408+
bytesRead = int(self.dataOffset - self.hcb["data_start"])
409+
bytes_remaining = int(self.hcb["data_size"] - bytesRead)
410+
"""
411+
379412
# return the IQ data if needed for further processing if needed
380413
return samples
381414

382415

383-
def blue_to_sigmf(hcb, ext_entries, file_path):
416+
def construct_sigmf(hcb: dict, ext_entries: list, blue_path: Path, out_path: Path):
384417
"""
385418
Build a SigMF metadata dict from parsed Bluefile HCB and extended header.
386419
@@ -390,8 +423,10 @@ def blue_to_sigmf(hcb, ext_entries, file_path):
390423
Header Control Block from read_hcb().
391424
ext_entries : list of dict
392425
Parsed extended header entries from parse_extended_header().
393-
file_path : str
426+
blue_path : Path
394427
Path to the original blue file.
428+
out_path : Path
429+
Path to output SigMF metadata file.
395430
396431
Returns
397432
-------
@@ -556,14 +591,12 @@ def compute_sha512(path, bufsize=1024 * 1024):
556591
chunk = handle.read(bufsize)
557592
return hash_obj.hexdigest()
558593

559-
# strip the extension from the original file path
560-
base_file_name = os.path.splitext(file_path)[0]
561-
562594
# build the .sigmf-data path
563-
data_file_path = base_file_name + ".sigmf-data"
595+
data_path = out_path.with_suffix(".sigmf-data")
596+
meta_path = out_path.with_suffix(".sigmf-meta")
564597

565598
# compute SHA-512 of the data file
566-
data_sha512 = compute_sha512(data_file_path) # path to the .sigmf-data file
599+
data_sha512 = compute_sha512(data_path) # path to the .sigmf-data file
567600
global_md["core:sha512"] = data_sha512
568601

569602
# annotations array
@@ -615,36 +648,43 @@ def compute_sha512(path, bufsize=1024 * 1024):
615648
"annotations": annotations,
616649
}
617650

618-
# write .sigmf-meta file
619-
base_file_name = os.path.splitext(file_path)[0]
620-
meta_path = base_file_name + ".sigmf-meta"
621-
622651
with open(meta_path, "w") as meta_handle:
623652
json.dump(sigmf_metadata, meta_handle, indent=2)
624653
log.info(f"wrote SigMF metadata to {meta_path}")
625654

626655
return sigmf_metadata
627656

628657

629-
def blue_file_to_sigmf(file_path):
658+
def convert_blue(
659+
blue_path: str,
660+
out_path: Optional[str] = None,
661+
) -> np.ndarray:
630662
"""
631663
Convert a MIDIS Bluefile to SigMF metadata and data.
632664
633665
Parameters
634666
----------
635-
file_path : str
667+
blue_path : str
636668
Path to the Blue file.
669+
out_path : str
670+
Path to the output SigMF metadata file.
637671
638672
Returns
639673
-------
640674
numpy.ndarray
641675
IQ Data.
642676
"""
643-
log.info("starting blue file processing")
677+
log.debug("starting blue file processing")
644678

645-
# read Header control block (HCB) from blue file to determine how to process the rest of the file
646-
hcb = read_hcb(file_path)
679+
blue_path = Path(blue_path)
680+
if out_path is None:
681+
# extension will be changed later
682+
out_path = Path(blue_path)
683+
else:
684+
out_path = Path(out_path)
647685

686+
# read Header control block (HCB) from blue file to determine how to process the rest of the file
687+
hcb = read_hcb(blue_path)
648688
log.debug("Header Control Block (HCB) Fields")
649689
for name, _, _, _, desc in HCB_LAYOUT:
650690
log.debug(f"{name:10s}: {hcb[name]!r} # {desc}")
@@ -663,7 +703,7 @@ def blue_file_to_sigmf(file_path):
663703
raise ValueError(f"Unknown head_rep value: {extended_header_endianness}")
664704

665705
# parse extended header entries
666-
ext_entries = parse_extended_header(file_path, hcb, ext_endianness)
706+
ext_entries = parse_extended_header(blue_path, hcb, ext_endianness)
667707
log.debug("Extended Header Keywords")
668708
for entry in ext_entries:
669709
log.debug(f"{entry['tag']:20s}:{entry['value']}")
@@ -676,12 +716,12 @@ def blue_file_to_sigmf(file_path):
676716
# parse key data values
677717
# iq_data will be available if needed for further processing.
678718
try:
679-
iq_data = parse_data_values(file_path, hcb, data_endianness)
719+
iq_data = parse_data_values(blue_path, out_path, hcb, data_endianness)
680720
except Exception as error:
681721
raise RuntimeError(f"Failed to parse data values: {error}") from error
682722

683723
# call the SigMF conversion for metadata generation
684-
blue_to_sigmf(hcb, ext_entries, file_path)
724+
construct_sigmf(hcb, ext_entries, blue_path, out_path)
685725

686726
# return the IQ data if needed for further processing if needed
687727
return iq_data
@@ -704,4 +744,4 @@ def main() -> None:
704744
}
705745
logging.basicConfig(level=level_lut[min(args.verbose, 2)])
706746

707-
blue_file_to_sigmf(args.input)
747+
convert_blue(args.input)

0 commit comments

Comments
 (0)