fix: full-repo ruff lint cleanup and CI migration to uv
Resolve all 374 ruff errors across 36 Python files (E501, E702, E722, E741, F821, F841, invalid-syntax) bringing `ruff check .` to zero errors repo-wide with line-length=100. Rewrite CI workflow to use uv for dependency management, whole-repo `ruff check .`, py_compile syntax gate, and merged python-tests job. Add pyproject.toml with ruff config and uv dependency groups. CI structure proposed by hcm444.
This commit is contained in:
@@ -358,7 +358,10 @@ def compare_scenario(scenario_name):
|
||||
|
||||
# ---- First/last sample comparison ----
|
||||
print("\nFirst 10 samples (after alignment):")
|
||||
print(f" {'idx':>4s} {'RTL_I':>8s} {'Py_I':>8s} {'Err_I':>6s} {'RTL_Q':>8s} {'Py_Q':>8s} {'Err_Q':>6s}")
|
||||
print(
|
||||
f" {'idx':>4s} {'RTL_I':>8s} {'Py_I':>8s} {'Err_I':>6s} "
|
||||
f"{'RTL_Q':>8s} {'Py_Q':>8s} {'Err_Q':>6s}"
|
||||
)
|
||||
for k in range(min(10, aligned_len)):
|
||||
ei = aligned_rtl_i[k] - aligned_py_i[k]
|
||||
eq = aligned_rtl_q[k] - aligned_py_q[k]
|
||||
|
||||
@@ -199,14 +199,17 @@ class NCO:
|
||||
# Wait - let me re-derive. The Verilog is:
|
||||
# phase_accumulator <= phase_accumulator + frequency_tuning_word;
|
||||
# phase_accum_reg <= phase_accumulator; // OLD value (NBA)
|
||||
# phase_with_offset <= phase_accum_reg + {phase_offset, 16'b0}; // OLD phase_accum_reg
|
||||
# phase_with_offset <= phase_accum_reg + {phase_offset, 16'b0};
|
||||
# // OLD phase_accum_reg
|
||||
# Since all are NBA (<=), they all read the values from BEFORE this edge.
|
||||
# So: new_phase_accumulator = old_phase_accumulator + ftw
|
||||
# new_phase_accum_reg = old_phase_accumulator
|
||||
# new_phase_with_offset = old_phase_accum_reg + offset
|
||||
old_phase_accumulator = (self.phase_accumulator - ftw) & 0xFFFFFFFF # reconstruct
|
||||
self.phase_accum_reg = old_phase_accumulator
|
||||
self.phase_with_offset = (old_phase_accum_reg + ((phase_offset << 16) & 0xFFFFFFFF)) & 0xFFFFFFFF
|
||||
self.phase_with_offset = (
|
||||
old_phase_accum_reg + ((phase_offset << 16) & 0xFFFFFFFF)
|
||||
) & 0xFFFFFFFF
|
||||
# phase_accumulator was already updated above
|
||||
|
||||
# ---- Stage 3a: Register LUT address + quadrant ----
|
||||
@@ -607,8 +610,14 @@ class FIRFilter:
|
||||
if (old_valid_pipe >> 0) & 1:
|
||||
for i in range(16):
|
||||
# Sign-extend products to ACCUM_WIDTH
|
||||
a = sign_extend(mult_results[2*i] & ((1 << self.PRODUCT_WIDTH) - 1), self.PRODUCT_WIDTH)
|
||||
b = sign_extend(mult_results[2*i+1] & ((1 << self.PRODUCT_WIDTH) - 1), self.PRODUCT_WIDTH)
|
||||
a = sign_extend(
|
||||
mult_results[2 * i] & ((1 << self.PRODUCT_WIDTH) - 1),
|
||||
self.PRODUCT_WIDTH,
|
||||
)
|
||||
b = sign_extend(
|
||||
mult_results[2 * i + 1] & ((1 << self.PRODUCT_WIDTH) - 1),
|
||||
self.PRODUCT_WIDTH,
|
||||
)
|
||||
self.add_l0[i] = a + b
|
||||
|
||||
# ---- Stage 2 (Level 1): 8 pairwise sums ----
|
||||
@@ -1365,7 +1374,10 @@ def _self_test():
|
||||
mag_sq = s * s + c * c
|
||||
expected = 32767 * 32767
|
||||
error_pct = abs(mag_sq - expected) / expected * 100
|
||||
print(f" Quadrature check: sin^2+cos^2={mag_sq}, expected~{expected}, error={error_pct:.2f}%")
|
||||
print(
|
||||
f" Quadrature check: sin^2+cos^2={mag_sq}, "
|
||||
f"expected~{expected}, error={error_pct:.2f}%"
|
||||
)
|
||||
print(" NCO: OK")
|
||||
|
||||
# --- Mixer test ---
|
||||
|
||||
@@ -218,7 +218,8 @@ def generate_long_chirp_test():
|
||||
if seg == 0:
|
||||
buffer_write_ptr = 0
|
||||
else:
|
||||
# Overlap-save: copy buffer[SEGMENT_ADVANCE:SEGMENT_ADVANCE+OVERLAP] -> buffer[0:OVERLAP]
|
||||
# Overlap-save: copy
|
||||
# buffer[SEGMENT_ADVANCE:SEGMENT_ADVANCE+OVERLAP] -> buffer[0:OVERLAP]
|
||||
for i in range(OVERLAP_SAMPLES):
|
||||
input_buffer_i[i] = input_buffer_i[i + SEGMENT_ADVANCE]
|
||||
input_buffer_q[i] = input_buffer_q[i + SEGMENT_ADVANCE]
|
||||
|
||||
@@ -335,7 +335,9 @@ def run_ddc(adc_samples):
|
||||
for n in range(n_samples):
|
||||
integrators[0][n + 1] = (integrators[0][n] + mixed_i[n]) & ((1 << CIC_ACC_WIDTH) - 1)
|
||||
for s in range(1, CIC_STAGES):
|
||||
integrators[s][n + 1] = (integrators[s][n] + integrators[s - 1][n + 1]) & ((1 << CIC_ACC_WIDTH) - 1)
|
||||
integrators[s][n + 1] = (
|
||||
integrators[s][n] + integrators[s - 1][n + 1]
|
||||
) & ((1 << CIC_ACC_WIDTH) - 1)
|
||||
|
||||
# Downsample by 4
|
||||
n_decimated = n_samples // CIC_DECIMATION
|
||||
@@ -580,8 +582,11 @@ def run_range_bin_decimator(range_fft_i, range_fft_q,
|
||||
decimated_i = np.zeros((n_chirps, output_bins), dtype=np.int64)
|
||||
decimated_q = np.zeros((n_chirps, output_bins), dtype=np.int64)
|
||||
|
||||
print(f"[DECIM] Decimating {n_in}→{output_bins} bins, mode={'peak' if mode==1 else 'avg' if mode==2 else 'simple'}, "
|
||||
f"start_bin={start_bin}, {n_chirps} chirps")
|
||||
mode_str = 'peak' if mode == 1 else 'avg' if mode == 2 else 'simple'
|
||||
print(
|
||||
f"[DECIM] Decimating {n_in}→{output_bins} bins, mode={mode_str}, "
|
||||
f"start_bin={start_bin}, {n_chirps} chirps"
|
||||
)
|
||||
|
||||
for c in range(n_chirps):
|
||||
# Index into input, skip start_bin
|
||||
@@ -678,7 +683,9 @@ def run_doppler_fft(range_data_i, range_data_q, twiddle_file_16=None):
|
||||
if twiddle_file_16 and os.path.exists(twiddle_file_16):
|
||||
cos_rom_16 = load_twiddle_rom(twiddle_file_16)
|
||||
else:
|
||||
cos_rom_16 = np.round(32767 * np.cos(2 * np.pi * np.arange(n_fft // 4) / n_fft)).astype(np.int64)
|
||||
cos_rom_16 = np.round(
|
||||
32767 * np.cos(2 * np.pi * np.arange(n_fft // 4) / n_fft)
|
||||
).astype(np.int64)
|
||||
|
||||
LOG2N_16 = 4
|
||||
doppler_map_i = np.zeros((n_range, n_total), dtype=np.int64)
|
||||
@@ -835,7 +842,10 @@ def run_dc_notch(doppler_i, doppler_q, width=2):
|
||||
notched_i = doppler_i.copy()
|
||||
notched_q = doppler_q.copy()
|
||||
|
||||
print(f"[DC NOTCH] width={width}, {n_range} range bins x {n_doppler} Doppler bins (dual sub-frame)")
|
||||
print(
|
||||
f"[DC NOTCH] width={width}, {n_range} range bins x "
|
||||
f"{n_doppler} Doppler bins (dual sub-frame)"
|
||||
)
|
||||
|
||||
if width == 0:
|
||||
print(" Pass-through (width=0)")
|
||||
@@ -1167,7 +1177,12 @@ def main():
|
||||
parser = argparse.ArgumentParser(description="AERIS-10 FPGA golden reference model")
|
||||
parser.add_argument('--frame', type=int, default=0, help='Frame index to process')
|
||||
parser.add_argument('--plot', action='store_true', help='Show plots')
|
||||
parser.add_argument('--threshold', type=int, default=10000, help='Detection threshold (L1 magnitude)')
|
||||
parser.add_argument(
|
||||
'--threshold',
|
||||
type=int,
|
||||
default=10000,
|
||||
help='Detection threshold (L1 magnitude)'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Paths
|
||||
@@ -1175,7 +1190,11 @@ def main():
|
||||
fpga_dir = os.path.abspath(os.path.join(script_dir, '..', '..', '..'))
|
||||
data_base = os.path.expanduser("~/Downloads/adi_radar_data")
|
||||
amp_data = os.path.join(data_base, "amp_radar", "phaser_amp_4MSPS_500M_300u_256_m3dB.npy")
|
||||
amp_config = os.path.join(data_base, "amp_radar", "phaser_amp_4MSPS_500M_300u_256_m3dB_config.npy")
|
||||
amp_config = os.path.join(
|
||||
data_base,
|
||||
"amp_radar",
|
||||
"phaser_amp_4MSPS_500M_300u_256_m3dB_config.npy"
|
||||
)
|
||||
twiddle_1024 = os.path.join(fpga_dir, "fft_twiddle_1024.mem")
|
||||
output_dir = os.path.join(script_dir, "hex")
|
||||
|
||||
@@ -1290,7 +1309,10 @@ def main():
|
||||
q_val = int(fc_doppler_q[rbin, dbin]) & 0xFFFF
|
||||
packed = (q_val << 16) | i_val
|
||||
f.write(f"{packed:08X}\n")
|
||||
print(f" Wrote {fc_doppler_packed_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)")
|
||||
print(
|
||||
f" Wrote {fc_doppler_packed_file} ("
|
||||
f"{DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)"
|
||||
)
|
||||
|
||||
# Save numpy arrays for the full-chain path
|
||||
np.save(os.path.join(output_dir, "decimated_range_i.npy"), decim_i)
|
||||
@@ -1336,7 +1358,10 @@ def main():
|
||||
q_val = int(notched_q[rbin, dbin]) & 0xFFFF
|
||||
packed = (q_val << 16) | i_val
|
||||
f.write(f"{packed:08X}\n")
|
||||
print(f" Wrote {fc_notched_packed_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)")
|
||||
print(
|
||||
f" Wrote {fc_notched_packed_file} ("
|
||||
f"{DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)"
|
||||
)
|
||||
|
||||
# CFAR on DC-notched data
|
||||
CFAR_GUARD = 2
|
||||
@@ -1385,7 +1410,10 @@ def main():
|
||||
with open(cfar_det_list_file, 'w') as f:
|
||||
f.write("# AERIS-10 Full-Chain CFAR Detection List\n")
|
||||
f.write(f"# Chain: decim -> MTI -> Doppler -> DC notch(w={DC_NOTCH_WIDTH}) -> CA-CFAR\n")
|
||||
f.write(f"# CFAR: guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X}, mode={CFAR_MODE}\n")
|
||||
f.write(
|
||||
f"# CFAR: guard={CFAR_GUARD}, train={CFAR_TRAIN}, "
|
||||
f"alpha=0x{CFAR_ALPHA:02X}, mode={CFAR_MODE}\n"
|
||||
)
|
||||
f.write("# Format: range_bin doppler_bin magnitude threshold\n")
|
||||
for det in cfar_detections:
|
||||
r, d = det
|
||||
@@ -1481,12 +1509,18 @@ def main():
|
||||
print(f" Chirps processed: {DOPPLER_CHIRPS}")
|
||||
print(f" Samples/chirp: {FFT_SIZE}")
|
||||
print(f" Range FFT: {FFT_SIZE}-point → {snr_range:.1f} dB vs float")
|
||||
print(f" Doppler FFT (direct): {DOPPLER_FFT_SIZE}-point Hamming → {snr_doppler:.1f} dB vs float")
|
||||
print(
|
||||
f" Doppler FFT (direct): {DOPPLER_FFT_SIZE}-point Hamming "
|
||||
f"→ {snr_doppler:.1f} dB vs float"
|
||||
)
|
||||
print(f" Detections (direct): {len(detections)} (threshold={args.threshold})")
|
||||
print(" Full-chain decimator: 1024→64 peak detection")
|
||||
print(f" Full-chain detections: {len(fc_detections)} (threshold={args.threshold})")
|
||||
print(f" MTI+CFAR chain: decim → MTI → Doppler → DC notch(w={DC_NOTCH_WIDTH}) → CA-CFAR")
|
||||
print(f" CFAR detections: {len(cfar_detections)} (guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X})")
|
||||
print(
|
||||
f" CFAR detections: {len(cfar_detections)} "
|
||||
f"(guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X})"
|
||||
)
|
||||
print(f" Hex stimulus files: {output_dir}/")
|
||||
print(" Ready for RTL co-simulation with Icarus Verilog")
|
||||
|
||||
|
||||
@@ -199,7 +199,10 @@ def test_long_chirp():
|
||||
avg_mag = sum(magnitudes) / len(magnitudes)
|
||||
|
||||
print(f" Magnitude: min={min_mag:.1f}, max={max_mag:.1f}, avg={avg_mag:.1f}")
|
||||
print(f" Max magnitude as fraction of Q15 range: {max_mag/32767:.4f} ({max_mag/32767*100:.2f}%)")
|
||||
print(
|
||||
f" Max magnitude as fraction of Q15 range: "
|
||||
f"{max_mag/32767:.4f} ({max_mag/32767*100:.2f}%)"
|
||||
)
|
||||
|
||||
# Check if this looks like it came from generate_reference_chirp_q15
|
||||
# That function uses 32767 * 0.9 scaling => max magnitude ~29490
|
||||
@@ -262,7 +265,10 @@ def test_long_chirp():
|
||||
# Check if bandwidth roughly matches expected
|
||||
bw_match = abs(f_range - CHIRP_BW) / CHIRP_BW < 0.5 # within 50%
|
||||
if bw_match:
|
||||
print(f" Bandwidth {f_range/1e6:.2f} MHz roughly matches expected {CHIRP_BW/1e6:.2f} MHz")
|
||||
print(
|
||||
f" Bandwidth {f_range/1e6:.2f} MHz roughly matches expected "
|
||||
f"{CHIRP_BW/1e6:.2f} MHz"
|
||||
)
|
||||
else:
|
||||
warn(f"Bandwidth {f_range/1e6:.2f} MHz does NOT match expected {CHIRP_BW/1e6:.2f} MHz")
|
||||
|
||||
@@ -415,8 +421,11 @@ def test_chirp_vs_model():
|
||||
print(f" Max phase diff: {max_phase_diff:.4f} rad ({math.degrees(max_phase_diff):.2f} deg)")
|
||||
|
||||
phase_match = max_phase_diff < 0.5 # within 0.5 rad
|
||||
check(phase_match,
|
||||
f"Phase shape match: max diff = {math.degrees(max_phase_diff):.1f} deg (tolerance: 28.6 deg)")
|
||||
check(
|
||||
phase_match,
|
||||
f"Phase shape match: max diff = {math.degrees(max_phase_diff):.1f} deg "
|
||||
f"(tolerance: 28.6 deg)",
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -521,8 +530,11 @@ def test_memory_addressing():
|
||||
addr_from_concat = (seg << 10) | 0 # {seg[1:0], 10'b0}
|
||||
addr_end = (seg << 10) | 1023
|
||||
|
||||
check(addr_from_concat == base,
|
||||
f"Seg {seg} base address: {{{seg}[1:0], 10'b0}} = {addr_from_concat} (expected {base})")
|
||||
check(
|
||||
addr_from_concat == base,
|
||||
f"Seg {seg} base address: {{{seg}[1:0], 10'b0}} = {addr_from_concat} "
|
||||
f"(expected {base})",
|
||||
)
|
||||
check(addr_end == end,
|
||||
f"Seg {seg} end address: {{{seg}[1:0], 10'h3FF}} = {addr_end} (expected {end})")
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ from enum import Enum
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QTabWidget, QLabel, QPushButton, QComboBox, QSpinBox, QDoubleSpinBox,
|
||||
QGroupBox, QGridLayout, QSplitter, QFrame, QStatusBar, QCheckBox, QTableWidget, QTableWidgetItem,
|
||||
QGroupBox, QGridLayout, QSplitter, QFrame, QStatusBar, QCheckBox,
|
||||
QTableWidget, QTableWidgetItem,
|
||||
QHeaderView
|
||||
)
|
||||
from PyQt6.QtCore import (
|
||||
@@ -554,11 +555,20 @@ class RadarMapWidget(QWidget):
|
||||
if (!radarMarker) return;
|
||||
|
||||
var content = '<div class="popup-title">Radar System</div>' +
|
||||
'<div class="popup-row"><span class="popup-label">Latitude:</span><span class="popup-value">' +
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Latitude:</span>' +
|
||||
'<span class="popup-value">'
|
||||
) +
|
||||
radarMarker.getLatLng().lat.toFixed(6) + '</span></div>' +
|
||||
'<div class="popup-row"><span class="popup-label">Longitude:</span><span class="popup-value">' +
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Longitude:</span>' +
|
||||
'<span class="popup-value">'
|
||||
) +
|
||||
radarMarker.getLatLng().lng.toFixed(6) + '</span></div>' +
|
||||
'<div class="popup-row"><span class="popup-label">Status:</span><span class="popup-value status-approaching">Active</span></div>';
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Status:</span>' +
|
||||
'<span class="popup-value status-approaching">Active</span></div>'
|
||||
);
|
||||
|
||||
radarMarker.bindPopup(content);
|
||||
}}
|
||||
@@ -570,10 +580,22 @@ class RadarMapWidget(QWidget):
|
||||
var div = L.DomUtil.create('div', 'legend');
|
||||
div.innerHTML =
|
||||
'<div class="legend-title">Target Legend</div>' +
|
||||
'<div class="legend-item"><div class="legend-color" style="background:#F44336"></div>Approaching</div>' +
|
||||
'<div class="legend-item"><div class="legend-color" style="background:#2196F3"></div>Receding</div>' +
|
||||
'<div class="legend-item"><div class="legend-color" style="background:#9E9E9E"></div>Stationary</div>' +
|
||||
'<div class="legend-item"><div class="legend-color" style="background:#FF5252"></div>Radar</div>';
|
||||
(
|
||||
'<div class="legend-item"><div class="legend-color" ' +
|
||||
'style="background:#F44336"></div>Approaching</div>'
|
||||
) +
|
||||
(
|
||||
'<div class="legend-item"><div class="legend-color" ' +
|
||||
'style="background:#2196F3"></div>Receding</div>'
|
||||
) +
|
||||
(
|
||||
'<div class="legend-item"><div class="legend-color" ' +
|
||||
'style="background:#9E9E9E"></div>Stationary</div>'
|
||||
) +
|
||||
(
|
||||
'<div class="legend-item"><div class="legend-color" ' +
|
||||
'style="background:#FF5252"></div>Radar</div>'
|
||||
);
|
||||
return div;
|
||||
}};
|
||||
|
||||
@@ -590,7 +612,9 @@ class RadarMapWidget(QWidget):
|
||||
updateRadarPopup();
|
||||
|
||||
if (bridge) {{
|
||||
bridge.logFromJS('Radar position updated: ' + lat.toFixed(4) + ', ' + lon.toFixed(4));
|
||||
bridge.logFromJS(
|
||||
'Radar position updated: ' + lat.toFixed(4) + ', ' + lon.toFixed(4)
|
||||
);
|
||||
}}
|
||||
}}
|
||||
|
||||
@@ -717,19 +741,40 @@ class RadarMapWidget(QWidget):
|
||||
(target.velocity < -1 ? 'Receding' : 'Stationary');
|
||||
|
||||
var content = '<div class="popup-title">Target #' + target.id + '</div>' +
|
||||
'<div class="popup-row"><span class="popup-label">Range:</span><span class="popup-value">' +
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Range:</span>' +
|
||||
'<span class="popup-value">'
|
||||
) +
|
||||
target.range.toFixed(1) + ' m</span></div>' +
|
||||
'<div class="popup-row"><span class="popup-label">Velocity:</span><span class="popup-value">' +
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Velocity:</span>' +
|
||||
'<span class="popup-value">'
|
||||
) +
|
||||
target.velocity.toFixed(1) + ' m/s</span></div>' +
|
||||
'<div class="popup-row"><span class="popup-label">Azimuth:</span><span class="popup-value">' +
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Azimuth:</span>' +
|
||||
'<span class="popup-value">'
|
||||
) +
|
||||
target.azimuth.toFixed(1) + '°</span></div>' +
|
||||
'<div class="popup-row"><span class="popup-label">Elevation:</span><span class="popup-value">' +
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Elevation:</span>' +
|
||||
'<span class="popup-value">'
|
||||
) +
|
||||
target.elevation.toFixed(1) + '°</span></div>' +
|
||||
'<div class="popup-row"><span class="popup-label">SNR:</span><span class="popup-value">' +
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">SNR:</span>' +
|
||||
'<span class="popup-value">'
|
||||
) +
|
||||
target.snr.toFixed(1) + ' dB</span></div>' +
|
||||
'<div class="popup-row"><span class="popup-label">Track ID:</span><span class="popup-value">' +
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Track ID:</span>' +
|
||||
'<span class="popup-value">'
|
||||
) +
|
||||
target.track_id + '</span></div>' +
|
||||
'<div class="popup-row"><span class="popup-label">Status:</span><span class="popup-value ' +
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Status:</span>' +
|
||||
'<span class="popup-value '
|
||||
) +
|
||||
statusClass + '">' + statusText + '</span></div>';
|
||||
|
||||
targetMarkers[target.id].bindPopup(content);
|
||||
@@ -770,7 +815,11 @@ class RadarMapWidget(QWidget):
|
||||
if (visible) {{
|
||||
// Create trails for all existing markers using stored history
|
||||
for (var id in targetMarkers) {{
|
||||
if (!targetTrails[id] && targetTrailHistory[id] && targetTrailHistory[id].length > 1) {{
|
||||
if (
|
||||
!targetTrails[id] &&
|
||||
targetTrailHistory[id] &&
|
||||
targetTrailHistory[id].length > 1
|
||||
) {{
|
||||
// Get color from current marker position (approximate)
|
||||
var color = '#4CAF50'; // Default green
|
||||
targetTrails[id] = L.polyline(targetTrailHistory[id], {{
|
||||
|
||||
@@ -1,37 +1,52 @@
|
||||
|
||||
def update_gps_display(self):
|
||||
"""Step 18: Update GPS display and center map"""
|
||||
try:
|
||||
while not self.gps_data_queue.empty():
|
||||
gps_data = self.gps_data_queue.get_nowait()
|
||||
self.current_gps = gps_data
|
||||
|
||||
# Update GPS label
|
||||
self.gps_label.config(
|
||||
text=f"GPS: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m")
|
||||
|
||||
# Update map
|
||||
self.update_map_display(gps_data)
|
||||
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
def update_map_display(self, gps_data):
|
||||
"""Step 18: Update map display with current GPS position"""
|
||||
try:
|
||||
self.map_label.config(text=f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n"
|
||||
f"Altitude: {gps_data.altitude:.1f}m\n"
|
||||
f"Coverage: 50km radius\n"
|
||||
f"Map centered on GPS coordinates")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error updating map display: {e}")
|
||||
import logging
|
||||
import queue
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
|
||||
|
||||
class RadarGUI:
|
||||
def update_gps_display(self):
|
||||
"""Step 18: Update GPS display and center map"""
|
||||
try:
|
||||
while not self.gps_data_queue.empty():
|
||||
gps_data = self.gps_data_queue.get_nowait()
|
||||
self.current_gps = gps_data
|
||||
|
||||
# Update GPS label
|
||||
self.gps_label.config(
|
||||
text=(
|
||||
f"GPS: Lat {gps_data.latitude:.6f}, "
|
||||
f"Lon {gps_data.longitude:.6f}, "
|
||||
f"Alt {gps_data.altitude:.1f}m"
|
||||
)
|
||||
)
|
||||
|
||||
# Update map
|
||||
self.update_map_display(gps_data)
|
||||
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
def update_map_display(self, gps_data):
|
||||
"""Step 18: Update map display with current GPS position"""
|
||||
try:
|
||||
self.map_label.config(
|
||||
text=(
|
||||
f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n"
|
||||
f"Altitude: {gps_data.altitude:.1f}m\n"
|
||||
f"Coverage: 50km radius\n"
|
||||
f"Map centered on GPS coordinates"
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error updating map display: {e}")
|
||||
|
||||
def main():
|
||||
def main():
|
||||
"""Main application entry point"""
|
||||
try:
|
||||
root = tk.Tk()
|
||||
app = RadarGUI(root)
|
||||
_app = RadarGUI(root)
|
||||
root.mainloop()
|
||||
except Exception as e:
|
||||
logging.error(f"Application error: {e}")
|
||||
|
||||
+439
-374
File diff suppressed because it is too large
Load Diff
+490
-411
File diff suppressed because it is too large
Load Diff
+546
-460
File diff suppressed because it is too large
Load Diff
+312
-275
@@ -2,13 +2,9 @@ import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||
from matplotlib.figure import Figure
|
||||
import matplotlib.patches as patches
|
||||
from scipy import signal
|
||||
from scipy.fft import fft, fftshift
|
||||
from scipy.signal import butter, filtfilt
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict, Tuple
|
||||
@@ -17,7 +13,8 @@ import queue
|
||||
import time
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||||
|
||||
|
||||
@dataclass
|
||||
class RadarTarget:
|
||||
@@ -29,12 +26,13 @@ class RadarTarget:
|
||||
chirp_type: str
|
||||
timestamp: float
|
||||
|
||||
|
||||
class SignalProcessor:
|
||||
def __init__(self):
|
||||
self.range_resolution = 1.0 # meters
|
||||
self.velocity_resolution = 0.1 # m/s
|
||||
self.cfar_threshold = 15.0 # dB
|
||||
|
||||
|
||||
def doppler_fft(self, iq_data: np.ndarray, fs: float = 100e6) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Perform Doppler FFT on IQ data
|
||||
@@ -42,101 +40,111 @@ class SignalProcessor:
|
||||
"""
|
||||
# Window function for FFT
|
||||
window = np.hanning(len(iq_data))
|
||||
windowed_data = (iq_data['I_value'].values + 1j * iq_data['Q_value'].values) * window
|
||||
|
||||
windowed_data = (iq_data["I_value"].values + 1j * iq_data["Q_value"].values) * window
|
||||
|
||||
# Perform FFT
|
||||
doppler_fft = fft(windowed_data)
|
||||
doppler_fft = fftshift(doppler_fft)
|
||||
|
||||
|
||||
# Frequency axis
|
||||
N = len(iq_data)
|
||||
freq_axis = np.linspace(-fs/2, fs/2, N)
|
||||
|
||||
freq_axis = np.linspace(-fs / 2, fs / 2, N)
|
||||
|
||||
# Convert to velocity (assuming radar frequency = 10 GHz)
|
||||
radar_freq = 10e9
|
||||
wavelength = 3e8 / radar_freq
|
||||
velocity_axis = freq_axis * wavelength / 2
|
||||
|
||||
|
||||
return velocity_axis, np.abs(doppler_fft)
|
||||
|
||||
def mti_filter(self, iq_data: np.ndarray, filter_type: str = 'single_canceler') -> np.ndarray:
|
||||
|
||||
def mti_filter(self, iq_data: np.ndarray, filter_type: str = "single_canceler") -> np.ndarray:
|
||||
"""
|
||||
Moving Target Indicator filter
|
||||
Removes stationary clutter with better shape handling
|
||||
"""
|
||||
if iq_data is None or len(iq_data) < 2:
|
||||
return np.array([], dtype=complex)
|
||||
|
||||
|
||||
try:
|
||||
# Ensure we're working with complex data
|
||||
complex_data = iq_data.astype(complex)
|
||||
|
||||
if filter_type == 'single_canceler':
|
||||
|
||||
if filter_type == "single_canceler":
|
||||
# Single delay line canceler
|
||||
if len(complex_data) < 2:
|
||||
return np.array([], dtype=complex)
|
||||
filtered = np.zeros(len(complex_data) - 1, dtype=complex)
|
||||
for i in range(1, len(complex_data)):
|
||||
filtered[i-1] = complex_data[i] - complex_data[i-1]
|
||||
filtered[i - 1] = complex_data[i] - complex_data[i - 1]
|
||||
return filtered
|
||||
|
||||
elif filter_type == 'double_canceler':
|
||||
|
||||
elif filter_type == "double_canceler":
|
||||
# Double delay line canceler
|
||||
if len(complex_data) < 3:
|
||||
return np.array([], dtype=complex)
|
||||
filtered = np.zeros(len(complex_data) - 2, dtype=complex)
|
||||
for i in range(2, len(complex_data)):
|
||||
filtered[i-2] = complex_data[i] - 2*complex_data[i-1] + complex_data[i-2]
|
||||
filtered[i - 2] = (
|
||||
complex_data[i] - 2 * complex_data[i - 1] + complex_data[i - 2]
|
||||
)
|
||||
return filtered
|
||||
|
||||
|
||||
else:
|
||||
return complex_data
|
||||
except Exception as e:
|
||||
logging.error(f"MTI filter error: {e}")
|
||||
return np.array([], dtype=complex)
|
||||
|
||||
|
||||
def cfar_detection(self, range_profile: np.ndarray, guard_cells: int = 2,
|
||||
training_cells: int = 10, threshold_factor: float = 3.0) -> List[Tuple[int, float]]:
|
||||
def cfar_detection(
|
||||
self,
|
||||
range_profile: np.ndarray,
|
||||
guard_cells: int = 2,
|
||||
training_cells: int = 10,
|
||||
threshold_factor: float = 3.0,
|
||||
) -> List[Tuple[int, float]]:
|
||||
detections = []
|
||||
N = len(range_profile)
|
||||
|
||||
|
||||
# Ensure guard_cells and training_cells are integers
|
||||
guard_cells = int(guard_cells)
|
||||
training_cells = int(training_cells)
|
||||
|
||||
|
||||
for i in range(N):
|
||||
# Convert to integer indices
|
||||
i_int = int(i)
|
||||
if i_int < guard_cells + training_cells or i_int >= N - guard_cells - training_cells:
|
||||
continue
|
||||
|
||||
|
||||
# Leading window - ensure integer indices
|
||||
lead_start = i_int - guard_cells - training_cells
|
||||
lead_end = i_int - guard_cells
|
||||
lead_cells = range_profile[lead_start:lead_end]
|
||||
|
||||
|
||||
# Lagging window - ensure integer indices
|
||||
lag_start = i_int + guard_cells + 1
|
||||
lag_end = i_int + guard_cells + training_cells + 1
|
||||
lag_cells = range_profile[lag_start:lag_end]
|
||||
|
||||
|
||||
# Combine training cells
|
||||
training_cells_combined = np.concatenate([lead_cells, lag_cells])
|
||||
|
||||
|
||||
# Calculate noise floor (mean of training cells)
|
||||
if len(training_cells_combined) > 0:
|
||||
noise_floor = np.mean(training_cells_combined)
|
||||
|
||||
|
||||
# Apply threshold
|
||||
threshold = noise_floor * threshold_factor
|
||||
|
||||
|
||||
if range_profile[i_int] > threshold:
|
||||
detections.append((i_int, float(range_profile[i_int]))) # Ensure float magnitude
|
||||
|
||||
detections.append(
|
||||
(i_int, float(range_profile[i_int]))
|
||||
) # Ensure float magnitude
|
||||
|
||||
return detections
|
||||
|
||||
def range_fft(self, iq_data: np.ndarray, fs: float = 100e6, bw: float = 20e6) -> Tuple[np.ndarray, np.ndarray]:
|
||||
|
||||
def range_fft(
|
||||
self, iq_data: np.ndarray, fs: float = 100e6, bw: float = 20e6
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Perform range FFT on IQ data
|
||||
Returns range profile
|
||||
@@ -144,54 +152,55 @@ class SignalProcessor:
|
||||
# Window function
|
||||
window = np.hanning(len(iq_data))
|
||||
windowed_data = np.abs(iq_data) * window
|
||||
|
||||
|
||||
# Perform FFT
|
||||
range_fft = fft(windowed_data)
|
||||
|
||||
|
||||
# Range calculation
|
||||
N = len(iq_data)
|
||||
range_max = (3e8 * N) / (2 * bw)
|
||||
range_axis = np.linspace(0, range_max, N)
|
||||
|
||||
|
||||
return range_axis, np.abs(range_fft)
|
||||
|
||||
def process_chirp_sequence(self, df: pd.DataFrame, chirp_type: str = 'LONG') -> Dict:
|
||||
|
||||
def process_chirp_sequence(self, df: pd.DataFrame, chirp_type: str = "LONG") -> Dict:
|
||||
try:
|
||||
# Filter data by chirp type
|
||||
chirp_data = df[df['chirp_type'] == chirp_type]
|
||||
|
||||
chirp_data = df[df["chirp_type"] == chirp_type]
|
||||
|
||||
if len(chirp_data) == 0:
|
||||
return {}
|
||||
|
||||
|
||||
# Group by chirp number
|
||||
chirp_numbers = chirp_data['chirp_number'].unique()
|
||||
chirp_numbers = chirp_data["chirp_number"].unique()
|
||||
num_chirps = len(chirp_numbers)
|
||||
|
||||
|
||||
if num_chirps == 0:
|
||||
return {}
|
||||
|
||||
|
||||
# Get samples per chirp and ensure consistency
|
||||
samples_per_chirp_list = [len(chirp_data[chirp_data['chirp_number'] == num])
|
||||
for num in chirp_numbers]
|
||||
|
||||
samples_per_chirp_list = [
|
||||
len(chirp_data[chirp_data["chirp_number"] == num]) for num in chirp_numbers
|
||||
]
|
||||
|
||||
# Use minimum samples to ensure consistent shape
|
||||
samples_per_chirp = min(samples_per_chirp_list)
|
||||
|
||||
|
||||
# Create range-Doppler matrix with consistent shape
|
||||
range_doppler_matrix = np.zeros((samples_per_chirp, num_chirps), dtype=complex)
|
||||
|
||||
|
||||
for i, chirp_num in enumerate(chirp_numbers):
|
||||
chirp_samples = chirp_data[chirp_data['chirp_number'] == chirp_num]
|
||||
chirp_samples = chirp_data[chirp_data["chirp_number"] == chirp_num]
|
||||
# Take only the first samples_per_chirp samples to ensure consistent shape
|
||||
chirp_samples = chirp_samples.head(samples_per_chirp)
|
||||
|
||||
|
||||
# Create complex IQ data
|
||||
iq_data = chirp_samples['I_value'].values + 1j * chirp_samples['Q_value'].values
|
||||
|
||||
iq_data = chirp_samples["I_value"].values + 1j * chirp_samples["Q_value"].values
|
||||
|
||||
# Ensure the shape matches
|
||||
if len(iq_data) == samples_per_chirp:
|
||||
range_doppler_matrix[:, i] = iq_data
|
||||
|
||||
|
||||
# Apply MTI filter along slow-time (chirp-to-chirp)
|
||||
mti_filtered = np.zeros_like(range_doppler_matrix)
|
||||
for i in range(samples_per_chirp):
|
||||
@@ -204,178 +213,184 @@ class SignalProcessor:
|
||||
# Handle shape mismatch by padding or truncating
|
||||
if len(filtered) < num_chirps:
|
||||
padded = np.zeros(num_chirps, dtype=complex)
|
||||
padded[:len(filtered)] = filtered
|
||||
padded[: len(filtered)] = filtered
|
||||
mti_filtered[i, :] = padded
|
||||
else:
|
||||
mti_filtered[i, :] = filtered[:num_chirps]
|
||||
|
||||
|
||||
# Perform Doppler FFT along slow-time dimension
|
||||
doppler_fft_result = np.zeros((samples_per_chirp, num_chirps), dtype=complex)
|
||||
for i in range(samples_per_chirp):
|
||||
doppler_fft_result[i, :] = fft(mti_filtered[i, :])
|
||||
|
||||
|
||||
return {
|
||||
'range_doppler_matrix': np.abs(doppler_fft_result),
|
||||
'chirp_type': chirp_type,
|
||||
'num_chirps': num_chirps,
|
||||
'samples_per_chirp': samples_per_chirp
|
||||
"range_doppler_matrix": np.abs(doppler_fft_result),
|
||||
"chirp_type": chirp_type,
|
||||
"num_chirps": num_chirps,
|
||||
"samples_per_chirp": samples_per_chirp,
|
||||
}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in process_chirp_sequence: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
class RadarGUI:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("Radar Signal Processor - CSV Analysis")
|
||||
self.root.geometry("1400x900")
|
||||
|
||||
|
||||
# Initialize processor
|
||||
self.processor = SignalProcessor()
|
||||
|
||||
|
||||
# Data storage
|
||||
self.df = None
|
||||
self.processed_data = {}
|
||||
self.detected_targets = []
|
||||
|
||||
|
||||
# Create GUI
|
||||
self.create_gui()
|
||||
|
||||
|
||||
# Start background processing
|
||||
self.processing_queue = queue.Queue()
|
||||
self.processing_thread = threading.Thread(target=self.background_processing, daemon=True)
|
||||
self.processing_thread.start()
|
||||
|
||||
|
||||
# Update GUI periodically
|
||||
self.root.after(100, self.update_gui)
|
||||
|
||||
|
||||
def create_gui(self):
|
||||
"""Create the main GUI layout"""
|
||||
# Main frame
|
||||
main_frame = ttk.Frame(self.root)
|
||||
main_frame.pack(fill='both', expand=True, padx=10, pady=10)
|
||||
|
||||
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
# Control panel
|
||||
control_frame = ttk.LabelFrame(main_frame, text="File Controls")
|
||||
control_frame.pack(fill='x', pady=5)
|
||||
|
||||
control_frame.pack(fill="x", pady=5)
|
||||
|
||||
# File selection
|
||||
ttk.Button(control_frame, text="Load CSV File",
|
||||
command=self.load_csv_file).pack(side='left', padx=5, pady=5)
|
||||
|
||||
ttk.Button(control_frame, text="Load CSV File", command=self.load_csv_file).pack(
|
||||
side="left", padx=5, pady=5
|
||||
)
|
||||
|
||||
self.file_label = ttk.Label(control_frame, text="No file loaded")
|
||||
self.file_label.pack(side='left', padx=10, pady=5)
|
||||
|
||||
self.file_label.pack(side="left", padx=10, pady=5)
|
||||
|
||||
# Processing controls
|
||||
ttk.Button(control_frame, text="Process Data",
|
||||
command=self.process_data).pack(side='left', padx=5, pady=5)
|
||||
|
||||
ttk.Button(control_frame, text="Run CFAR Detection",
|
||||
command=self.run_cfar_detection).pack(side='left', padx=5, pady=5)
|
||||
|
||||
ttk.Button(control_frame, text="Process Data", command=self.process_data).pack(
|
||||
side="left", padx=5, pady=5
|
||||
)
|
||||
|
||||
ttk.Button(control_frame, text="Run CFAR Detection", command=self.run_cfar_detection).pack(
|
||||
side="left", padx=5, pady=5
|
||||
)
|
||||
|
||||
# Status
|
||||
self.status_label = ttk.Label(control_frame, text="Status: Ready")
|
||||
self.status_label.pack(side='right', padx=10, pady=5)
|
||||
|
||||
self.status_label.pack(side="right", padx=10, pady=5)
|
||||
|
||||
# Display area
|
||||
display_frame = ttk.Frame(main_frame)
|
||||
display_frame.pack(fill='both', expand=True, pady=5)
|
||||
|
||||
display_frame.pack(fill="both", expand=True, pady=5)
|
||||
|
||||
# Create matplotlib figures
|
||||
self.create_plots(display_frame)
|
||||
|
||||
|
||||
# Targets list
|
||||
targets_frame = ttk.LabelFrame(main_frame, text="Detected Targets")
|
||||
targets_frame.pack(fill='x', pady=5)
|
||||
|
||||
self.targets_tree = ttk.Treeview(targets_frame,
|
||||
columns=('Range', 'Velocity', 'Azimuth', 'Elevation', 'SNR', 'Chirp Type'),
|
||||
show='headings', height=8)
|
||||
|
||||
self.targets_tree.heading('Range', text='Range (m)')
|
||||
self.targets_tree.heading('Velocity', text='Velocity (m/s)')
|
||||
self.targets_tree.heading('Azimuth', text='Azimuth (°)')
|
||||
self.targets_tree.heading('Elevation', text='Elevation (°)')
|
||||
self.targets_tree.heading('SNR', text='SNR (dB)')
|
||||
self.targets_tree.heading('Chirp Type', text='Chirp Type')
|
||||
|
||||
self.targets_tree.column('Range', width=100)
|
||||
self.targets_tree.column('Velocity', width=100)
|
||||
self.targets_tree.column('Azimuth', width=80)
|
||||
self.targets_tree.column('Elevation', width=80)
|
||||
self.targets_tree.column('SNR', width=80)
|
||||
self.targets_tree.column('Chirp Type', width=100)
|
||||
|
||||
self.targets_tree.pack(fill='x', padx=5, pady=5)
|
||||
|
||||
targets_frame.pack(fill="x", pady=5)
|
||||
|
||||
self.targets_tree = ttk.Treeview(
|
||||
targets_frame,
|
||||
columns=("Range", "Velocity", "Azimuth", "Elevation", "SNR", "Chirp Type"),
|
||||
show="headings",
|
||||
height=8,
|
||||
)
|
||||
|
||||
self.targets_tree.heading("Range", text="Range (m)")
|
||||
self.targets_tree.heading("Velocity", text="Velocity (m/s)")
|
||||
self.targets_tree.heading("Azimuth", text="Azimuth (°)")
|
||||
self.targets_tree.heading("Elevation", text="Elevation (°)")
|
||||
self.targets_tree.heading("SNR", text="SNR (dB)")
|
||||
self.targets_tree.heading("Chirp Type", text="Chirp Type")
|
||||
|
||||
self.targets_tree.column("Range", width=100)
|
||||
self.targets_tree.column("Velocity", width=100)
|
||||
self.targets_tree.column("Azimuth", width=80)
|
||||
self.targets_tree.column("Elevation", width=80)
|
||||
self.targets_tree.column("SNR", width=80)
|
||||
self.targets_tree.column("Chirp Type", width=100)
|
||||
|
||||
self.targets_tree.pack(fill="x", padx=5, pady=5)
|
||||
|
||||
def create_plots(self, parent):
|
||||
"""Create matplotlib plots"""
|
||||
# Create figure with subplots
|
||||
self.fig = Figure(figsize=(12, 8))
|
||||
self.canvas = FigureCanvasTkAgg(self.fig, parent)
|
||||
self.canvas.get_tk_widget().pack(fill='both', expand=True)
|
||||
|
||||
self.canvas.get_tk_widget().pack(fill="both", expand=True)
|
||||
|
||||
# Create subplots
|
||||
self.ax1 = self.fig.add_subplot(221) # Range profile
|
||||
self.ax2 = self.fig.add_subplot(222) # Doppler spectrum
|
||||
self.ax3 = self.fig.add_subplot(223) # Range-Doppler map
|
||||
self.ax4 = self.fig.add_subplot(224) # MTI filtered data
|
||||
|
||||
|
||||
# Set titles
|
||||
self.ax1.set_title('Range Profile')
|
||||
self.ax1.set_xlabel('Range (m)')
|
||||
self.ax1.set_ylabel('Magnitude')
|
||||
self.ax1.set_title("Range Profile")
|
||||
self.ax1.set_xlabel("Range (m)")
|
||||
self.ax1.set_ylabel("Magnitude")
|
||||
self.ax1.grid(True)
|
||||
|
||||
self.ax2.set_title('Doppler Spectrum')
|
||||
self.ax2.set_xlabel('Velocity (m/s)')
|
||||
self.ax2.set_ylabel('Magnitude')
|
||||
|
||||
self.ax2.set_title("Doppler Spectrum")
|
||||
self.ax2.set_xlabel("Velocity (m/s)")
|
||||
self.ax2.set_ylabel("Magnitude")
|
||||
self.ax2.grid(True)
|
||||
|
||||
self.ax3.set_title('Range-Doppler Map')
|
||||
self.ax3.set_xlabel('Doppler Bin')
|
||||
self.ax3.set_ylabel('Range Bin')
|
||||
|
||||
self.ax4.set_title('MTI Filtered Data')
|
||||
self.ax4.set_xlabel('Sample')
|
||||
self.ax4.set_ylabel('Magnitude')
|
||||
|
||||
self.ax3.set_title("Range-Doppler Map")
|
||||
self.ax3.set_xlabel("Doppler Bin")
|
||||
self.ax3.set_ylabel("Range Bin")
|
||||
|
||||
self.ax4.set_title("MTI Filtered Data")
|
||||
self.ax4.set_xlabel("Sample")
|
||||
self.ax4.set_ylabel("Magnitude")
|
||||
self.ax4.grid(True)
|
||||
|
||||
|
||||
self.fig.tight_layout()
|
||||
|
||||
|
||||
def load_csv_file(self):
|
||||
"""Load CSV file generated by testbench"""
|
||||
filename = filedialog.askopenfilename(
|
||||
title="Select CSV file",
|
||||
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")]
|
||||
title="Select CSV file", filetypes=[("CSV files", "*.csv"), ("All files", "*.*")]
|
||||
)
|
||||
|
||||
# Add magnitude and phase calculations after loading CSV
|
||||
|
||||
# Add magnitude and phase calculations after loading CSV
|
||||
if self.df is not None:
|
||||
# Calculate magnitude from I/Q values
|
||||
self.df['magnitude'] = np.sqrt(self.df['I_value']**2 + self.df['Q_value']**2)
|
||||
|
||||
# Calculate phase from I/Q values
|
||||
self.df['phase_rad'] = np.arctan2(self.df['Q_value'], self.df['I_value'])
|
||||
|
||||
self.df["magnitude"] = np.sqrt(self.df["I_value"] ** 2 + self.df["Q_value"] ** 2)
|
||||
|
||||
# Calculate phase from I/Q values
|
||||
self.df["phase_rad"] = np.arctan2(self.df["Q_value"], self.df["I_value"])
|
||||
|
||||
# If you used magnitude_squared in CSV, calculate actual magnitude
|
||||
if 'magnitude_squared' in self.df.columns:
|
||||
self.df['magnitude'] = np.sqrt(self.df['magnitude_squared'])
|
||||
if "magnitude_squared" in self.df.columns:
|
||||
self.df["magnitude"] = np.sqrt(self.df["magnitude_squared"])
|
||||
if filename:
|
||||
try:
|
||||
self.status_label.config(text="Status: Loading CSV file...")
|
||||
self.df = pd.read_csv(filename)
|
||||
self.file_label.config(text=f"Loaded: {filename.split('/')[-1]}")
|
||||
self.status_label.config(text=f"Status: Loaded {len(self.df)} samples")
|
||||
|
||||
|
||||
# Show basic info
|
||||
self.show_file_info()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to load CSV file: {e}")
|
||||
self.status_label.config(text="Status: Error loading file")
|
||||
|
||||
|
||||
def show_file_info(self):
|
||||
"""Display basic information about loaded data"""
|
||||
if self.df is not None:
|
||||
@@ -383,296 +398,318 @@ class RadarGUI:
|
||||
info_text += f"Chirps: {self.df['chirp_number'].nunique()} | "
|
||||
info_text += f"Long: {len(self.df[self.df['chirp_type'] == 'LONG'])} | "
|
||||
info_text += f"Short: {len(self.df[self.df['chirp_type'] == 'SHORT'])}"
|
||||
|
||||
|
||||
self.file_label.config(text=info_text)
|
||||
|
||||
|
||||
def process_data(self):
|
||||
"""Process loaded CSV data"""
|
||||
if self.df is None:
|
||||
messagebox.showwarning("Warning", "Please load a CSV file first")
|
||||
return
|
||||
|
||||
|
||||
self.status_label.config(text="Status: Processing data...")
|
||||
|
||||
|
||||
# Add to processing queue
|
||||
self.processing_queue.put(('process', self.df))
|
||||
|
||||
self.processing_queue.put(("process", self.df))
|
||||
|
||||
def run_cfar_detection(self):
|
||||
"""Run CFAR detection on processed data"""
|
||||
if self.df is None:
|
||||
messagebox.showwarning("Warning", "Please load and process data first")
|
||||
return
|
||||
|
||||
|
||||
self.status_label.config(text="Status: Running CFAR detection...")
|
||||
self.processing_queue.put(('cfar', self.df))
|
||||
|
||||
self.processing_queue.put(("cfar", self.df))
|
||||
|
||||
def background_processing(self):
|
||||
|
||||
while True:
|
||||
try:
|
||||
task_type, data = self.processing_queue.get(timeout=1.0)
|
||||
|
||||
if task_type == 'process':
|
||||
|
||||
if task_type == "process":
|
||||
self._process_data_background(data)
|
||||
elif task_type == 'cfar':
|
||||
elif task_type == "cfar":
|
||||
self._run_cfar_background(data)
|
||||
else:
|
||||
logging.warning(f"Unknown task type: {task_type}")
|
||||
|
||||
|
||||
self.processing_queue.task_done()
|
||||
|
||||
|
||||
except queue.Empty:
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.error(f"Background processing error: {e}")
|
||||
# Update GUI to show error state
|
||||
self.root.after(0, lambda: self.status_label.config(
|
||||
text=f"Status: Processing error - {e}"
|
||||
))
|
||||
|
||||
self.root.after(
|
||||
0,
|
||||
lambda: self.status_label.config(
|
||||
text=f"Status: Processing error - {e}" # noqa: F821
|
||||
),
|
||||
)
|
||||
|
||||
def _process_data_background(self, df):
|
||||
try:
|
||||
# Process long chirps
|
||||
long_chirp_data = self.processor.process_chirp_sequence(df, 'LONG')
|
||||
|
||||
long_chirp_data = self.processor.process_chirp_sequence(df, "LONG")
|
||||
|
||||
# Process short chirps
|
||||
short_chirp_data = self.processor.process_chirp_sequence(df, 'SHORT')
|
||||
|
||||
short_chirp_data = self.processor.process_chirp_sequence(df, "SHORT")
|
||||
|
||||
# Store results
|
||||
self.processed_data = {
|
||||
'long': long_chirp_data,
|
||||
'short': short_chirp_data
|
||||
}
|
||||
|
||||
self.processed_data = {"long": long_chirp_data, "short": short_chirp_data}
|
||||
|
||||
# Update GUI in main thread
|
||||
self.root.after(0, self._update_plots_after_processing)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Processing error: {e}")
|
||||
error_msg = str(e)
|
||||
self.root.after(0, lambda msg=error_msg: self.status_label.config(
|
||||
text=f"Status: Processing error - {msg}"
|
||||
))
|
||||
|
||||
self.root.after(
|
||||
0,
|
||||
lambda msg=error_msg: self.status_label.config(
|
||||
text=f"Status: Processing error - {msg}"
|
||||
),
|
||||
)
|
||||
|
||||
def _run_cfar_background(self, df):
|
||||
try:
|
||||
# Get first chirp for CFAR demonstration
|
||||
first_chirp = df[df['chirp_number'] == df['chirp_number'].min()]
|
||||
|
||||
first_chirp = df[df["chirp_number"] == df["chirp_number"].min()]
|
||||
|
||||
if len(first_chirp) == 0:
|
||||
return
|
||||
|
||||
|
||||
# Create IQ data - FIXED TYPO: first_chirp not first_chip
|
||||
iq_data = first_chirp['I_value'].values + 1j * first_chirp['Q_value'].values
|
||||
|
||||
iq_data = first_chirp["I_value"].values + 1j * first_chirp["Q_value"].values
|
||||
|
||||
# Perform range FFT
|
||||
range_axis, range_profile = self.processor.range_fft(iq_data)
|
||||
|
||||
|
||||
# Run CFAR detection
|
||||
detections = self.processor.cfar_detection(range_profile)
|
||||
|
||||
|
||||
# Convert to target objects
|
||||
self.detected_targets = []
|
||||
for range_bin, magnitude in detections:
|
||||
target = RadarTarget(
|
||||
range=range_axis[range_bin],
|
||||
velocity=0, # Would need Doppler processing for velocity
|
||||
azimuth=0, # From actual data
|
||||
elevation=0, # From actual data
|
||||
azimuth=0, # From actual data
|
||||
elevation=0, # From actual data
|
||||
snr=20 * np.log10(magnitude + 1e-9), # Convert to dB
|
||||
chirp_type='LONG',
|
||||
timestamp=time.time()
|
||||
chirp_type="LONG",
|
||||
timestamp=time.time(),
|
||||
)
|
||||
self.detected_targets.append(target)
|
||||
|
||||
|
||||
# Update GUI in main thread
|
||||
self.root.after(0, lambda: self._update_cfar_results(range_axis, range_profile, detections))
|
||||
|
||||
self.root.after(
|
||||
0, lambda: self._update_cfar_results(range_axis, range_profile, detections)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"CFAR detection error: {e}")
|
||||
error_msg = str(e)
|
||||
self.root.after(0, lambda msg=error_msg: self.status_label.config(
|
||||
text=f"Status: CFAR error - {msg}"
|
||||
))
|
||||
|
||||
self.root.after(
|
||||
0,
|
||||
lambda msg=error_msg: self.status_label.config(text=f"Status: CFAR error - {msg}"),
|
||||
)
|
||||
|
||||
def _update_plots_after_processing(self):
|
||||
try:
|
||||
# Clear all plots
|
||||
for ax in [self.ax1, self.ax2, self.ax3, self.ax4]:
|
||||
ax.clear()
|
||||
|
||||
|
||||
# Plot 1: Range profile from first chirp
|
||||
if self.df is not None and len(self.df) > 0:
|
||||
try:
|
||||
first_chirp_num = self.df['chirp_number'].min()
|
||||
first_chirp = self.df[self.df['chirp_number'] == first_chirp_num]
|
||||
|
||||
first_chirp_num = self.df["chirp_number"].min()
|
||||
first_chirp = self.df[self.df["chirp_number"] == first_chirp_num]
|
||||
|
||||
if len(first_chirp) > 0:
|
||||
iq_data = first_chirp['I_value'].values + 1j * first_chirp['Q_value'].values
|
||||
iq_data = first_chirp["I_value"].values + 1j * first_chirp["Q_value"].values
|
||||
range_axis, range_profile = self.processor.range_fft(iq_data)
|
||||
|
||||
|
||||
if len(range_axis) > 0 and len(range_profile) > 0:
|
||||
self.ax1.plot(range_axis, range_profile, 'b-')
|
||||
self.ax1.set_title('Range Profile - First Chirp')
|
||||
self.ax1.set_xlabel('Range (m)')
|
||||
self.ax1.set_ylabel('Magnitude')
|
||||
self.ax1.plot(range_axis, range_profile, "b-")
|
||||
self.ax1.set_title("Range Profile - First Chirp")
|
||||
self.ax1.set_xlabel("Range (m)")
|
||||
self.ax1.set_ylabel("Magnitude")
|
||||
self.ax1.grid(True)
|
||||
except Exception as e:
|
||||
logging.warning(f"Range profile plot error: {e}")
|
||||
self.ax1.set_title('Range Profile - Error')
|
||||
|
||||
self.ax1.set_title("Range Profile - Error")
|
||||
|
||||
# Plot 2: Doppler spectrum
|
||||
if self.df is not None and len(self.df) > 0:
|
||||
try:
|
||||
sample_data = self.df.head(1024)
|
||||
if len(sample_data) > 10:
|
||||
iq_data = sample_data['I_value'].values + 1j * sample_data['Q_value'].values
|
||||
iq_data = sample_data["I_value"].values + 1j * sample_data["Q_value"].values
|
||||
velocity_axis, doppler_spectrum = self.processor.doppler_fft(iq_data)
|
||||
|
||||
|
||||
if len(velocity_axis) > 0 and len(doppler_spectrum) > 0:
|
||||
self.ax2.plot(velocity_axis, doppler_spectrum, 'g-')
|
||||
self.ax2.set_title('Doppler Spectrum')
|
||||
self.ax2.set_xlabel('Velocity (m/s)')
|
||||
self.ax2.set_ylabel('Magnitude')
|
||||
self.ax2.plot(velocity_axis, doppler_spectrum, "g-")
|
||||
self.ax2.set_title("Doppler Spectrum")
|
||||
self.ax2.set_xlabel("Velocity (m/s)")
|
||||
self.ax2.set_ylabel("Magnitude")
|
||||
self.ax2.grid(True)
|
||||
except Exception as e:
|
||||
logging.warning(f"Doppler spectrum plot error: {e}")
|
||||
self.ax2.set_title('Doppler Spectrum - Error')
|
||||
|
||||
self.ax2.set_title("Doppler Spectrum - Error")
|
||||
|
||||
# Plot 3: Range-Doppler map
|
||||
if (self.processed_data.get('long') and
|
||||
'range_doppler_matrix' in self.processed_data['long'] and
|
||||
self.processed_data['long']['range_doppler_matrix'].size > 0):
|
||||
|
||||
if (
|
||||
self.processed_data.get("long")
|
||||
and "range_doppler_matrix" in self.processed_data["long"]
|
||||
and self.processed_data["long"]["range_doppler_matrix"].size > 0
|
||||
):
|
||||
try:
|
||||
rd_matrix = self.processed_data['long']['range_doppler_matrix']
|
||||
rd_matrix = self.processed_data["long"]["range_doppler_matrix"]
|
||||
# Use integer indices for extent
|
||||
extent = [0, int(rd_matrix.shape[1]), 0, int(rd_matrix.shape[0])]
|
||||
|
||||
im = self.ax3.imshow(10 * np.log10(rd_matrix + 1e-9),
|
||||
aspect='auto', cmap='hot',
|
||||
extent=extent)
|
||||
self.ax3.set_title('Range-Doppler Map (Long Chirps)')
|
||||
self.ax3.set_xlabel('Doppler Bin')
|
||||
self.ax3.set_ylabel('Range Bin')
|
||||
self.fig.colorbar(im, ax=self.ax3, label='dB')
|
||||
|
||||
im = self.ax3.imshow(
|
||||
10 * np.log10(rd_matrix + 1e-9), aspect="auto", cmap="hot", extent=extent
|
||||
)
|
||||
self.ax3.set_title("Range-Doppler Map (Long Chirps)")
|
||||
self.ax3.set_xlabel("Doppler Bin")
|
||||
self.ax3.set_ylabel("Range Bin")
|
||||
self.fig.colorbar(im, ax=self.ax3, label="dB")
|
||||
except Exception as e:
|
||||
logging.warning(f"Range-Doppler map plot error: {e}")
|
||||
self.ax3.set_title('Range-Doppler Map - Error')
|
||||
|
||||
self.ax3.set_title("Range-Doppler Map - Error")
|
||||
|
||||
# Plot 4: MTI filtered data
|
||||
if self.df is not None and len(self.df) > 0:
|
||||
try:
|
||||
sample_data = self.df.head(100)
|
||||
if len(sample_data) > 10:
|
||||
iq_data = sample_data['I_value'].values + 1j * sample_data['Q_value'].values
|
||||
|
||||
iq_data = sample_data["I_value"].values + 1j * sample_data["Q_value"].values
|
||||
|
||||
# Original data
|
||||
original_mag = np.abs(iq_data)
|
||||
|
||||
|
||||
# MTI filtered
|
||||
mti_filtered = self.processor.mti_filter(iq_data)
|
||||
|
||||
|
||||
if mti_filtered is not None and len(mti_filtered) > 0:
|
||||
mti_mag = np.abs(mti_filtered)
|
||||
|
||||
|
||||
# Use integer indices for plotting
|
||||
x_original = np.arange(len(original_mag))
|
||||
x_mti = np.arange(len(mti_mag))
|
||||
|
||||
self.ax4.plot(x_original, original_mag, 'b-', label='Original', alpha=0.7)
|
||||
self.ax4.plot(x_mti, mti_mag, 'r-', label='MTI Filtered', alpha=0.7)
|
||||
self.ax4.set_title('MTI Filter Comparison')
|
||||
self.ax4.set_xlabel('Sample Index')
|
||||
self.ax4.set_ylabel('Magnitude')
|
||||
|
||||
self.ax4.plot(
|
||||
x_original, original_mag, "b-", label="Original", alpha=0.7
|
||||
)
|
||||
self.ax4.plot(x_mti, mti_mag, "r-", label="MTI Filtered", alpha=0.7)
|
||||
self.ax4.set_title("MTI Filter Comparison")
|
||||
self.ax4.set_xlabel("Sample Index")
|
||||
self.ax4.set_ylabel("Magnitude")
|
||||
self.ax4.legend()
|
||||
self.ax4.grid(True)
|
||||
except Exception as e:
|
||||
logging.warning(f"MTI filter plot error: {e}")
|
||||
self.ax4.set_title('MTI Filter - Error')
|
||||
|
||||
self.ax4.set_title("MTI Filter - Error")
|
||||
|
||||
# Adjust layout and draw
|
||||
self.fig.tight_layout()
|
||||
self.canvas.draw()
|
||||
self.status_label.config(text="Status: Processing complete")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Plot update error: {e}")
|
||||
error_msg = str(e)
|
||||
self.status_label.config(text=f"Status: Plot error - {error_msg}")
|
||||
|
||||
|
||||
def _update_cfar_results(self, range_axis, range_profile, detections):
|
||||
try:
|
||||
# Clear the plot
|
||||
self.ax1.clear()
|
||||
|
||||
|
||||
# Plot range profile
|
||||
self.ax1.plot(range_axis, range_profile, 'b-', label='Range Profile')
|
||||
|
||||
self.ax1.plot(range_axis, range_profile, "b-", label="Range Profile")
|
||||
|
||||
# Plot detections - ensure we use integer indices
|
||||
if detections and len(range_axis) > 0:
|
||||
detection_ranges = []
|
||||
detection_mags = []
|
||||
|
||||
|
||||
for bin_idx, mag in detections:
|
||||
# Convert bin_idx to integer and ensure it's within bounds
|
||||
bin_idx_int = int(bin_idx)
|
||||
if 0 <= bin_idx_int < len(range_axis):
|
||||
detection_ranges.append(range_axis[bin_idx_int])
|
||||
detection_mags.append(mag)
|
||||
|
||||
|
||||
if detection_ranges: # Only plot if we have valid detections
|
||||
self.ax1.plot(detection_ranges, detection_mags, 'ro',
|
||||
markersize=8, label='CFAR Detections')
|
||||
|
||||
self.ax1.set_title('Range Profile with CFAR Detections')
|
||||
self.ax1.set_xlabel('Range (m)')
|
||||
self.ax1.set_ylabel('Magnitude')
|
||||
self.ax1.plot(
|
||||
detection_ranges,
|
||||
detection_mags,
|
||||
"ro",
|
||||
markersize=8,
|
||||
label="CFAR Detections",
|
||||
)
|
||||
|
||||
self.ax1.set_title("Range Profile with CFAR Detections")
|
||||
self.ax1.set_xlabel("Range (m)")
|
||||
self.ax1.set_ylabel("Magnitude")
|
||||
self.ax1.legend()
|
||||
self.ax1.grid(True)
|
||||
|
||||
|
||||
# Update targets list
|
||||
self.update_targets_list()
|
||||
|
||||
|
||||
self.canvas.draw()
|
||||
self.status_label.config(text=f"Status: CFAR complete - {len(detections)} targets detected")
|
||||
|
||||
self.status_label.config(
|
||||
text=f"Status: CFAR complete - {len(detections)} targets detected"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"CFAR results update error: {e}")
|
||||
error_msg = str(e)
|
||||
self.status_label.config(text=f"Status: CFAR results error - {error_msg}")
|
||||
|
||||
|
||||
def update_targets_list(self):
|
||||
"""Update the targets list display"""
|
||||
# Clear current list
|
||||
for item in self.targets_tree.get_children():
|
||||
self.targets_tree.delete(item)
|
||||
|
||||
|
||||
# Add detected targets
|
||||
for i, target in enumerate(self.detected_targets):
|
||||
self.targets_tree.insert('', 'end', values=(
|
||||
f"{target.range:.1f}",
|
||||
f"{target.velocity:.1f}",
|
||||
f"{target.azimuth}",
|
||||
f"{target.elevation}",
|
||||
f"{target.snr:.1f}",
|
||||
target.chirp_type
|
||||
))
|
||||
|
||||
self.targets_tree.insert(
|
||||
"",
|
||||
"end",
|
||||
values=(
|
||||
f"{target.range:.1f}",
|
||||
f"{target.velocity:.1f}",
|
||||
f"{target.azimuth}",
|
||||
f"{target.elevation}",
|
||||
f"{target.snr:.1f}",
|
||||
target.chirp_type,
|
||||
),
|
||||
)
|
||||
|
||||
def update_gui(self):
|
||||
"""Periodic GUI update"""
|
||||
# You can add any periodic updates here
|
||||
self.root.after(100, self.update_gui)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main application entry point"""
|
||||
try:
|
||||
root = tk.Tk()
|
||||
app = RadarGUI(root)
|
||||
_app = RadarGUI(root)
|
||||
root.mainloop()
|
||||
except Exception as e:
|
||||
logging.error(f"Application error: {e}")
|
||||
messagebox.showerror("Fatal Error", f"Application failed to start: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+700
-608
File diff suppressed because it is too large
Load Diff
+499
-419
File diff suppressed because it is too large
Load Diff
+130
-30
@@ -194,7 +194,9 @@ class MapGenerator:
|
||||
var targetMarker = new google.maps.Marker({{
|
||||
position: {{lat: target.lat, lng: target.lng}},
|
||||
map: map,
|
||||
title: `Target: ${{target.range:.1f}}m, ${{target.velocity:.1f}}m/s`,
|
||||
title: (
|
||||
`Target: ${{target.range:.1f}}m, ${{target.velocity:.1f}}m/s`
|
||||
),
|
||||
icon: {{
|
||||
path: google.maps.SymbolPath.CIRCLE,
|
||||
scale: 6,
|
||||
@@ -276,8 +278,14 @@ class FT601Interface:
|
||||
found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid)
|
||||
for dev in found_devices:
|
||||
try:
|
||||
product = usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "FT601 USB3.0"
|
||||
serial = usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else "Unknown"
|
||||
product = (
|
||||
usb.util.get_string(dev, dev.iProduct)
|
||||
if dev.iProduct else "FT601 USB3.0"
|
||||
)
|
||||
serial = (
|
||||
usb.util.get_string(dev, dev.iSerialNumber)
|
||||
if dev.iSerialNumber else "Unknown"
|
||||
)
|
||||
|
||||
# Create FTDI URL for the device
|
||||
url = f"ftdi://{vid:04x}:{pid:04x}:{serial}/1"
|
||||
@@ -541,8 +549,14 @@ class STM32USBInterface:
|
||||
found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid)
|
||||
for dev in found_devices:
|
||||
try:
|
||||
product = usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "STM32 CDC"
|
||||
serial = usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else "Unknown"
|
||||
product = (
|
||||
usb.util.get_string(dev, dev.iProduct)
|
||||
if dev.iProduct else "STM32 CDC"
|
||||
)
|
||||
serial = (
|
||||
usb.util.get_string(dev, dev.iSerialNumber)
|
||||
if dev.iSerialNumber else "Unknown"
|
||||
)
|
||||
devices.append({
|
||||
'description': f"{product} ({serial})",
|
||||
'vendor_id': vid,
|
||||
@@ -561,7 +575,11 @@ class STM32USBInterface:
|
||||
except Exception as e:
|
||||
logging.error(f"Error listing USB devices: {e}")
|
||||
# Return mock devices for testing
|
||||
return [{'description': 'STM32 Virtual COM Port', 'vendor_id': 0x0483, 'product_id': 0x5740}]
|
||||
return [{
|
||||
'description': 'STM32 Virtual COM Port',
|
||||
'vendor_id': 0x0483,
|
||||
'product_id': 0x5740,
|
||||
}]
|
||||
|
||||
def open_device(self, device_info):
|
||||
"""Open STM32 USB CDC device"""
|
||||
@@ -586,12 +604,18 @@ class STM32USBInterface:
|
||||
# Find bulk endpoints (CDC data interface)
|
||||
self.ep_out = usb.util.find_descriptor(
|
||||
intf,
|
||||
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT
|
||||
custom_match=lambda e: (
|
||||
usb.util.endpoint_direction(e.bEndpointAddress)
|
||||
== usb.util.ENDPOINT_OUT
|
||||
)
|
||||
)
|
||||
|
||||
self.ep_in = usb.util.find_descriptor(
|
||||
intf,
|
||||
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN
|
||||
custom_match=lambda e: (
|
||||
usb.util.endpoint_direction(e.bEndpointAddress)
|
||||
== usb.util.ENDPOINT_IN
|
||||
)
|
||||
)
|
||||
|
||||
if self.ep_out is None or self.ep_in is None:
|
||||
@@ -826,7 +850,13 @@ class USBPacketParser:
|
||||
lon = float(parts[1])
|
||||
alt = float(parts[2])
|
||||
pitch = float(parts[3]) # Pitch angle in degrees
|
||||
return GPSData(latitude=lat, longitude=lon, altitude=alt, pitch=pitch, timestamp=time.time())
|
||||
return GPSData(
|
||||
latitude=lat,
|
||||
longitude=lon,
|
||||
altitude=alt,
|
||||
pitch=pitch,
|
||||
timestamp=time.time(),
|
||||
)
|
||||
|
||||
# Try binary format (30 bytes with pitch)
|
||||
if len(data) >= 30 and data[0:4] == b'GPSB':
|
||||
@@ -918,7 +948,10 @@ class RadarPacketParser:
|
||||
|
||||
crc_calculated = self.calculate_crc(packet[0:4+length])
|
||||
if crc_calculated != crc_received:
|
||||
logging.warning(f"CRC mismatch: got {crc_received:04X}, calculated {crc_calculated:04X}")
|
||||
logging.warning(
|
||||
f"CRC mismatch: got {crc_received:04X}, "
|
||||
f"calculated {crc_calculated:04X}"
|
||||
)
|
||||
return None
|
||||
|
||||
if packet_type == 0x01:
|
||||
@@ -1037,7 +1070,13 @@ class RadarGUI:
|
||||
|
||||
# Counters
|
||||
self.received_packets = 0
|
||||
self.current_gps = GPSData(latitude=41.9028, longitude=12.4964, altitude=0, pitch=0.0, timestamp=0)
|
||||
self.current_gps = GPSData(
|
||||
latitude=41.9028,
|
||||
longitude=12.4964,
|
||||
altitude=0,
|
||||
pitch=0.0,
|
||||
timestamp=0,
|
||||
)
|
||||
self.corrected_elevations = []
|
||||
self.map_file_path = None
|
||||
self.google_maps_api_key = "YOUR_GOOGLE_MAPS_API_KEY"
|
||||
@@ -1219,9 +1258,20 @@ class RadarGUI:
|
||||
targets_frame = ttk.LabelFrame(display_frame, text="Detected Targets (Pitch Corrected)")
|
||||
targets_frame.pack(side='right', fill='y', padx=5)
|
||||
|
||||
self.targets_tree = ttk.Treeview(targets_frame,
|
||||
columns=('ID', 'Range', 'Velocity', 'Azimuth', 'Elevation', 'Corrected Elev', 'SNR'),
|
||||
show='headings', height=20)
|
||||
self.targets_tree = ttk.Treeview(
|
||||
targets_frame,
|
||||
columns=(
|
||||
'ID',
|
||||
'Range',
|
||||
'Velocity',
|
||||
'Azimuth',
|
||||
'Elevation',
|
||||
'Corrected Elev',
|
||||
'SNR',
|
||||
),
|
||||
show='headings',
|
||||
height=20,
|
||||
)
|
||||
self.targets_tree.heading('ID', text='Track ID')
|
||||
self.targets_tree.heading('Range', text='Range (m)')
|
||||
self.targets_tree.heading('Velocity', text='Velocity (m/s)')
|
||||
@@ -1239,7 +1289,11 @@ class RadarGUI:
|
||||
self.targets_tree.column('SNR', width=70)
|
||||
|
||||
# Add scrollbar to targets tree
|
||||
tree_scroll = ttk.Scrollbar(targets_frame, orient="vertical", command=self.targets_tree.yview)
|
||||
tree_scroll = ttk.Scrollbar(
|
||||
targets_frame,
|
||||
orient="vertical",
|
||||
command=self.targets_tree.yview,
|
||||
)
|
||||
self.targets_tree.configure(yscrollcommand=tree_scroll.set)
|
||||
self.targets_tree.pack(side='left', fill='both', expand=True, padx=5, pady=5)
|
||||
tree_scroll.pack(side='right', fill='y', padx=(0, 5), pady=5)
|
||||
@@ -1288,7 +1342,9 @@ class RadarGUI:
|
||||
if not self.ft601_interface.open_device_direct(ft601_devices[ft601_index]):
|
||||
device_url = ft601_devices[ft601_index]['url']
|
||||
if not self.ft601_interface.open_device(device_url):
|
||||
logging.warning("Failed to open FT601 device, continuing without radar data")
|
||||
logging.warning(
|
||||
"Failed to open FT601 device, continuing without radar data"
|
||||
)
|
||||
messagebox.showwarning("Warning", "Failed to open FT601 device")
|
||||
else:
|
||||
# Configure burst mode if enabled
|
||||
@@ -1382,7 +1438,11 @@ class RadarGUI:
|
||||
gps_data = self.usb_packet_parser.parse_gps_data(data)
|
||||
if gps_data:
|
||||
self.gps_data_queue.put(gps_data)
|
||||
logging.info(f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°")
|
||||
logging.info(
|
||||
f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, "
|
||||
f"Lon {gps_data.longitude:.6f}, "
|
||||
f"Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°"
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing GPS data via USB: {e}")
|
||||
time.sleep(0.1)
|
||||
@@ -1395,7 +1455,10 @@ class RadarGUI:
|
||||
|
||||
# Apply pitch correction to elevation
|
||||
raw_elevation = packet['elevation']
|
||||
corrected_elevation = self.apply_pitch_correction(raw_elevation, self.current_gps.pitch)
|
||||
corrected_elevation = self.apply_pitch_correction(
|
||||
raw_elevation,
|
||||
self.current_gps.pitch,
|
||||
)
|
||||
|
||||
# Store correction for display
|
||||
self.corrected_elevations.append({
|
||||
@@ -1423,16 +1486,25 @@ class RadarGUI:
|
||||
|
||||
elif packet['type'] == 'doppler':
|
||||
lambda_wavelength = 3e8 / self.settings.system_frequency
|
||||
velocity = (packet['doppler_real'] / 32767.0) * (self.settings.prf1 * lambda_wavelength / 2)
|
||||
velocity = (packet['doppler_real'] / 32767.0) * (
|
||||
self.settings.prf1 * lambda_wavelength / 2
|
||||
)
|
||||
self.update_target_velocity(packet, velocity)
|
||||
|
||||
elif packet['type'] == 'detection':
|
||||
if packet['detected']:
|
||||
# Apply pitch correction to detection elevation
|
||||
raw_elevation = packet['elevation']
|
||||
corrected_elevation = self.apply_pitch_correction(raw_elevation, self.current_gps.pitch)
|
||||
corrected_elevation = self.apply_pitch_correction(
|
||||
raw_elevation,
|
||||
self.current_gps.pitch,
|
||||
)
|
||||
|
||||
logging.info(f"CFAR Detection: Raw Elev {raw_elevation}°, Corrected Elev {corrected_elevation:.1f}°, Pitch {self.current_gps.pitch:.1f}°")
|
||||
logging.info(
|
||||
f"CFAR Detection: Raw Elev {raw_elevation}°, "
|
||||
f"Corrected Elev {corrected_elevation:.1f}°, "
|
||||
f"Pitch {self.current_gps.pitch:.1f}°"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing radar packet: {e}")
|
||||
@@ -1480,7 +1552,11 @@ class RadarGUI:
|
||||
info_frame = ttk.Frame(map_frame)
|
||||
info_frame.pack(fill='x', pady=5)
|
||||
|
||||
self.map_info_label = ttk.Label(info_frame, text="No GPS data received yet", font=('Arial', 10))
|
||||
self.map_info_label = ttk.Label(
|
||||
info_frame,
|
||||
text="No GPS data received yet",
|
||||
font=('Arial', 10),
|
||||
)
|
||||
self.map_info_label.pack()
|
||||
|
||||
def open_map_in_browser(self):
|
||||
@@ -1488,7 +1564,10 @@ class RadarGUI:
|
||||
if self.map_file_path and os.path.exists(self.map_file_path):
|
||||
webbrowser.open('file://' + os.path.abspath(self.map_file_path))
|
||||
else:
|
||||
messagebox.showwarning("Warning", "No map file available. Generate map first by receiving GPS data.")
|
||||
messagebox.showwarning(
|
||||
"Warning",
|
||||
"No map file available. Generate map first by receiving GPS data.",
|
||||
)
|
||||
|
||||
def refresh_map(self):
|
||||
"""Refresh the map with current data"""
|
||||
@@ -1502,7 +1581,12 @@ class RadarGUI:
|
||||
|
||||
try:
|
||||
# Create temporary HTML file
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w',
|
||||
suffix='.html',
|
||||
delete=False,
|
||||
encoding='utf-8',
|
||||
) as f:
|
||||
map_html = self.map_generator.generate_map(
|
||||
self.current_gps,
|
||||
self.radar_processor.detected_targets,
|
||||
@@ -1533,7 +1617,12 @@ class RadarGUI:
|
||||
|
||||
# Update GPS label
|
||||
self.gps_label.config(
|
||||
text=f"GPS: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m")
|
||||
text=(
|
||||
f"GPS: Lat {gps_data.latitude:.6f}, "
|
||||
f"Lon {gps_data.longitude:.6f}, "
|
||||
f"Alt {gps_data.altitude:.1f}m"
|
||||
)
|
||||
)
|
||||
|
||||
# Update pitch label with color coding
|
||||
pitch_text = f"Pitch: {gps_data.pitch:+.1f}°"
|
||||
@@ -1581,8 +1670,11 @@ class RadarGUI:
|
||||
entry.grid(row=i, column=1, padx=5, pady=5)
|
||||
self.settings_vars[attr] = var
|
||||
|
||||
ttk.Button(settings_frame, text="Apply Settings",
|
||||
command=self.apply_settings).grid(row=len(entries), column=0, columnspan=2, pady=10)
|
||||
ttk.Button(
|
||||
settings_frame,
|
||||
text="Apply Settings",
|
||||
command=self.apply_settings,
|
||||
).grid(row=len(entries), column=0, columnspan=2, pady=10)
|
||||
|
||||
def apply_settings(self):
|
||||
"""Step 13: Apply and send radar settings via USB"""
|
||||
@@ -1678,7 +1770,11 @@ class RadarGUI:
|
||||
gps_data = self.usb_packet_parser.parse_gps_data(data)
|
||||
if gps_data:
|
||||
self.gps_data_queue.put(gps_data)
|
||||
logging.info(f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°")
|
||||
logging.info(
|
||||
f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, "
|
||||
f"Lon {gps_data.longitude:.6f}, "
|
||||
f"Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°"
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing GPS data via USB: {e}")
|
||||
time.sleep(0.1)
|
||||
@@ -1688,8 +1784,12 @@ class RadarGUI:
|
||||
try:
|
||||
# Update status with pitch information
|
||||
if self.running:
|
||||
self.status_label.config(
|
||||
text=f"Status: Running - Packets: {self.received_packets} - Pitch: {self.current_gps.pitch:+.1f}°")
|
||||
self.status_label.config(
|
||||
text=(
|
||||
f"Status: Running - Packets: {self.received_packets} - "
|
||||
f"Pitch: {self.current_gps.pitch:+.1f}°"
|
||||
)
|
||||
)
|
||||
|
||||
# Update range-Doppler map
|
||||
if hasattr(self, 'range_doppler_plot'):
|
||||
|
||||
@@ -621,7 +621,9 @@ class RadarDemoGUI:
|
||||
self.update_rate.grid(row=0, column=1, padx=10, pady=5)
|
||||
self.update_rate_value = ttk.Label(frame, text="20")
|
||||
self.update_rate_value.grid(row=0, column=2, sticky='w')
|
||||
self.update_rate.configure(command=lambda v: self.update_rate_value.config(text=f"{float(v):.0f}"))
|
||||
self.update_rate.configure(
|
||||
command=lambda v: self.update_rate_value.config(text=f"{float(v):.0f}")
|
||||
)
|
||||
|
||||
# Color map
|
||||
ttk.Label(frame, text="Color Map:").grid(row=1, column=0, sticky='w', pady=5)
|
||||
@@ -658,7 +660,9 @@ class RadarDemoGUI:
|
||||
self.noise_floor.grid(row=0, column=1, padx=10, pady=5)
|
||||
self.noise_value = ttk.Label(frame, text="10")
|
||||
self.noise_value.grid(row=0, column=2, sticky='w')
|
||||
self.noise_floor.configure(command=lambda v: self.noise_value.config(text=f"{float(v):.1f}"))
|
||||
self.noise_floor.configure(
|
||||
command=lambda v: self.noise_value.config(text=f"{float(v):.1f}")
|
||||
)
|
||||
|
||||
# Clutter level
|
||||
ttk.Label(frame, text="Clutter Level:").grid(row=1, column=0, sticky='w', pady=5)
|
||||
@@ -668,7 +672,9 @@ class RadarDemoGUI:
|
||||
self.clutter_level.grid(row=1, column=1, padx=10, pady=5)
|
||||
self.clutter_value = ttk.Label(frame, text="5")
|
||||
self.clutter_value.grid(row=1, column=2, sticky='w')
|
||||
self.clutter_level.configure(command=lambda v: self.clutter_value.config(text=f"{float(v):.1f}"))
|
||||
self.clutter_level.configure(
|
||||
command=lambda v: self.clutter_value.config(text=f"{float(v):.1f}")
|
||||
)
|
||||
|
||||
# Number of targets
|
||||
ttk.Label(frame, text="Number of Targets:").grid(row=2, column=0, sticky='w', pady=5)
|
||||
@@ -678,7 +684,9 @@ class RadarDemoGUI:
|
||||
self.num_targets.grid(row=2, column=1, padx=10, pady=5)
|
||||
self.targets_value = ttk.Label(frame, text="5")
|
||||
self.targets_value.grid(row=2, column=2, sticky='w')
|
||||
self.num_targets.configure(command=lambda v: self.targets_value.config(text=f"{float(v):.0f}"))
|
||||
self.num_targets.configure(
|
||||
command=lambda v: self.targets_value.config(text=f"{float(v):.0f}")
|
||||
)
|
||||
|
||||
# Reset button
|
||||
ttk.Button(frame, text="Reset Simulation",
|
||||
|
||||
@@ -321,7 +321,10 @@ class TestDataRecorder(unittest.TestCase):
|
||||
os.rmdir(self.tmpdir)
|
||||
|
||||
@unittest.skipUnless(
|
||||
(lambda: (__import__("importlib.util") and __import__("importlib").util.find_spec("h5py") is not None))(),
|
||||
(lambda: (
|
||||
__import__("importlib.util")
|
||||
and __import__("importlib").util.find_spec("h5py") is not None
|
||||
))(),
|
||||
"h5py not installed"
|
||||
)
|
||||
def test_record_and_stop(self):
|
||||
|
||||
@@ -755,7 +755,9 @@ class RadarDashboard(QMainWindow):
|
||||
self._det_thresh_spin.setValue(self._processing_config.detection_threshold_db)
|
||||
self._det_thresh_spin.setSuffix(" dB")
|
||||
self._det_thresh_spin.setSingleStep(1.0)
|
||||
self._det_thresh_spin.setToolTip("SNR threshold above noise floor (used when CFAR is disabled)")
|
||||
self._det_thresh_spin.setToolTip(
|
||||
"SNR threshold above noise floor (used when CFAR is disabled)"
|
||||
)
|
||||
p_layout.addWidget(self._det_thresh_spin, row, 1)
|
||||
row += 1
|
||||
|
||||
@@ -906,8 +908,11 @@ class RadarDashboard(QMainWindow):
|
||||
if idx2 >= 0 and idx2 < len(self._ft2232hq_devices):
|
||||
url = self._ft2232hq_devices[idx2]["url"]
|
||||
if not self._ft2232hq.open_device(url):
|
||||
QMessageBox.warning(self, "Warning",
|
||||
"Failed to open FT2232HQ device. Radar data may not be available.")
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Warning",
|
||||
"Failed to open FT2232HQ device. Radar data may not be available.",
|
||||
)
|
||||
|
||||
# Send start flag + settings
|
||||
if not self._stm32.send_start_flag():
|
||||
|
||||
@@ -305,7 +305,10 @@ function initMap() {{
|
||||
function setTileServer(id) {{
|
||||
var cfg = tileServers[id]; if(!cfg) return;
|
||||
if(currentTileLayer) map.removeLayer(currentTileLayer);
|
||||
currentTileLayer = L.tileLayer(cfg.url, {{ attribution:cfg.attribution, maxZoom:cfg.maxZoom }}).addTo(map);
|
||||
currentTileLayer = L.tileLayer(
|
||||
cfg.url,
|
||||
{{ attribution:cfg.attribution, maxZoom:cfg.maxZoom }}
|
||||
).addTo(map);
|
||||
}}
|
||||
|
||||
function updateRadarPopup() {{
|
||||
@@ -313,9 +316,18 @@ function updateRadarPopup() {{
|
||||
var ll = radarMarker.getLatLng();
|
||||
radarMarker.bindPopup(
|
||||
'<div class="popup-title">Radar System</div>'+
|
||||
'<div class="popup-row"><span class="popup-label">Lat:</span><span class="popup-value">'+ll.lat.toFixed(6)+'</span></div>'+
|
||||
'<div class="popup-row"><span class="popup-label">Lon:</span><span class="popup-value">'+ll.lng.toFixed(6)+'</span></div>'+
|
||||
'<div class="popup-row"><span class="popup-label">Status:</span><span class="popup-value status-approaching">Active</span></div>'
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Lat:</span>'+
|
||||
'<span class="popup-value">'+ll.lat.toFixed(6)+'</span></div>'
|
||||
)+
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Lon:</span>'+
|
||||
'<span class="popup-value">'+ll.lng.toFixed(6)+'</span></div>'
|
||||
)+
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Status:</span>'+
|
||||
'<span class="popup-value status-approaching">Active</span></div>'
|
||||
)
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -325,10 +337,22 @@ function addLegend() {{
|
||||
var d = L.DomUtil.create('div','legend');
|
||||
d.innerHTML =
|
||||
'<div class="legend-title">Target Legend</div>'+
|
||||
'<div class="legend-item"><div class="legend-color" style="background:#F44336"></div>Approaching</div>'+
|
||||
'<div class="legend-item"><div class="legend-color" style="background:#2196F3"></div>Receding</div>'+
|
||||
'<div class="legend-item"><div class="legend-color" style="background:#9E9E9E"></div>Stationary</div>'+
|
||||
'<div class="legend-item"><div class="legend-color" style="background:#FF5252"></div>Radar</div>';
|
||||
(
|
||||
'<div class="legend-item"><div class="legend-color" '+
|
||||
'style="background:#F44336"></div>Approaching</div>'
|
||||
)+
|
||||
(
|
||||
'<div class="legend-item"><div class="legend-color" '+
|
||||
'style="background:#2196F3"></div>Receding</div>'
|
||||
)+
|
||||
(
|
||||
'<div class="legend-item"><div class="legend-color" '+
|
||||
'style="background:#9E9E9E"></div>Stationary</div>'
|
||||
)+
|
||||
(
|
||||
'<div class="legend-item"><div class="legend-color" '+
|
||||
'style="background:#FF5252"></div>Radar</div>'
|
||||
);
|
||||
return d;
|
||||
}};
|
||||
legend.addTo(map);
|
||||
@@ -365,7 +389,12 @@ function updateTargets(targetsJson) {{
|
||||
}}
|
||||
}} else {{
|
||||
var marker = L.marker([lat,lon], {{ icon:makeIcon(color,sz) }}).addTo(map);
|
||||
marker.on('click', (function(id){{ return function(){{ if(bridge) bridge.onMarkerClick(id); }}; }})(t.id));
|
||||
marker.on(
|
||||
'click',
|
||||
(function(id){{
|
||||
return function(){{ if(bridge) bridge.onMarkerClick(id); }};
|
||||
}})(t.id)
|
||||
);
|
||||
targetMarkers[t.id] = marker;
|
||||
if(showTrails) {{
|
||||
targetTrails[t.id] = L.polyline(targetTrailHistory[t.id], {{
|
||||
@@ -389,24 +418,50 @@ function makeIcon(color,sz) {{
|
||||
return L.divIcon({{
|
||||
className:'target-icon',
|
||||
html:'<div style="background-color:'+color+';width:'+sz+'px;height:'+sz+'px;'+
|
||||
'border-radius:50%;border:2px solid white;box-shadow:0 2px 6px rgba(0,0,0,0.4);"></div>',
|
||||
(
|
||||
'border-radius:50%;border:2px solid white;'+
|
||||
'box-shadow:0 2px 6px rgba(0,0,0,0.4);'
|
||||
)+'</div>',
|
||||
iconSize:[sz,sz], iconAnchor:[sz/2,sz/2]
|
||||
}});
|
||||
}}
|
||||
|
||||
function updateTargetPopup(t) {{
|
||||
if(!targetMarkers[t.id]) return;
|
||||
var sc = t.velocity>1?'status-approaching':(t.velocity<-1?'status-receding':'status-stationary');
|
||||
var sc = t.velocity>1
|
||||
? 'status-approaching'
|
||||
: (t.velocity<-1 ? 'status-receding' : 'status-stationary');
|
||||
var st = t.velocity>1?'Approaching':(t.velocity<-1?'Receding':'Stationary');
|
||||
targetMarkers[t.id].bindPopup(
|
||||
'<div class="popup-title">Target #'+t.id+'</div>'+
|
||||
'<div class="popup-row"><span class="popup-label">Range:</span><span class="popup-value">'+t.range.toFixed(1)+' m</span></div>'+
|
||||
'<div class="popup-row"><span class="popup-label">Velocity:</span><span class="popup-value">'+t.velocity.toFixed(1)+' m/s</span></div>'+
|
||||
'<div class="popup-row"><span class="popup-label">Azimuth:</span><span class="popup-value">'+t.azimuth.toFixed(1)+'°</span></div>'+
|
||||
'<div class="popup-row"><span class="popup-label">Elevation:</span><span class="popup-value">'+t.elevation.toFixed(1)+'°</span></div>'+
|
||||
'<div class="popup-row"><span class="popup-label">SNR:</span><span class="popup-value">'+t.snr.toFixed(1)+' dB</span></div>'+
|
||||
'<div class="popup-row"><span class="popup-label">Track:</span><span class="popup-value">'+t.track_id+'</span></div>'+
|
||||
'<div class="popup-row"><span class="popup-label">Status:</span><span class="popup-value '+sc+'">'+st+'</span></div>'
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Range:</span>'+
|
||||
'<span class="popup-value">'+t.range.toFixed(1)+' m</span></div>'
|
||||
)+
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Velocity:</span>'+
|
||||
'<span class="popup-value">'+t.velocity.toFixed(1)+' m/s</span></div>'
|
||||
)+
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Azimuth:</span>'+
|
||||
'<span class="popup-value">'+t.azimuth.toFixed(1)+'°</span></div>'
|
||||
)+
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Elevation:</span>'+
|
||||
'<span class="popup-value">'+t.elevation.toFixed(1)+'°</span></div>'
|
||||
)+
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">SNR:</span>'+
|
||||
'<span class="popup-value">'+t.snr.toFixed(1)+' dB</span></div>'
|
||||
)+
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Track:</span>'+
|
||||
'<span class="popup-value">'+t.track_id+'</span></div>'
|
||||
)+
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Status:</span>'+
|
||||
'<span class="popup-value '+sc+'">'+st+'</span></div>'
|
||||
)
|
||||
);
|
||||
}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user