Skip to content

Commit 5cc4204

Browse files
committed
core update for calibration hook
1 parent be56d98 commit 5cc4204

1 file changed

Lines changed: 62 additions & 3 deletions

File tree

hackrfpy/src/hackrfpy/core.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -601,8 +601,67 @@ def decode_iq(self, raw):
601601
iq /= 128.0 # normalize int8 full-scale to ~[-1, 1)
602602
return iq
603603

604-
def load_iq(self, path, count=None, offset_samples=0):
605-
# Instance convenience for module-level load_iq (below).
604+
# ---- power / relative calibration (Level 1) ----------------------------
605+
# These turn the device's arbitrary dBFS amplitude into something that is
606+
# at least CONSISTENT across gain settings (and, with a user-supplied
607+
# offset, approximately absolute). They are PURE math on values you give
608+
# them -- the library never invents reference data or claims a calibrated
609+
# dBm figure it cannot honestly produce. Producing the offset and any
610+
# frequency-correction table is a hardware+reference workflow; see
611+
# examples/calibrate.py.
612+
613+
@staticmethod
614+
def power_dbfs(iq):
615+
# Mean power of a complex64 block in dBFS (dB relative to full scale).
616+
# 0 dBFS == |amplitude| 1.0 (ADC full scale). Always <= 0 for real
617+
# captures. This is the raw, UNCALIBRATED reading.
618+
if len(iq) == 0:
619+
return float("-inf")
620+
power = float(np.mean((iq.real.astype(np.float64) ** 2
621+
+ iq.imag.astype(np.float64) ** 2)))
622+
return 10.0 * np.log10(power + 1e-20)
623+
624+
@staticmethod
625+
def gain_db(lna=0, vga=0, amp=False):
626+
# Total RX gain through the chain in dB: LNA (IF) + VGA (baseband) +
627+
# the fixed ~14 dB front-end amp if enabled. This is the quantity that
628+
# makes a raw dBFS reading ambiguous -- the SAME signal reads ~36 dB
629+
# different between min and max gain.
630+
return float(lna) + float(vga) + (C.AMP_DB if amp else 0.0)
631+
632+
def relative_power_db(self, iq_or_dbfs, *, lna=None, vga=None, amp=None,
633+
offset_db=0.0, freq_hz=None, freq_correction=None):
634+
# Gain-normalized power: subtract the gain chain so readings taken at
635+
# DIFFERENT gain settings are directly comparable. This is the Level 1
636+
# relative calibration -- still not absolute dBm, but consistent.
637+
#
638+
# value = dBFS - total_gain_dB + offset_db [- freq_correction(freq)]
639+
#
640+
# iq_or_dbfs: a complex64 block (its power is measured) OR a dBFS float
641+
# lna/vga/amp: gain settings; if omitted, taken from last_params
642+
# offset_db: your single-point reference offset (Level 3), if any.
643+
# With a correct offset this approximates dBm; without
644+
# one it is relative dB (still gain-consistent).
645+
# freq_hz + freq_correction: optional per-frequency correction. If you
646+
# pass a callable freq_correction(freq_hz)->dB (e.g. built
647+
# from a sweep of a flat source, see examples/calibrate.py),
648+
# it is subtracted to flatten the front-end response
649+
# (Level 2). The library ships NO built-in curve, because
650+
# front-end response varies per unit.
651+
dbfs = (iq_or_dbfs if isinstance(iq_or_dbfs, (int, float))
652+
else self.power_dbfs(iq_or_dbfs))
653+
lp = self.last_params or {}
654+
if lna is None:
655+
lna = lp.get("lna_gain", lp.get("lna", 0))
656+
if vga is None:
657+
vga = lp.get("vga_gain", lp.get("vga", 0))
658+
if amp is None:
659+
amp = lp.get("amp", False)
660+
value = dbfs - self.gain_db(lna, vga, amp) + float(offset_db)
661+
if freq_hz is not None and freq_correction is not None:
662+
value -= float(freq_correction(freq_hz))
663+
return value
664+
606665
return load_iq(path, count=count, offset_samples=offset_samples)
607666

608667
def estimate_capture(self, sample_rate, num_samples=None, duration=None,
@@ -675,4 +734,4 @@ def load_iq(path: str, count: int | None = None,
675734
iq.real = a[0::2]
676735
iq.imag = a[1::2]
677736
iq /= 128.0
678-
return iq
737+
return iq

0 commit comments

Comments
 (0)