Hardware first, AI later
Hardware first, AI later
Building a device-agnostic validation framework for autonomous systems. Starting where it matters: can we talk to a drone and listen to the spectrum?
The idea, in one paragraph
I want to build a framework that can validate autonomous systems — drones, ground vehicles, surface vehicles, whatever. Command the device, inject environmental conditions, evaluate the response. Device-agnostic: swap the adapter, same test runs. Eventually it'll generate scenarios from requirements, produce certification evidence, handle DO-178C traceability. But none of that matters if I can't talk to the hardware first. So that's where we start.
Phase 0 — Hardware reality check
Three devices to verify. This should take about 15 minutes if nothing is broken, or an evening if drivers are involved.
RTL-SDR
Software-defined radio receiver. Receive-only — can listen across ~24 MHz to 1.7 GHz, cannot transmit.
Verify modelDJI Tello
WiFi-controlled mini drone. Simple UDP command protocol. Python SDK available (djitellopy).
Verify connectivityDJI Mavic
Consumer drone. Most models lack an SDK. Need to identify exact model to know what's available.
Identify model1RTL-SDR — identify and test
Plug in the dongle and find out what chipset and tuner you have. Every RTL-SDR uses an RTL2832U demodulator, but the tuner chip determines frequency range and quality.
Install the tools:
# Linux (Debian/Ubuntu)
sudo apt install rtl-sdr
# macOS
brew install librtlsdr
# Windows: download from osmocom.org/projects/rtl-sdr
# Install Zadig driver first (WinUSB for Bulk-In Interface 0)
Run the test:
rtl_test -t
What to look for in the output:
# Example output:
Found 1 device(s):
0: Realtek, RTL2838UHIDIR, SN: 00000001
Using device 0: Generic RTL2832U OEM
Found Rafael Micro R820T tuner # <-- this is the key line
Quick functional test — receive FM radio:
# Tune to a local FM station (e.g., 96.3 MHz = 96.3e6 Hz)
rtl_fm -f 96.3e6 -M wbfm -s 200000 -r 48000 - | aplay -r 48000 -f S16_LE
# macOS: pipe to `play` (from sox) instead of aplay
rtl_fm -f 96.3e6 -M wbfm -s 200000 -r 48000 - | play -r 48000 -t raw -e signed -b 16 -c 1 -
If you hear a radio station, the SDR works. That's all we need for Phase 0. We're not doing anything fancy with it yet — just confirming the hardware is alive and the drivers are installed.
2Tello — connect and query
The Tello speaks a simple UDP protocol on port 8889. Power it on, join its WiFi network (TELLO-XXXXXX), and run:
# Install the SDK
pip install djitellopy
# tello_check.py
from djitellopy import Tello
drone = Tello()
drone.connect()
print(f"Battery: {drone.get_battery()}%")
print(f"Temperature: {drone.get_temperature()}°C")
print(f"Barometer: {drone.get_barometer()} cm")
print(f"TOF: {drone.get_distance_tof()} cm")
print(f"WiFi SNR: {drone.query_wifi_snr()} dB")
print(f"SDK version: {drone.query_sdk_version()}")
drone.end()
Expected output: six lines of telemetry data. If you get them, the Tello is ready.
3Mavic — identify the model
Check the label on the drone or open DJI Fly and look at the device name. This matters because most consumer Mavics have no programmable SDK:
No SDK (consumer line):
Mavic Mini, Mini 2, Mini 3/3 Pro
Mavic Air, Air 2, Air 2S
Mavic 3 (standard/Cine)
SDK available (enterprise/developer):
Mavic 2 Enterprise (Advanced/Dual)
Mavic 3 Enterprise/Thermal
Matrice 30/300/350 series
If it's a consumer model: note it and move on. It won't be a Phase 1 adapter, but it's useful context for later. The Tello is the primary development drone. If it's an Enterprise model: bonus — DJI Mobile SDK or MSDK 5 applies, and we'll build an adapter for it later.
Phase 1 — Two tracks, side by side
Once hardware is confirmed, we build two independent scripts. They don't integrate with each other yet. The goal is clean, working code for each device that we'll later wrap in a common interface.
Project layout
├── sdr/
│ ├── receive.py # RTL-SDR receiver script
│ ├── spectrum.py # Power spectrum scanner
│ └── adsb.py # ADS-B aircraft decoder (optional)
├── drone/
│ ├── tello_fly.py # Tello flight + telemetry script
│ └── tello_telemetry.py # Telemetry-only (no flight)
├── logs/ # Telemetry and signal logs
├── requirements.txt
└── README.md
Track A: RTL-SDR — receive and log
The "hello world" for SDR is receiving FM radio. But that's analog audio — useful for confirming the hardware, not useful for a validation framework. The first real exercise is a power spectrum scan: sweep a frequency range and log the signal power at each step. This is the primitive that everything else builds on — anomaly detection, interference monitoring, spectrum characterization.
# sdr/spectrum.py
# Sweep a frequency range and log power at each step
# Requires: pip install pyrtlsdr numpy
import numpy as np
from rtlsdr import RtlSdr
import json
from datetime import datetime
def scan_spectrum(center_freq: float, bandwidth: float, num_samples: int = 1024):
"""Capture power spectrum at a given center frequency."""
sdr = RtlSdr()
sdr.sample_rate = 2.048e6 # 2.048 MHz
sdr.center_freq = center_freq # Hz
sdr.gain = 'auto'
samples = sdr.read_samples(num_samples)
sdr.close()
# Compute power spectral density
fft = np.fft.fftshift(np.fft.fft(samples))
power_db = 20 * np.log10(np.abs(fft) + 1e-10)
freqs = np.fft.fftshift(
np.fft.fftfreq(len(samples), 1.0 / sdr.sample_rate)
) + center_freq
return {
"timestamp": datetime.utcnow().isoformat(),
"center_freq_hz": center_freq,
"sample_rate": sdr.sample_rate,
"num_samples": num_samples,
"peak_power_db": float(np.max(power_db)),
"mean_power_db": float(np.mean(power_db)),
"freq_at_peak_hz": float(freqs[np.argmax(power_db)]),
}
if __name__ == "__main__":
# Scan a few interesting bands
bands = {
"FM broadcast": 96.3e6,
"ADS-B (1090)": 1090e6,
"GPS L1": 1575.42e6,
"ISM 433 MHz": 433.92e6,
"ISM 915 MHz": 915e6,
}
results = []
for name, freq in bands.items():
try:
result = scan_spectrum(freq)
result["band_name"] = name
results.append(result)
print(f"{name:16} peak: {result['peak_power_db']:6.1f} dB mean: {result['mean_power_db']:6.1f} dB")
except Exception as e:
print(f"{name}: skipped ({e})")
# Save to log
with open("logs/spectrum_scan.jsonl", "a") as f:
for r in results:
f.write(json.dumps(r) + "\n")
Track B: Tello — fly a mission and log telemetry
The first flight script does three things: take off, fly a simple pattern, land. While flying, it logs telemetry (battery, height, temperature, barometer) to JSONL at 1-second intervals. The telemetry logging runs in a background thread so it doesn't block the flight commands.
# drone/tello_fly.py
# First flight: takeoff, move, land. Log telemetry throughout.
# Requires: pip install djitellopy
import json
import time
import threading
from datetime import datetime
from djitellopy import Tello
def telemetry_logger(drone: Tello, stop_event: threading.Event):
"""Background thread: log telemetry every second."""
with open("logs/tello_telemetry.jsonl", "a") as f:
while not stop_event.is_set():
entry = {
"timestamp": datetime.utcnow().isoformat(),
"battery_pct": drone.get_battery(),
"height_cm": drone.get_height(),
"temperature_c": drone.get_temperature(),
"barometer_cm": drone.get_barometer(),
"tof_cm": drone.get_distance_tof(),
"flight_time_s": drone.get_flight_time(),
}
f.write(json.dumps(entry) + "\n")
print(f" alt={entry['height_cm']}cm bat={entry['battery_pct']}% temp={entry['temperature_c']}°C")
time.sleep(1)
def main():
drone = Tello()
drone.connect()
print(f"Connected. Battery: {drone.get_battery()}%")
# Start telemetry logging
stop = threading.Event()
logger = threading.Thread(target=telemetry_logger, args=(drone, stop))
logger.start()
try:
print("Taking off...")
drone.takeoff()
time.sleep(3)
print("Moving forward 50cm...")
drone.move_forward(50)
time.sleep(2)
print("Rotating 90°...")
drone.rotate_clockwise(90)
time.sleep(2)
print("Moving forward 50cm...")
drone.move_forward(50)
time.sleep(2)
print("Landing...")
drone.land()
time.sleep(3)
except Exception as e:
print(f"Error: {e}")
drone.land()
finally:
stop.set()
logger.join()
drone.end()
print("Done. Telemetry saved to logs/tello_telemetry.jsonl")
if __name__ == "__main__":
main()
The telemetry-only option
If you want to develop without flying (testing parsing, logging, interfaces), this script connects to the Tello and reads telemetry without taking off:
# drone/tello_telemetry.py
# Read telemetry from Tello without flying. Good for dev/test.
from djitellopy import Tello
import json
import time
from datetime import datetime
drone = Tello()
drone.connect()
print(f"Connected. Battery: {drone.get_battery()}%\n")
with open("logs/tello_ground.jsonl", "a") as f:
for i in range(10):
state = {
"timestamp": datetime.utcnow().isoformat(),
"battery_pct": drone.get_battery(),
"temperature_c": drone.get_temperature(),
"barometer_cm": drone.get_barometer(),
"tof_cm": drone.get_distance_tof(),
"wifi_snr": drone.query_wifi_snr(),
}
print(json.dumps(state, indent=2))
f.write(json.dumps(state) + "\n")
time.sleep(1)
drone.end()
print("\nSaved to logs/tello_ground.jsonl")
Exit criteria
Phase 0 and Phase 1 are done when all of this is true:
- RTL-SDR model and tuner chip identified and recorded
- RTL-SDR receives FM radio or completes a spectrum scan
- Tello connects over WiFi and returns telemetry
- Tello completes a basic flight mission (takeoff, move, land)
- Both devices produce JSONL log files in
logs/ - Mavic model identified; SDK availability noted
What comes next
Phase 2 wraps both devices behind a common interface. Not a platform, not an orchestrator — just a Python protocol that any device can implement. The same script that flies the Tello should be able to drive a PX4 simulator, or a ground rover, or anything else, by swapping one config line.
But that's the next post. Right now: plug things in, see if they work, and commit the logs.
Thoughts?
Comments are threads on GitHub, so a GitHub account is needed to post. No account? Email me instead — I read everything.