mirror of
https://github.com/greatscottgadgets/hackrf.git
synced 2026-02-19 16:22:52 +01:00
1419 lines
58 KiB
Python
1419 lines
58 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import sys
|
|
import subprocess
|
|
import time
|
|
import os, shutil
|
|
import usb
|
|
from datetime import datetime
|
|
from dataclasses import dataclass
|
|
import argparse
|
|
import numpy as np
|
|
import traceback
|
|
import re
|
|
|
|
DFU_UTIL = "/usr/bin/dfu-util"
|
|
TMP_DIR = "/tmp/"
|
|
WAVEFORM = TMP_DIR + "waveform100"
|
|
NOISE = TMP_DIR + "noise"
|
|
VENDOR_ID = 0x1d50
|
|
PRODUCT_ID = 0x6089
|
|
DFU_VENDOR_ID = 0x1fc9
|
|
DFU_PRODUCT_ID = 0x000c
|
|
ONE_MHZ = 1000000
|
|
TIMEOUT = 5
|
|
MAX_PPM = 40
|
|
MAX_BASELINE = -20
|
|
SIGNAL_THRESHOLD = -25
|
|
MAX_DC_DBC = -33
|
|
MAX_M_DBC = -27
|
|
MAX_MH2_DBC = -40
|
|
MAX_H2_DBC = -39
|
|
MAX_OTHER_DBC = -34
|
|
LP2_CORRECTION = 10
|
|
MAX_POWER = -10
|
|
IF_REQUIRED_MIN = 2300
|
|
IF_REQUIRED_MAX = 2700
|
|
IF_TEST_RANGE = 256
|
|
SLEEP_TIME = 0.05 # seconds to wait between start of TX and start of RX
|
|
|
|
@dataclass
|
|
class TestCase():
|
|
name: str
|
|
freq: int
|
|
amp: bool
|
|
direction: str
|
|
base_ecode: int
|
|
rx_lna_gain: int
|
|
rx_vga_gain: int
|
|
tx_gain: int
|
|
|
|
# Try to keep rx_lna_gain and rx_vga_gain between 8 and 32.
|
|
bp1txamp = TestCase(name="BP1-TX-amp", freq=2665, amp=True, direction="tx",
|
|
base_ecode=320, rx_lna_gain=16, rx_vga_gain=16, tx_gain=14)
|
|
lp1txamp = TestCase(name="LP1-TX-amp", freq=915, amp=True, direction="tx",
|
|
base_ecode=420, rx_lna_gain=16, rx_vga_gain=16, tx_gain=27)
|
|
lp2txamp = TestCase(name="LP2-TX-amp", freq=2, amp=True, direction="tx",
|
|
base_ecode=2420, rx_lna_gain=8, rx_vga_gain=16, tx_gain=30)
|
|
hp1txamp = TestCase(name="HP1-TX-amp", freq=5995, amp=True, direction="tx",
|
|
base_ecode=520, rx_lna_gain=32, rx_vga_gain=28, tx_gain=37)
|
|
|
|
bp1txnoamp = TestCase(name="BP1-TX-noamp", freq=2665, amp=False, direction="tx",
|
|
base_ecode=350, rx_lna_gain=16, rx_vga_gain=16, tx_gain=23)
|
|
lp1txnoamp = TestCase(name="LP1-TX-noamp", freq=915, amp=False, direction="tx",
|
|
base_ecode=450, rx_lna_gain=16, rx_vga_gain=16, tx_gain=39)
|
|
lp2txnoamp = TestCase(name="LP2-TX-noamp", freq=2, amp=False, direction="tx",
|
|
base_ecode=2450, rx_lna_gain=8, rx_vga_gain=16, tx_gain=47)
|
|
hp1txnoamp = TestCase(name="HP1-TX-noamp", freq=5995, amp=False, direction="tx",
|
|
base_ecode=550, rx_lna_gain=32, rx_vga_gain=32, tx_gain=44)
|
|
|
|
# Much of the information in the RX test cases is a duplication of information
|
|
# in the TX test cases, but it's easier to keep legacy error codes this way.
|
|
bp1rxamp = TestCase(name="BP1-RX-amp", freq=2665, amp=True, direction="rx",
|
|
base_ecode=620, rx_lna_gain=16, rx_vga_gain=16, tx_gain=14)
|
|
lp1rxamp = TestCase(name="LP1-RX-amp", freq=915, amp=True, direction="rx",
|
|
base_ecode=720, rx_lna_gain=16, rx_vga_gain=16, tx_gain=29)
|
|
lp2rxamp = TestCase(name="LP2-RX-amp", freq=2, amp=True, direction="rx",
|
|
base_ecode=2720, rx_lna_gain=8, rx_vga_gain=16, tx_gain=33)
|
|
hp1rxamp = TestCase(name="HP1-RX-amp", freq=5995, amp=True, direction="rx",
|
|
base_ecode=820, rx_lna_gain=32, rx_vga_gain=28, tx_gain=39)
|
|
|
|
bp1rxnoamp = TestCase(name="BP1-RX-noamp", freq=2665, amp=False, direction="rx",
|
|
base_ecode=650, rx_lna_gain=16, rx_vga_gain=16, tx_gain=23)
|
|
lp1rxnoamp = TestCase(name="LP1-RX-noamp", freq=915, amp=False, direction="rx",
|
|
base_ecode=750, rx_lna_gain=16, rx_vga_gain=16, tx_gain=39)
|
|
lp2rxnoamp = TestCase(name="LP2-RX-noamp", freq=2, amp=False, direction="rx",
|
|
base_ecode=2750, rx_lna_gain=8, rx_vga_gain=16, tx_gain=47)
|
|
hp1rxnoamp = TestCase(name="HP1-RX-noamp", freq=5995, amp=False, direction="rx",
|
|
base_ecode=850, rx_lna_gain=32, rx_vga_gain=32, tx_gain=44)
|
|
|
|
ifmin = TestCase(name="IF-min", freq=IF_REQUIRED_MIN, amp=None, direction=None,
|
|
base_ecode=None, rx_lna_gain=16, rx_vga_gain=16, tx_gain=33)
|
|
ifmax = TestCase(name="IF-max", freq=IF_REQUIRED_MAX, amp=None, direction=None,
|
|
base_ecode=None, rx_lna_gain=16, rx_vga_gain=16, tx_gain=33)
|
|
|
|
ppm = TestCase(name="PPM", freq=2707, amp=False, direction=None,
|
|
base_ecode=None, rx_lna_gain=16, rx_vga_gain=16, tx_gain=26)
|
|
|
|
noise = TestCase(name="Noise", freq=6000, amp=True, direction=None,
|
|
base_ecode=None, rx_lna_gain=40, rx_vga_gain=52, tx_gain=None)
|
|
|
|
all_rf_test_cases = (
|
|
bp1txamp, lp1txamp, lp2txamp, hp1txamp,
|
|
bp1txnoamp, lp1txnoamp, lp2txnoamp, hp1txnoamp,
|
|
bp1rxamp, lp1rxamp, lp2rxamp, hp1rxamp,
|
|
bp1rxnoamp, lp1rxnoamp, lp2rxnoamp, hp1rxnoamp)
|
|
|
|
r9_tester_cal = [1.1, 0.4, 0.4, 1.1, 0.8, 0.2, 0.6, 1.6, -0.4, -1.2, -1.1, 1.1, -1.4, -0.9, 0.1, 1.8]
|
|
r9_eut_cal = [-1.1, -1.6, -1.4, -1.3, -1.3, -0.9, 0.1, 1.8, 0.8, 1.1, 0.9, -0.2, 0.9, 0.2, 0.6, 1.5]
|
|
og_tester_cal = [-0.4, -0.3, -0.2, -0.4, -0.8, -0.5, -0., -0.1, 1.4, 2., 1.6, -0.1, 1.5, 0.7, 0.5, -0.3]
|
|
og_eut_cal = [1.8, 1.8, 1.6, 2., 1.3, 0.7, 0.5, -0.3, 0.2, -0.3, -0.5, 1.2, -0.8, -0.5, -0., -0.1]
|
|
|
|
emessages = {
|
|
# TODO: replace the placeholder code according to each fail instance
|
|
1: "Placeholder error code",
|
|
60: "Unable to program firmware via DFU",
|
|
65: "EUT not detected after DFU programming",
|
|
70: "Unable to program SPI flash",
|
|
75: "EUT not detected after flashing",
|
|
80: "Could not find EUT",
|
|
81: "Could not find TESTER",
|
|
82: "Found multiple candidate EUTs",
|
|
83: "Found multiple candidate TESTERs",
|
|
84: "EUT running from RAM instead of flash",
|
|
85: "TESTER running from RAM instead of flash",
|
|
86: "Couldn't parse hackrf_info output",
|
|
88: "Incorrect EUT hardware revision",
|
|
89: "Incorrect TESTER hardware revision",
|
|
90: "EUT has incorrect manufacturer pin strap",
|
|
91: "TESTER has incorrect manufacturer pin strap",
|
|
99: "Incorrect device name",
|
|
100: "EUT couldn't verify clkgen register",
|
|
101: "TESTER couldn't verify clkgen register",
|
|
120: "EUT couldn't verify xcvr register",
|
|
121: "TESTER couldn't verify xcvr register",
|
|
140: "EUT couldn't verify mixer register",
|
|
141: "TESTER couldn't verify mixer register",
|
|
150: "Failed RX for IF verification",
|
|
152: "Couldn't parse received power",
|
|
154: "Received power out of bounds",
|
|
160: "Insufficient IF range",
|
|
170: "RX operation failed",
|
|
172: "Couldn't parse hackrf_sweep output",
|
|
180: "Subprocess timed out",
|
|
190: "Failed to configure LEDs",
|
|
200: "hackrf_clock command failed",
|
|
210: "TESTER failed to detect EUT CLKOUT",
|
|
211: "EUT failed to detect TESTER CLKOUT",
|
|
212: "TESTER detected spurious CLKIN",
|
|
213: "EUT detected spurious CLKIN",
|
|
220: "Failed TESTER CLKIN activation RX",
|
|
221: "Failed EUT CLKIN activation RX",
|
|
230: "Failed TESTER PLL check",
|
|
231: "Failed EUT PLL check",
|
|
232: "TESTER PLL failed to lock",
|
|
233: "EUT PLL failed to lock",
|
|
240: "Clock synchronization failure",
|
|
250: "Failed PPM detection sweep",
|
|
252: "Couldn't parse PPM test sweep data",
|
|
254: "TESTER couldn't detect PPM test transmission",
|
|
255: "EUT couldn't detect PPM test transmission",
|
|
260: "Frequency error exceeds limit",
|
|
300: "Failed baseline RX",
|
|
302: "Couldn't parse baseline RX power",
|
|
306: "Baseline RX power too high",
|
|
310: "Failed noise RX",
|
|
311: "Couldn't parse noise RX power",
|
|
312: "Noise RX power out of range",
|
|
313: "Required noise gain out of range",
|
|
315: "Couldn't read noise RX file",
|
|
316: "Too little variation in ADC bit",
|
|
317: "ADC bit correlates strongly with another bit",
|
|
321: "Excessive TX DC offset in AMP txbp1",
|
|
322: "insufficient signal strength in AMP txbp1",
|
|
323: "AMP txbp1 clipping",
|
|
325: "peak found at unexpected frequency in AMP txbp1",
|
|
330: "harmonics detected in AMP txbp1",
|
|
335: "neighbor magnitude exceeded in AMP txbp1",
|
|
340: "neighbor magnitude exceeded in AMP txbp1",
|
|
345: "spectrum mirrored or inverted in AMP txbp1",
|
|
350: "excessive power out of channel in AMP txbp1",
|
|
351: "Excessive TX DC offset in NOAMP txbp1",
|
|
352: "insufficient signal strength in NOAMP txbp1",
|
|
353: "NOAMP txbp1 clipping",
|
|
355: "peak found at unexpected frequency in NOAMP txbp1",
|
|
360: "harmonics detected in NOAMP txbp1",
|
|
365: "neighbor magnitude exceeded in NOAMP txbp1",
|
|
370: "neighbor magnitude exceeded in NOAMP txbp1",
|
|
375: "spectrum mirrored or inverted in NOAMP txbp1",
|
|
380: "excessive power out of channel in NOAMP txbp1",
|
|
421: "Excessive TX DC offset in AMP txlp1",
|
|
422: "insufficient signal strength in AMP txlp1",
|
|
423: "AMP txlp1 clipping",
|
|
425: "peak found at unexpected frequency in AMP txlp1",
|
|
430: "harmonics detected in AMP txlp1",
|
|
435: "neighbor magnitude exceeded in AMP txlp1",
|
|
440: "neighbor magnitude exceeded in AMP txlp1",
|
|
445: "spectrum mirrored or inverted in AMP txlp1",
|
|
450: "excessive power out of channel in AMP txlp1",
|
|
451: "Excessive TX DC offset in NOAMP txlp1",
|
|
452: "insufficient signal strength in NOAMP txlp1",
|
|
453: "NOAMP txlp1 clipping",
|
|
455: "peak found at unexpected frequency in NOAMP txlp1",
|
|
460: "harmonics detected in NOAMP txlp1",
|
|
465: "neighbor magnitude exceeded in NOAMP txlp1",
|
|
470: "neighbor magnitude exceeded in NOAMP txlp1",
|
|
475: "spectrum mirrored or inverted in NOAMP txlp1",
|
|
480: "excessive power out of channel in NOAMP txlp1",
|
|
2421: "Excessive TX DC offset in AMP txlp2",
|
|
2422: "insufficient signal strength in AMP txlp2",
|
|
2423: "AMP txlp2 clipping",
|
|
2425: "peak found at unexpected frequency in AMP txlp2",
|
|
2430: "harmonics detected in AMP txlp2",
|
|
2435: "neighbor magnitude exceeded in AMP txlp2",
|
|
2440: "neighbor magnitude exceeded in AMP txlp2",
|
|
2445: "spectrum mirrored or inverted in AMP txlp2",
|
|
2450: "excessive power out of channel in AMP txlp2",
|
|
2451: "Excessive TX DC offset in NOAMP txlp2",
|
|
2452: "insufficient signal strength in NOAMP txlp2",
|
|
2453: "NOAMP txlp2 clipping",
|
|
2455: "peak found at unexpected frequency in NOAMP txlp2",
|
|
2460: "harmonics detected in NOAMP txlp2",
|
|
2465: "neighbor magnitude exceeded in NOAMP txlp2",
|
|
2470: "neighbor magnitude exceeded in NOAMP txlp2",
|
|
2475: "spectrum mirrored or inverted in NOAMP txlp2",
|
|
2480: "excessive power out of channel in NOAMP txlp2",
|
|
521: "Excessive TX DC offset in AMP txhp1",
|
|
522: "insufficient signal strength in AMP txhp1",
|
|
523: "AMP txhp1 clipping",
|
|
525: "peak found at unexpected frequency in AMP txhp1",
|
|
530: "harmonics detected in AMP txhp1",
|
|
535: "neighbor magnitude exceeded in AMP txhp1",
|
|
540: "neighbor magnitude exceeded in AMP txhp1",
|
|
545: "spectrum mirrored or inverted in AMP txhp1",
|
|
550: "excessive power out of channel in AMP txhp1",
|
|
551: "Excessive TX DC offset in NOAMP txhp1",
|
|
552: "insufficient signal strength in NOAMP txhp1",
|
|
553: "NOAMP txhp1 clipping",
|
|
555: "peak found at unexpected frequency in NOAMP txhp1",
|
|
560: "harmonics detected in NOAMP txhp1",
|
|
565: "neighbor magnitude exceeded in NOAMP txhp1",
|
|
570: "neighbor magnitude exceeded in NOAMP txhp1",
|
|
575: "spectrum mirrored or inverted in NOAMP txhp1",
|
|
580: "excessive power out of channel in NOAMP txhp1",
|
|
621: "Excessive TX DC offset in AMP rxbp1",
|
|
622: "insufficient signal strength in AMP rxbp1",
|
|
623: "AMP rxbp1 clipping",
|
|
625: "peak found at unexpected frequency in AMP rxbp1",
|
|
630: "harmonics detected in AMP rxbp1",
|
|
635: "neighbor magnitude exceeded in AMP rxbp1",
|
|
640: "neighbor magnitude exceeded in AMP rxbp1",
|
|
645: "spectrum mirrored or inverted in AMP rxbp1",
|
|
650: "excessive power out of channel in AMP rxbp1",
|
|
651: "Excessive TX DC offset in NOAMP rxbp1",
|
|
652: "insufficient signal strength in NOAMP rxbp1",
|
|
653: "NOAMP rxbp1 clipping",
|
|
655: "peak found at unexpected frequency in NOAMP rxbp1",
|
|
660: "harmonics detected in NOAMP rxbp1",
|
|
665: "neighbor magnitude exceeded in NOAMP rxbp1",
|
|
670: "neighbor magnitude exceeded in NOAMP rxbp1",
|
|
675: "spectrum mirrored or inverted in NOAMP rxbp1",
|
|
680: "excessive power out of channel in NOAMP rxbp1",
|
|
721: "Excessive TX DC offset in AMP rxlp1",
|
|
722: "insufficient signal strength in AMP rxlp1",
|
|
723: "AMP rxlp1 clipping",
|
|
725: "peak found at unexpected frequency in AMP rxlp1",
|
|
730: "harmonics detected in AMP rxlp1",
|
|
735: "neighbor magnitude exceeded in AMP rxlp1",
|
|
740: "neighbor magnitude exceeded in AMP rxlp1",
|
|
745: "spectrum mirrored or inverted in AMP rxlp1",
|
|
750: "excessive power out of channel in AMP rxlp1",
|
|
751: "Excessive TX DC offset in NOAMP rxlp1",
|
|
752: "insufficient signal strength in NOAMP rxlp1",
|
|
753: "NOAMP rxlp1 clipping",
|
|
755: "peak found at unexpected frequency in NOAMP rxlp1",
|
|
760: "harmonics detected in NOAMP rxlp1",
|
|
765: "neighbor magnitude exceeded in NOAMP rxlp1",
|
|
770: "neighbor magnitude exceeded in NOAMP rxlp1",
|
|
775: "spectrum mirrored or inverted in NOAMP rxlp1",
|
|
780: "excessive power out of channel in NOAMP rxlp1",
|
|
2721: "Excessive TX DC offset in AMP rxlp2",
|
|
2722: "insufficient signal strength in AMP rxlp2",
|
|
2723: "AMP rxlp2 clipping",
|
|
2725: "peak found at unexpected frequency in AMP rxlp2",
|
|
2730: "harmonics detected in AMP rxlp2",
|
|
2735: "neighbor magnitude exceeded in AMP rxlp2",
|
|
2740: "neighbor magnitude exceeded in AMP rxlp2",
|
|
2745: "spectrum mirrored or inverted in AMP rxlp2",
|
|
2750: "excessive power out of channel in AMP rxlp2",
|
|
2751: "Excessive TX DC offset in NOAMP rxlp2",
|
|
2752: "insufficient signal strength in NOAMP rxlp2",
|
|
2753: "NOAMP rxlp2 clipping",
|
|
2755: "peak found at unexpected frequency in NOAMP rxlp2",
|
|
2760: "harmonics detected in NOAMP rxlp2",
|
|
2765: "neighbor magnitude exceeded in NOAMP rxlp2",
|
|
2770: "neighbor magnitude exceeded in NOAMP rxlp2",
|
|
2775: "spectrum mirrored or inverted in NOAMP rxlp2",
|
|
2780: "excessive power out of channel in NOAMP rxlp2",
|
|
821: "Excessive TX DC offset in AMP rxhp1",
|
|
822: "insufficient signal strength in AMP rxhp1",
|
|
823: "AMP rxhp1 clipping",
|
|
825: "peak found at unexpected frequency in AMP rxhp1",
|
|
830: "harmonics detected in AMP rxhp1",
|
|
835: "neighbor magnitude exceeded in AMP rxhp1",
|
|
840: "neighbor magnitude exceeded in AMP rxhp1",
|
|
845: "spectrum mirrored or inverted in AMP rxhp1",
|
|
850: "excessive power out of channel in AMP rxhp1",
|
|
851: "Excessive TX DC offset in NOAMP rxhp1",
|
|
852: "insufficient signal strength in NOAMP rxhp1",
|
|
853: "NOAMP rxhp1 clipping",
|
|
855: "peak found at unexpected frequency in NOAMP rxhp1",
|
|
860: "harmonics detected in NOAMP rxhp1",
|
|
865: "neighbor magnitude exceeded in NOAMP rxhp1",
|
|
870: "neighbor magnitude exceeded in NOAMP rxhp1",
|
|
875: "spectrum mirrored or inverted in NOAMP rxhp1",
|
|
880: "excessive power out of channel in NOAMP rxhp1",
|
|
3000: "BP TX failure",
|
|
3010: "BP RX failure",
|
|
3020: "HP TX failure",
|
|
3030: "HP RX failure",
|
|
3040: "LP TX failure",
|
|
3050: "LP RX failure",
|
|
3060: "TX AMP failure",
|
|
3070: "RX AMP failure",
|
|
3080: "AMP bypass failure",
|
|
3090: "LPF failure",
|
|
3100: "HPF failure",
|
|
3110: "mixer bypass failure",
|
|
3120: "mixer failure",
|
|
3130: "TX failure",
|
|
3140: "RX failure",
|
|
3150: "mixer TX failure",
|
|
3160: "mixer RX failure",
|
|
3170: "LP2 TX failure",
|
|
3180: "LP2 RX failure",
|
|
3190: "LP2 TX/RX failure",
|
|
3200: "total RF failure",
|
|
3210: "multiple RF failures, see list above",
|
|
3220: "multiple clipping failures"
|
|
}
|
|
|
|
def fail_rf(errors):
|
|
# check for combinations of RF errors
|
|
ecode = 0
|
|
eset = set(errors)
|
|
|
|
# error affects only bp tx
|
|
if ((322 in eset) and (422 not in eset) and (2422 not in eset) and (522 not in eset) and (622 not in eset) and (722 not in eset) and (2722 not in eset) and (822 not in eset)):
|
|
ecode = 3000
|
|
errors.append(ecode)
|
|
|
|
# error affects only bp rx
|
|
if ((322 not in eset) and (422 not in eset) and (2422 not in eset) and (522 not in eset) and (622 in eset) and (722 not in eset) and (2722 not in eset) and (822 not in eset)):
|
|
ecode = 3010
|
|
errors.append(ecode)
|
|
|
|
# error affects only hp tx
|
|
if ((322 not in eset) and (422 not in eset) and (2422 not in eset) and (522 in eset) and (622 not in eset) and (722 not in eset) and (2722 not in eset) and (822 not in eset)):
|
|
ecode = 3020
|
|
errors.append(ecode)
|
|
|
|
# error affects only hp rx
|
|
if ((322 not in eset) and (422 not in eset) and (2422 not in eset) and (522 not in eset) and (622 not in eset) and (722 not in eset) and (2722 not in eset) and (822 in eset)):
|
|
ecode = 3030
|
|
errors.append(ecode)
|
|
|
|
# error affects only lp tx (both lp1 and lp2)
|
|
if ((322 not in eset) and (422 in eset) and (2422 in eset) and (522 not in eset) and (622 not in eset) and (722 not in eset) and (2722 not in eset) and (822 not in eset)):
|
|
ecode = 3040
|
|
errors.append(ecode)
|
|
|
|
# error affects only lp rx (both lp1 and lp2)
|
|
if ((322 not in eset) and (422 not in eset) and (2422 not in eset) and (522 not in eset) and (622 not in eset) and (722 in eset) and (2722 in eset) and (822 not in eset)):
|
|
ecode = 3050
|
|
errors.append(ecode)
|
|
|
|
# error in tx amp path
|
|
if ((2422 in eset) and (352 not in eset) and (452 not in eset) and (2452 not in eset)):
|
|
ecode = 3060
|
|
errors.append(ecode)
|
|
|
|
# error in rx amp path
|
|
if ((2722 in eset) and (652 not in eset) and (752 not in eset) and (2752 not in eset)):
|
|
ecode = 3070
|
|
errors.append(ecode)
|
|
|
|
# error in amp bypass path
|
|
if ((352 in eset) and (452 in eset) and (2452 in eset) and (652 in eset) and (752 in eset) and (2752 in eset) and (322 not in eset) and (422 not in eset) and (2422 not in eset) and (622 not in eset) and (722 not in eset) and (2722 not in eset)):
|
|
ecode = 3080
|
|
errors.append(ecode)
|
|
|
|
# error in lpf path
|
|
if (((422 in eset) and (2422 in eset)) and ((722 in eset) and (2722 in eset)) and ((322 not in eset) and (622 not in eset)) and ((522 not in eset) and (822 not in eset))):
|
|
ecode = 3090
|
|
errors.append(ecode)
|
|
|
|
# error in hpf path
|
|
if (((522 in eset) and (822 in eset)) and ((322 not in eset) and (622 not in eset)) and ((422 not in eset) and (2422 not in eset) and (722 not in eset) and (2722 not in eset))):
|
|
ecode = 3100
|
|
errors.append(ecode)
|
|
|
|
# error affects both mixer bypass paths
|
|
if ((322 in eset) and (422 not in eset) and (2422 not in eset) and (522 not in eset) and (622 in eset) and (722 not in eset) and (2722 not in eset) and (822 not in eset)):
|
|
ecode = 3110
|
|
errors.append(ecode)
|
|
|
|
# error in mixer tx/rx
|
|
if ((322 not in eset) and ((422 in eset) or (2422 in eset)) and (522 in eset) and (622 not in eset) and ((722 in eset) or (2722 in eset) or (822 in eset))):
|
|
ecode = 3120
|
|
errors.append(ecode)
|
|
|
|
if ((322 not in eset) and ((422 in eset) or (2422 in eset) or (522 in eset)) and (622 not in eset) and ((722 in eset) and (2722 in eset) and (822 in eset))):
|
|
ecode = 3120
|
|
errors.append(ecode)
|
|
|
|
# error in all tx, no rx
|
|
if ((322 in eset) and (422 in eset) and (2422 in eset) and (522 in eset) and (622 not in eset) and (722 not in eset) and (2722 not in eset) and (822 not in eset) and (352 in eset) and (452 in eset) and (2452 in eset) and (552 in eset)):
|
|
ecode = 3130
|
|
errors.append(ecode)
|
|
|
|
# error in all rx, no tx
|
|
if ((322 not in eset) and (422 not in eset) and (2422 not in eset) and (522 not in eset) and (622 in eset) and (722 in eset) and (2722 in eset) and (822 in eset) and (652 in eset) and (752 in eset) and (2752 in eset) and (852 in eset)):
|
|
ecode = 3140
|
|
errors.append(ecode)
|
|
|
|
# error in all mixer tx
|
|
if ((322 not in eset) and (422 in eset) and (2422 in eset) and (522 in eset) and (622 not in eset) and (722 not in eset) and (2722 not in eset) and (822 not in eset)):
|
|
ecode = 3150
|
|
errors.append(ecode)
|
|
|
|
# error in all mixer rx
|
|
if ((322 not in eset) and (422 not in eset) and (2422 not in eset) and (522 not in eset) and (622 not in eset) and (722 in eset) and (2722 in eset) and (822 in eset)):
|
|
ecode = 3160
|
|
errors.append(ecode)
|
|
|
|
# error affecting only lp2 tx
|
|
if ((322 not in eset) and (422 not in eset) and (2422 in eset) and (522 not in eset) and (622 not in eset) and (722 not in eset) and (2722 not in eset) and (822 not in eset) and (2752 in eset)):
|
|
ecode = 3170
|
|
errors.append(ecode)
|
|
|
|
# error affecting only lp2 rx
|
|
if ((322 not in eset) and (422 not in eset) and (2422 not in eset) and (522 not in eset) and (622 not in eset) and (722 not in eset) and (2722 in eset) and (822 not in eset) and (2752 in eset)):
|
|
ecode = 3180
|
|
errors.append(ecode)
|
|
|
|
# error affecting only lp2 rx/tx
|
|
if ((322 not in eset) and (422 not in eset) and (2422 in eset) and (522 not in eset) and (622 not in eset) and (722 not in eset) and (2722 in eset) and (822 not in eset)):
|
|
ecode = 3190
|
|
errors.append(ecode)
|
|
|
|
# error in all rx/tx
|
|
if ((322 in eset) and (422 in eset) and (2422 in eset) and (522 in eset) and (622 in eset) and (722 in eset) and (2722 in eset) and (822 in eset)):
|
|
ecode = 3200
|
|
errors.append(ecode)
|
|
|
|
clipping_count = 0
|
|
for code in (323, 423, 2423, 523, 353, 453, 2453, 553, 623, 723, 2723, 823, 653, 753, 2753, 853):
|
|
if code in eset:
|
|
clipping_count += 1
|
|
if clipping_count > 2:
|
|
ecode = 3220
|
|
errors.append(ecode)
|
|
elif (not ecode) and (len(errors) > 1):
|
|
# multiple failures without a logical grouping
|
|
ecode = 3210
|
|
errors.append(ecode)
|
|
|
|
for error in errors[:-1]:
|
|
out(f"RF failure {error}: {emessages[error]}")
|
|
fail(errors[-1])
|
|
|
|
def log(message=""):
|
|
if args.log:
|
|
with open(args.log, "a") as log_file:
|
|
print(message, file=log_file)
|
|
return
|
|
|
|
def out(message=""):
|
|
log(message)
|
|
print(message)
|
|
|
|
def fail(error_code):
|
|
out(f"FAIL {error_code}: {emessages[error_code]}")
|
|
sys.exit(2)
|
|
|
|
class HackRF:
|
|
def __init__(self, name, bin_dir, serial, mf_check, revision=None, unique_bin=False):
|
|
self.name = name
|
|
self.bin_dir = bin_dir
|
|
self.unique_bin = unique_bin
|
|
self.revision = None
|
|
self.serial = None
|
|
self.clkout_connected = False
|
|
self.tester_cal = [0] * 16
|
|
self.eut_cal = [0] * 16
|
|
self.errors = []
|
|
self.unit_number = 0
|
|
self.partner = None
|
|
|
|
if self.name == "TESTER":
|
|
self.unit_number = 1
|
|
elif self.name != "EUT":
|
|
fail(99)
|
|
|
|
info = subprocess.run([bin_dir + "hackrf_info"], capture_output=True,
|
|
encoding="utf-8", timeout=TIMEOUT)
|
|
log(info.stdout + info.stderr)
|
|
if info.returncode != 0 or "failed" in info.stderr:
|
|
fail(80 + self.unit_number)
|
|
|
|
# parse the hackrf_info output
|
|
count = info.stdout.count("Found HackRF")
|
|
if count < 1:
|
|
fail(80 + self.unit_number)
|
|
if count > 1 and self.name == "TESTER" and self.unique_bin:
|
|
fail(83)
|
|
if mf_check and "does not appear to have been manufactured by Great Scott Gadgets" in info.stdout:
|
|
fail(90 + self.unit_number)
|
|
try:
|
|
devices_list = re.findall(r'Found HackRF\n.+\n.+\n.+\n.+\n.+\n.+', info.stdout)
|
|
for device in devices_list:
|
|
sn = re.search(r'Serial number: \w+', device).group().split(': ')[1]
|
|
rev = re.search(r'Hardware Revision: .+', device).group().split(': ')[1]
|
|
if sn == serial:
|
|
self.serial = sn
|
|
self.revision = rev
|
|
break
|
|
out(f"{self.name} serial number: {self.serial}")
|
|
out(f"{self.name} revision: {self.revision}")
|
|
out(f"{self.name} binary directory {self.bin_dir}")
|
|
except:
|
|
log(traceback.format_exc())
|
|
fail(1)
|
|
|
|
if revision and self.revision != revision:
|
|
fail(88 + self.unit_number)
|
|
|
|
if self.revision == "r9":
|
|
self.tester_cal = r9_tester_cal
|
|
self.eut_cal = r9_eut_cal
|
|
else:
|
|
self.tester_cal = og_tester_cal
|
|
self.eut_cal = og_eut_cal
|
|
|
|
def clkin(self):
|
|
command = subprocess.run([self.bin_dir + "hackrf_clock", "-i", "-d",
|
|
self.serial], capture_output=True, encoding="utf-8",
|
|
timeout=TIMEOUT)
|
|
if command.returncode != 0:
|
|
log(command.stdout + command.stderr)
|
|
out(f"{self.name} CLKIN request failed")
|
|
fail(200)
|
|
if "no clock signal detected" in command.stdout:
|
|
log(f"{self.name}: no clock signal detected")
|
|
return False
|
|
elif "clock signal detected" in command.stdout:
|
|
log(f"{self.name}: clock signal detected")
|
|
return True
|
|
else:
|
|
log(command.stdout + command.stderr)
|
|
out(f"Couldn't parse {self.name} CLKIN status")
|
|
fail(200)
|
|
|
|
# Warning: Avoid clock loops by disabling your partner's CLKOUT before
|
|
# enabling your own.
|
|
def clkout(self, enable):
|
|
command = subprocess.run([self.bin_dir + "hackrf_clock", "-o",
|
|
"1" if enable else "0", "-d", self.serial], capture_output=True,
|
|
encoding="utf-8", timeout=TIMEOUT)
|
|
if command.returncode != 0:
|
|
log(command.stdout + command.stderr)
|
|
out(f"{self.name} CLKOUT control failed")
|
|
fail(200)
|
|
|
|
if self.partner and self.clkout_connected:
|
|
timeout = time.time() + 1
|
|
while time.time() < timeout:
|
|
if self.partner.clkin() == enable:
|
|
break
|
|
if self.partner.clkin() != enable:
|
|
if enable:
|
|
fail(210 + self.unit_number)
|
|
else:
|
|
if self.name == "EUT":
|
|
out("Note: use --tcxo to use TCXO or external reference on TESTER")
|
|
fail(212 + self.unit_number)
|
|
|
|
# transceiver operation to switch clock source
|
|
source = "CLKIN" if enable else "crystal"
|
|
log(f"switching {self.partner.name} to {source}")
|
|
receive = subprocess.run([self.partner.bin_dir + "hackrf_transfer", "-d",
|
|
self.partner.serial, "-n1", "-r", "/dev/null", "-f", "2500000000",
|
|
"-a", "0", "-l", "0", "-g", "0"], capture_output=True,
|
|
encoding="utf-8", timeout=TIMEOUT)
|
|
if receive.returncode != 0:
|
|
log(receive.stdout + receive.stderr)
|
|
fail(220 + self.unit_number)
|
|
|
|
# confirm that PLL locked to new source
|
|
expected_value = "0x01" if (enable and self.partner.revision != "r9") else "0x51"
|
|
timeout = time.time() + 1
|
|
while time.time() < timeout:
|
|
debug = subprocess.run([self.partner.bin_dir + "hackrf_debug", "-d",
|
|
self.partner.serial, "-srn0"], capture_output=True,
|
|
encoding="utf-8", timeout=TIMEOUT)
|
|
log(debug.stdout)
|
|
if debug.returncode != 0:
|
|
log(debug.stderr)
|
|
fail(230 + self.unit_number)
|
|
if expected_value in debug.stdout:
|
|
break
|
|
if expected_value not in debug.stdout:
|
|
fail(232 + self.unit_number)
|
|
time.sleep(0.1)
|
|
else:
|
|
time.sleep(0.2)
|
|
|
|
def test_clkout(self):
|
|
assert self.clkout_connected
|
|
self.partner.clkout(False)
|
|
self.clkout(True)
|
|
frequency_error_ppm = self.clock_error_ppm()
|
|
log(f"{self.name}->{self.partner.name} CLK: {frequency_error_ppm} ppm")
|
|
if abs(frequency_error_ppm) > 1:
|
|
out(f"{self.partner.name} CLKIN: unexpected frequency error while synchronized")
|
|
fail(240)
|
|
|
|
def test_clkin(self):
|
|
self.partner.test_clkout()
|
|
|
|
def clock_error_ppm(self):
|
|
# ppm.freq is in MHz, but we use it as Hz for the FFT bin width,
|
|
# resulting in bins with a width of 1 ppm of the transmission
|
|
# frequency.
|
|
bin_width = str(ppm.freq)
|
|
tx_frequency = str(ppm.freq * ONE_MHZ)
|
|
sweep_range = f"{ppm.freq-1}:{ppm.freq+19}"
|
|
expected_bin = round(ONE_MHZ/ppm.freq)
|
|
|
|
log(f"{self.name} transmitting to {self.partner.name} for PPM measurement")
|
|
transmit = subprocess.Popen([self.bin_dir + "hackrf_transfer", "-d",
|
|
self.serial, "-c", "127", "-a", "0", "-x", f"{ppm.tx_gain}", "-f",
|
|
tx_frequency], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
encoding="utf-8")
|
|
|
|
time.sleep(SLEEP_TIME)
|
|
sweep = subprocess.run([self.partner.bin_dir + "hackrf_sweep", "-d",
|
|
self.partner.serial, "-N", "13", "-w", bin_width, "-f", sweep_range, "-a",
|
|
"0", "-l", f"{ppm.rx_lna_gain}", "-g", f"{ppm.rx_vga_gain}"],
|
|
capture_output=True, encoding="utf-8", timeout=TIMEOUT)
|
|
if sweep.returncode != 0:
|
|
log(sweep.stdout + sweep.stderr)
|
|
fail(250)
|
|
|
|
# parse the hackrf_sweep output
|
|
try:
|
|
rows = sweep.stdout.split("\n")
|
|
# Note: skipping data from the first twelve sweeps until issue #1230 is resolved.
|
|
row = rows[48].split(", ")
|
|
bins = [float(bin) for bin in row[6:]]
|
|
peak_bin = bins.index(max(bins))
|
|
frequency_error_ppm = peak_bin - expected_bin
|
|
except:
|
|
log(sweep.stdout + sweep.stderr)
|
|
log(traceback.format_exc())
|
|
fail(252)
|
|
|
|
log(f"PPM max bin {peak_bin} power: {bins[peak_bin]}")
|
|
for row in rows[0:49:4]:
|
|
bins = [float(bin) for bin in row.split(", ")[6:]]
|
|
peak_bin = bins.index(max(bins))
|
|
log(f"peak bin {peak_bin}: {bins[peak_bin]}")
|
|
if max(bins) < SIGNAL_THRESHOLD:
|
|
fail(254 + self.unit_number)
|
|
transmit.terminate()
|
|
tx_out, tx_err = transmit.communicate(timeout=TIMEOUT)
|
|
if transmit.returncode != 0:
|
|
log(tx_out + tx_err)
|
|
out(f"Warning: {self.name} PPM test transmit failed")
|
|
return frequency_error_ppm
|
|
|
|
def test_xtal(self):
|
|
self.clkout(False)
|
|
self.partner.clkout(False)
|
|
frequency_error_ppm = self.clock_error_ppm()
|
|
out(f"{self.name} crystal frequency error: {frequency_error_ppm} ppm")
|
|
if abs(frequency_error_ppm) > MAX_PPM:
|
|
fail(260)
|
|
|
|
def verify_register(self, target, address, value):
|
|
command = subprocess.run([self.bin_dir + "hackrf_debug",
|
|
f"--{target}", "--register", address, "--read", "--device",
|
|
self.serial], capture_output=True, encoding="UTF-8")
|
|
log(f"Checking {target} register {address}")
|
|
if value in command.stdout:
|
|
return True
|
|
else:
|
|
log("Unexpected result:")
|
|
log(command.stdout + command.stderr)
|
|
return False
|
|
|
|
def test_clkgen(self):
|
|
if not self.verify_register("si5351", "2", "0x03"):
|
|
fail(100 + self.unit_number)
|
|
|
|
def test_mixer(self):
|
|
if not self.verify_register("rffc5072", "2", "0x9055"):
|
|
fail(140 + self.unit_number)
|
|
|
|
def test_xcvr(self):
|
|
if not self.verify_register("max283", "28", "0x0c0"):
|
|
fail(120 + self.unit_number)
|
|
|
|
def test_serial(self):
|
|
log(f"Testing {self.name} serial communications")
|
|
self.test_clkgen()
|
|
self.test_mixer()
|
|
self.test_xcvr()
|
|
|
|
# Ensure that hackrf_transfer works and that the received power is within
|
|
# the expected range with no transmission active.
|
|
def baseline_rx(self, test_case):
|
|
# Test at the TX tuning frequency of measure_rf()
|
|
center_freq = (test_case.freq * ONE_MHZ) + 1500000
|
|
receive = subprocess.run([self.bin_dir + "hackrf_transfer", "-d",
|
|
self.serial, "-n10000", "-r", "/dev/null", "-f",
|
|
f"{center_freq}", "-a", "0", "-l", f"{test_case.rx_lna_gain}",
|
|
"-g", f"{test_case.rx_vga_gain}", "-b",
|
|
"1750000"], capture_output=True, encoding="utf-8", timeout=TIMEOUT)
|
|
if receive.returncode != 0:
|
|
log(receive.stdout + receive.stderr)
|
|
log(f"Failed {self.name} {test_case.name} baseline RX")
|
|
fail(300)
|
|
|
|
output = str(receive.stderr).split()
|
|
try:
|
|
power = float(output[output.index("power") + 1])
|
|
except:
|
|
log(receive.stdout + receive.stderr)
|
|
log(traceback.format_exc())
|
|
log(f"Couldn't parse {self.name} {test_case.name} baseline RX power")
|
|
fail(302)
|
|
# The theoretical minimum power is about -48 dB. The expected power is
|
|
# about -30 dB.
|
|
log(f"{self.name} {test_case.name} baseline power: {power}")
|
|
if power > MAX_BASELINE:
|
|
fail(306)
|
|
|
|
# Check the ADC for shorts or other unexpected behavior by sampling
|
|
# amplified noise.
|
|
def noise_rx(self, test_case):
|
|
num_samples = 1000
|
|
log(f"Testing {self.name} {test_case.name}")
|
|
center_freq = test_case.freq * ONE_MHZ
|
|
|
|
low_gain = 0
|
|
high_gain = 16
|
|
while high_gain > low_gain:
|
|
gain = (high_gain + low_gain) // 2
|
|
receive = subprocess.run([self.bin_dir + "hackrf_transfer", "-d",
|
|
self.serial, "-n", f"{num_samples*10}", "-r", f"{NOISE}", "-f",
|
|
f"{center_freq}", "-a", "1" if test_case.amp else "0", "-l",
|
|
f"{test_case.rx_lna_gain}", "-g", f"{gain*2 + 32}", "-b",
|
|
"28000000"], capture_output=True, encoding="utf-8", timeout=TIMEOUT)
|
|
if receive.returncode != 0:
|
|
log(receive.stdout + receive.stderr)
|
|
fail(310)
|
|
output = str(receive.stderr).split()
|
|
try:
|
|
power = float(output[output.index("power") + 1])
|
|
except:
|
|
log(receive.stdout + receive.stderr)
|
|
log(traceback.format_exc())
|
|
fail(311)
|
|
log(f"{self.name} {test_case.name} RX power at -g {gain*2 + 32}: {power}")
|
|
if power > -2:
|
|
high_gain = gain
|
|
elif power > -5:
|
|
break
|
|
else:
|
|
low_gain = gain + 1
|
|
|
|
# We want the power to be between -1 and -6 dB to ensure that all eight
|
|
# bits are flipped without much clipping.
|
|
if power < -6 or power > -1:
|
|
fail(312)
|
|
elif gain > 14 or gain < 4:
|
|
fail(313)
|
|
|
|
bit_count = [0] * 8
|
|
matches = []
|
|
for bit in range(8):
|
|
matches.append([0] * 8)
|
|
|
|
try:
|
|
file = open(f"{NOISE}", "rb")
|
|
data = file.read()
|
|
assert len(data) == num_samples * 20
|
|
data = data[-num_samples*2:]
|
|
for byte in data:
|
|
for bit in range(8):
|
|
bit_count[bit] += (byte>>bit)&1
|
|
for match_bit in range(bit + 1, 8):
|
|
if (byte>>bit)&1 == (byte>>match_bit)&1:
|
|
matches[bit][match_bit] += 1
|
|
except:
|
|
fail(315)
|
|
log(f"{self.name} ADC bit counts: {bit_count}")
|
|
log(f"{self.name} ADC bit matches: {matches}")
|
|
for bit in range(7):
|
|
if bit_count[bit] < (0.2 * len(data)) or bit_count[bit] > (0.8 * len(data)):
|
|
log(f"{self.name} ADC bit {bit} set in {bit_count[bit]} out of {len(data)} bytes")
|
|
fail(316)
|
|
# Check for correlations between adjacent bits except between 6 and 7 (highest).
|
|
#for match_bit in range(bit + 1, 7):
|
|
for match_bit in range(bit + 1, 8):
|
|
if matches[bit][match_bit] > (0.999 * len(data)):
|
|
out(f"{self.name} ADC bit {bit} correlates with bit {match_bit}")
|
|
fail(317)
|
|
# DC bias can cause bit 7 to be predominantly unchanged, so only
|
|
# error if there is less than 1% variation.
|
|
if bit_count[7] < (0.001 * len(data)) or bit_count[bit] > (0.999 * len(data)):
|
|
out(f"{self.name} ADC bit 7 set in {bit_count[7]} out of {len(data)} bytes")
|
|
fail(316)
|
|
|
|
def analyze_transfer(self, transfer, test_case):
|
|
# parse the hackrf_transfer output
|
|
output = str(transfer.stderr).split()
|
|
try:
|
|
power = float(output[output.index("power") + 1])
|
|
except:
|
|
log(transfer.stdout + transfer.stderr)
|
|
log(traceback.format_exc())
|
|
fail(152)
|
|
log(f"{test_case.name} power at {test_case.freq} MHz: {power}")
|
|
|
|
if test_case.base_ecode:
|
|
if power < SIGNAL_THRESHOLD:
|
|
log(f"{test_case.name} signal not strong enough")
|
|
self.errors.append(test_case.base_ecode + 2)
|
|
if power > -3:
|
|
log(f"{test_case.name} signal clipping")
|
|
self.errors.append(test_case.base_ecode + 3)
|
|
elif power < -48 or power > 0:
|
|
fail(154)
|
|
return power
|
|
|
|
# Verify that signal power at a given IF in MHz is detected.
|
|
def verify_if_tuning(self, test_case):
|
|
transmit = subprocess.Popen([self.bin_dir + "hackrf_transfer", "-d",
|
|
self.serial, "-c", "127", "-a", "0", "-x", f"{test_case.tx_gain}",
|
|
"-i", f"{test_case.freq*ONE_MHZ}", "-m", "0", "-F"],
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8")
|
|
|
|
time.sleep(SLEEP_TIME)
|
|
receive = subprocess.run([self.partner.bin_dir + "hackrf_transfer", "-d",
|
|
self.partner.serial, "-n10000", "-r", "/dev/null", "-f",
|
|
f"{(test_case.freq+1)*ONE_MHZ}", "-a", "0", "-l",
|
|
f"{test_case.rx_lna_gain}", "-g", f"{test_case.rx_vga_gain}", "-b",
|
|
"2500000"], capture_output=True, encoding="utf-8", timeout=TIMEOUT)
|
|
transmit.terminate()
|
|
if receive.returncode != 0:
|
|
log(receive.stdout + receive.stderr)
|
|
fail(150)
|
|
|
|
power = self.analyze_transfer(receive, test_case)
|
|
|
|
tx_out, tx_err = transmit.communicate(timeout=TIMEOUT)
|
|
if transmit.returncode != 0:
|
|
log(tx_out + tx_err)
|
|
out(f"Warning: {self.name} IF transmit failed")
|
|
return power > SIGNAL_THRESHOLD
|
|
|
|
def test_if_range(self, min_case, max_case):
|
|
low_end = IF_REQUIRED_MIN - IF_TEST_RANGE + 1
|
|
min_freq = IF_REQUIRED_MIN + 1
|
|
while min_freq > low_end:
|
|
min_case.freq = (min_freq + low_end) // 2
|
|
if self.verify_if_tuning(min_case):
|
|
min_freq = min_case.freq
|
|
else:
|
|
low_end = min_case.freq + 1
|
|
|
|
high_end = IF_REQUIRED_MAX + IF_TEST_RANGE - 1
|
|
max_freq = IF_REQUIRED_MAX - 1
|
|
while max_freq < high_end:
|
|
max_case.freq = (max_freq + high_end + 1) // 2
|
|
if self.verify_if_tuning(max_case):
|
|
max_freq = max_case.freq
|
|
else:
|
|
high_end = max_case.freq - 1
|
|
|
|
out(f"{self.name} IF range: {min_freq}-{max_freq} MHz")
|
|
if min_freq > IF_REQUIRED_MIN or max_freq < IF_REQUIRED_MAX:
|
|
fail(160)
|
|
|
|
def analyze_sweep(self, sweep, test_case):
|
|
eut_cal = self.eut_cal[all_rf_test_cases.index(test_case)]
|
|
tester_cal = self.partner.tester_cal[all_rf_test_cases.index(test_case)]
|
|
correction = LP2_CORRECTION if test_case.freq < 30 else 0
|
|
|
|
# parse the hackrf_sweep output
|
|
try:
|
|
rows = sweep.stdout.split("\n")
|
|
# Note: skipping data from the first twelve sweeps until issue #1230 is resolved.
|
|
bins = np.zeros(225)
|
|
# average 10 sweeps
|
|
row_count = 0
|
|
for row in rows[48:88:4]:
|
|
bins += np.array([float(x) for x in row.split(", ")[6:231]])
|
|
row_count += 1
|
|
assert row_count == 10
|
|
bins /= 10
|
|
# log the raw hackrf_sweep data before calibration
|
|
log(f"{', '.join(rows[12].split(', ')[:6])}, {np.array2string(bins, separator=', ', max_line_width=3000)[1:-1]}")
|
|
# apply calibration
|
|
bins -= eut_cal
|
|
bins -= tester_cal
|
|
peak_bin = bins.argmax()
|
|
except:
|
|
log(sweep.stdout + sweep.stderr)
|
|
log(traceback.format_exc())
|
|
out(f"Couldn't parse {test_case.name} sweep data")
|
|
fail(172)
|
|
|
|
# ensure acceptable performance
|
|
log(f"peak ({peak_bin}) {test_case.name}: {bins[peak_bin]:.2f}")
|
|
if bins[peak_bin] < SIGNAL_THRESHOLD:
|
|
for row in rows[0:88:4]:
|
|
bins = [float(bin) for bin in row.split(", ")[6:]]
|
|
peak_bin = bins.index(max(bins))
|
|
log(f"peak bin {peak_bin}: {bins[peak_bin]}")
|
|
log(f"{test_case.name} signal not strong enough")
|
|
self.errors.append(test_case.base_ecode + 2)
|
|
return bins[peak_bin]
|
|
elif bins[peak_bin] > MAX_POWER:
|
|
log(f"{test_case.name} signal clipping")
|
|
self.errors.append(test_case.base_ecode + 3)
|
|
return bins[peak_bin]
|
|
tx_dc_power_dbc = bins[112] - bins[peak_bin]
|
|
if tx_dc_power_dbc > MAX_DC_DBC + correction:
|
|
log(f"{test_case.name} excessive TX DC offset {tx_dc_power_dbc:.1f} dBc")
|
|
self.errors.append(test_case.base_ecode + 1)
|
|
if peak_bin == 157:
|
|
log(f"{test_case.name} TX spectrum inverted")
|
|
self.errors.append(test_case.base_ecode + 25)
|
|
return bins[peak_bin]
|
|
elif peak_bin == 112:
|
|
log(f"{test_case.name} peak found at DC")
|
|
self.errors.append(test_case.base_ecode + 1)
|
|
return bins[peak_bin]
|
|
elif peak_bin >= 52 and peak_bin < 67:
|
|
log(f"{test_case.name} peak found at adjacent frequency")
|
|
self.errors.append(test_case.base_ecode + 15)
|
|
return bins[peak_bin]
|
|
elif peak_bin > 67 and peak_bin <= 82:
|
|
log(f"{test_case.name} peak found at adjacent frequency")
|
|
self.errors.append(test_case.base_ecode + 20)
|
|
return bins[peak_bin]
|
|
elif peak_bin != 67:
|
|
log(f"{test_case.name} peak found at unexpected frequency")
|
|
self.errors.append(test_case.base_ecode + 5)
|
|
return bins[peak_bin]
|
|
|
|
if bins[157] - bins[67] > MAX_M_DBC + correction:
|
|
log(f"{test_case.name} TX spectrum mirrored")
|
|
self.errors.append(test_case.base_ecode + 25)
|
|
if (bins[22] - bins[67]) > MAX_H2_DBC + correction:
|
|
log(f"{test_case.name} excessive TX harmonic (bin 22): {bins[22]}")
|
|
self.errors.append(test_case.base_ecode + 10)
|
|
if (bins[202] - bins[67]) > MAX_MH2_DBC + correction:
|
|
log(f"{test_case.name} excessive TX harmonic (bin 202): {bins[202]}")
|
|
self.errors.append(test_case.base_ecode + 10)
|
|
worst_other_dbc = -999
|
|
worst_other_bin = 999
|
|
for i in range(225):
|
|
# Skip various frequencies where spurs are expected (most of which have their own tests above).
|
|
if (i < 52 and i != 22) or (i > 82 and i < 111) or (i > 113 and i < 156) or (i > 158 and i != 202):
|
|
other_dbc = bins[i] - bins[67]
|
|
if other_dbc > worst_other_dbc:
|
|
worst_other_dbc = other_dbc
|
|
worst_other_bin = i
|
|
if worst_other_dbc > MAX_OTHER_DBC + correction:
|
|
log(f"{test_case.name} excessive power at unexpected frequency ({worst_other_bin}: {bins[worst_other_bin]})")
|
|
self.errors.append(test_case.base_ecode + 30)
|
|
log(f"h2 (22) {test_case.name}: {(bins[22] - bins[67]):.2f}")
|
|
log(f"DC (112) {test_case.name}: {(bins[112] - bins[67]):.2f}")
|
|
log(f"m (157) {test_case.name}: {(bins[157] - bins[67]):.2f}")
|
|
log(f"mh2 (202) {test_case.name}: {(bins[202] - bins[67]):.2f}")
|
|
log(f"other ({worst_other_bin}) {test_case.name}: {worst_other_dbc:.2f}")
|
|
#FIXME look in another row for RX inversion?
|
|
return bins[peak_bin]
|
|
|
|
def measure_rf(self, test_case, sweep=True):
|
|
log(f"Executing RF test case {test_case.name}, sweep={sweep}")
|
|
if test_case.direction == "tx":
|
|
transmitter = self
|
|
tx_amp = "1" if test_case.amp else "0"
|
|
receiver = self.partner
|
|
rx_amp = "0"
|
|
elif test_case.direction == "rx":
|
|
transmitter = self.partner
|
|
tx_amp = "0"
|
|
receiver = self
|
|
rx_amp = "1" if test_case.amp else "0"
|
|
else:
|
|
out(f"Error: Invalid direction in {test_case.name}")
|
|
sys.exit(1)
|
|
|
|
# The nominal test frequency (as specified in test_case) is 1 MHz above
|
|
# the low end of the sweep range. The transmitted signal is 500 kHz
|
|
# above the nominal frequency, and the tuning (center) frequency of the
|
|
# transmitting device is 1 MHz above that.
|
|
tx_center_freq = (test_case.freq * ONE_MHZ) + 1500000
|
|
sweep_range = f"{test_case.freq-1}:{test_case.freq+19}"
|
|
|
|
transmit = subprocess.Popen([transmitter.bin_dir + "hackrf_transfer", "-d",
|
|
transmitter.serial, "-R", "-t", WAVEFORM, "-a", tx_amp, "-x",
|
|
f"{test_case.tx_gain}", "-f", str(tx_center_freq)],
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8")
|
|
|
|
time.sleep(SLEEP_TIME)
|
|
if sweep:
|
|
receive = subprocess.run([receiver.bin_dir + "hackrf_sweep",
|
|
"-d", receiver.serial, "-N", "22", "-w", "22222", "-f",
|
|
sweep_range, "-a", rx_amp, "-l", f"{test_case.rx_lna_gain}",
|
|
"-g", f"{test_case.rx_vga_gain}"], capture_output=True,
|
|
encoding="utf-8", timeout=TIMEOUT)
|
|
else:
|
|
receive = subprocess.run([receiver.bin_dir + "hackrf_transfer",
|
|
"-d", receiver.serial, "-n10000", "-r", "/dev/null", "-f",
|
|
f"{(test_case.freq)*ONE_MHZ}", "-a", rx_amp, "-l",
|
|
f"{test_case.rx_lna_gain}", "-g", f"{test_case.rx_vga_gain}",
|
|
"-b", "2500000"], capture_output=True, encoding="utf-8",
|
|
timeout=TIMEOUT)
|
|
transmit.terminate()
|
|
if receive.returncode != 0:
|
|
log(receive.stdout + receive.stderr)
|
|
log(f"{receiver.name} Failed RX, sweep={sweep}")
|
|
fail(170)
|
|
|
|
if sweep:
|
|
rssi = self.analyze_sweep(receive, test_case)
|
|
else:
|
|
rssi = self.analyze_transfer(receive, test_case)
|
|
|
|
tx_out, tx_err = transmit.communicate(timeout=TIMEOUT)
|
|
if transmit.returncode != 0:
|
|
log(tx_out + tx_err)
|
|
out(f"Warning: {transmitter.name} transmit failed")
|
|
return rssi
|
|
|
|
def test_rf_preliminary(self):
|
|
results = []
|
|
|
|
for test_case in (all_rf_test_cases):
|
|
if test_case.amp == False:
|
|
if test_case.direction == "rx":
|
|
self.baseline_rx(test_case)
|
|
elif self.partner:
|
|
self.partner.baseline_rx(test_case)
|
|
|
|
self.noise_rx(noise)
|
|
# FIXME test for RX DC offset?
|
|
|
|
if not self.partner:
|
|
out("Skipping RF tests")
|
|
return
|
|
|
|
for test_case in (all_rf_test_cases):
|
|
results.append(int(round(self.measure_rf(test_case, sweep=False) - (SIGNAL_THRESHOLD + 11))))
|
|
|
|
log(f"Preliminary results: {results}")
|
|
if self.errors:
|
|
fail_rf(self.errors)
|
|
|
|
def test_rf(self):
|
|
assert self.partner
|
|
results = []
|
|
|
|
for test_case in (all_rf_test_cases):
|
|
results.append(int(round(self.measure_rf(test_case, sweep=True) - (SIGNAL_THRESHOLD + 5))))
|
|
|
|
out(f"RF results summary: {results}")
|
|
if self.errors:
|
|
fail_rf(self.errors)
|
|
|
|
def activate_leds(self, enable):
|
|
if enable:
|
|
out(f"Activating {self.name} LEDs")
|
|
state = 7
|
|
else:
|
|
state = 1
|
|
command = subprocess.run([self.bin_dir + "hackrf_debug", "-d", self.serial, "--leds",
|
|
f"{state}"], capture_output=True, encoding="utf-8", timeout=TIMEOUT)
|
|
if command.returncode != 0:
|
|
log(command.stdout + command.stderr)
|
|
log(f"Failed to configure {self.name} LEDs ({state})")
|
|
fail(190)
|
|
|
|
|
|
# This waveform is a full-scale complex exponential with a frequency of -1/10 the
|
|
# sample rate. When transmitted at 10 Msps, the frequency is 1 MHz below the
|
|
# center frequency.
|
|
|
|
def write_waveform():
|
|
tx_bytes = b'\x7f\x00\x67\xb5\x27\x87\xd9\x87\x99\xb5\x81\x00\x99\x4b\xd9\x79\x27\x79\x67\x4b'
|
|
# defective waveform with DC offset:
|
|
#tx_bytes = b'\x7f\x10\x67\xa5\x27\x97\xd9\x97\x99\xc5\x81\x10\x99\x5b\xd9\x7f\x27\x7f\x67\x5b'
|
|
# defective waveform with missing Q channel:
|
|
#tx_bytes = b'\x7f\x00\x67\x00\x27\x00\xd9\x00\x99\x00\x81\x00\x99\x00\xd9\x00\x27\x00\x67\x00'
|
|
with open(WAVEFORM, "wb") as bin_file:
|
|
for i in range(50000): # 1MB file size
|
|
bin_file.write(tx_bytes)
|
|
|
|
|
|
def program(bin_dir, fw_dir, serial, unattended=False):
|
|
if "build" in fw_dir:
|
|
binary = "hackrf_usb.bin"
|
|
dfu_stub = "hackrf_usb.dfu"
|
|
else:
|
|
binary = "hackrf_one_usb.bin"
|
|
dfu_stub = "hackrf_one_usb.dfu"
|
|
|
|
# EUTs in unattended (including Jenkins CI) setups have pins jumped to always boot into DFU mode, so resetting is undesirable
|
|
if unattended:
|
|
reset_write = "-w"
|
|
else:
|
|
reset_write = "-Rw"
|
|
|
|
if not usb.core.find(idVendor=DFU_VENDOR_ID, idProduct=DFU_PRODUCT_ID):
|
|
print("ACTION REQUIRED: Press and release EUT RESET button while holding DFU button.")
|
|
while not usb.core.find(idVendor=DFU_VENDOR_ID, idProduct=DFU_PRODUCT_ID):
|
|
time.sleep(0.1)
|
|
|
|
out("Programming EUT in DFU mode")
|
|
dfu = subprocess.run([f"{DFU_UTIL}", "--device",
|
|
f"{DFU_VENDOR_ID}:{DFU_PRODUCT_ID}", "--alt", "0", "--download",
|
|
f"{fw_dir}{dfu_stub}"], capture_output=True, encoding="utf-8",
|
|
timeout=TIMEOUT)
|
|
|
|
#dfu-util: unable to read DFU status after completion (LIBUSB_ERROR_IO)
|
|
# despite successful download; present as of at least dfu-util 0.11
|
|
if "Download done." not in dfu.stdout:
|
|
fail(60)
|
|
|
|
# Wait for device to boot from DFU.
|
|
then = time.time()
|
|
dfu_device_found = False
|
|
while time.time() < (then + 50):
|
|
dfu_info = subprocess.run([bin_dir + "hackrf_info"], capture_output=True,
|
|
encoding="utf-8", timeout=TIMEOUT)
|
|
|
|
if "RunningFromRAM" in dfu_info.stdout:
|
|
if serial in dfu_info.stdout and serial != "RunningFromRAM":
|
|
out("wrong device in DFU mode")
|
|
fail(1)
|
|
else:
|
|
dfu_device_found = True
|
|
break
|
|
time.sleep(0.1)
|
|
if not dfu_device_found:
|
|
out("DFU device not found")
|
|
fail(1)
|
|
|
|
out("Programming EUT SPI flash")
|
|
spiflash = subprocess.run([bin_dir + "hackrf_spiflash", "-d",
|
|
"RunningFromRAM", reset_write, f"{fw_dir}{binary}"],
|
|
capture_output=True, encoding="utf-8", timeout=TIMEOUT)
|
|
if spiflash.returncode != 0:
|
|
log(spiflash.stdout + spiflash.stderr)
|
|
fail(70)
|
|
|
|
then = time.time()
|
|
device_found = False
|
|
while time.time() < (then + 5):
|
|
flash_info = subprocess.run([bin_dir + "hackrf_info"], capture_output=True,
|
|
timeout=TIMEOUT)
|
|
|
|
if serial in flash_info.stdout.decode('utf-8', errors='ignore'):
|
|
device_found = True
|
|
break
|
|
time.sleep(0.1)
|
|
if not device_found:
|
|
fail(75)
|
|
|
|
|
|
def find_sn(name, bin_dir, claimed_sns=[]):
|
|
while True:
|
|
sn_list = []
|
|
info = subprocess.run([bin_dir + "hackrf_info"], capture_output=True,
|
|
encoding="utf-8", timeout=TIMEOUT)
|
|
devices_list = re.findall(r'Found HackRF\n.+\n.+\n.+\n.+\n.+\n.+', info.stdout)
|
|
|
|
out(f"\nPotential {name}(s) found:\n")
|
|
for device in devices_list:
|
|
new_sn = re.search(r'Serial number: \w+', device).group().split(': ')[1]
|
|
if new_sn not in claimed_sns:
|
|
sn_list.append(new_sn)
|
|
if len(sn_list) > 1:
|
|
for i in range(len(sn_list)):
|
|
out(f"{i}: {sn_list[i]}")
|
|
choice = int(input(f"\nEnter the index of your target {name}s serial number: "))
|
|
if 0 <= choice < len(sn_list):
|
|
sn = sn_list[choice]
|
|
break
|
|
else:
|
|
out("Incorrect choice, try again.")
|
|
elif len(sn_list) == 1:
|
|
sn = sn_list[0]
|
|
out(f"{sn}\n")
|
|
break
|
|
else:
|
|
out("Failed to parse a serial number from hackrf_info")
|
|
fail(1)
|
|
return sn
|
|
|
|
|
|
def get_version():
|
|
version = subprocess.run(["git", "log", "-n" "1", "--format=%h"],
|
|
capture_output=True, encoding="utf-8", timeout=TIMEOUT)
|
|
if version.returncode != 0:
|
|
return "2026.01.3+"
|
|
elif version.returncode == 0:
|
|
dirty = subprocess.run(["git", "status", "-s", "--untracked-files=no"],
|
|
capture_output=True, encoding="utf-8", timeout=TIMEOUT)
|
|
if dirty.stdout != "":
|
|
dirty_flag = "*"
|
|
else:
|
|
dirty_flag = ""
|
|
return f"git-{version.stdout.strip()}{dirty_flag}"
|
|
|
|
|
|
def main():
|
|
|
|
write_waveform()
|
|
|
|
parser = argparse.ArgumentParser(description="Utility for testing HackRF One")
|
|
parser.add_argument("-c", "--noclk", action="store_true", help="skip CLKIN, CLKOUT, and PPM tests")
|
|
parser.add_argument("-i", "--noif", action="store_true", help="skip IF tuning range test")
|
|
parser.add_argument("-l", "--loop", action="store_true", help="repeat the test until terminated with ctrl-c")
|
|
parser.add_argument("-m", "--manufacturer", action="store_true", help="check the manufacturer of target devices")
|
|
parser.add_argument("-r", "--rev", metavar="<hardware rev> or \"any\"", type=str,
|
|
help="check EUT pin straps for specific hardware revision (default: \"r9\")",
|
|
default="r9")
|
|
parser.add_argument("-s", "--solo", action="store_true", help="perform limited tests on a single EUT with no TESTER")
|
|
parser.add_argument("-t", "--tcxo", action="store_true", help="skip CLKIN/CLKOUT tests and use TCXO instead")
|
|
parser.add_argument("-E", "--eut", metavar="<EUT SN>", type=str,
|
|
help="EUT serial number")
|
|
parser.add_argument("-T", "--tester", metavar="<TESTER SN>", type=str,
|
|
help="TESTER serial number")
|
|
parser.add_argument("-f", "--fwupdate", metavar="<path to firmware binaries>", type=str,
|
|
help="update firmware with binaries from specified directory")
|
|
parser.add_argument("-H", "--hostdir", metavar="<path to installed host tools>", type=str,
|
|
help="location of installed hackrf host tools, will attempt to find them if omitted")
|
|
parser.add_argument("-b", "--testerdir", metavar="<separate path to TESTER host tools>", type=str,
|
|
help="necessary only if EUT/TESTER have separate host binaries")
|
|
parser.add_argument("-C", "--ci", action="store_true", help="For use with Jenkins CI user")
|
|
parser.add_argument("-u", "--unattended", action="store_true", help="For use with unattended hardware")
|
|
parser.add_argument("-L", "--log", metavar="<log file>", type=str,
|
|
help="log file location")
|
|
global args
|
|
args = parser.parse_args()
|
|
|
|
if args.ci:
|
|
user = "jenkins"
|
|
else:
|
|
user = os.getlogin()
|
|
|
|
if "any" in args.rev.lower():
|
|
args.rev = None
|
|
elif args.rev.lower() == "r1" or args.rev.lower() == "r4":
|
|
args.rev = "older than r6"
|
|
|
|
if args.hostdir:
|
|
eut_host_dir = args.hostdir
|
|
else:
|
|
try:
|
|
eut_host_dir = os.path.dirname(shutil.which('hackrf_info')) + "/"
|
|
except:
|
|
out("No path to hackrf host tools found. Please provide a directory via --hostdir")
|
|
fail(1)
|
|
|
|
if args.testerdir:
|
|
if args.testerdir == eut_host_dir:
|
|
out("Specified TESTER bin directory must be different from the found EUT bin directory")
|
|
out("If shared TESTER/EUT bin directory is intended, omit --testerdir")
|
|
fail(1)
|
|
else:
|
|
tester_host_dir = args.testerdir
|
|
else:
|
|
tester_host_dir = eut_host_dir
|
|
|
|
unique_bin = (tester_host_dir != eut_host_dir)
|
|
count = 0
|
|
|
|
while True:
|
|
out()
|
|
out("================================")
|
|
out(datetime.now())
|
|
out(sys.argv)
|
|
out(f"user: {user}")
|
|
out(f"version: {get_version()}")
|
|
out(f"pid: {os.getpid()}")
|
|
if args.loop:
|
|
out(f"repeat count: {count}")
|
|
out("================================")
|
|
out()
|
|
|
|
if args.eut:
|
|
eut_sn = args.eut
|
|
else:
|
|
if args.tester:
|
|
eut_sn = find_sn("EUT", eut_host_dir, [args.tester])
|
|
else:
|
|
eut_sn = find_sn("EUT", eut_host_dir)
|
|
if args.tester:
|
|
tester_sn = args.tester
|
|
else:
|
|
tester_sn = find_sn("TESTER", tester_host_dir, [eut_sn])
|
|
|
|
if eut_sn == tester_sn:
|
|
out("TESTER and EUT cannot be the same device")
|
|
fail(1)
|
|
|
|
if args.fwupdate:
|
|
program(eut_host_dir, args.fwupdate, eut_sn, args.unattended)
|
|
|
|
if not args.rev:
|
|
out("Skipping EUT hardware revision check")
|
|
eut = HackRF("EUT", eut_host_dir, eut_sn, args.manufacturer, revision=args.rev)
|
|
|
|
if args.solo:
|
|
tester = None
|
|
else:
|
|
tester = HackRF("TESTER", tester_host_dir, tester_sn, args.manufacturer, unique_bin=unique_bin)
|
|
tester.test_serial()
|
|
tester.partner = eut
|
|
eut.partner = tester
|
|
|
|
out("Testing EUT")
|
|
|
|
if args.solo:
|
|
eut.clkout_connected = False
|
|
elif args.noclk:
|
|
eut.clkout_connected = False
|
|
tester.clkout_connected = False
|
|
elif args.tcxo:
|
|
eut.clkout_connected = False
|
|
tester.clkout_connected = True
|
|
else:
|
|
eut.clkout_connected = True
|
|
tester.clkout_connected = True
|
|
|
|
if count > 0 and not args.fw_update:
|
|
eut.activate_leds(False)
|
|
|
|
eut.test_serial()
|
|
|
|
# limited RF test without clock synchronization
|
|
eut.test_rf_preliminary()
|
|
|
|
if args.noclk or args.solo:
|
|
out("Skipping CLKIN, CLKOUT, and PPM tests")
|
|
else:
|
|
eut.test_xtal()
|
|
if not args.tcxo:
|
|
eut.test_clkout()
|
|
eut.test_clkin()
|
|
|
|
eut.clkout(False)
|
|
|
|
if tester:
|
|
tester.clkout(True)
|
|
|
|
if args.tcxo:
|
|
if args.solo:
|
|
out("Error: --solo is incompatible with --tcxo")
|
|
sys.exit(1)
|
|
if tester.clkin():
|
|
out("Using TCXO on TESTER")
|
|
else:
|
|
out("Warning: No TCXO or external clock detected on TESTER")
|
|
out("Skipping CLKOUT test")
|
|
if not args.noclk and not eut.clkin():
|
|
fail(211)
|
|
|
|
# full RF test with clock synchronization
|
|
if tester:
|
|
eut.test_rf()
|
|
|
|
if args.noif or args.solo:
|
|
out("Skipping IF tuning range test")
|
|
else:
|
|
eut.test_if_range(ifmin, ifmax)
|
|
|
|
eut.activate_leds(True)
|
|
|
|
out()
|
|
if (args.solo or args.noif or args.noclk or args.tcxo or not args.rev):
|
|
out("Limited test complete")
|
|
else:
|
|
out("PASS if all 6 LEDs active")
|
|
|
|
count += 1
|
|
if not args.loop:
|
|
break
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
print()
|
|
out("KeyboardInterrupt")
|
|
sys.exit(0)
|
|
except subprocess.TimeoutExpired:
|
|
log(traceback.format_exc())
|
|
fail(180)
|