Skip to content

LC-Linkous/nanoVNA_python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

46 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nanoVNA_python

simple non-GUI Python interfacing and data saving for the NanoVNA. Includes examples

AN UNOFFICIAL Python Library for the NanoVNA Device Series

A Non-GUI Python API class for the NanoVNA series of devices. This repository uses official resources and documentation but is NOT endorsed by the official NanoVNA product or company. See the references section for further reading. See the official NanoVNA resources and the active user group for device features.

There also exists several officially recognized resources:

This library, nanoVNA_python, is a non-GUI based library with access to low-level interfacing. It has been written as a companion to the tinySA_python library. Even though there is a lot of similarities between the devices, the library GitHub repositories are separate in order to make it clear which examples, tips, and documentation go to which device.

This library covers most of the documented commands for the NanoVNA device series. The documentation is sorted based on the serial command, with some provided usage examples. While some error checking exists in both the device and the library, it is not exhaustive. It is strongly suggested to read the official documentation before attempting to script with your device.

Done:

  • examples for common use and functionality
  • documentation for original command usage and library functions
  • some Debian-flavored Linux testing

Working on it:

  • filling in unfinished args and any new NanoVNA features
    • Ranges and device specs need to be added (or at least changable) for error detection.
  • An argparse option + some example scripts
  • Beginner notes, vocab, and some examples for common usage

Table of Contents

The NanoVNA Series of Devices

The NanoVNA line of devices are a series of portable and pretty user-friendly vector network analyzer devices. There are several devices with different frequency ranges, so refer to official documentation to select one for your needs. There are also some very convincing knock-off devices, so ensure that you are purchasing an actual device from a reputable vendor.

This device is often compared to the [tinySA series of devices]https://tinysa.org/. The NanoVNA series is a handheld vector network analyzer (VNA), which measures the S-parameters (loosely: a type of response of a device or antenna) over at different frequencies, while a spectrum analyzer measures the amplitude of RF signals at different frequencies. There's a lot of overlap with the use of both devices, but the measurements are very different. A signal generator (one of the features of the tinySA) is exactly what it sounds like - it generates a signal at a specific frequency or frequencies at a specified power level.

Official documentation can be found at https://nanovna.com/. The official Wiki is going to be more up to date than this repo with new versions and features, and they also have links to GUI-based software. Several community projects also exist on GitHub.

There is also a very active NanoVNA community at https://groups.io/g/nanovna-users/ exploring the device capabilities and its many features.

The end of this README will have some references and links to supporting material, but it is STRONGLY suggested to do some basic research and become familiar with your device before attempting to script or write code for it.

Requirements

This project requires numpy, pandas and pyserial.

Use 'pip install -r requirements.txt' to install the following dependencies:

pyserial
numpy
pandas

The above dependencies are only for the API interfacing of the nanoSA_python library. Additional dependencies should be installed if you are following the examples in this README. These can be installed with 'pip install -r test_requirements.txt':

pyserial
numpy
pandas
matplotlib
pillow
pyQt5

For anyone unfamiliar with using requirements files, or having issues with the libraries, these can also be installed manually in the terminal (we recommend a Python virtual environment) with:

pip install pyserial numpy pandas matplotlib pillow pyQt5

pyQt5 is used with matplotlib to draw the figures. pyQT5 needs to be installed in Linux systems to follow the examples included in nanoVNA_python, but is not needed on all Windows machines. Install both if you have doubts; they're small packages and commonly used.

Library Usage

This library is currently only available as the NanoVNA class in 'nanoVNA_python.py' in this repository. It is very much under development and missing some key error checking and handling. HOWEVER, ‘any’ error checking is currently more than the ‘no’ error checking provided by interfacing directly with the device. The code that is included in this repository has been tested on at least one NanoVNA device and is relatively stable.

Several usage examples are provided in the Example Implementations section, including working with the hardware and plotting results with matplotlib.

Error Handling

Some error handling has been implemented for the individual functions in this library, but not for the device configuration. Most functions have a list of acceptable formats for input, which is included in the documentation and the library_help function. The nanoVNA_help function will get output from the current version of firmware running on the connected tinySA device.

Detailed error messages can be returned by toggling 'verbose' on.

Example Implementations

This library was developed on Windows and has been lightly tested on Linux. The main difference (so far) has been in the permissions for first access of the serial port, but there may be smaller bugs in format that have not been detected yet.

Finding the Serial Port

To start, a serial connection between the NanoVNA and user PC device must be created. There are several ways to list available serial ports. The library supports some rudimentary autodetection, but if that does not work instructions in this section also support manual detection.

Autoconnection with the nanoVNA_python Library

The nanoVNA_python currently has some autodetection capabilities, but these are new and not very complex. If multiple devices have the same VID, then the first one found is used. If you are connecting multiple devices to a user PC, then it is suggested to connect them manually (for now). The NanoVNA and tinySA devices have the same VID and hardware identification for the serial ports.

# import nanoVNA library
# (NOTE: check library path relative to script path)
from src.nanoVNA_python import nanoVNA 

# create a new nanoVNA object    
nvna = nanoVNA()

# set the return message preferences 
nvna.set_verbose(True) #detailed messages
nvna.set_error_byte_return(True) #get explicit b'ERROR' if error thrown


# attempt to autoconnect
found_bool, connected_bool = nvna.autoconnect()

# if port found and connected, then complete task(s) and disconnect
if connected_bool == True: 
    print("device connected")

    msg = nvna.get_info() 
    print(msg)
    

    nvna.disconnect()
else:
    print("ERROR: could not connect to port")

Manually Finding a Port on Windows

  1. Open Device Manager, scroll down to Ports (COM & LPT), and expand the menu. There should be a COM# port listing "USB Serial Device(COM #)". If your NanoVNA is set up to work with Serial, this will be it.

  2. This uses the pyserial library requirement already installed for this library. It probably also works on Linux systems, but has not been tested yet.

import serial.tools.list_ports

ports = serial.tools.list_ports.comports()

for port, desc, hwid in ports:
    print(f"Port: {port}, Description: {desc}, Hardware ID: {hwid}")

Example output for this method (on Windows) is as follows:

Port: COM4, Description: Standard Serial over Bluetooth link (COM4), Hardware ID: BTHENUM\{00001101-0000-1000-8000-00805F9B34FB}_LOCALMFG&0000\7&D0D1EE&0&000000000000_00000000
Port: COM3, Description: Standard Serial over Bluetooth link (COM3), Hardware ID: BTHENUM\{00001101-0000-1000-8000-00805F9B34FB}_LOCALMFG&0002\7&D0D1EE&0&B8B3DC31CBA8_C00000000
Port: COM22, Description: USB Serial Device (COM10), Hardware ID: USB VID:PID=0483:5740 SER=400 LOCATION=1-3

"COM22" is the port location of the NanoVNA that is used in the examples in this README.

Manually Finding a Port on Linux

import serial.tools.list_ports

ports = serial.tools.list_ports.comports()

for port, desc, hwid in ports:
    print(f"Port: {port}, Description: {desc}, Hardware ID: {hwid}")
TO BE ADDED

This method identified the /dev/ttyACM0. Now, when attempting to use the autoconnection feature, the following error was initially returned:

[Errno 13] could not open port /dev/ttyACM0: [Errno 13] Permission denied: '/dev/ttyACM0'

This was due to not having permission to access the port. In this case, this error was solved by opening a terminal and executing sudo chmod a+rw /dev/ttyACM0. Should this issue be persistent, other solutions related to user groups and access will need to be investigated.

Serial Message Return Format

This library returns strings as cleaned byte arrays. The command and first \r\n pair are removed from the front, and the ch> is removed from the end of the NanoVNA serial return.

The original message format:

bytearray(b'info\r\nModel:        NanoVNA-F_V2\r\nFrequency:    50k ~ 3GHz\r\nBuild time:   Mar  2 2021 - 09:40:50 CST\r\nch> \r\n')

Cleaned version:

bytearray(b'Model:        NanoVNA-F_V2\r\nFrequency:    50k ~ 3GHz\r\nBuild time:   Mar  2 2021 - 09:40:50 CST\r')

Connecting and Disconnecting the Device

This example shows the process for initializing, opening the serial port, getting device info, and disconnecting.

# import nanoVNA library
# (NOTE: check library path relative to script path)
from src.nanoVNA_python import nanoVNA 

# create a new nanoVNA object    
nvna = nanoVNA()

# set the return message preferences 
nvna.set_verbose(True) #detailed messages
nvna.set_error_byte_return(True) #get explicit b'ERROR' if error thrown


# attempt to autoconnect
found_bool, connected_bool = nvna.autoconnect()

# if port found and connected, then complete task(s) and disconnect
if connected_bool == True: 
    print("device connected")

    msg = nvna.get_info() 
    print(msg)
    

    nvna.disconnect()
else:
    print("ERROR: could not connect to port")

Example output for this method is as follows:

bytearray(b'Model:        NanoVNA-F_V2\r\nFrequency:    50k ~ 3GHz\r\nBuild time:   Mar  2 2021 - 09:40:50 CST\r')

Toggle Error Messages

Currently, the following can be used to turn on or off returned error messages.

  1. the 'verbose' option. When enabled, detailed messages are printed out.
# detailed messages are ON
nvna.set_verbose(True) 

# detailed messages are OFF
nvna.set_verbose(False) 
  1. the 'errorByte' option. When enabled, if there is an error with the command or configuration, b'ERROR' is returned instead of the default b''.
# when an error occurs, b'ERROR' is returned
nvna.set_error_byte_return(True) 

# when an error occurs, the default b'' might be returned
nvna.set_error_byte_return(False) 

Device and Library Help

The help return can be accessed via the help() function call. This will interface with the device directly and return functions based on the newest firmware, not information from the nanoVNA_python.py library.

nvna.help()

The help command returns bytearray in the format bytearray(b'commands:......')

Getting Data from Active Screen

See other sections for the following examples:

The most straight forward way to get data from an active screen is with the data command. This will pull data from an active screen. It will not adjust the range or number of points before a read. If the range needs to be adjusted prior to a read, use scan instead.

# import NanoVNA library
# (NOTE: check library path relative to script path)
from src.nanoVNA_python import nanoVNA 
import time
import serial

# create a new tinySA object    
nvna = nanoVNA()

# set the return message preferences 
nvna.set_verbose(True) #detailed messages
nvna.set_error_byte_return(True) #get explicit b'ERROR' if error thrown


# attempt to autoconnect
found_bool, connected_bool = nvna.autoconnect()

# if port closed, then return error message
if connected_bool == False:
    print("ERROR: could not connect to port")
else: # if port found and connected, then complete task(s) and disconnect

    # DATA gets the data on the screen
    # get the S11 data
    s11 = nvna.get_s11_data()
    print(s11)
    # get the S21 data
    s21 = nvna.get_s21_data()
    print(s21)

    nvna.resume() #resume 

    nvna.disconnect()

Analysis of the Returned Data from the NanoVNA

This first example shows how to get measured data on the screen (using data) or to specify the read range and then measure with scan. Data returned will always be in a bytearray, but it will need to be converted in order to work with it.

# import NanoVNA library
# (NOTE: check library path relative to script path)
from src.nanoVNA_python import nanoVNA 
import time
import serial

# create a new tinySA object    
nvna = nanoVNA()

# set the return message preferences 
nvna.set_verbose(True) #detailed messages
nvna.set_error_byte_return(True) #get explicit b'ERROR' if error thrown


# attempt to autoconnect
found_bool, connected_bool = nvna.autoconnect()

# if port closed, then return error message
if connected_bool == False:
    print("ERROR: could not connect to port")
else: # if port found and connected, then complete task(s) and disconnect

    # set up some parameters for the scan
    # NanoVNA takes freq in Hz, as ints
    start = int(1e9) # 1 GHz, as an int. 
    stop = int(3e9)  # 3 GHz, as an int.
    # max number of points is 200, UP TO 201
    pts = 200

    # SCAN can change range and number of pts
    # get the frequency valuess (the Y Axis of the screen)
    freq = nvna.get_scan_frequencies(start, stop, pts)
    print(freq)
    # get the S11 data
    s11 = nvna.get_scan_s11(start, stop, pts)
    print(s11)
    # get the S21 data
    s21 = nvna.get_scan_s21(start, stop, pts)
    print(s21)

    # DATA gets the data on the screen
    # get the S11 data
    s11 = nvna.get_s11_data()
    print(s11)
    # get the S21 data
    s21 = nvna.get_s21_data()
    print(s21)

    nvna.resume() #resume 

    nvna.disconnect()

The requested frequencies are in the following format:

bytearray(b'1000000 \r\n1020000 \r\n1040000 \r\n1060000 \r\n1080000 \r\n1100000 \r\n1120000 \r\n1140000 \r\n1160000 \r\n1180000 \r\n1200000 \r\n1220000 \r\n1240000 \r\n1260000 \r\n1280000 \r\n1300000 \r\n1320000 \r\n1340000 \r\n1360000 \r\n1380000 \r\n1400000 \r\n1420000 \r\n1440000 \r\n1460000 \r\n1480000 \r\n1500000 \r\n1520000 \r\n1540000 \r\n1560000 \r\n1580000 \r\n1600000 \r\n1620000 \r\n1640000 \r\n1660000 \r\n1680000 \r\n1700000 \r\n1720000 \r\n1740000 \r\n1760000 \r\n1780000 \r\n1800000 \r\n1820000 \r\n1840000 \r\n1860000 \r\n1880000 \r\n1900000 \r\n1920000 \r\n1940000 \r\n1960000 \r\n1980000 \r\n2000000 \r\n2020000 \r\n2040000 \r\n2060000 \r\n2080000 \r\n2100000 \r\n2120000 \r\n2140000 \r\n2160000 \r\n2180000 \r\n2200000 \r\n2220000 \r\n2240000 \r\n2260000 \r\n2280000 \r\n2300000 \r\n2320000 \r\n2340000 \r\n2360000 \r\n2380000 \r\n2400000 \r\n2420000 \r\n2440000 \r\n2460000 \r\n2480000 \r\n2500000 \r\n2520000 \r\n2540000 \r\n2560000 \r\n2580000 \r\n2600000 \r\n2620000 \r\n2640000 \r\n2660000 \r\n2680000 \r\n2700000 \r\n2720000 \r\n2740000 \r\n2760000 \r\n2780000 \r\n2800000 \r\n2820000 \r\n2840000 \r\n2860000 \r\n2880000 \r\n2900000 \r\n2920000 \r\n2940000 \r\n2960000 \r\n2980000 \r\n3000000 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r\n0 \r')

These frequencies represent where in the frequency range the measurements have been taken, and are returned in kHz. That is, the last reading before the padding 0-s start is '3000000'. This value is actually '3000000e3', or 3 GHz, not 3 MHz.

Returned S11 and S21 data are in the format:

bytearray(b'0.414528 0.623509 \r\n0.512547 0.542835 \r\n0.552637 0.489537 \r\n0.602180 0.444314 \r\n0.674851 0.374883 \r\n0.721209 0.316875 \r\n0.770192 0.216239 \r\n0.790564 0.140702 \r\n0.804490 0.037844 \r\n0.802676 -0.086580 \r\n0.784412 -0.188284 \r\n0.757563 -0.260674 \r\n0.718249 -0.342237 \r\n0.664428 -0.416341 \r\n0.617326 -0.480844 \r\n0.568474 -0.533208 \r\n0.492809 -0.581321 \r\n0.460760 -0.616593 \r\n0.384558 -0.668559 \r\n0.314043 -0.682421 \r\n0.245858 -0.719574 \r\n0.194356 -0.714597 \r\n0.118920 -0.738018 \r\n0.066108 -0.737820 \r\n0.010847 -0.744120 \r\n-0.045027 -0.754767 \r\n-0.101323 -0.761777 \r\n-0.172184 -0.763956 \r\n-0.248799 -0.752455 \r\n-0.322770 -0.734302 \r\n-0.386569 -0.712150 \r\n-0.465589 -0.678363 \r\n-0.538067 -0.640787 \r\n-0.614912 -0.575165 \r\n-0.675761 -0.518579 \r\n-0.719431 -0.459212 \r\n-0.762334 -0.381576 \r\n-0.804731 -0.287837 \r\n-0.828170 -0.205799 \r\n-0.847075 -0.126603 \r\n-0.857135 -0.029673 \r\n-0.849052 0.066982 \r\n-0.834792 0.182195 \r\n-0.805081 0.263328 \r\n-0.756838 0.355655 \r\n-0.699059 0.442736 \r\n-0.627638 0.513114 \r\n-0.526945 0.592252 \r\n-0.424149 0.632364 \r\n-0.313906 0.655601 \r\n-0.181712 0.653672 \r\n-0.125724 0.615638 \r\n-0.053355 0.591899 \r\n0.009786 0.579131 \r\n0.073450 0.570709 \r\n0.166064 0.538408 \r\n0.250542 0.481270 \r\n0.324851 0.387106 \r\n0.371485 0.280715 \r\n0.392284 0.164942 \r\n0.372584 0.048407 \r\n0.321884 -0.054561 \r\n0.248533 -0.125397 \r\n0.153565 -0.178265 \r\n0.061536 -0.194453 \r\n-0.013059 -0.182995 \r\n-0.085104 -0.155251 \r\n-0.138454 -0.125033 \r\n-0.184962 -0.078549 \r\n-0.225650 -0.029733 \r\n-0.239032 0.054426 \r\n-0.229694 0.126419 \r\n-0.195660 0.184412 \r\n-0.150762 0.222993 \r\n-0.089336 0.251350 \r\n-0.039771 0.261744 \r\n0.009027 0.261116 \r\n0.065524 0.260008 \r\n0.110400 0.234943 \r\n0.140959 0.225260 \r\n0.193066 0.202232 \r\n0.260522 0.159201 \r\n0.309473 0.106962 \r\n0.343722 0.029135 \r\n0.362852 -0.053262 \r\n0.367468 -0.136931 \r\n0.371292 -0.227675 \r\n0.363786 -0.314572 \r\n0.318734 -0.411707 \r\n0.260611 -0.519830 \r\n0.184881 -0.579277 \r\n0.113375 -0.670024 \r\n-0.006515 -0.709672 \r\n-0.088562 -0.746757 \r\n-0.209147 -0.763733 \r\n-0.318236 -0.751811 \r\n-0.424684 -0.736606 \r\n-0.525702 -0.713903 \r\n-0.608816 -0.662751 \r\n-0.673664 -0.612937 \r\n-0.737695 -0.554410 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r')

The last value before the padding is -0.737695 -0.554410. The first number is the real part of the signal, and the second number is the imaginary part of the signal. All signals are returned in two parts for each measurement.

Saving Screen Images

The capture() function can be used to capture the screen and output it to an image file. Note that the screen size varies by device. The library itself does not have a function for saving to an image (requires an additional library), but examples and the CLI wrapper have this functionality.

This example truncates the last hex value, so a single padding x00 value has been added. This will eventually be investigated, but it's not hurting the output right now.

# import NanoVNA library
# (NOTE: check library path relative to script path)
from src.nanoVNA_python import nanoVNA 

# imports FOR THE EXAMPLE
import numpy as np
from PIL import Image
import struct

def convert_data_to_image(data_bytes, width, height):
    # calculate the expected data size
    expected_size = width * height * 2  # 16 bits per pixel (BGR565), 2 bytes per pixel
    
    # error checking
    if len(data_bytes) < expected_size:
        print(f"Data size is too small. Expected {expected_size} bytes, got {len(data_bytes)} bytes.")
        if len(data_bytes) == expected_size - 1:
            print("Data size is 1 byte smaller than expected. Adding 1 byte of padding.")
            data_bytes.append(0)
        else:
            return
    elif len(data_bytes) > expected_size:
        data_bytes = data_bytes[:expected_size]
        print("Data is larger than the expected size. truncating. check data.")
    
    num_pixels = width * height
    
    # Unpack as little-endian 16-bit values
    x = struct.unpack(f"<{num_pixels}H", data_bytes)
    arr = np.array(x, dtype=np.uint32)
    

    # Convert RGB565 to RGBA
    # The NanoVNA uses BGR565 format.
    # This is a difference from the tinySA_python library which used RGB565. This is pulled out
    # into variables to make it clearer where/what the switch is.
    blue = ((arr & 0xF800) >> 11) * 255 // 31    # Blue in high bits (15-11)
    green = ((arr & 0x07E0) >> 5) * 255 // 63    # Green in middle bits (10-5)
    red = (arr & 0x001F) * 255 // 31             # Red in low bits (4-0)
    
    # Combine into RGBA format (Alpha = 255 for opaque)
    arr_rgba = 0xFF000000 + (red << 16) + (green << 8) + blue
    
    # reshape array to match the image dimensions
    arr_rgba = arr_rgba.reshape((height, width))
    
    # create and save the image
    img = Image.frombuffer('RGBA', (width, height), arr_rgba.tobytes(), 'raw', 'RGBA', 0, 1)
    img.save("example_screen_capture_demo.png")
    img.show()

# create a new tinySA object    
nvna = nanoVNA()

# set the return message preferences 
nvna.set_verbose(True) #detailed messages
nvna.set_error_byte_return(True) #get explicit b'ERROR' if error thrown


# attempt to autoconnect
found_bool, connected_bool = nvna.autoconnect()

# if port found and connected, then complete task(s) and disconnect
if connected_bool == True: 
    print("device connected")

    
    msg = nvna.get_info() 
    print(msg)
    
    # get the trace data
    data_bytes = nvna.capture() 
    # Printed out for fun. 
    # You do NOT need to print this to use it
    print(data_bytes)

    # disconnect device since we're not using it
    nvna.disconnect()

    # processing after disconnect (just for this example)
    # test with 800x480 resolution for NanoVNA-F V2
    convert_data_to_image(data_bytes, 800, 480) 

else:
    print("ERROR: could not connect to port")


Capture of On-screen Trace Data

Capture On-Screen Trace Data from 1 GHz to 3 GHzz

Plotting Data with Matplotlib

Example 1: Plot Trace Data

This example plots the last/current sweep of data from the NanoVNA device. scan() gets the trace data. byteArrayToNumArray(byteArr) takes in the returned trace data and frequency information and converts them to arrays that are then plotted using matplotlib

This example has 4 subplots because there is a lot of information returned with each sweep of the NanoVNA. The top, left plot shows the real and imaginary parts of the signal. This is the data as it is returned directly from the NanoVNA device. The top, right plot shows the calculated magnitude data. The bottom plots are the calculated phase response and Smith Chart, on the left and right, respectively.

# import NanoVNA library
# (NOTE: check library path relative to script path)
from src.nanoVNA_python import nanoVNA
# imports FOR THE EXAMPLE
import numpy as np
import matplotlib.pyplot as plt

def convert_s11_data_to_arrays(start, stop, pts, data):
    # Convert the raw device S11 data to frequency and S11 arrays.
    # given the format of the data, this is assuming the data 
    # contains PAIRS of values (real/imag or mag/phase).

    # Create frequency array
    freq_arr = np.linspace(start, stop, pts)
    
    # Parse data into pairs of values
    lines = data.decode('utf-8').split('\n')
    real_parts = []
    imag_parts = []
    
    for line in lines:
        if line.strip():  # Skip empty lines
            values = line.split()
            if len(values) >= 2:
                try:
                    real_val = float(values[0])
                    imag_val = float(values[1])
                    
                    # Skip zero pairs (padding data)
                    if real_val != 0.0 or imag_val != 0.0:
                        real_parts.append(real_val)
                        imag_parts.append(imag_val)
                except ValueError:
                    continue  # Skip malformed lines
    
    # Convert to numpy arrays
    real_arr = np.array(real_parts)
    imag_arr = np.array(imag_parts)
    
    # Calculate derived values
    # If data is real/imaginary components:
    magnitude_db = 20 * np.log10(np.sqrt(real_arr**2 + imag_arr**2))
    phase_deg = np.degrees(np.arctan2(imag_arr, real_arr))
    
    # Adjust frequency array to match actual data length
    actual_pts = len(real_arr)
    if actual_pts != pts:
        freq_arr = np.linspace(start, stop, actual_pts)
    
    return freq_arr, real_arr, imag_arr, magnitude_db, phase_deg



# create a new tinySA object    
nvna = nanoVNA()
# set the return message preferences
nvna.set_verbose(True) # detailed messages
nvna.set_error_byte_return(True) # get explicit b'ERROR' if error thrown

# attempt to autoconnect
found_bool, connected_bool = nvna.autoconnect()

# if port closed, then return error message
if connected_bool == False:
    print("ERROR: could not connect to port")
else: # if port found and connected, then complete task(s) and disconnect
    # set scan values
    start = int(1e9)  # 1 GHz
    stop = int(3e9)   # 3 GHz
    pts = 200         # sample points. MAX 201
    outmask = 2       # get measured data (y axis)
    
    # scan
    data_bytes = nvna.scan(start, stop, pts, outmask)
    print("Raw data received:")
    print(data_bytes)
    
    nvna.resume() # resume so screen isn't still frozen
    nvna.disconnect()
    
    # processing after disconnect
    # This is typical for the examples, but does not need to be done
    # if you are still using the device or collecting data.


    # convert data to arrays
    freq_arr, real_arr, imag_arr, magnitude_db, phase_deg = convert_s11_data_to_arrays(start, stop, pts, data_bytes)
    
    # Create subplots for comprehensive S11 visualization
    # this is different from the tinySA plots, which only showed the frequency data overlapped
    # because we are collecting more data with each sweep. 
    # Data has been sorted into 4 plots
     # The Antenna used in data collection is a 2.4 GHz monopole

    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(12, 10))
    
    # Plot 1: Real and Imaginary parts
    ax1.plot(freq_arr/1e9, real_arr, 'b-', label='Real', linewidth=1.5)
    ax1.plot(freq_arr/1e9, imag_arr, 'r-', label='Imaginary', linewidth=1.5)
    ax1.set_xlabel("Frequency (GHz)")
    ax1.set_ylabel("S11 Components")
    ax1.set_title("S11 Real and Imaginary Components")
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    
    # Plot 2: Magnitude in dB
    ax2.plot(freq_arr/1e9, magnitude_db, 'g-', linewidth=1.5)
    ax2.set_xlabel("Frequency (GHz)")
    ax2.set_ylabel("S11 Magnitude (dB)")
    ax2.set_title("S1 Magnitude Response")
    ax2.grid(True, alpha=0.3)
    
    # Plot 3: Phase
    ax3.plot(freq_arr/1e9, phase_deg, 'm-', linewidth=1.5)
    ax3.set_xlabel("Frequency (GHz)")
    ax3.set_ylabel("S11 Phase (degrees)")
    ax3.set_title("S11 Phase Response")
    ax3.grid(True, alpha=0.3)
    
    # Plot 4: Smith Chart representation (simplified)
    ax4.scatter(real_arr, imag_arr, c=freq_arr/1e9, cmap='viridis', s=20)
    ax4.set_xlabel("Real Part")
    ax4.set_ylabel("Imaginary Part")
    ax4.set_title("S11 Complex Plane (Simplified Smith Chart)")
    ax4.grid(True, alpha=0.3)
    ax4.axis('equal')
    
    # Add colorbar for frequency reference
    cbar = plt.colorbar(ax4.collections[0], ax=ax4)
    cbar.set_label('Frequency (GHz)')
    
    plt.tight_layout()
    plt.show()
    
    # Print summary statistics
    print(f"\nData Summary:")
    print(f"Number of valid data points: {len(real_arr)}")
    print(f"Frequency range: {freq_arr[0]/1e9:.3f} - {freq_arr[-1]/1e9:.3f} GHz")
    print(f"S_{11} Magnitude range: {np.min(magnitude_db):.2f} to {np.max(magnitude_db):.2f} dB")
    print(f"S_{11} Phase range: {np.min(phase_deg):.1f} to {np.max(phase_deg):.1f} degrees")

Plot of On-screen Trace Data

Plotted On-Screen Trace Data of a Frequency Sweep from 1 GHz to 3 GHz

Example 2: Plot a Static Waterfall using SCAN and Calculated Frequencies

This example uses the scan() read to get the data over a specified number of reads and then display it in the four plots described in Example 1, above. Data is exported to a specified .csv for logging. The scan can be interrupted at any time in the terminal (typically ctrl + C).

# import nanoVNA library
# (NOTE: check library path relative to script path)
from src.nanoVNA_python import nanoVNA

# imports FOR THE EXAMPLE
import csv
import numpy as np
import matplotlib.pyplot as plt
import time
from datetime import datetime

def convert_s11_data_to_arrays(start, stop, pts, data):
    # Convert the raw device S11 data to frequency and S11 arrays.
    # given the format of the data, this is assuming the data 
    # contains PAIRS of values (real/imag or mag/phase).
    # Create frequency array
    freq_arr = np.linspace(start, stop, pts)
    
    # Parse data into pairs of values
    lines = data.decode('utf-8').split('\n')
    real_parts = []
    imag_parts = []
    
    for line in lines:
        if line.strip():  # Skip empty lines
            values = line.split()
            if len(values) >= 2:
                try:
                    real_val = float(values[0])
                    imag_val = float(values[1])
                    
                    # Skip zero pairs (padding data)
                    if real_val != 0.0 or imag_val != 0.0:
                        real_parts.append(real_val)
                        imag_parts.append(imag_val)
                except ValueError:
                    continue  # Skip malformed lines
    
    # Convert to numpy arrays
    real_arr = np.array(real_parts)
    imag_arr = np.array(imag_parts)
    
    # Calculate derived values
    magnitude_db = 20 * np.log10(np.sqrt(real_arr**2 + imag_arr**2))
    phase_deg = np.degrees(np.arctan2(imag_arr, real_arr))
    
    # Adjust frequency array to match actual data length
    actual_pts = len(real_arr)
    if actual_pts != pts:
        freq_arr = np.linspace(start, stop, actual_pts)
    
    return freq_arr, real_arr, imag_arr, magnitude_db, phase_deg

def collect_s11_waterfall_data(nvna, start, stop, pts, outmask, num_scans, scan_interval):
    # collects the scans for the waterfall plot

    waterfall_real = []      # 2D array for real components
    waterfall_imag = []      # 2D array for imaginary components  
    waterfall_magnitude = [] # 2D array for magnitude in dB
    waterfall_phase = []     # 2D array for phase in degrees
    timestamps = []
    freq_arr = None
    
    print(f"Collecting {num_scans} S11 scans with {scan_interval}s intervals...")
    
    for i in range(num_scans):
        print(f"Scan {i+1}/{num_scans}")
        
        # Perform scan
        data_bytes = nvna.scan(start, stop, pts, outmask)
        
        # Convert to arrays
        if freq_arr is None:
            freq_arr, real_arr, imag_arr, mag_arr, phase_arr = convert_s11_data_to_arrays(start, stop, pts, data_bytes)
        else:
            _, real_arr, imag_arr, mag_arr, phase_arr = convert_s11_data_to_arrays(start, stop, pts, data_bytes)
        
        # Store data and timestamp
        waterfall_real.append(real_arr)
        waterfall_imag.append(imag_arr)
        waterfall_magnitude.append(mag_arr)
        waterfall_phase.append(phase_arr)
        timestamps.append(datetime.now())
        
        # Wait before next scan (except for last scan)
        if i < num_scans - 1:
            time.sleep(scan_interval)
    
    return (freq_arr, 
            np.array(waterfall_real), 
            np.array(waterfall_imag),
            np.array(waterfall_magnitude), 
            np.array(waterfall_phase), 
            timestamps)

def plot_s11_waterfall(freq_arr, waterfall_real, waterfall_imag, waterfall_magnitude, waterfall_phase, timestamps, start, stop):
    # Create figure with subplots
    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
    
    # Create time array for y-axis
    time_arr = np.arange(len(timestamps))
    freq_mesh, time_mesh = np.meshgrid(freq_arr, time_arr)
    
    # Plot 1: S11 Magnitude waterfall
    im1 = ax1.pcolormesh(freq_mesh/1e9, time_mesh, waterfall_magnitude, 
                        shading='nearest', cmap='viridis')
    ax1.set_xlabel('Frequency (GHz)')
    ax1.set_ylabel('Scan Number')
    ax1.set_title(f'S11 Magnitude Waterfall: {start/1e9:.1f} - {stop/1e9:.1f} GHz')
    cbar1 = plt.colorbar(im1, ax=ax1)
    cbar1.set_label('S11 Magnitude (dB)')
    
    # Plot 2: S11 Phase waterfall
    im2 = ax2.pcolormesh(freq_mesh/1e9, time_mesh, waterfall_phase, 
                        shading='nearest', cmap='plasma')
    ax2.set_xlabel('Frequency (GHz)')
    ax2.set_ylabel('Scan Number')
    ax2.set_title('S11 Phase Waterfall')
    cbar2 = plt.colorbar(im2, ax=ax2)
    cbar2.set_label('S11 Phase (degrees)')
    
    # Plot 3: Latest S11 Magnitude scan
    ax3.plot(freq_arr/1e9, waterfall_magnitude[-1], 'b-', linewidth=1.5)
    ax3.set_xlabel('Frequency (GHz)')
    ax3.set_ylabel('S11 Magnitude (dB)')
    ax3.set_title('Latest S11 Magnitude Scan')
    ax3.grid(True, alpha=0.3)
    
    # Plot 4: Latest S11 Phase scan
    ax4.plot(freq_arr/1e9, waterfall_phase[-1], 'r-', linewidth=1.5)
    ax4.set_xlabel('Frequency (GHz)')
    ax4.set_ylabel('S11 Phase (degrees)')
    ax4.set_title('Latest S11 Phase Scan')
    ax4.grid(True, alpha=0.3)
    
    plt.tight_layout()
    return fig

# create a new nanoVNA object    
nvna = nanoVNA()
# set the return message preferences
nvna.set_verbose(True) # detailed messages
nvna.set_error_byte_return(True) # get explicit b'ERROR' if error thrown

# attempt to autoconnect
found_bool, connected_bool = nvna.autoconnect()

# if port closed, then return error message
if connected_bool == False:
    print("ERROR: could not connect to port")
else: # if port found and connected, then complete task(s) and disconnect
    try:
        # set scan values
        start = int(1e9)  # 1 GHz
        stop = int(3e9)   # 3 GHz
        pts = 200         # sample points
        outmask = 2       # get measured data (y axis)
        
        # waterfall parameters
        num_scans = 20        # number of scans to collect
        scan_interval = 1.0   # seconds between scans
        
        # collect waterfall data
        (freq_arr, waterfall_real, waterfall_imag, 
         waterfall_magnitude, waterfall_phase, timestamps) = collect_s11_waterfall_data(
            nvna, start, stop, pts, outmask, num_scans, scan_interval)
        
        print("S11 data collection complete!")
        
        # resume and disconnect
        nvna.resume() # resume so screen isn't still frozen
        nvna.disconnect()
        
        # processing after disconnect
        print("Creating S11 waterfall plots...")
        
        # create waterfall plot
        fig = plot_s11_waterfall(freq_arr, waterfall_real, waterfall_imag, 
                                waterfall_magnitude, waterfall_phase, timestamps, start, stop)
        
        # Save data to CSV
        filename = "s11_waterfall_sample.csv"
        
        # Create CSV with comprehensive S11 data
        with open(filename, 'w', newline='') as csvfile:
            writer = csv.writer(csvfile)
            
            # Write header row
            header = ['Scan_Number', 'Timestamp']
            for freq in freq_arr:
                header.extend([f'{freq:.0f}_Real', f'{freq:.0f}_Imag', 
                              f'{freq:.0f}_Mag_dB', f'{freq:.0f}_Phase_deg'])
            writer.writerow(header)
            
            # Write data rows
            for i in range(len(timestamps)):
                row = [i+1, timestamps[i].strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]]
                for j in range(len(freq_arr)):
                    row.extend([
                        f'{waterfall_real[i][j]:.6f}',
                        f'{waterfall_imag[i][j]:.6f}',
                        f'{waterfall_magnitude[i][j]:.3f}',
                        f'{waterfall_phase[i][j]:.2f}'
                    ])
                writer.writerow(row)
        
        print(f"S11 waterfall data saved to {filename}")
        print(f"CSV contains {len(timestamps)} scans with {len(freq_arr)} frequency points each")
        print(f"Each frequency point includes: Real, Imaginary, Magnitude (dB), Phase (deg)")
        
        # Statistics
        print(f"\nScan Statistics:")
        print(f"Frequency range: {freq_arr[0]/1e9:.3f} - {freq_arr[-1]/1e9:.3f} GHz")
        print(f"S11 Magnitude range: {np.min(waterfall_magnitude):.2f} to {np.max(waterfall_magnitude):.2f} dB")
        print(f"S11 Phase range: {np.min(waterfall_phase):.1f} to {np.max(waterfall_phase):.1f} degrees")
        
        # show plot
        plt.show()

    except KeyboardInterrupt:
        print("\nScan interrupted by user")
        nvna.resume()
        nvna.disconnect()
    except Exception as e:
        print(f"Error occurred: {e}")
        nvna.resume()
        nvna.disconnect()

Waterfall Plot for SCAN Data Over 20 Readings

Waterfall Plot for SCAN Data Over 20 Readings

Example 3: Plot a Realtime Waterfall using SCAN and Calculated Frequencies

This example uses the scan() read to get the data directly from the NanoVNA device. After each read, the four plots on the matplotlib figure are updated. The scan can be interrupted at any time by closing the figure window.

# import nanoVNA library
from src.nanoVNA_python import nanoVNA

# imports FOR THE EXAMPLE
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from collections import deque
import time
from datetime import datetime
import threading
import queue

def convert_s11_data_to_arrays(start, stop, pts, data):
    # Convert the raw device S11 data to frequency and S11 arrays.
    # given the format of the data, this is assuming the data 
    # contains PAIRS of values (real/imag or mag/phase).

    # Create frequency array
    freq_arr = np.linspace(start, stop, pts)
    
   
    # Parse data into pairs of values
    lines = data.decode('utf-8').split('\n')
    real_parts = []
    imag_parts = []
    
    for line in lines:
        if line.strip():
            values = line.split()
            if len(values) >= 2:
                try:
                    real_val = float(values[0])
                    imag_val = float(values[1])
                    
                    # Skip zero pairs (padding data)
                    if real_val != 0.0 or imag_val != 0.0:
                        real_parts.append(real_val)
                        imag_parts.append(imag_val)
                except ValueError:
                    continue
    
    # Convert to numpy arrays
    real_arr = np.array(real_parts)
    imag_arr = np.array(imag_parts)
    
    # Calculate derived values
    magnitude_db = 20 * np.log10(np.sqrt(real_arr**2 + imag_arr**2))
    phase_deg = np.degrees(np.arctan2(imag_arr, real_arr))
    
    # Adjust frequency array to match actual data length
    actual_pts = len(real_arr)
    if actual_pts != pts:
        freq_arr = np.linspace(start, stop, actual_pts)
    
    return freq_arr, real_arr, imag_arr, magnitude_db, phase_deg

class LiveS11Plotter:
    def __init__(self, nvna, start, stop, pts, outmask, max_history=50):
        self.nvna = nvna
        self.start = start
        self.stop = stop
        self.pts = pts
        self.outmask = outmask
        self.max_history = max_history
        
        # Data storage
        self.freq_arr = None
        self.magnitude_history = deque(maxlen=max_history)
        self.phase_history = deque(maxlen=max_history)
        self.real_history = deque(maxlen=max_history)
        self.imag_history = deque(maxlen=max_history)
        self.timestamps = deque(maxlen=max_history)
        
        # Threading for data acquisition
        self.data_queue = queue.Queue()
        self.running = False
        self.data_thread = None
        
        # Current data for single-trace plots
        self.current_magnitude = None
        self.current_phase = None
        self.current_real = None
        self.current_imag = None
        
    def data_acquisition_thread(self):
        #background thread for continuous data acquisition
        while self.running:
            try:
                # Get scan data
                data_bytes = self.nvna.scan(self.start, self.stop, self.pts, self.outmask)
                
                # Convert to arrays
                freq_arr, real_arr, imag_arr, mag_arr, phase_arr = convert_s11_data_to_arrays(
                    self.start, self.stop, self.pts, data_bytes)
                
                # Put data in queue for main thread
                self.data_queue.put({
                    'freq': freq_arr,
                    'real': real_arr,
                    'imag': imag_arr,
                    'magnitude': mag_arr,
                    'phase': phase_arr,
                    'timestamp': datetime.now()
                })
                
                time.sleep(0.15)  # Small delay to prevent overwhelming the device
                        # this might need to be tuned based on the device and how many points are taken
                
            except Exception as e:
                print(f"Data acquisition error: {e}")
                break
    
    def start_acquisition(self):
        # start the thread
        self.running = True
        self.data_thread = threading.Thread(target=self.data_acquisition_thread)
        self.data_thread.daemon = True
        self.data_thread.start()
    
    def stop_acquisition(self):
       # stop the thread
        self.running = False
        if self.data_thread:
            self.data_thread.join()
    
    def update_plots(self, frame):
        
        # Get all available data from queue
        while not self.data_queue.empty():
            try:
                data = self.data_queue.get_nowait()
                
                # Store frequency array (first time only)
                if self.freq_arr is None:
                    self.freq_arr = data['freq']
                
                # Update current data
                self.current_magnitude = data['magnitude']
                self.current_phase = data['phase']
                self.current_real = data['real']
                self.current_imag = data['imag']
                
                # Add to history
                self.magnitude_history.append(data['magnitude'])
                self.phase_history.append(data['phase'])
                self.real_history.append(data['real'])
                self.imag_history.append(data['imag'])
                self.timestamps.append(data['timestamp'])
                
            except queue.Empty:
                break
        
        # Clear all plots
        for ax in [ax1, ax2, ax3, ax4]:
            ax.clear()
        
        if self.freq_arr is not None and self.current_magnitude is not None:
            # Plot 1: Current S11 Magnitude
            ax1.plot(self.freq_arr/1e9, self.current_magnitude, 'b-', linewidth=1.5)
            ax1.set_xlabel('Frequency (GHz)')
            ax1.set_ylabel('S11 Magnitude (dB)')
            ax1.set_title('Live S11 Magnitude')
            ax1.grid(True, alpha=0.3)
            
            # Plot 2: Current S11 Phase
            ax2.plot(self.freq_arr/1e9, self.current_phase, 'r-', linewidth=1.5)
            ax2.set_xlabel('Frequency (GHz)')
            ax2.set_ylabel('S11 Phase (degrees)')
            ax2.set_title('Live S11 Phase')
            ax2.grid(True, alpha=0.3)
            
            # Plot 3: S11 Magnitude Waterfall (recent history)
            if len(self.magnitude_history) > 1:
                waterfall_mag = np.array(list(self.magnitude_history))
                time_arr = np.arange(len(waterfall_mag))
                freq_mesh, time_mesh = np.meshgrid(self.freq_arr, time_arr)
                
                im = ax3.pcolormesh(freq_mesh/1e9, time_mesh, waterfall_mag, 
                                   shading='nearest', cmap='viridis')
                ax3.set_xlabel('Frequency (GHz)')
                ax3.set_ylabel('Time (scans ago)')
                ax3.set_title('S11 Magnitude History')
            
            # Plot 4: Complex plane (Smith chart style)
            ax4.scatter(self.current_real, self.current_imag, 
                       c=self.freq_arr/1e9, cmap='plasma', s=10, alpha=0.7)
            ax4.set_xlabel('Real Part')
            ax4.set_ylabel('Imaginary Part')
            ax4.set_title('S11 Complex Plane')
            ax4.grid(True, alpha=0.3)
            ax4.axis('equal')
        
        # Add timestamp
        if self.timestamps:
            fig.suptitle(f'Live S11 Measurement - {self.timestamps[-1].strftime("%H:%M:%S")}', 
                        fontsize=14)

# Main execution
if __name__ == "__main__":
    # create a new nanoVNA object    
    nvna = nanoVNA()
    # set the return message preferences
    nvna.set_verbose(True)
    nvna.set_error_byte_return(True)

    # attempt to autoconnect
    found_bool, connected_bool = nvna.autoconnect()

    if not connected_bool:
        print("ERROR: could not connect to port")
    else:
        try:
            print("Starting live S11 measurement...")
            print("Close the plot window to stop measurement")
            
            # Scan parameters
            start = int(1e9)  # 1 GHz
            stop = int(3e9)   # 3 GHz
            pts = 150         # Reduced points for faster updates
            outmask = 2       # get measured data
            
            # Create plotter
            plotter = LiveS11Plotter(nvna, start, stop, pts, outmask, max_history=30)
            
            # Set up the plot
            fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(14, 10))
            plt.subplots_adjust(hspace=0.3, wspace=0.3)
            
            # Start data acquisition
            plotter.start_acquisition()
            
            # Create animation
            ani = animation.FuncAnimation(fig, plotter.update_plots, 
                                        interval=200, blit=False)
            
            # Show plot (this blocks until window is closed)
            plt.show()
            
            # Cleanup
            plotter.stop_acquisition()
            nvna.resume()
            nvna.disconnect()
            
            print("Live measurement stopped")
            
        except KeyboardInterrupt:
            print("\nMeasurement interrupted by user")
            nvna.resume()
            nvna.disconnect()
        except Exception as e:
            print(f"Error occurred: {e}")
            nvna.resume()
            nvna.disconnect()

Waterfall Plot for SCAN Data in Realtime

Waterfall Plot for SCAN Data in Realtime

Saving SCAN Data to CSV

# import NanoVNA library
# (NOTE: check library path relative to script path)
from src.nanoVNA_python import nanoVNA
# imports FOR THE EXAMPLE
import csv
import numpy as np

def convert_s11_data_to_arrays(start, stop, pts, data):
    # Convert the raw data so that the frequency, real, and imaginary are all stored.

    # Create frequency array
    freq_arr = np.linspace(start, stop, pts)
    
    # Parse data into pairs of values (real/imaginary)
    lines = data.decode('utf-8').split('\n')
    real_parts = []
    imag_parts = []
    
    for line in lines:
        if line.strip():
            values = line.split()
            if len(values) >= 2:
                try:
                    real_val = float(values[0])
                    imag_val = float(values[1])
                    real_parts.append(real_val)
                    imag_parts.append(imag_val)
                except ValueError:
                    continue
    
    # Convert to numpy arrays
    real_arr = np.array(real_parts)
    imag_arr = np.array(imag_parts)
    
    # Adjust frequency array to match actual data length
    actual_pts = len(real_arr)
    if actual_pts != pts:
        freq_arr = np.linspace(start, stop, actual_pts)
    
    return freq_arr, real_arr, imag_arr

# create a new nanoVNA object    
nvna = nanoVNA()
# set the return message preferences
nvna.set_verbose(True) #detailed messages
nvna.set_error_byte_return(True) #get explicit b'ERROR' if error thrown

# attempt to autoconnect
found_bool, connected_bool = nvna.autoconnect()

# if port closed, then return error message
if connected_bool == False:
    print("ERROR: could not connect to port")
else: 
    # if port found and connected, then complete task(s) and disconnect
    # the S11 (return loss) data is the default collection for this tutorial
    print("Connected to nanoVNA - collecting S11 data...")
    
    # set scan values
    start = int(1e9)  # 1 GHz
    stop = int(3e9)   # 3 GHz
    pts = 200         # sample points
    outmask = 2       # get measured data 
    
    # scan for S11 data
    data_bytes = nvna.scan(start, stop, pts, outmask)
    print(f"Received {len(data_bytes)} bytes of S11 data")

    nvna.resume() #resume so screen isn't still frozen

    # disconnect because in this example we're done reading from device
    nvna.disconnect()
    
    # processing after disconnect
    # convert data to 3 arrays: frequency, real, imaginary
    freq_arr, real_arr, imag_arr = convert_s11_data_to_arrays(start, stop, pts, data_bytes)
    
    # Save the RAW data to CSV
    filename = "s11_raw_data.csv"
       
    # Write out to csv: frequency, real, imaginary
    with open(filename, 'w', newline='') as csvfile:
        writer = csv.writer(csvfile)

        # Write header row
        writer.writerow(['Frequency_Hz', 'S11_Real', 'S11_Imaginary'])

        # Write data rows (frequency, real, imaginary triplets)
        for freq, real, imag in zip(freq_arr, real_arr, imag_arr):
            writer.writerow([f'{freq:.0f}', f'{real:.6f}', f'{imag:.6f}'])

    print(f"RAW S11 data saved to {filename}")
    print(f"Total: {len(freq_arr)} data points saved")

Accessing the NanoVNA Directly

In some cases, this library may not cover all possible command versions, or new features might not be included yet. The NanoVNA can be accessed directly using the command() function. There is NO ERROR CHECKING on this function. It takes the full argument, just as if arguments were entered on the command line.

# import NanoVNA library
# (NOTE: check library path relative to script path)
from src.nanoVNA_python import nanoVNA 


# create a new tinySA object    
nvna = nanoVNA()

# set the return message preferences 
nvna.set_verbose(True) #detailed messages
nvna.set_error_byte_return(True) #get explicit b'ERROR' if error thrown


# attempt to autoconnect
found_bool, connected_bool = nvna.autoconnect()

# if port closed, then return error message
if connected_bool == False:
    print("ERROR: could not connect to port")
else: # if port found and connected, then complete task(s) and disconnect

    # get device info
    msg = nvna.command("info")
    print(msg)


    # get device Id
    msg = nvna.command("SN")
    print(msg)

    # scan example data
    # NOTE: scan REQUIRES integers,
    #  so the 1e9 fo 1 GHz notation does not work in a string
    data_bytes = nvna.command("scan 1000000000 2500000000 200 2")
    print(data_bytes)

    nvna.resume() #resume 

    nvna.disconnect()

List of NanoVNA Commands and their Library Commands

Library functions are organized based on the command passed to the device. For example, any functions with shortcuts for using the sweep command will be grouped under sweep. This list and the following list in the Additional Library Commands section describe the functions in this library.

This section is sorted by the NanoVNA commands, and includes:

  • A brief description of what the command does
  • What the original usage looked like
  • The nanoVNA_python function call, or calls if multiple options exist
  • Example return, or example format of return
  • Any additional notes about the usage

All of the listed commands are included in this API to some degree, but error checking may be incomplete.

Quick Link Table:

beep cal capture clearconfig cwfreq data edelay
frequencies help info LCD LCD_ID lcd marker
pause port pwm recall reset resolution restart
resume save saveconfig scan SN sweep touchcal
touchtest trace version

beep

  • Description: Turn the beep on or off.
  • Original Usage: beep [on|off]
  • Direct Library Function Call: beep()
  • Example Return: b''
  • Alias Functions:
    • beep_on()
    • beep_off()
    • beep_time(val=Int)
  • CLI Wrapper Usage:
  • Notes: Beep plays a continious tone until it is turned off.

cal

  • Description: Work through the calibration process. Requires physical interaction with the device
  • Original Usage: cal [load|open|short|thru|done|reset|on|off]
  • Direct Library Function Call: cal(val=load|open|short|thru|done|reset|on|off|in)
  • Example Return: ``
  • Alias Functions:
    • cal_load() - calibrate with the load connector
    • cal_open() - calibrate with the open connector
    • cal_short()- calibrate with the short connector
    • cal_thru() - calibrate with cable connected to both ports
    • cal_done() - done with calibration
    • cal_reset() - reset calibration data. Do this BEFORE calibrating
    • cal_on() - start measuring with calibration, apply it to device
    • cal_off() - stop messing with calibration being applied to device
  • CLI Wrapper Usage:
  • Notes:
    • cal - no argument gets the calibration status
    • cal load - calibrate with the load connector. Hardware must be attached before calibration
    • cal open - calibrate with the open connector. Hardware must be attached before calibration
    • cal short - calibrate with the open connector. Hardware must be attached before calibration
    • cal thru - calibrate with cable connected to both ports. Hardware must be attached before calibration
    • cal done - complete the calibration
    • cal reset - reset calibration data. Do this BEFORE calibrating
    • cal on - start measuring with calibration, apply it to device
    • cal off - stop measuring with calibration being applied to device
    • cal in - this is in the documentation, but has no button on the NanoVNA-F V2. Might be a later feature.

capture

  • Description: Requests a screen dump to be sent in binary format of HEIGHTxWIDTH pixels of each 2 bytes
  • Original Usage: capture
  • Direct Library Function Call: capture()
  • Example Return: format:'\x00\x00\x00\x00\x00\x00\x00\...x00\x00\x00'
  • Alias Functions:
    • capture_screen()
  • CLI Wrapper Usage:
  • Notes: Data is in little-endian mode. Screen resolution is 800*480 for NanoVNA-F V2 and V3

clearconfig

  • Description: Resets the configuration data to factory defaults
  • Original Usage: clearconfig
  • Direct Library Function Call: clear_config()
  • Example Return: b'Config and all calibration data cleared. \r\n Do reset manually to take effect. Then do touch calibration and save.\r'
  • Alias Functions:
    • clear_and_reset()
  • CLI Wrapper Usage:
  • Notes: Requires password '1234'. Hardcoded. Other functions need to be used with this to complete the process. This causes the deletion of ALL settings and calibration. USE WITH CAUTION.

cwfreq

  • Description: Set the continuous wave (CW) pulse frequency
  • Original Usage: cwfreq {frequency in Hz}
  • Direct Library Function Call: cwfreq(val=Int|Freq in Hz)
  • Example Return: ``
  • Alias Functions:
    • set_cwfreq(val=Int|Freq in Hz)
  • CLI Wrapper Usage:
  • Notes:

data

  • Description: Gets the trace data for either S11 or S21, or the calibration.
  • Original Usage: data {0..6}
  • Direct Library Function Call: data(val=None|0|1|2|3|4|5|6)
  • Example Return:
    • data 0: format bytearray(b'-0.086151 0.957274\r\n1.013057 -0.197761\r\n0.944041 -0.348532\r\n0.858225 -0....\r\n-0.588183 -0.481691\r\n-0.646600 -0.426130\r')
    • data 7: out of bounds. bytearray(b'usage: data [array]\r')
  • Alias Functions:
    • get_s11_data()
    • get_s21_data()
    • get_load_cal_data()
    • get_open_cal_data()
    • get_short_cal_data()
    • get_thru_cal_data()
    • get_isolation_cal_data()
  • CLI Wrapper Usage:
  • Notes: S11 data is printed by default, but can be selected with input 0 for S11 and input 1 for S21. Higher values are returns for the calibration, according to some documenation online (see references).
    • data 0 - S11
    • data 1 - S21
    • data 2 - cal load
    • data 3 - cal open
    • data 4 - cal short
    • data 5 - cal thru
    • data 6 - cal isolation

edelay

  • Description: electrical delay. This lets users compensate for time delay caused by components attached to the port, such as cables, adapters, etc.
  • Original Usage: edelay id
  • Direct Library Function Call: edelay(val=None|Int|Float)
  • Example Return: empty bytearray
  • Alias Functions:
    • get_edelay()
    • set_edelay(val=Int|Float)
  • CLI Wrapper Usage:
  • Notes: No params should get the current edelay value. If there is 1 parameter, the delay is in nanoseconds.

frequencies

  • Description: Gets the frequencies used by the last sweep
  • Original Usage: frequencies
  • Direct Library Function Call: frequencies()
  • Example Return: b'1500000000\r\n... \r\n3000000000\r'
  • Alias Functions:
    • get_last_freqs()
  • CLI Wrapper Usage:
  • Notes:

help

  • Description: Gets a list of the available commands. Can be used to call NanoVNA help directly.
  • Original Usage: help
  • Direct Library Function Call: help(val=None|0|1)
  • Example Return:
    bytearray(b'There are all commands\r\n
    help:                lists all the registered commands\r\n
    reset:               usage: reset\r\n
    cwfreq:        
    usage: cwfreq {frequency(Hz)}\r\n
    saveconfig:          usage: saveconfig\r\n
    clearconfig:         usage: clearconfig {protection key}\r\n
    data:  
    usage: data [array]\r\n
    frequencies:         usage: frequencies\r\n
    port:                usage: port {1:S11 2:S21}\r\n
    scan:
    usage: scan {start(Hz)} [stop] [points] [outmask]\r\n
    sweep:               usage: sweep {start(Hz)} [stop] [points]\r\n
    touchcal:            usage: touchcal\r\n
    touchtest:           usage: touchtest\r\n
    pause:               usage: pause\r\n
    resume:              usage: resume\r\n
    cal:
    usage: cal [load|open|short|thru|done|reset|on|off|in]\r\n
    save:                usage: save {id}\r\n
    recall:              usage: recall {id}\r\n
    trace:               usage: trace {id}\r\n
    marker:              usage: marker [n] [off|{index}]\r\n
    edelay:              usage: edelay {id}\r\n
    pwm:       
    usage: pwm {0.0-1.0}\r\n
    beep:                usage: beep on/off\r\n
    lcd:                 usage: lcd X Y WIDTH HEIGHT FFFF\r\n
    capture:      
    usage: capture\r\n
    version:             usage: Show NanoVNA version\r\n
    info:                usage: NanoVNA-F info\r\n
    SN:                  usage: NanoVNA-F ID\r\n
    resolution:          usage: LCD resolution\r\n
    LCD_ID:              usage: LCD ID\r')   
  • Alias Functions:
    • NanoVNA_Help()
  • CLI Wrapper Usage:
  • Notes:

info

  • Description: Displays various software/firmware and hardware information
  • Original Usage: info
  • Direct Library Function Call: info()
  • Example Return: bytearray(b'Model: NanoVNA-F_V2\r\nFrequency: 50k ~ 3GHz\r\nBuild time: Mar 2 2021 - 09:40:50 CST\r')
  • Alias Functions:
    • get_info()
  • CLI Wrapper Usage:
  • Notes:

lcd

  • Description: Draw rectangles on the screen
  • Original Usage: lcd {X} {Y} {WIDTH} {HEIGHT} {FFFF}
  • Direct Library Function Call: lcd()
  • Example Return: empty bytearray
  • Alias Functions:
    • draw_rect(X=Int, Y=Int, W=Int, H=Int, COL=4 digit hex)
  • CLI Wrapper Usage:
  • Notes: Pause the screen first, and then draw. When the screen refreshes, the rectangle will be erased from left to right.

LCD_ID

  • Description: Get the ID of the LCD screen
  • Original Usage: LCD_ID
  • Direct Library Function Call: LCD_ID()
  • Example Return: bytearray(b'118200\r')
  • Alias Functions:
    • get_LCD_ID()
  • CLI Wrapper Usage:
  • Notes:

marker

  • Description: sets or dumps marker info
  • Original Usage:
    • marker [n] [on|off|{index}]
    • marker [n] [off|{index}]
    • marker [n] peak
  • Direct Library Function Call: marker(ID=Int|1..4, val="on"|"off"|"peak", idx=None|Int)
  • Example Return:
    • marker with no active markers:
      • bytearray(b'') - no active markers
      • bytearray(b'1 0 50\r\n2 40 0\r') - 2 active markers
    • marker 1 25 - marker 1, data reading point 25
      • bytearray(b'')
    • marker 1 - information about location
      • bytearray(b'1 25 2940000\r')
    • marker 1 peak - moves marker 1 to peak
      • bytearray(b'')
  • Alias Functions:
    • get_all_marker_positions()
    • get_marker_position(ID=Int)
    • set_marker_position(ID=Int, idx=Int) - idx is a point between 0-201, or whatever the limits of the reading for the device is if it's higher.
    • marker_peak(ID=Int)
    • marker_on(ID=Int)
    • marker_off(ID=Int)
  • CLI Wrapper Usage:
  • Notes:
    • Marker indexes depend on what the device lists. 0 i
    • marker no argument gets the attributes of the active markers.
    • marker {ID=integer} gets the attributes of that marker
    • The frequency must be within the selected sweep range mode.
    • Alias functions need error checking.

pause

  • Description: Pauses the sweep
  • Original Usage: pause
  • Direct Library Function Call: pause()
  • Example Return: bytearray(b'')
  • Alias Functions:
    • None
  • CLI Wrapper Usage:
  • Notes:

pwm

  • Description: Adjusts the PWM of the screen. This is screen brightness in this application.
  • Original Usage: pwm
  • Direct Library Function Call: pwm(val=Float|0.0-1.0)
  • Example Return: bytearray(b'')
  • Alias Functions:
    • set_screen_brightness(val=Float|0.0-1.0)
  • CLI Wrapper Usage:
  • Notes:
    • 0.1 is 10% brightness, etc.

recall

  • Description: Loads a previously stored calibration from the device
  • Original Usage: recall 0..4...6
  • Direct Library Function Call: recall(val=0|1|2|3|4|5|6)
  • Example Return: empty bytearray
  • Alias Functions:
    • None
  • CLI Wrapper Usage:
  • Notes: where 0 is the startup preset. No arguments prints the frequency range of the save results. Appears to be the same as save()

reset

  • Description: Resets the NanoVNA device.
  • Original Usage: reset
  • Direct Library Function Call: reset()
  • Example Return: empty bytearray, serial error message. depends on the system.
  • Alias Functions:
    • reset_device()
  • CLI Wrapper Usage:
  • Notes: Disconnects the serial too, so will need to reconnect to continue using.

restart

  • Description: Restarts the tinySA after the specified number of seconds
  • Original Usage: restart {seconds}
  • Direct Library Function Call: restart(val=0...)
  • Example Return: empty bytearray
  • Alias Functions:
    • restart_device()
    • cancel_restart()
  • CLI Wrapper Usage:
  • Notes:
    • Has not worked in testing on development DUT, but appears to work on some devices online.
    • 0 seconds stops the restarting process.

resolution

  • Description: Get the resolution of the LCD screen in pixels
  • Original Usage: resolution
  • Direct Library Function Call: resolution()
  • Example Return: bytearray(b'800,480\r')
  • Alias Functions:
    • get_resolution()
    • lcd_resolution()
  • CLI Wrapper Usage:
  • Notes: The screen resolution for the NanoVNA-F V2 and V3 is 800x480 pixels (width x height)

resume

  • Description: Resumes the sweep
  • Original Usage: resume
  • Direct Library Function Call: resume()
  • Example Return: empty bytearray
  • Alias Functions:
    • None
  • CLI Wrapper Usage:
  • Notes:

save

  • Description: Saves the current calibration data. Might save the current trace settings and marker position.
  • Original Usage: save 0..4...6
  • Direct Library Function Call: save(val=None|0..4..6)
  • Example Return: empty bytearray
  • Alias Functions:
    • None
  • CLI Wrapper Usage:
  • Notes: where 0 is the startup preset. No arguments prints the frequency range of the save results.

saveconfig

  • Description: Saves the device configuration data. This includes language and touch calibration.
  • Original Usage: saveconfig
  • Direct Library Function Call: save_config()
  • Example Return: empty bytearray
  • Alias Functions:
    • None
  • CLI Wrapper Usage:
  • Notes:

scan

  • Description: Performs a scan and optionally outputs the measured data
  • Original Usage: scan {start(Hz)} {stop(Hz)} [points] [outmask]
  • Direct Library Function Call: scan(start, stop, pts, outmask)
  • Example Return:
    • scan 1000000 2000000
      • No return. Sets the screen to scan between 1 MHz and 2 MHz.
    • scan 1000000 2000000 200
      • bytearray(b''). The outmask is 0 by default, so there's no printout.
    • scan 1000000 2000000 200 1
      • bytearray(b'1000 \r\n1010 \r\n1020 \r\n1030 ... \r\n1980 \r\n1990 \r\n2000 ... \r\n0 \r'
      • The freuency points are returned, including a buffer of \r\n0
      • Values are returned in kHz
    • scan 1000000 2000000 200 1
      • bytearray(b'-1.134857 0.890570 \r\n-1.143237 0.889276... \r\n-1.411501 1.746581 \r\n-1.400607 1.754247 ... \r\n0.000000 0.000000 \r\n0.000000 0.000000 \r')
      • The S11 data is complex, with real and imaginary values. The padding is also complex.
    • scan 1000000 2000000 200 3
      • bytearray(b'1000 -1.124556 0.885832 \r\n1010 -1.133325 0.882054....\r\n0 0.000000 0.000000 \r\n0 0.000000 0.000000 \r')
      • When the frequency and S11 are returned, the data is the freq in KHz and then the 2 parts of the complex signal. The padding is has 3 blank float values.
    • scan 1000000 2000000 200 7
      • bytearray(b'1000 -1.133184 0.885893 -0.000045 -0.000008 \r\n....\r\n0 0.000000 0.000000 0.000000 0.000000 \r'))
      • When the frequency, S11, and S21 are returned, the data is the freq in KHz and then the 2 parts of the EACH complex signal, 4 signal parts in total. The padding has 5 blank float values.
    • Returns for invalid input:
      • bytearray(b'sweep points exceeds range 51 -201\r')
      • bytearray(b'frequency range is invalid\r')
  • Alias Functions:
    • scan_range(start=Int, stop=Int) - Scans. sets boundaries, does not return data
    • get_scan_frequencies(start=Int, stop=Int, pts=Int) - returns frequency data
    • get_scan_s11(start=Int, stop=Int, pts=Int) - returns S11 data
    • get_scan_freqs_s11(start=Int, stop=Int, pts=Int) - returns frequency and S11 data
    • get_scan_s21(start=Int, stop=Int, pts=Int) - returns S21 data
    • get_scan_freqs_s21(start=Int, stop=Int, pts=Int) - returns frequency and S21 data
    • get_scan_s11_s21(start=Int, stop=Int, pts=Int) - returns S11 and S21 data
    • get_scan_freqs_s11_s21(start=Int, stop=Int, pts=Int) - returns frequency, S11, and S21 data
  • CLI Wrapper Usage:
  • Notes:
    • start and stop are required values of frequencies are in Hz. Frequency returns are in kHz.
    • [points] is the number of points in the scan. The MAX points is device dependent. 201 is a common max, end not inclusive.
    • [outmask]
    • 0 = no printout
    • 1 = frequency vals
    • 2 = S11 of sweep points
    • 3 = frequency values & S11 of sweep pts
    • 4 = S21 of sweep pts
    • 5 = frequency values and & S21 data of sweep pts
    • 6 = S11 and S21 data of sweep points
    • 7 = frequency values, S11 and S21 data of sweep points

SN

  • Description: Get the unique serial number of the NanoVNA.
  • Original Usage: SN
  • Direct Library Function Call: SN(None)
  • Example Return: bytearray(b'63507468C\r')
  • Alias Functions:
    • get_SN()
  • CLI Wrapper Usage:
  • Notes:
    • NanoVNA-F ID (hint returned by help for DUT)
    • Example number changed from actual return. This is a 16-Bit serial number.

sweep

  • Description: Set sweep mode, frequency and points
  • Original Usage:
    • sweep {start(Hz)} {stop(Hz)} {points}
    • sweep {start|stop|center|span|cw|points} {freq(Hz)}
  • Direct Library Function Call: config_sweep(argName=start|stop|center|span|cw, val=Int|Float) AND preform_sweep(start, stop, pts)
  • Example Return:
    • empty bytearray b''
    • bytearray(b'0 800000000 450\r')
  • Alias Functions:
    • get_sweep_params()
    • set_sweep_start(val=Int) - val is frequency in Hz
    • set_sweep_stop(val=Int) - val is frequency in Hz
    • set_sweep_center(val=Int) - val is frequency in Hz
    • set_sweep_span(val=Int) - val is frequency in Hz
    • set_sweep_cw(val=Int) - val is frequency in Hz
    • run_sweep(start=Int, stop=Int, pts=Int)
  • CLI Wrapper Usage:
  • Notes:
  • sweep with no arguments lists the current sweep settings, the frequencies specified should be within the permissible range.
  • sweep {integer} is interpreted as start frequency value.
  • sweep {integer} {integer} is interpreted as start and stop frequencies.
  • sweep {integer} {integer} {integer} is interpreted as start and stop frequencies, and the umber of points.
  • sweep start {integer}: sets the start frequency of the sweep.
  • sweep stop {integer}: sets the stop frequency of the sweep.
  • sweep center {integer}: sets the center frequency of the sweep.
  • sweep span {integer}: sets the span of the sweep.
  • sweep cw {integer}: sets the continuous wave frequency (zero span sweep).

touchcal

  • Description: starts the touch calibration. Physical interaction with the device screen is required.
  • Original Usage: touchcal
  • Direct Library Function Call: touch_cal()
  • Example Return: empty bytearray
  • Alias Functions:
    • start_touch_cal()
  • CLI Wrapper Usage:
  • Notes: To save this, saveconfig must be used.

touchtest

  • Description: starts the touch test. When this command is used, the screen can be drawn on to check responsiveness.
  • Original Usage: touchtest
  • Direct Library Function Call: touch_test()
  • Example Return: empty bytearray
  • Alias Functions:
    • start_touch_test()
  • CLI Wrapper Usage:
  • Notes: There may be instructions on screen. Pause the screen first to see the marks made on the screen.

trace

  • Description: displays all or one trace information or sets trace related information. INCOMPLETE due to how many combinations are possible.
  • Original Usage:
    • trace [0|1|2|3|all] [off|logmag|linear|phase|smith|swr|polar|delay|refpos|channel] [value]
    • read the above as trace {ID} {format/action} {value/channel}
  • Direct Library Function Call: trace(ID=None|Int, trace_format=None|String, val=None|Int)
  • Example Return:
    • empty bytearray b''
    • trace
      • bytearray(b'0 LOGMAG S11 1.000000 7.000000\r\n1 LOGMAG S21 1.000000 7.000000\r\n2 SMITH S11 1.000000 0.000000\r')
      • summary of all active traces
    • trace 0 - Information on Trace 0, S11
      • bytearray(b'0 LOGMAG S11\r')
    • trace 1 - Information on Trace 1, S21
      • bytearray(b'1 LOGMAG S21\r')
    • trace 0 linear 1 - Set trace 0 to linear, and set the input from chanel 1 (port 2)
      • bytearray(b'')
    • trace 0 linear - set the format of trace 0 to linear. This trace is by default on channel 0 (port 1)
      • bytearray(b'')
  • Alias Functions:
    • get_all_trace_attr()
    • get_trace_attr(ID=Int|"all")
    • trace_off(ID=Int|"all")
    • set_trace_logmag(ID=Int|"all")
    • set_trace_linear(ID=Int|"all")
    • set_trace_phase(ID=Int|"all")
    • set_trace_smith(ID=Int|"all")
    • set_trace_polar(ID=Int|"all")
    • set_trace_swr(ID=Int|"all",val=Float|Int)
    • set_trace_refposition(ID=Int|"all",val=Float|Int)
    • set_trace_delay(ID=Int|"all",val=Float|Int)
    • set_trace_channel(ID=Int|"all", val=Int)
  • CLI Wrapper Usage:
  • Notes:
    • NOTE: Traces can be turned OFF programatically, but not ON.
    • trace no args returns characteristics of active traces
    • trace {ID=integer} gets characteristics of that trace. using 'all' returns information for all traces.
    • trace {ID=integer} {str=logmag|phase|smith|linear|delay|swr} The ID sets the trace ID, and the second argument indicates what trace data format is returned.
    • trace {ID=integer} {off} turn the trace off. using 'all' will toggle all traces off. Traces cannot be turned 'on' with this method (conflicting documentation)
    • trace {ID=integer} {str=scale|refpos|channel} {val=int} the first argument is the ID of the trace. The second argument is an action to scale the trace by a numeric value, to set the reference position (refpos), or to set the channel. The third value specifies the value for the action.

version

  • Description: returns the firmware version
  • Original Usage: version
  • Direct Library Function Call: version()
  • Example Return: bytearray(b'0.2.1\r')
  • Alias Functions:
    • get_version()
  • CLI Wrapper Usage:
  • Notes:

Additional Library Functions for Advanced Use

command

  • Description: override library functions to run commands on the NanoVNA device directly.
  • Original Usage: None.
  • Direct Library Function Call: command(val=Str)
  • Example Usage::
    • example: command("version")
    • return: b'tinySA4_v1.4-143-g864bb27\r\nHW Version:V0.4.5.1.1 \r'
    • example: command("trace 1")
    • return: b'1: dBm 0.000000000 10.000000000 \r'
    • example: command("scan 150e6 200e6 5 2")
    • return: b'5.750000e+00 0.000000000 \r\n6.250000e+00 0.000000000 \r\n6.750000e+00 0.000000000 \r\n6.250000e+00 0.000000000 \r\n6.750000e+00 0.000000000 \r'
  • Example Return: command dependent
  • Alias Functions:
    • None
  • CLI Wrapper Usage:
  • Notes: If unfamiliar with device and operation, DO NOT USE THIS. There is no error checking and you will be interfacing with the NanoVNA device directly.

Unrecognized Commands that Appear in Documentation

These commands return the error message Command not recognised. from the device, not the library. They may appear in some versions of the firmware, but have not done anything to the DUT (NanoVNA-F V2).

  • bandwidth
  • color
  • dump
  • freq (frequency command is fine, but not shorter version)
  • power
  • scan_bin
  • smooth
  • tcxo
  • threshold
  • time
  • transform
  • vbat
  • zero
  • s21offset

Notes for Beginners

This is a brief section for anyone that might have jumped in with a bit too much ambition. It is highly suggested to read the manual.

Very useful, important documentation can be found at:

This library was modified from the tinySA_python library, and much of the material orignated there. These ARE NOT the same device, and have very different functionality, but some of the menus and commands are in the same format.

Vocab Check

Running list of words and acronyms that get tossed around with little to no explanation. Googling is recommended if you are not familiar with these terms as they're essential to understanding device usage.

  • AGC - Automatic Gain Control. This controls the overall dynamic range of the output when the input level(s) changes.
  • Baud - Baud, or baud rate. The rate that information is transferred in a communication channel. A baud rate of 9600 means a max of 9600 bits per second is transmitted.
  • DANL - Displayed Average Noise Level (DANL) refers to the average noise level displayed on a spectrum analyzer.
  • dB - dB (decibel) and dBm (decibel-milliwatts). dB (unitless) quantifies the ratio between two values, whereas dBm expresses the absolute power level (always relative to 1mW).
  • DUT - Device Under Test. Used here to refer to the singular device used while initially writing the API.
  • IF - Intermediate Frequency. A frequency to which a carrier wave is shifted as an intermediate step in transmission or reception - Wikipedia
  • LNA - Low Noise Amplifier. An electronic component that amplifies a very low-power signal without significantly degrading its signal-to-noise ratio - Wikipedia
  • Outmask - "outmask" refers to a setting that determines additional formatting or optional features that are not a core argument for a command.
    • For example, with the hop command, this value controls whether the device's output is a frequency or a level (power) signal. When the outmask is set to "1", the tinySA will output a frequency signal. When set to "2", the outmask will cause the tinySA to output a level signal, which is a measure of the signal's power or intensity
  • RBW - Resolution Bandwidth. Frequency span of the final filter (IF filter) that is applied to the input signal. Determines the fast-Fourier transform (FFT) bin size.
  • SDR* - Software Defined Radio. This is a software (computer) controlled radio system capable of sending and receiving RF signals. This type of device uses software to control functions such as modulation, demodulation, filtering, and other signal processing tasks. Messages (packets) can be sent and received with this device.
  • Signal Generator - used to create various types of repeating or non-repeating electronic signals for testing and evaluating electronic devices and systems.
  • S-parameters - are a way to characterize the behavior of radio frequency (RF) networks and components. They describe how much of a signal is reflected, transmitted or transferred between PORTS. In case of s11 (s-one-one), the return loss of a single antenna or port is measured. In s12 (s-one-two) or s21 (s-two-one), the interaction between ports is measured.
  • SA - Spectrum Analyzer. A device that measures the (power) magnitude of an input signal vs frequency. It shows signal as a spectrum. * This is what the 'SA' in 'tinySA' is!
  • SA - Signal Analyzer. A device that measures the properties of a single frequency signal. This can include power, magnitude, phase, and other features such as modulation.
  • SNA - Scalar Network Analyzer. A device that measures amplitude as it passes through the device. It can be used to determine gain, attenuation, or frequency response.
  • SWR - Standing Wave Ratio. SWR is an indication of how well an antenna is matched to a transmission line. A low SWR (close to 1:1) means there is minimal signal reflection and the power is being transmitted down the line eddiciently. A high SWR indicats and impedance mismatch between a DUT (or antenna, or network) and the transmission line, which in this case is internal to the NanoVNA.
  • VNA - Vector Network Analyzer. A device that measures the network parameters of electrical networks (typically, s-parameters). Can measure both measures both amplitude and phase properties. The wiki article on network analyzers covers the topic in detail.

VNA vs. SA vs. LNA vs. SNA vs. SDR vs Signal Generator

aka “what am I looking at and did I buy the right thing?”

**tinySA Vs. NanoVNA **: The tinySA and NanoVNA look a lot alike, and have some similar code, but they are NOT the same device. They are designed to measure different things. The tinySA is a spectrum analyzer (SA) while the NanoVNA is a vector network analyzer (VNA). Both have signal generation capabilities (to an extent, as OUTPUT), but the tinySA (currently) has expanded features for generating signals. This library was made for the NanoVNA line of devices. There is some overlap with the tinySA, but there is a seperate library for that device at tinySA_python.

SA - This one is context dependent. SA can mean either 'Spectrum Analyzer' (multiple frequencies) or 'Signal Analyzer' (single frequency). In the case of the tinySA it is 'Spectrum Analyzer' because multiple frequencies are being measured. A spectrum analyzer measures the magnitude of an external input signal vs frequency. It shows signal as a spectrum. The signal source does not need to be directly, physically connected to the SA, which allows for analysis of the wireless spectrum. This is the primary functionality of the tinySA, but it does have other features (such as signal generation).

VNA – a vector network analyzer (VNA) measures parameters such as s-parameters, impedance and reflection coefficient of a radio frequency (RF) device under test (DUT). A VNA is used to characterize the transmission and reflection properties of the DUT by generating a stimulus signal and then measuring the device's response. This can be used to characterize and measure the behavior of RF devices and individual components. * "What is a Vector Network Analyzer and How Does it Work?" - Tektronix * NanoVNA @ https://nanovna.com/

Signal Generator - A signal generator is used to create various types of repeating or non-repeating electronic signals for testing and evaluating electronic devices and systems. These can be used for calibration, design, or testing. Some signal generators will only have sine, square, or pulses, while others allow for AM and FM modulation (which begins to crossover into SDR territory)

SNA – a scalar network analyzer (SNA) measures amplitude as it passes through the device. It can be used to determine gain, attenuation, or frequency response. scalar network analyzers are less expensive than VNAs because they only measure the magnitude of the signal, not the phase.

SDR - a software defined radio (SDR) is a software (computer) controlled radio system capable of sending and receiving RF signals. This type of device uses software to control functions such as modulation, demodulation, filtering, and other signal processing tasks. Messages can be sent and received with this device.

LNA - an electronic component designed to amplify weak incoming signals with minimal noise addition, thus improving the signal-to-noise ratio (SNR). This hardware is often attached (or built in) to the devices above. It is not a stand-alone device for signal generation or analysis.

Calibration Setup

Some tips:

  • The open, sort, and load pieces should be finger tight. If the piece will not turn, there's a high risk of cross threading if it's forced.
  • The thru calibration should be done with the included cable. The cable impedance needs to match the impedance of the calibration kit. These are usually 50 ohms, but may be 75 ohms.
  • When the cable is attatched to port 1 and port 2, both connectors should be finger tight.

Some General NanoVNA Notes

These are notes collected from various references as this README documentation is built out. Some are obvious, some were not at the time.

FAQs

How should I be using this?

Right now, this library is set up as a class that can be added to a Python program. I recommend adding the contents of the ./src folder on the same level (or lower) than the main program you're writing. If that doesn't make a lot of sense, check out the hello_world.py file in this repo. Because that example file is at the same level as the ./src folder, we aren't dealing with path imports or checking. This works well for beginners, which is whom the bulk of the documentation is intended for.

Will this be made into a REAL Python library I can import into my project?

That's the plan! Right now, the core library is made of functions for directly interfacing with the NanoVNA series of devices. There are several examples in this README, which will be integrated into the core library as the error checking and features are stabilized. We're probably 3-6 months of development and testing away from an official release or library creation.

How often is this library updated?

This library is updated in spurts. June-August are going to be the most active development months, but it will get monthly-ish updates otherwise. Development is pretty constant on the backend, but only stable code is released publicly. Bug fixes will be addressed as they happen.

References

The original documentation for this project comes from the related tinySA_python library. That library was taken and applied to the NanoVNA to get a baseline of what commands were shared, and what might be new (to the library) for the NanoVNA. These ARE NOT the same device, and have very different functionality, but some of the menus and commands are in the same format.

The NanoVNA main site:

Licensing

The code in this repository has been released under GPL-2.0 for right now (and to have something in place rather than nothing). This licensing does NOT take priority over the official releases and the decisions of the NanoVNA team. This licensing does NOT take priority for any of their products, including the devices that can be used with this software.

This software is released AS-IS, meaning that there may be bugs (especially as it is under development).

This software is UNOFFICIAL, meaning that the NanoVNA team does not offer tech support for it, does not maintain it, and has no responsibility for any of the contents.

About

An unofficial (simple) Python API for the nanoVNA device line

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages