Support the researchFund a year of R&D on GoFundMe

← KORENT

Making VNA and GPS Analysis Easier: A One-Button Logger for Position-Tagged RF Sweeps

Can measuring what a device does to radio signals, and where it was when you measured it, be as simple as pressing one button?

Korent open project notebook. Why field measurements with a vector network analyzer and a GPS are harder than they need to be, and the small open Python toolkit we built to fix that, broken down file by file: what each piece of code does and the need it serves.

Last updated

VNA app GPS logger sweep files track file match by hand two programs, two files, one guess
Before. The analyzer and the GPS each log on their own. Working out where each sweep was taken means lining up two files by their timestamps afterwards.

Abstract

Low-cost vector network analyzers (VNAs) such as the NanoVNA and LiteVNA have made it cheap to measure what an antenna, cable or filter does to a radio signal. Taking those measurements out into the world is still hard. The usual PC software is a desktop app built for a bench. Different analyzer models speak different protocols. A GPS receiver logs to its own file, so matching each sweep to where it was taken is left to the person afterwards.

This notebook breaks down a small Python toolkit we built to remove those steps. It runs on Linux and on a Raspberry Pi, finds whichever NanoVNA is plugged in, and with one button writes one standard Touchstone file per sweep, with the GPS fix from that moment at the top of the file. Each log covers one piece of the code and the need it serves.

The VNA side is tested on three analyzers: a LiteVNA at 5.86 sweeps per second, a NanoVNA-H, and a NanoVNA-F V2 clone whose faulty firmware the toolkit now detects and refuses to record. The GPS side connects to the receiver on the Pi but hasn't produced a fix yet. The software was written with help from Claude AI and tested against the real hardware at each step.

Keywords: NanoVNA, LiteVNA, vector network analyzer, GPS, GNSS, Raspberry Pi, Touchstone, field measurement, Python

Record ! GPS fix S-parameters capture_….s2p one button, one file per sweep
After. One button. Every sweep is saved as its own standard file, with the GPS fix from that moment written at the top.

How to read this notebook

Logs 1 and 2 set out the problem and give an overview of the toolkit. Logs 3 to 9 each cover one part of it: the need it answers, what the code does function by function, and real output from testing on hardware. Every log carries a tag that says how far along that part is.

Overview Tested on hardware Partly working Not working yet Planned
Overview:
background, not code.
Tested on hardware:
run against the real device, with the output checked.
Partly working:
runs, but one part isn't confirmed yet.
Not working yet:
written and connected, but not producing results yet.
Planned:
the next steps, in order.

Box colours

The need: what a part of the toolkit is for.
The code: what it does, function by function.
Real output from a test on hardware.
Note: a problem we hit, or a detail worth knowing.

The software was written with help from Claude AI. Each part was tested against the real hardware before we relied on it, and the problems those tests turned up are in the logs. If we get something wrong, we'll correct it here in the open and say what changed.

Overview

The need

A vector network analyzer sends a signal into a device and measures what comes back and what gets through, at each frequency. That tells you how well an antenna is matched, how much a cable loses, or what a filter passes. Bench VNAs stay on the bench. The small battery-powered NanoVNA family can be carried, or flown, which opens up a different kind of measurement: how a device's response changes as it moves.

That only works if every sweep comes with the place it was taken. When we tried to do it, the hard part wasn't the radio. It was everything around it.

Need 1.1 (the two numbers a VNA gives you). At each frequency, \(S_{11}\) is the fraction of the signal sent into port 1 that reflects back, and \(S_{21}\) is the fraction that arrives at port 2. Both are complex numbers: a size and a phase shift. In decibels, the return loss is

$$\text{RL} = -20\log_{10}|S_{11}|. \tag{1.1}$$

A well-matched antenna reflects little (high return loss). An open or shorted cable reflects everything (\(|S_{11}| = 1\), 0 dB).[1]

Need 1.2 (a position-tagged sweep). What we want from every measurement: the list of frequencies with \(S_{11}\) and \(S_{21}\) at each, saved together with the GPS fix (position, accuracy, time) taken at the same moment, in a format any RF tool can open.

What stands in the way, and what answers it

The needWhy it's hard todayWhat answers it
Run the analyzer from a small Linux computer, without vendor desktop softwareThe PC program we started with, Solver64, is Windows-only, and a desktop app doesn't suit a Raspberry Pi strapped to something moving.litevna.py, nanovna_classic.py
Work with whichever NanoVNA is plugged inNanoVNA models speak two unrelated protocols, and detecting the wrong way can upset the analyzer.nanovna.py
Know where each sweep was takenThe GPS logs separately. Matching two files by timestamp afterwards is slow and easy to get wrong.gps_dr.py, capture_combined.py
Never save bad data without saying soOne analyzer we tested returned sweeps that were mostly empty, with no error.The sweep check in nanovna_classic.py
Open the results in any RF toolCustom formats lock the data into one program.Standard Touchstone .s2p files (Log 3, Log 7)[2]
Start with one button, even on a Pi with no internetPython setups break easily when copied between machines, and an offline Pi can't download anything.record.sh, install_launcher.sh, offline_packages/
Overview

The toolkit at a glance

The toolkit is a folder of plain Python scripts. Its only dependencies are pySerial, numpy, matplotlib and pyubx2.[7][6] Pressing Record runs the chain below.

Every file, and the need it serves

FileRoleNeed it serves
litevna.pyDriverRun a LiteVNA or NanoVNA-V2 from Python (Log 3)
nanovna_classic.pyDriverRun the older text-command NanoVNAs, and catch bad sweeps (Log 4)
nanovna.pyDriverFind and connect to any NanoVNA automatically (Log 5)
gps_dr.pyDriverRead position and dead-reckoning status from the GPS (Log 6)
capture_combined.pyMain toolOne file per sweep with its GPS fix (Log 7)
vna_measure.py, vna_live.py, gps_live.pyOther toolsA quick single sweep, live viewing of several analyzers, and a GPS-only track (Log 8)
record.sh, run_vna_live.sh, install_launcher.sh, Record.desktopLaunchersStart everything from one icon (Log 7, Log 9)
offline_packages/SetupInstall on a Pi with no internet (Log 9)
README.txtDocsCopy-and-paste setup for a new machine, and a guide to every file

Hardware

"Search" links open a store search, because many sellers make the same part. Prices and stock change.
PartRoleWhere to buy
LiteVNA (ZeenKo)Main analyzer. Swept 1 MHz–3 GHz in our tests.Search
NanoVNA-HSecond analyzer, the older text-command type.Search
NanoVNA-F V2 (SYSJOINT)Third analyzer. Its firmware returns mostly empty sweeps (Log 4).Search
Raspberry Pi 4 Model BField computer that runs the logger.Search
SparkFun GPS-RTK Dead Reckoning pHAT (u-blox ZED-F9R)GPS with a built-in motion sensor for dead reckoning. Plugs onto the Pi's GPIO header.SparkFun GPS-16475
GNSS antenna with U.FL connectorThe GPS board won't get a fix without one.Search
Tested on hardware

litevna.py: talking to a LiteVNA without vendor software

Need 3.1. Control a LiteVNA or NanoVNA-V2 from plain Python on Linux, with nothing but a USB cable and the pyserial library.

The code 3.2. These analyzers speak a binary register protocol: short commands such as READ (0x10), WRITE (0x20) and READFIFO (0x18), each aimed at a numbered register.[4]

  • LiteVNA(port) opens the USB serial port in raw mode. Without raw mode, Linux's terminal layer alters some binary bytes. It can also take an already-open connection, so a port is never closed and reopened (Log 5).
  • set_sweep(start, stop, points) writes the start frequency, step and number of points into the analyzer's registers. It only accepts the point counts the hardware supports (51, 101, 201 … 25601), and refuses anything else with a clear error instead of quietly changing your request.
  • sweep() reads the results in chunks of up to 255 points, 32 bytes each: the forward, reflected and through signals plus a frequency index. It computes \(S_{11}\) and \(S_{21}\) from them and retries twice if a read comes back incomplete.
  • firmware_string() and read_vbat() report the firmware version and battery voltage.
  • save_s1p() and save_s2p() write standard Touchstone files that any RF tool can open.

The analyzer measures a sample of the outgoing signal \(v_f\), the reflected signal \(v_r\) and the through signal \(v_t\). The driver divides:

$$S_{11}=\frac{v_r}{v_f},\qquad S_{21}=\frac{v_t}{v_f}. \tag{3.1}$$

Dividing by the outgoing signal cancels any drift in the source's strength from sweep to sweep.

Output 3.3. A one-shot sweep with nothing attached to the port, August 26, 2026:

Connected: /dev/ttyACM0
FW 2.2  HW variant 2 rev 2
Battery: 3.72 V
Sweeping 1.000-3000.000 MHz, 51 points...
Saved measurement.s2p

The file's first point reads \(S_{11} = 1.000555 - 0.024789i\) at 1 MHz: a magnitude of 1.0009, almost exactly the 1 that an open port should give. A second run minutes later, by a different program, agreed to within \(5.3\times10^{-4}\).

Asking for 21 points, which the hardware can't do, gets a clear refusal:

ValueError: points must be one of (51, 101, 201, 401, 801, 1024, 1601, 3201, 4501, 6401, 12801, 25601)

Note 3.4 (speed). At first the driver entered the analyzer's raw-sample mode for every sweep and left it afterwards. Re-entering that mode is slow because the analyzer re-locks each time. Staying in it between sweeps cut a 51-point sweep from about 300 ms to about 170 ms: 5.86 sweeps per second, the most this analyzer can do.

Note 3.5 (what the numbers are and aren't). The LiteVNA only sends from port 1, so the two other S-parameters (\(S_{12}\), \(S_{22}\)) are never measured. The two-port file format still needs them, so the files set \(S_{12}=S_{21}\) and \(S_{22}=0\) and say so in a comment. The readings are also uncalibrated: raw ratios, before any correction with known reference loads. Adding calibration is in Log 10.

Tested on hardware

nanovna_classic.py: older NanoVNAs that speak text

Need 4.1. Support the original NanoVNA and the H, H4 and F models, which use a text command line instead of binary registers, behind exactly the same functions as the LiteVNA driver. Never pass on a sweep the analyzer didn't actually measure.

The code 4.2. These analyzers show a ch> prompt and take typed commands.[3]

  • detect(ser) sends a carriage return and looks for the ch> prompt. It only runs after the binary check has failed (Log 5).
  • NanoVNAClassic(port) reads the firmware version and picks the right sweep command for it: scan with an output mask on firmware 0.7.1 and later, plain scan from 0.2.0, and sweep before that.
  • _exec(command) sends a command and collects the reply lines until the prompt returns. It waits longer for bigger sweeps, so a slow analyzer isn't mistaken for a dead one.
  • set_sweep() and sweep() have the same names and meaning as in the LiteVNA driver. sweep() reads \(S_{11}\) from data 0 and \(S_{21}\) from data 1, one line per frequency.
  • _check_sweep_complete() rejects any sweep in which more than 10% of the points read exactly zero for both \(S_{11}\) and \(S_{21}\).

Output 4.3 (the clone that skipped most of each sweep). A NanoVNA-F V2 returned the right number of points, but every point after a cutoff read exactly 0.000000 0.000000, with no error. Before the check existed, those zeros were being saved as if they were measurements. Now the logger reports it and tries again:

[VNA] lost connection (Incomplete sweep: 36/51 points never measured (device returned exact 0+0j for both S11 and S21)); searching again...
Points the NanoVNA-F V2 actually measured, on firmware 0.6.0, August 26, 2026. Firmware 0.5.4 behaved the same way at 33–36%. Download: nanovna-f-v2-sweep-cutoff.csv
RequestedMeasuredFraction
11327%
21629%
511529%
1012929%
2015829%

The measured points are always the lowest frequencies. The cutoff didn't move with waits of up to 10 s, with re-triggering, or with a different sweep command. The zeros are already in the text the analyzer sends, so this is in its firmware and can't be fixed from the computer.

Note 4.4 (why the check is safe). A real measurement essentially never reads exactly zero for both values at once. Even a good 50 Ω load still reflects about 1%. So the check only looks for a sign that the instrument failed, and never throws out a reading just because it looks unexpected.

Note 4.5. A NanoVNA-H worked correctly through this driver, with complete sweeps at 2.71 per second (Log 8).

Tested on hardware

nanovna.py: plug in any NanoVNA

Need 5.1. Plug in any NanoVNA, on any USB port, and have it just work, without the user knowing which protocol it speaks or which port it landed on. Also handle several analyzers at once.

The code 5.2.

  • _candidate_ports() lists every USB serial port: /dev/ttyACM* and /dev/ttyUSB*.
  • _looks_like_v2() checks for the binary protocol in a harmless way: a protocol reset (eight zero bytes), then a read of two firmware registers. A text shell that just echoes the bytes back isn't mistaken for a reply.
  • _probe_port() tries the binary check first, then the text check only if that fails, and hands the same open connection to the matching driver.
  • find_and_open() returns the first analyzer found. connect(port) does the same for one named port.
  • device_id_for_port() gives each analyzer a stable name from /dev/serial/by-id, such as usb-ZeenKo.tech_LiteVNA6_v1.0-if00, so its files keep the same name when it's unplugged and plugged back in.
  • probe_all() returns every connected analyzer, for running several at once.

Both drivers offer the same functions (set_sweep, sweep, firmware_string, read_vbat, close), so the tools never need to know which kind they got.

Note 5.3 (why the order of the checks matters). Our first version tried the text check first. A carriage return is the byte 0x0D, which in the binary protocol is itself a command. Sending it knocked a real LiteVNA out of binary mode. Trying the harmless binary read first means a binary analyzer never sees that byte.

Note 5.4 (why the connection is reused). Closing the port after the check and opening it again for the driver made the LiteVNA unstable, most likely because closing the port toggles a USB control line that resets it. Handing over the open connection fixed it.

Output 5.5. The same call found each of our three analyzers and picked the right driver. The LiteVNA was found by the binary check:

[VNA] connected on /dev/ttyACM0: FW 2.2  HW variant 2 rev 2

The NanoVNA-F V2 failed the binary check and answered the text check:

Model:        NanoVNA-F_V2
Frequency:    50k ~ 3GHz
Build time:   Jun 18 2025 - 13:48:38 CST
Not working yet

gps_dr.py: reading the GPS

Need 6.1. Get position, its accuracy, and whether the receiver is filling gaps with dead reckoning, from the ZED-F9R on the Pi. Do it without changing any setting the module keeps after a power cycle.

The code 6.2. It uses u-blox's binary UBX messages, read with the pyubx2 library.[5][6]

  • find_port() picks the first of /dev/serial0, /dev/ttyAMA0 and /dev/ttyS0 that exists.
  • GPSReceiver(port, baudrate=115200, rate_hz=5) opens the Pi's serial port and a UBX reader on it.
  • _configure() sends one configuration message that sets 5 fixes per second and turns on two outputs: NAV-PVT (position, velocity, time) and ESF-STATUS (sensor fusion). It writes to the module's temporary memory only, so a power cycle puts the module back exactly as it was.
  • read_fix() waits up to 2 s for the next NAV-PVT and returns a GNSSFix: latitude and longitude, height, horizontal and vertical accuracy, ground speed, heading, satellite count, fix type, and the latest fusion mode. It converts the module's millimetres to metres.
  • fix_to_csv_row() and CSV_HEADER turn a fix into one line of text for the output files.

Note 6.3 (why UBX and not the usual NMEA text). Most GPS tools read NMEA, the common text output. NMEA doesn't report whether the ZED-F9R is blending satellites with its motion sensor. UBX does, so every saved fix says whether its position came from satellites, from dead reckoning (under trees or a bridge), or both.

Note 6.4 (Pi setup). The GPS board talks over the Pi's GPIO serial port, which has to be switched on in raspi-config, with the login shell over serial turned off. On a Pi 4, Bluetooth uses the better of the Pi's two serial ports by default. Adding dtoverlay=disable-bt to /boot/firmware/config.txt hands that port to the GPIO pins instead. The project README has the exact steps.

Status: the code connects to the module on the Pi and the module responds, but no fix has come through yet. At 115200 baud, the speed the code assumes, the serial port shows garbled characters, which points to a speed mismatch. Finding the module's real speed is the first step in Log 10.

Partly working

capture_combined.py: one button, one file per sweep

Need 7.1. Every sweep should carry its own position, with nothing to match up afterwards, and the whole thing should start from one icon and keep going if a cable drops.

The code 7.2.

  • GPSFeeder runs in the background. It keeps the GPS open, always holds the newest fix (latest_fix()), and reconnects by itself if the GPS drops, so a GPS problem never stops the VNA logging.
  • main() searches for an analyzer every second, printing "still searching" every 5 s. Once one is found, it sets the sweep (1 MHz–3 GHz, 51 points by default), then loops: sweep, take the newest fix, write a file, print a status line. If the analyzer drops or returns a bad sweep, it goes back to searching.
  • write_combined_file() writes the GPS fix as comment lines at the top, then the normal S-parameter data.
  • timestamped_path() names each file by the time to the millisecond, for example capture_20260826_081717_721.s2p, so files sort in order and never overwrite each other.
  • CAPTURE_INTERVAL_SECONDS, a clearly marked number at the top of the file, sets how often a file is written. Setting it to 0 writes one for every sweep. --interval changes it for a single run.
  • record.sh is what the Record icon runs. It uses the project's own Python environment if there is one, and the system Python if not, and keeps the window open at the end so any message can be read.

The file it writes

The start of a real file from a test with no GPS attached:

! Generated by capture_combined.py
! GPS no fix available at capture time
! S12 assumed = S21 (reciprocity); S22 not measured by this single forward-sweep hardware and set to 0
# Hz S RI R 50
1000000 1.000727e+00 -2.528906e-02 9.154901e-07 5.808659e-06 9.154901e-07 5.808659e-06 0.000000e+00 0.000000e+00
60980000 2.253148e-01 -9.952042e-01 1.315307e-05 -1.283828e-05 1.315307e-05 -1.283828e-05 0.000000e+00 0.000000e+00
…

With a fix, the second line becomes two lines: a list of column names, then the values. They are time, fix type, number of satellites, latitude, longitude, height, horizontal and vertical accuracy, ground speed, heading, and dead-reckoning fusion mode.

In the Touchstone format, lines that start with ! are comments.[2] Every file still opens in standard RF software, and the position travels inside the measurement it belongs to.

Note 7.3 (how closely the fix matches the sweep). At 5 fixes per second, the newest fix is never more than 0.2 s old when a sweep finishes, and a 51-point sweep takes 0.17 s. So a sweep and its fix are at most about 0.2 s apart, plus the short delay for the fix to arrive over the serial port. Moving at speed \(v\), that is a position mismatch of roughly

$$\Delta x \approx v \times 0.2\ \text{s}: \quad 0.3\ \text{m walking},\qquad 2\ \text{m for a drone at 10 m/s}. \tag{7.1}$$

Output 7.4. A 30-second run with the LiteVNA and the interval set to 1 s, August 26, 2026, on a computer with no GPS attached:

Searching for NanoVNA...
[GPS] searching... (Could not configure port: (5, 'Input/output error'))
[VNA] connected on /dev/ttyACM0: FW 2.2  HW variant 2 rev 2
[capture 1] captures/capture_20260826_081717_721.s2p  (no GPS fix yet, 2.24/s)
[capture 2] captures/capture_20260826_081718_723.s2p  (no GPS fix yet, 1.38/s)
…
[capture 32] captures/capture_20260826_081748_752.s2p  (no GPS fix yet, 1.02/s)

It wrote 32 files, every one with all 51 points, settling to 1.02 files per second. It also shows the logger doing what it should with no GPS: it says so plainly in every file and on screen, and keeps recording the VNA.

What the recorded files show

142capture files from three Record runs, every one with all 51 points
5.85/sat full speed: 98 files in 16.6 s, one per sweep, 0.171 s apart
0.55 dBthe most \(|S_{11}|\) varied at any frequency across all 142 sweeps

\(|S_{11}|\) with the port open, 142 sweeps: median line, lowest-to-highest band (uncalibrated)

0-2-4-6050010001500200025003000 Frequency (MHz) |S₁₁| (dB)
Every sweep the LiteVNA recorded through Record on August 26, 2026, 1 MHz–3 GHz, with nothing attached to the port. The curve's shape is the analyzer's own uncalibrated response, not the open port's (Note 3.5). The point is how thin the band is: sweep after sweep, the logger records the same thing. Downloads: all 142 capture files (zip), per-frequency summary (CSV).
Show the numbers as a table
MHzMedian (dB)Lowest (dB)Highest (dB)
1.00+0.009+0.005+0.013
60.98+0.173+0.165+0.180
120.96-0.156-0.161-0.152
180.94+0.054+0.046+0.062
240.92-0.023-0.028-0.000
300.90-0.372-0.376-0.344
360.88-0.024-0.030+0.003
420.86+0.415+0.407+0.502
480.84+0.132+0.126+0.281
540.82-0.885-0.891-0.767
600.80-1.099-1.104-1.022
660.78-0.021-0.029+0.054
720.76+0.654+0.646+0.755
780.74-0.478-0.484-0.324
840.72-2.594-2.605-2.404
900.70-1.966-1.973-1.886
960.68-0.027-0.036+0.000
1020.66+0.341+0.327+0.398
1080.64-1.981-1.989-1.877
1140.62-3.860-3.868-3.782
1200.60-2.214-2.220-2.196
1260.58-0.651-0.656-0.581
1320.56-2.127-2.136-1.817
1380.54-5.773-5.786-5.264
1440.52-6.032-6.041-5.695
1500.50-3.073-3.080-2.861
1560.48-1.882-1.886-1.685
1620.46-2.880-2.890-2.670
1680.44-5.765-5.774-5.449
1740.42-5.373-5.393-5.097
1800.40-2.430-2.447-2.262
1860.38-1.476-1.486-1.357
1920.36-2.740-2.755-2.597
1980.34-5.011-5.037-4.840
2040.32-3.064-3.075-3.001
2100.30-1.162-1.171-1.077
2160.28-1.382-1.386-1.249
2220.26-3.433-3.449-3.164
2280.24-5.155-5.166-4.898
2340.22-2.508-2.522-2.396
2400.20-1.180-1.193-1.052
2460.18-1.680-1.689-1.469
2520.16-4.428-4.452-4.034
2580.14-4.699-4.714-4.541
2640.12-1.795-1.996-1.667
2700.10-0.841-0.859-0.665
2760.08-2.067-2.083-1.693
2820.06-5.510-5.543-4.997
2880.04-3.236-3.254-3.161
2940.02-0.734-0.748-0.624
3000.00-0.642-0.682-0.244

Status: the VNA half is tested on hardware. The GPS half of every file stays empty until the GPS link works (Log 6).

Tested on hardware

The other tools

Not every job needs a file per sweep. These three tools use the same drivers for other everyday needs.

ToolNeed it servesWhat it does
vna_measure.py"I just want one sweep, a plot and a file."Connects, sweeps once, saves a .s1p or .s2p file and shows a plot. Options: --start, --stop, --points, --format, --output, --no-plot, --port.
vna_live.py"I want to watch one or more analyzers live."Runs one worker thread per analyzer, each rewriting its own file, captures/live_<device-id>.s2p, after every sweep. Shows a live plot. Analyzers can be unplugged and plugged back in at any time. --no-plot gives the fastest rate.
gps_live.py"I want a GPS track on its own."Appends every fix as a row of a CSV file, captures/gps_<time>.csv, saving after each row so a crash loses nothing. If no message arrives it says so on screen, so a silent receiver never looks like a hang.

Output 8.1 (two analyzers at once). With vna_live.py running a LiteVNA and a NanoVNA-H together, the LiteVNA held 5.86 sweeps per second, as fast as on its own, and the NanoVNA-H ran at 2.71. Each wrote its own file, with no interference.

Note 8.2 (a faster live plot). At first the plot only redrew once a second, because redrawing waited on the device check. Now the plot has its own refresh timer (PLOT_REFRESH_INTERVAL, 0.03 s) and only redraws an analyzer's graph when a new sweep has arrived. Matplotlib's faster "blitting" mode left ghost traces on this Linux setup, so the plot redraws in full.

Partly working

Setting up a Pi with no internet

Need 9.1. The Pi in the field has no internet. Copying the project folder over on a USB stick and running one command should be enough to make the Record icon work.

What broke, and what fixed it

What brokeWhyFix
The Python environment copied from the development PC didn't work on the PiIt's tied to the PC's processor type (x86-64, while the Pi is ARM), and FAT32/exFAT USB sticks strip the links inside it.Don't copy it. Install fresh on the Pi.
pip install failedpip downloads packages from the internet.offline_packages/: 15 package files downloaded ahead of time on a machine with internet, for the Pi's exact platform.
No matching distribution found for numpyThe first set was built for Python 3.10. The Pi runs 3.13, and these files must match the Python version exactly.Downloaded again for Python 3.13 on ARM64.
externally-managed-environmentThe new environment on the Pi was created without its own pip, so installs fell through to the system Python, which Debian protects.[8]Install straight into the system Python from the local files. record.sh now uses the system Python when there's no project environment.
The Record icon pointed at the wrong computerDesktop launcher files store full paths, so a copied one still points at the old machine's folder.install_launcher.sh rewrites Record.desktop for the folder it's in, makes the scripts runnable, and marks the icon as trusted.

The one command

Run from inside the project folder on the Pi:

python3 -m pip install --break-system-packages --no-index --find-links=offline_packages pyserial numpy matplotlib pyubx2 pynmeagps pyrtcm && chmod +x record.sh run_vna_live.sh install_launcher.sh && ./install_launcher.sh && python3 -c "import serial, numpy, matplotlib, pyubx2; print('READY')"

--no-index --find-links=offline_packages tells pip to install only from the local folder and never look online. The last part checks that every library imports and prints READY.

Status: the package files and the command are built for the Pi's Python 3.13. A successful run on the Pi isn't confirmed yet.

Planned

What's next

  1. Find the GPS module's serial speed. Try each common baud rate until clean messages appear, then set the code to match. Nothing on the GPS side can be tested until the module and the Pi agree on a speed.
  2. First position-tagged sweeps. Walk a known route with Record running, and check that every file holds a fix with its satellite count, accuracy and fix type.
  3. Save the GPS's own time in every file. The driver already reads the time stamp inside each GPS message, but doesn't save it yet. Saving it next to the Pi's clock would measure the serial delay in Note 7.3.
  4. Add calibration. Measure a short, an open and a 50 Ω load at the analyzer's connector, and correct every sweep with them, so the files hold calibrated values instead of raw ratios.
  5. An analysis tool for a folder of captures. Read every capture_*.s2p in a folder and produce one table (time, latitude, longitude, fix type, and \(|S_{11}|\) and \(|S_{21}|\) at chosen frequencies), plus a map. Recording is now easy. This is the step that makes the analysis easy too.
  6. Test more analyzers. The drivers should also handle the NanoVNA-H4, NanoVNA-V2 and LiteVNA-64, but we haven't tried them yet.
Open question

Join the conversation

We'd rather be corrected early than be wrong in public later. If you use NanoVNAs or GPS in the field, know a better way to do any of this, or have hit the same problems, send it. Students, hams, engineers and skeptics are all welcome.

Questions we'd like help with

  • What would you want out of a folder of position-tagged sweeps: a map, a table, something else?
  • Has anyone else seen a NanoVNA-F V2 return a partial sweep padded with zeros? Is there firmware that fixes it?
  • Which other NanoVNA models should we test?
  • How do you keep a VNA calibration valid in the field, when the cables flex as it moves?
  • What's the most reliable way to find a u-blox receiver's serial speed?

Messages come to us directly and aren't published.

Message sent. Thank you. If you left an email, we'll reply there.

You can also email Hexapole6@proton.me or Eodymy1@proton.me, or comment on any video on our YouTube channel.