https://github.com/lc-linkous/tinysa_python
An unofficial Python API for the tinySA device line (now a PyPI package!)
https://github.com/lc-linkous/tinysa_python
examples-python pypi-package spectrum-analyzer tinysa
Last synced: 22 days ago
JSON representation
An unofficial Python API for the tinySA device line (now a PyPI package!)
- Host: GitHub
- URL: https://github.com/lc-linkous/tinysa_python
- Owner: LC-Linkous
- Created: 2025-01-06T02:58:20.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2026-06-03T19:16:53.000Z (24 days ago)
- Last Synced: 2026-06-03T20:06:15.947Z (24 days ago)
- Topics: examples-python, pypi-package, spectrum-analyzer, tinysa
- Language: Python
- Homepage:
- Size: 887 KB
- Stars: 13
- Watchers: 1
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# tinySA_python
[](https://badge.fury.io/py/tsapython)
[](https://pypi.org/project/tsapython/)
[](https://pypi.org/project/tsapython/)
[](https://pepy.tech/project/tsapython)
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
[](https://doi.org/10.5281/zenodo.20546764)
## AN UNOFFICIAL Python API for the tinySA Device Series
A Non-GUI Python API for the tinySA series of devices. This repository uses official resources and documentation but is NOT endorsed by the official tinySA product or company. See the [references](#references) section for further reading. See the [official tinySA resources](https://www.tinysa.org/wiki/) for device features.
This library covers most documented commands for the tinySA device series, and is planned to include configurable, device-specific commands and memory. The documentation (after the examples) is sorted based on the serial command for the device, with some provided usage examples. While some error checking exists in both the device and the library, it is not exhaustive. It is strongly advised to read the official documentation before attempting to script with your tinySA device. Operating the device experimentally or without referencing the official documents runs the risk of **destroying your device**. See the [tinySA First use page](https://tinysa.org/wiki/pmwiki.php?n=Main.FirstUse) for some setup tips and warnings.
This README provides example code for connecting to the device, scanning and plotting data, saving to CSV, and creating real-time waterfall plots. Examples are not exhaustive. Refer to the [List of tinySA Commands and their Library Commands](#list-of-tinysa-commands-and-their-library-commands) for all of the tested commands for this library. Alias functions have been provided for convenience, but are not exhaustive.
If you are interested in developing the PyPI package, or making a custom local version, see [Library Development](#library-development) towards the end of this README.
The primary GitHub: [https://github.com/LC-Linkous/tinySA_python](https://github.com/LC-Linkous/tinySA_python)
The PyPI page: [https://pypi.org/project/tsapython/](https://pypi.org/project/tsapython/)
Zenodo archive with DOI: [https://doi.org/10.5281/zenodo.20546764](https://doi.org/10.5281/zenodo.20546764)
## Table of Contents
* [The tinySA Series of Devices](#the-tinysa-series-of-devices)
* [Library Usage](#library-usage)
* [PyPI Install](#pypi-install)
* [Local Install Using UV](#local-install-using-uv)
* [Requirements](#requirements)
* [Structure](#structure)
* [Running Tests](#running-tests)
* [Error Handling](#error-handling)
* [Example Implementations](#example-implementations)
* [Finding the Serial Port](#finding-the-serial-port)
* [Autoconnection with the tinySA_python Library](#autoconnection-with-the-tinysa_python-library)
* [Manually Finding a Port on Windows](#manually-finding-a-port-on-windows)
* [Manually Finding a Port on Linux](#manually-finding-a-port-on-linux)
* [Serial Message Return Format](#serial-message-return-format)
* [Connecting and Disconnecting the Device](#connecting-and-disconnecting-the-device)
* [Toggle Error Messages](#toggle-error-messages)
* [Device and Library Help](#device-and-library-help)
* [Setting tinySA Parameters](#setting-tinysa-parameters)
* [Getting Data from Active Screen](#getting-data-from-active-screen)
* [Saving Screen Images](#saving-screen-images)
* [Plotting Data with Matplotlib](#plotting-data-with-matplotlib)
* [Example 1: Plot using On-Screen Trace Data and Frequencies](#example-1-plot-using-on-screen-trace-data-and-frequencies)
* [Example 2: Plot using Scan Data and Frequencies](#example-2-plot-using-scan-data-and-frequencies)
* [Example 3: Plot using SCAN and SCANRAW Data and Calculated Frequencies](#example-3-plot-using-scan-and-scanraw-data-and-calculated-frequencies)
* [Example 4: Plot using SCAN And Filters for Artifact Comparison](#example-4-plot-using-scan-and-filters-for-artifact-comparison)
* [Example 5: Plot a Waterfall using SCAN and Calculated Frequencies](#example-5-plot-a-waterfall-using-scan-and-calculated-frequencies)
* [Example 6: Finding Peaks in a Frequency Range](#example-6-finding-peaks-in-a-frequency-range)
* [Saving SCAN Data to CSV](#saving-scan-data-to-csv)
* [Accessing the tinySA Directly](#accessing-the-tinysa-directly)
* [List of tinySA Commands and their Library Commands](#list-of-tinysa-commands-and-their-library-commands)
* [List of Commands Removed from Library](#list-of-commands-removed-from-library)
* [Additional Library Functions for Advanced Use](#additional-library-functions-for-advanced-use)
* [Library Development](#library-development)
* [Notes for Beginners](#notes-for-beginners)
* [Vocab Check](#vocab-check)
* [VNA vs. SA vs. LNA vs. SNA vs. SDR vs Signal Generator](#vna-vs-sa-vs-lna-vs-sna-vs-sdr-vs-signal-generator)
* [Calibration Setup](#calibration-setup)
* [Some General tinySA Notes](#some-general-tinysa-notes)
* [FAQs](#faqs)
* [References](#references)
* [Licensing](#licensing)
## The tinySA Series of Devices
The [tinySA line of devices](https://tinysa.org/wiki/pmwiki.php?n=TinySA4.Comparison) are a series of portable and pretty user-friendly devices with both spectrum analyzer and signal generator capabilities. There are four main versions, all of which share the same key features. The Ultra and Ultra Plus versions build off of the original tinySA Basic. They look very similar to the [NanoVNA series](https://nanovna.com/), but are NOT the same device and have different functionalities. They are also made by different people.
The NanoVNA series is a handheld vector network analyzer (VNA), which measures the S-parameters (loosely: a type of system response of a device or antenna) 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 in RF design and testing, but the measurements are very different. A signal generator is exactly what it sounds like - it generates a controllable signal at a specific frequency or frequencies at a specified power level.
Official documentation for the tinySA can be found at [https://tinysa.org/](https://tinysa.org/). 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 (which is also under development). Several community projects exist on GitHub, and some may be official (this is not one of them!).
There is also a very active tinySA community at [https://groups.io/g/tinysa](https://groups.io/g/tinysa) exploring the device capabilities and its many features. There are in-depth Q&A topics there for device usage. That community **does not** support this library, and does not have anything to do with this library's development. They're a cool group, don't bother them with this.
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.
Improper usage may destroy your device.
## Library Usage
This library is now available via PyPI, local install, or just using the class. We recommend one of the library install options.
Several usage examples are provided in the [Example Implementations](#example-implementations) section, including working with the hardware and plotting results with matplotlib.
### PyPI Install
The `tsapython` package (from PyPI at [https://pypi.org/project/tsapython/3.0.0/](https://pypi.org/project/tsapython/3.0.0/)) can be installed with:
```python
pip install tsapython
```
The GitHub repository will continue to be named `tinySA_python` to differentiate the working versions and the additional documentation included here.
### Local Install Using UV
Developing a project, or running something custom? You can pull the code from GitHub and build+install the package locally.
(You can also use your favorite package manager. This is set up for UV, but the information for other setups should all be in the `tsapython` directory)
This is a summarized version of the instructions at [https://www.sarahglasmacher.com/how-to-build-python-package-uv/](https://www.sarahglasmacher.com/how-to-build-python-package-uv/):
```python
# install UV
pip install uv
# navigate to the tsapython directory
cd .\tsapython
# build the package
# a 'dist' directory should be created in tsapython
uv build
# install the package locally
pip install dist/tsapython-3.0.0-py3-none-any.whl
```
## Requirements
This project requires numpy and pyserial.
Use 'pip install -r requirements.txt' to install the following dependencies:
```python
pyserial
numpy
```
The above dependencies are only for the API interfacing of the tinySA_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`:
```python
pyserial
numpy
matplotlib
pyQt5 # Linux OS, some Windows machines
```
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:
```python
pip install pyserial numpy matplotlib pyQt5
```
`pyQt5` is used with `matplotlib` to draw the figures. It needs to be installed on Linux systems to follow the examples included in tinySA_python, but is not needed on all Windows machines.
If you are installing the package itself (rather than the loose requirements files), the same optional dependencies are available as extras defined in `pyproject.toml`:
```python
# library only (numpy + pyserial)
pip install tsapython
# library + plotting dependencies for the examples
pip install "tsapython[plotting]"
# development / running the test suite
pip install -e ".[test]"
```
## Previous Versions
Release history and archived versions of this library are available in a few places:
- **GitHub Releases** — tagged releases with source and notes:
[https://github.com/LC-Linkous/tinySA_python/releases](https://github.com/LC-Linkous/tinySA_python/releases)
(the 3.0.0 release: [releases/tag/v3.0.0](https://github.com/LC-Linkous/tinySA_python/releases/tag/v3.0.0))
- **PyPI release history** — every published version, installable with
`pip install tsapython==`:
[https://pypi.org/project/tsapython/#history](https://pypi.org/project/tsapython/#history)
- **Zenodo archive** — a citable, archived snapshot with a DOI:
[https://doi.org/10.5281/zenodo.20546764](https://doi.org/10.5281/zenodo.20546764)
(DOI `10.5281/zenodo.20546764`)
## Structure
The `tsapython` library, as it is available on PyPI, is structured as follows:
```
tsapython/
├── .python-version
├── pyproject.toml
├── README.md
├── LICENSE
├── .gitignore
├── src/
│ └── tsapython/
│ ├── __init__.py
│ ├── core.py
│ ├── py.typed
│ └── _commands/
│ ├── __init__.py
│ ├── acquisition.py
│ ├── calibration.py
│ ├── display_ui.py
│ ├── levels_gain.py
│ ├── markers_traces.py
│ ├── output_signal.py
│ ├── presets_config.py
│ └── system_info.py
└── tests/
├── __init__.py
├── conftest.py
├── test_smoke.py
├── test_acquisition.py
├── test_calibration.py
├── test_display_ui.py
├── test_levels_gain.py
├── test_markers_traces.py
├── test_output_signal.py
├── test_presets_config.py
├── test_system_info.py
├── test_parsing.py
├── test_hardware.py
├── test_captured_hardware.py
├── collect_samples.py
└── fixtures/
├── __init__.py
├── device_responses.py
└── captured_responses.py
```
The public API is unchanged: `from tsapython import tinySA` still exposes the full `tinySA` class. The per-command methods now live in mixin modules under `_commands/` and are composed onto the `tinySA` class in `core.py`, which keeps the shared state, serial handling, and helper methods.
A `docs` repository for the library will be added later in development for stable releases.
This library is also part of the `tinySA_python` repository, which includes more extensive documentation, runnable examples, and the working development. The GitHub repository is structured as follows:
```
tinySA_python/
├── README.md
├── requirements.txt
├── test_requirements.txt
├── media/
│ └── README images, screenshots
└── tsapython/
├── .python-version
├── pyproject.toml
├── README.md
├── LICENSE
├── .gitignore
├── examples/
│ ├── __init__.py
│ ├── complete_workflow.py
│ ├── hardware_walkthrough.py
│ ├── identifying_serial_ports.py
│ ├── using_autoconnect.py
│ ├── using_command_func.py
│ ├── plotting_scan.py
│ ├── plotting_scanraw.py
│ ├── plotting_waterfall_realtime.py
│ ├── plotting_waterfall_static.py
│ ├── save_scan_csv.py
│ ├── continuous_scanraw_live.py
│ └── continuous_scanraw_collect.py
├── src/
│ └── tsapython/
│ ├── __init__.py
│ ├── core.py
│ ├── py.typed
│ └── _commands/
│ ├── __init__.py
│ ├── acquisition.py
│ ├── calibration.py
│ ├── display_ui.py
│ ├── levels_gain.py
│ ├── markers_traces.py
│ ├── output_signal.py
│ ├── presets_config.py
│ └── system_info.py
└── tests/
├── __init__.py
├── conftest.py
├── test_smoke.py
├── test_acquisition.py
├── test_calibration.py
├── test_display_ui.py
├── test_levels_gain.py
├── test_markers_traces.py
├── test_output_signal.py
├── test_presets_config.py
├── test_system_info.py
├── test_parsing.py
├── test_hardware.py
├── test_captured_hardware.py
├── collect_samples.py
└── fixtures/
├── __init__.py
├── device_responses.py
└── captured_responses.py
```
## Running Tests
This is primarily for development or advanced troubleshooting. These tests are for the API.
The test suite uses [pytest](https://docs.pytest.org/). Tests must be run from the
`tsapython` project directory (the one containing `pyproject.toml`), since the pytest
configuration and the `hardware` marker are defined in `pyproject.toml`. Running from a
different directory will produce an `Unknown pytest.mark.hardware` warning.
Install the test dependencies first:
```bash
pip install -e ".[test]"
# or, using the requirements file:
pip install pytest pytest-cov
```
Run the suite (hardware tests self-skip when no device is connected):
```bash
python -m pytest
```
> **Note:** use `python -m pytest`, not `uv run pytest`. Running through `uv` here can
> create a stray virtual environment inside the project directory and tangle the test
> environment.
The suite is split into hardware-free tests and tests that need a connected tinySA.
The hardware tests are marked with `@pytest.mark.hardware` and are skipped automatically
when no device is detected:
```bash
# run ONLY the hardware-free tests (explicitly skip device tests)
python -m pytest -m "not hardware"
# run ONLY the hardware tests (requires a connected tinySA)
python -m pytest -m hardware
```
To see coverage while testing:
```bash
python -m pytest --cov=tsapython --cov-report=term-missing
```
### Capturing real device responses
`tests/collect_samples.py` is a helper (not a pytest test) that connects to a real device,
sends a set of read-only commands, and writes their responses to
`tests/fixtures/captured_responses.py`. The `test_captured_hardware.py` tests then run the
library's parsing logic against those real captures (these run without a device, since the
bytes are frozen in the fixture):
```bash
python tests/collect_samples.py
```
### Collecting device samples
`tests/collect_samples.py` is a manual helper (not a pytest test) for capturing real
device responses to use as parsing fixtures. Run it with a tinySA connected:
```python
# auto-detect the serial port
python tests/collect_samples.py
# or specify the port explicitly
python tests/collect_samples.py --port COM5 # Windows
python tests/collect_samples.py --port /dev/ttyACM0 # Linux/Mac
```
### Example scripts
The files in `examples/` are runnable demonstrations (not part of the automated test
suite) and require a connected device plus the plotting dependencies:
```python
pip install "tsapython[plotting]" # or: pip install -r test_requirements.txt
python examples/complete_workflow.py
python examples/hardware_walkthrough.py
```
## 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 `tinySA_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.
From the [official wiki USB Interface page](https://tinysa-org.translate.goog/wiki/pmwiki.php?n=Main.USBInterface&_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en-US):
```
There is limited error checking against incorrect parameters or incorrect device mode.
Some error checking will be integrated as the device configurations are included,
but this is not intended to be exhaustive.
```
Some error checking includes:
* Frequencies can be specified using an integer optionally postfixed with a the letter 'k' for kilo 'M' for Mega or 'G' for Giga. E.g. 0.1M (100kHz), 500k (0.5MHz) or 12000000 (12MHz)
* Levels are specified in dB(m) and can be specified using a floating point notation. E.g. 10 or 2.5
* Time is specified in seconds optionally postfixed with the letters 'm' for milli or 'u' for micro. E.g. 1 (1 second), 2.5 (2.5 seconds), 120m (120 milliseconds)
## 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 tinySA 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 tinySA_python Library
The tinySA_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).
```python
# import tinySA_python (tsapython) package
from tsapython import tinySA
# create a new tinySA object
tsa = tinySA()
# set the return message preferences
tsa.set_verbose(True) #detailed messages
tsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown
# attempt to autoconnect
found_bool, connected_bool = tsa.autoconnect()
# if port found and connected, then complete task(s) and disconnect
if connected_bool == True:
print("device connected")
msg = tsa.get_device_id()
print(msg)
tsa.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 tinySA Ultra 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.
```python
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:
```python
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: COM10, Description: USB Serial Device (COM10), Hardware ID: USB VID:PID=0483:5740 SER=400 LOCATION=1-3
```
"COM10" is the port location of the tinySA Ultra that is used in the examples in this README.
#### Manually Finding a Port on Linux
```python
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}")
```
```python
Port: /dev/ttyS0, Description: n/a, Hardware ID: n/a
Port: /dev/ttyS3, Description: n/a, Hardware ID: n/a
Port: /dev/ttyS2, Description: n/a, Hardware ID: n/a
Port: /dev/ttyS1, Description: n/a, Hardware ID: n/a
Port: /dev/ttyACM0, Description: tinySA4, Hardware ID: USB VID:PID=0483:5740 SER=400 LOCATION=3-3:1.0
```
This method identified the `/dev/ttyACM0`. Now, when attempting to use the autoconnect feature, the following error was initially returned:
```python
[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 tinySA serial return.
The original message format:
```python
bytearray(b'deviceid\r\ndeviceid 0\r\nch>')
```
Cleaned version:
```python
bytearray(b'deviceid 0\r')
```
### Connecting and Disconnecting the Device
This example shows the process for initializing, opening the serial port, getting device info, and disconnecting.
```python
# import tinySA_python (tsapython) package
from tsapython import tinySA
# create a new tinySA object
tsa = tinySA()
# set the return message preferences
tsa.set_verbose(True) #detailed messages
tsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown
# attempt to connect to previously discovered serial port
success = tsa.autoconnect()
# if port open, then get device information and disconnect
if success == False:
print("ERROR: could not connect to port")
else:
msg = tsa.info()
print(msg)
tsa.disconnect()
```
Example output for this method is as follows:
```python
bytearray(b'tinySA ULTRA\r\n2019-2024 Copyright @Erik Kaashoek\r\n2016-2020 Copyright @edy555\r\nSW licensed under GPL. See: https://github.com/erikkaashoek/tinySA\r\nVersion: tinySA4_v1.4-143-g864bb27\r\nBuild Time: Jan 10 2024 - 11:14:08\r\nKernel: 4.0.0\r\nCompiler: GCC 7.2.1 20170904 (release) [ARM/embedded-7-branch revision 255204]\r\nArchitecture: ARMv7E-M Core Variant: Cortex-M4F\r\nPort Info: Advanced kernel mode\r\nPlatform: STM32F303xC Analog & DSP\r')
```
### Toggle Error Messages
The following can be used to turn on or off returned error messages.
1) the 'verbose' option. When enabled, detailed messages are printed out.
```python
# detailed messages are ON
tsa.set_verbose(True)
# detailed messages are OFF
tsa.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''`.
```python
# when an error occurs, b'ERROR' is returned
tsa.set_error_byte_return(True)
# when an error occurs, the default b'' might be returned
tsa. set_error_byte_return(False)
```
### Device and Library Help
There are three options for help() with this library.
```python
# the default help function
# 1 = help for this library, other values call the tinySA device help function
tsa.help(1)
# calling the library help function directly
tsa.library_help()
# calling the tinySA device help directly
tsa.tinySA_help()
```
All three return a bytearray in the format `bytearray(b'commands:......')`
### Setting tinySA Parameters
Most device parameters are set through their corresponding library functions, documented in the [List of tinySA Commands and their Library Commands](#list-of-tinysa-commands-and-their-library-commands) section below. Each setter follows the same pattern: call the function with the desired value, and the library formats and sends the command. For example:
```python
tsa.rbw(100) # set resolution bandwidth to 100 kHz
tsa.set_sweep_center(96500000) # set sweep center to 96.5 MHz
tsa.set_attenuation(10) # set input attenuation
```
Acceptable value ranges and formats are listed per-command in the reference section. Where a value is out of range or the wrong type, the function returns an error rather than sending an invalid command to the device.
### Getting Data from Active Screen
See other sections for the following examples:
* [Saving Screen Images](#saving-screen-images)
* [Plotting Data with Matplotlib](#plotting-data-with-matplotlib)
This example shows several types of common data requests:
```python
# import tinySA_python (tsapython) package
from tsapython import tinySA
# create a new tinySA object
tsa = tinySA()
# set the return message preferences
tsa.set_verbose(True) #detailed messages
tsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown
# attempt to connect to previously discovered serial port
success = tsa.autoconnect()
# if port open, then complete task(s) and disconnect
if success == False:
print("ERROR: could not connect to port")
else:
# get current trace data on screen
msg = tsa.data(val=2)
print(msg)
# set current device ID
msg = tsa.device_id(3)
print(msg)
# get current device ID
msg = tsa.device_id()
print(msg)
# get device information
msg = tsa.info()
print(msg)
# pause sweeping
msg = tsa.pause()
print(msg)
# resume sweeping
msg = tsa.resume()
print(msg)
# get current battery voltage (mV)
msg = tsa.vbat()
print(msg)
tsa.disconnect()
```
### 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.
```python
# import tinySA_python (tsapython) package
from tsapython import tinySA
# imports FOR THE EXAMPLE
import numpy as np
from PIL import Image
import struct
def convert_data_to_image(data_bytes, width, height):
# this is not a particularly pretty example, and the data_bytes is sometimes a byte short
# calculate the expected data size
expected_size = width * height * 2 # 16 bits per pixel (RGB565), 2 bytes per pixel
# error checking - brute force, but fine while developing
if len(data_bytes) < expected_size:
print(f"Data size is too small. Expected {expected_size} bytes, got {len(data_bytes)} bytes.")
# if the data size is off by 1 byte, add a padding byte
if len(data_bytes) == expected_size - 1:
print("Data size is 1 byte smaller than expected. Adding 1 byte of padding.")
# add a padding byte (0x00) to make the size match
data_bytes.append(0)
else:
return
elif len(data_bytes) > expected_size:
# truncate the data to the expected size (in case it's larger than needed)
data_bytes = data_bytes[:expected_size]
print("Data is larger than the expected size. truncating. check data.")
# unpack the byte array to get pixel values (RGB565 format)
num_pixels = width * height
# unpacking as unsigned shorts (2 bytes each)
x = struct.unpack(f">{num_pixels}H", data_bytes)
# convert the RGB565 to RGBA
arr = np.array(x, dtype=np.uint32)
arr = 0xFF000000 + ((arr & 0xF800) >> 8) + ((arr & 0x07E0) << 5) + ((arr & 0x001F) << 19)
# reshape array to match the image dimensions. (height, width) format
arr = arr.reshape((height, width))
# create the image
img = Image.frombuffer('RGBA', (width, height), arr.tobytes(), 'raw', 'RGBA', 0, 1)
# save the image
img.save("capture_example.png")
# show the image
img.show()
# create a new tinySA object
tsa = tinySA()
# set the return message preferences
tsa.set_verbose(True) #detailed messages
tsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown
# attempt to connect to previously discovered serial port
success = tsa.autoconnect()
# if port closed, then return error message
if success == False:
print("ERROR: could not connect to port")
else: # port open, complete task(s) and disconnect
# get the trace data
data_bytes = tsa.capture()
print(data_bytes)
tsa.disconnect()
# processing after disconnect (just for this example)
# test with 480x320 resolution for tinySA Ultra
convert_data_to_image(data_bytes, 480, 320)
```
Capture On-Screen Trace Data of a Frequency Sweep from 100 kHz to 800 kHz
### Plotting Data with Matplotlib
#### **Example 1: Plot using On-Screen Trace Data and Frequencies**
This example plots the last/current sweep of data from the tinySA device.
`data()` gets the trace data. `frequencies()` gets the frequency values used.
`byteArrayToNumArray(byteArr)` takes in the returned trace data and frequency
bytearrays and converts them to arrays that are then plotted using `matplotlib`
This example works because `data()` returns a trace, which is going to be the same dimensionality of the `frequencies()` return because they have the same `RBW`
```python
# import tinySA_python (tsapython) package
from tsapython import tinySA
# import matplotlib FOR THE EXAMPLE
import matplotlib.pyplot as plt
# functions used in this example
def byteArrayToNumArray(byteArr, enc="utf-8"):
# decode the bytearray to a string
decodedStr = byteArr.decode(enc)
# split the string by newline characters
stringVals = decodedStr.splitlines()
# convert each value to a float
floatVals = [float(val) for val in stringVals]
return floatVals
# create a new tinySA object
tsa = tinySA()
# set the return message preferences
tsa.set_verbose(True) #detailed messages
tsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown
# attempt to connect to previously discovered serial port
success = tsa.autoconnect()
# if port closed, then return error message
if success == False:
print("ERROR: could not connect to port")
else: # port open, complete task(s) and disconnect
# get the trace data
data_bytes = tsa.data()
print(data_bytes)
# get the frequencies used by the last sweep
freq_bytes = tsa.frequencies()
tsa.disconnect()
# processing after disconnect (just for this example)
dataVals = byteArrayToNumArray(data_bytes)
print(len(dataVals)) # length of 450 data points
freqVals = byteArrayToNumArray(freq_bytes)
print(len(freqVals)) # length of 450 data points
# create the plot
plt.plot(freqVals, dataVals)
# add labels and title
plt.xlabel('Frequency (Hz)')
plt.ylabel('Measured Data (dBm)')
plt.title('tinySA Trace Plot')
# show the plot
plt.show()
```
Plotted On-Screen Trace Data of a Frequency Sweep from 100 kHz to 800 MHz
#### **Example 2: Plot using Scan Data and Frequencies**
This example uses `scan()` to take a data measurement of data that DOES NOT need to be on the screen, unlike **Example 1** above. Then, the frequencies on the x-axis are calculated between the `start` and `stop` frequencies using the `number of points`. This is done because `frequencies()` would have the values of the last scan, which are connected to `RBW` and not the `number of points`.
```python
# import tinySA_python (tsapython) package
from tsapython import tinySA
# imports FOR THE EXAMPLE
import numpy as np
import matplotlib.pyplot as plt
def convert_data_to_arrays(start, stop, pts, data):
# using the start and stop frequencies, and the number of points,
freq_arr = np.linspace(start, stop, pts) # note that the decimals might go out to many places.
# you can truncate this because it’s only used
# for plotting in this example
# As of the Jan. 2024 build in some data returned with SWEEP or SCAN calls there is error data.
# https://groups.io/g/tinysa/topic/tinasa_ultra_sweep_command/104194367
# this shows up as "-:.000000e+01".
# TEMP fix - replace the colon character with a -10. This puts the 'filled in' points around the noise floor.
# more advanced filtering should be applied for actual analysis.
data1 =bytearray(data.replace(b"-:.0", b"-10.0").replace(b":.0", b"10.0"))
# get both values in each row returned (for reference)
#data_arr = [list(map(float, line.split())) for line in data.decode('utf-8').split('\n') if line.strip()]
# get first value in each returned row
data_arr = [float(line.split()[0]) for line in data1.decode('utf-8').split('\n') if line.strip()]
return freq_arr, data_arr
# create a new tinySA object
tsa = tinySA()
# set the return message preferences
tsa.set_verbose(True) #detailed messages
tsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown
# attempt to autoconnect
found_bool, connected_bool = tsa.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 = 450 # sample points
outmask = 2 # get measured data (y axis)
# scan
data_bytes = tsa.scan(start, stop, pts, outmask)
print(data_bytes)
tsa.resume() #resume so screen isn't still frozen
tsa.disconnect()
# processing after disconnect (just for this example)
# convert data to 2 arrays
freq_arr, data_arr = convert_data_to_arrays(start, stop, pts, data_bytes)
# plot
plt.plot(freq_arr, data_arr)
plt.xlabel("Frequency (Hz)")
plt.ylabel("Measured Data (dBm)")
plt.title("tinySA Scan Plot")
plt.show()
```
Plotted Scan Data of a Frequency Sweep from 1 GHz to 3 GHz
#### **Example 3: Plot using SCAN and SCANRAW Data and Calculated Frequencies**
This example uses `scan()` and `scanraw()` to take a data measurement of data that DOES NOT need to been on the screen, unlike **Example 1** above. Then, the frequencies on the x-axis are calculated between the `start` and `stop` frequencies using the `number of points`. This is done because `frequencies()` would have the values of the last scan, which are connected to `RBW` and not the `number of points`. The following example shows several filters that can be used. If scanning is slow, check your device's RBW setting; 'auto' works best.
Extra processing needs to be done to get `dBm power` from `scanraw()`.
NOTE FOR LINUX USERS: the serial read with SCANRAW is finicky. It's also ONLY with this function on Linux. Reading the serial buffer after SCANRAW failed in several situations:
1. Requesting data too quickly after the last read
* Expected, as the tinySA needs to resume and re-measure.
2. Requesting data when the screen is frozen
* Mildly expected, user error can trigger this too. Turns out in some situations, the frozen screen is not the same as a `pause`, and there is no data to flush from the buffer because no more data has been taken. This is either a safe error state, a feature of how SCANRAW works, or potentially a bug with the device/firmware/this library. Using the `resume()` function after this will restart measurements.
3. {UNKNOWN}. There are several conditions that can cause issues, but it's unclear what 'symptoms' go to which problems
* On the first few reads after the tinySA has been turned on and operational for at least 1 minute.
* After sitting unused for more than a few minutes the returned buffer is < 50% the expected size or more than 5x the expected size. This is AFTER the flush command.
```python
# import tinySA_python (tsapython) package
from tsapython import tinySA
# imports FOR THE EXAMPLE
import numpy as np
import matplotlib.pyplot as plt
import struct
def convert_data_to_arrays(start, stop, pts, data):
# FOR PLOTTING
# using the start and stop frequencies, and the number of points,
freq_arr = np.linspace(start, stop, pts) # note that the decimals might go out to many places.
# you can truncate this because its only used
# for plotting in this example
# As of the Jan. 2024 build in some data returned with SWEEP or SCAN calls there is error data.
# https://groups.io/g/tinysa/topic/tinasa_ultra_sweep_command/104194367
# this shows up as "-:.000000e+01".
# TEMP fix - replace the colon character with a -10. This puts the 'filled in' points around the noise floor.
# more advanced filtering should be applied for actual analysis.
data1 =bytearray(data.replace(b"-:.0", b"-10.0").replace(b":.0", b"10.0"))
# get both values in each row returned (for reference)
#data_arr = [list(map(float, line.split())) for line in data.decode('utf-8').split('\n') if line.strip()]
# get first value in each returned row
data_arr = [float(line.split()[0]) for line in data1.decode('utf-8').split('\n') if line.strip()]
# NOTE: if repeated read errors with utf-8 occur, uncomment the below as an alternative to the
# line above. This will show you what value is being returned that caused the problem. It may
# indicate a different problem with the serial connection permissions
# data_arr = []
# for i, line in enumerate(data1.decode('utf-8').split('\n')):
# print(f"Line {i}: '{line}'") # Show the raw line
# line = line.strip()
# if line:
# try:
# value = float(line.split()[0])
# data_arr.append(value)
# # print(f" Parsed float: {value}")
# except ValueError as e:
# print(f" Could not convert line to float: {line} — Error: {e}")
return freq_arr, data_arr
# create a new tinySA object
tsa = tinySA()
# set the return message preferences
tsa.set_verbose(True) #detailed messages
tsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown
# attempt to autoconnect
found_bool, connected_bool = tsa.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(150e6) # 150 MHz
stop = int(500e6) # 500 MHz
pts = 450 # for tinySA Ultra
outmask = 2 # get measured data (y axis)
# scan raw call - reads until end of stream
# this CAN be run in a loop. the limiting factor is time to plot.
# SCAN
scan_data_bytes = tsa.scan(start, stop, pts, outmask)
# SCAN RAW
scanraw_data_bytes = tsa.scan_raw(start, stop, pts, outmask)
# for subsequent reads, the tinySA does freeze while performing SCANRAW
# if there's an error, the screen will stay frozen (for reading).
# So start it again so new data can be taken
tsa.resume()
# disconnect because we don't need the tinySA to process data
tsa.disconnect()
# process the SCAN data (this is already in dBm)
# convert data to 2 arrays for X and Y
freq_arr, data_arr = convert_data_to_arrays(start, stop, pts, scan_data_bytes)
# PROCESS SCANRAW into an array & reuse the FREQ_ARR value
# remove the intro curly brace ({)
bin_scanraw = scanraw_data_bytes[1:] #skip the first char because it's the remaining curly brace
# use struct.unpack() because of the repeating pattern
# <: indicates little-endian byte order, meaning the least significant byte is stored first
# 'xH'*pts: a repetition of the format 'xH' once per point.
# 'x': represents a pad byte, which is ignored
# 'H': represents an unsigned short integer (2 bytes)
expected_len = 3 * pts
actual_len = len(bin_scanraw)
print(f"Expected length: {expected_len}, Actual length: {actual_len}")
if actual_len == expected_len:
# SCANRAW has returned the expected amount of data for the read.
# sometimes this function (and not SCAN) does not read the buffer properly
# a fix is in progress for LINUX systems. it works fine for Windows
processed_scanraw = struct.unpack( '<' + 'xH'*pts, bin_scanraw ) # ignore trailing '}ch> '
processed_scanraw = np.array(processed_scanraw, dtype=np.uint16 ).reshape(-1, 1) #unit8 has overflow error
# CONVERT to dBm Power
# take the processed binary data and convert it to dBm.
# The equation is from tinySA.org & official documentation
SCALE_FACTOR = 174 # tinySA Basic: 128, tinySA Ultra and newer is 174
dBm_data = processed_scanraw / 32 - SCALE_FACTOR
print(dBm_data)
# plot
plt.plot(freq_arr, data_arr, label= 'SCAN data')
plt.plot(freq_arr, dBm_data, label= 'SCANRAW data')
plt.xlabel("frequency (hz)")
plt.ylabel("measured data (dBm)")
plt.title("tinySA SCAN and SCANRAW data")
plt.legend()
plt.show()
else:
print("SCANRAW did not return the expected amount of data for the read")
```
Plotted SCAN and SCANRAW Data of a Frequency Sweep from 150 MHz to 500 MHz
#### **Example 4: Plot using SCAN And Filters for Artifact Comparison**
This example uses `scan()` to collect measured data, then demonstrates how to clean up the `:` firmware artifact that occasionally appears in scan output. On some firmware builds, a malformed value like `:.000000e-01` shows up in the data. The `:` is ASCII `0x3A`, one position past `9`, which happens when the firmware overflows a single digit slot where it meant to write `10`. The standard handling (used in the other plotting examples) substitutes these with a value near the noise floor so the data stays parseable, but that leaves sharp spikes in the trace.
To show how those spikes can be smoothed out, this example plots the raw data alongside two filtered versions on a single matplotlib figure. A **median filter** removes the isolated spikes cleanly, while a **moving average** is included as a cautionary contrast — it smears the spikes into neighboring points rather than removing them. Comparing the three traces side by side makes the trade-offs of each approach easy to see. The filters are implemented in pure `numpy`, so no additional dependencies beyond the plotting extras are required.
```python
# import tinySA_python (tsapython) package
from tsapython import tinySA
# imports FOR THE EXAMPLE
try:
import numpy as np
import matplotlib.pyplot as plt
except ImportError as exc:
raise SystemExit(
"This example requires the plotting extra (numpy and matplotlib). "
'Install it with: pip install "tsapython[plotting]"'
) from exc
def parse_scan_levels(data_bytes, fix_artifact=True):
# Parse scan(outmask=2) output to an array of dBm levels.
# If fix_artifact is True, apply the standard ':' -> 10 substitution so the
# data is parseable. (With it False, the malformed rows would raise on
# float() -- shown here only to explain why the fix exists.)
raw = bytes(data_bytes)
if fix_artifact:
raw = raw.replace(b"-:.0", b"-10.0").replace(b":.0", b"10.0")
levels = []
for line in bytearray(raw).decode("utf-8").split("\n"):
line = line.strip()
if line:
levels.append(float(line.split()[0]))
return np.array(levels)
def median_filter(x, k=5):
# Pure-numpy median filter. k is forced odd. Edge-padded so length is kept.
if k % 2 == 0:
k += 1
pad = k // 2
xp = np.pad(x, pad, mode="edge")
return np.array([np.median(xp[i:i + k]) for i in range(len(x))])
def moving_average(x, k=5):
# Pure-numpy moving average. k is forced odd. Edge-padded so length is kept.
if k % 2 == 0:
k += 1
pad = k // 2
xp = np.pad(x, pad, mode="edge")
return np.convolve(xp, np.ones(k) / k, mode="valid")
def main():
tsa = tinySA()
tsa.set_verbose(False)
tsa.set_error_byte_return(True)
found, connected = tsa.autoconnect()
if not connected:
print("ERROR: could not connect to port")
return
start = int(1e9) # 1 GHz
stop = int(3e9) # 3 GHz
pts = 450
data_bytes = tsa.scan(start, stop, pts, 2) # outmask 2 = measured data
tsa.resume()
tsa.disconnect()
raw_levels = parse_scan_levels(data_bytes, fix_artifact=True)
freqs = np.linspace(start, stop, len(raw_levels))
# apply the two filters
med = median_filter(raw_levels, k=5)
avg = moving_average(raw_levels, k=5)
# report how many artifact-substituted points there were (points sitting at
# the -10 substitution value are the likely artifacts)
n_artifacts = int(np.sum(np.isclose(raw_levels, -10.0)))
print(f"Scanned {len(raw_levels)} points; "
f"{n_artifacts} look like ':'-artifact substitutions (~-10 dBm).")
# plot all three on one figure
plt.figure(figsize=(12, 7))
plt.plot(freqs / 1e9, raw_levels, lw=0.8, alpha=0.6,
label="raw (artifact-substituted, spikes visible)")
plt.plot(freqs / 1e9, med, lw=1.3,
label="median filter k=5 (removes spikes)")
plt.plot(freqs / 1e9, avg, lw=1.3, alpha=0.8,
label="moving average k=5 (smears spikes -- cautionary)")
plt.xlabel("Frequency (GHz)")
plt.ylabel("Power (dBm)")
plt.title("tinySA scan: ':' artifact and filtering comparison")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
if __name__ == "__main__":
main()
```
Plotted SCAN Artifact and Filtering Comparison
#### **Example 5: Plot a Waterfall using SCAN and Calculated Frequencies**
The first part of this example is a static report of the measurements taken over time. The time will vary a bit from the resolution. Data is collected and then displayed with matplotlib.
```python
# import tinySA_python (tsapython) package
from tsapython import tinySA
# imports FOR THE EXAMPLE
import csv
import numpy as np
import matplotlib.pyplot as plt
import time
from datetime import datetime
def convert_data_to_arrays(start, stop, pts, data):
# using the start and stop frequencies, and the number of points,
freq_arr = np.linspace(start, stop, pts) # note that the decimals might go out to many places.
# you can truncate this because its only used
# for plotting in this example
# As of the Jan. 2024 build in some data returned with SWEEP or SCAN calls there is error data.
# https://groups.io/g/tinysa/topic/tinasa_ultra_sweep_command/104194367
# this shows up as "-:.000000e+01".
# TEMP fix - replace the colon character with a -10. This puts the 'filled in' points around the noise floor.
# more advanced filtering should be applied for actual analysis.
data1 = bytearray(data.replace(b"-:.0", b"-10.0").replace(b":.0", b"10.0"))
# get both values in each row returned (for reference)
#data_arr = [list(map(float, line.split())) for line in data.decode('utf-8').split('\n') if line.strip()]
# get first value in each returned row
data_arr = [float(line.split()[0]) for line in data1.decode('utf-8').split('\n') if line.strip()]
return freq_arr, data_arr
def collect_waterfall_data(tsa, start, stop, pts, outmask, num_scans, scan_interval):
waterfall_data = [] # 2D array of scan data (time x frequency)
timestamps = []
freq_arr = None
print(f"Collecting {num_scans} scans with {scan_interval}s intervals...")
for i in range(num_scans):
print(f"Scan {i+1}/{num_scans}")
# Perform scan
data_bytes = tsa.scan(start, stop, pts, outmask)
# Convert to arrays
if freq_arr is None:
freq_arr, data_arr = convert_data_to_arrays(start, stop, pts, data_bytes)
else:
_, data_arr = convert_data_to_arrays(start, stop, pts, data_bytes)
# Store data and timestamp
waterfall_data.append(data_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_data), timestamps
def plot_waterfall(freq_arr, waterfall_data, timestamps, start, stop):
# Create figure with subplots
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))
# Waterfall plot (main plot)
# Create time array for y-axis (scan number or elapsed time)
time_arr = np.arange(len(timestamps))
# Create meshgrid for pcolormesh
freq_mesh, time_mesh = np.meshgrid(freq_arr, time_arr)
# Plot waterfall
im = ax1.pcolormesh(freq_mesh/1e9, time_mesh, waterfall_data,
shading='nearest', cmap='viridis')
ax1.set_xlabel('Frequency (GHz)')
ax1.set_ylabel('Scan Number')
ax1.set_title(f'Waterfall Plot: {start/1e9:.1f} - {stop/1e9:.1f} GHz')
# Add colorbar
cbar = plt.colorbar(im, ax=ax1)
cbar.set_label('Signal Strength (dBm)')
# Latest scan plot (bottom subplot)
ax2.plot(freq_arr/1e9, waterfall_data[-1])
ax2.set_xlabel('Frequency (GHz)')
ax2.set_ylabel('Signal Strength (dBm)')
ax2.set_title('Latest Scan')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
return fig
# create a new tinySA object
tsa = tinySA()
# set the return message preferences
tsa.set_verbose(True) #detailed messages
tsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown
# attempt to autoconnect
found_bool, connected_bool = tsa.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 = 450 # sample points
outmask = 2 # get measured data (y axis)
# waterfall parameters
num_scans = 50 # number of scans to collect
scan_interval = 0.5 # seconds between scans
# collect waterfall data
freq_arr, waterfall_data, timestamps = collect_waterfall_data(
tsa, start, stop, pts, outmask, num_scans, scan_interval)
print("Data collection complete!")
# resume and disconnect
tsa.resume() #resume so screen isn't still frozen
tsa.disconnect()
# processing after disconnect
print("Creating waterfall plot...")
# create waterfall plot
fig = plot_waterfall(freq_arr, waterfall_data, timestamps, start, stop)
# Save data out to .csv
filename = "waterfall_1_sample.csv"
# Create CSV with frequency headers and time/scan data
with open(filename, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
# Write header row with frequencies (in Hz)
header = ['Scan_Number', 'Timestamp'] + [f'{freq:.0f}' for freq in freq_arr]
writer.writerow(header)
# Write data rows
for i, (scan_data, timestamp) in enumerate(zip(waterfall_data, timestamps)):
row = [i+1, timestamp.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]] + scan_data.tolist()
writer.writerow(row)
print(f"Data saved to {filename}")
print(f"CSV contains {len(waterfall_data)} scans with {len(freq_arr)} frequency points each")
# show plot
plt.show()
except KeyboardInterrupt:
print("\nScan interrupted by user")
tsa.resume()
tsa.disconnect()
except Exception as e:
print(f"Error occurred: {e}")
tsa.resume()
tsa.disconnect()
```
Static Waterfall Plot for SCAN Data Over 50 Readings
The second part of the example is a realtime waterfall plot with peak tracking and a sample of the last reading.
```python
# import tinySA_python (tsapython) package
from tsapython import tinySA
# 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_data_to_arrays(start, stop, pts, data):
#Convert the raw tinySA data to frequency and power arrays.
# using the start and stop frequencies, and the number of points,
freq_arr = np.linspace(start, stop, pts) # note that the decimals might go out to many places.
# you can truncate this because its only used
# for plotting in this example
# As of the Jan. 2024 build in some data returned with SWEEP or SCAN calls there is error data.
# https://groups.io/g/tinysa/topic/tinasa_ultra_sweep_command/104194367
# this shows up as "-:.000000e+01".
# TEMP fix - replace the colon character with a -10. This puts the 'filled in' points around the noise floor.
# more advanced filtering should be applied for actual analysis.
data1 = bytearray(data.replace(b"-:.0", b"-10.0").replace(b":.0", b"10.0"))
# Get first value in each returned row (power in dBm)
try:
data_arr = [float(line.split()[0]) for line in data1.decode('utf-8').split('\n') if line.strip()]
except (ValueError, IndexError):
# If parsing fails, return zeros
data_arr = [0.0] * pts
# Ensure data array matches frequency array length
if len(data_arr) != pts:
# Pad or truncate to match expected points
# We do this to visualize what might be going wrong rather than outright throwing an error
# -100 is a very low noise floor, especially for a hand held device, so it's not a normal reading
if len(data_arr) < pts:
data_arr.extend([data_arr[-1] if data_arr else -100.0] * (pts - len(data_arr)))
else:
data_arr = data_arr[:pts]
return freq_arr, np.array(data_arr)
class LiveSpectrumPlotter:
def __init__(self, tsa, start, stop, pts, outmask, max_history=50):
self.tsa = tsa
self.start = start
self.stop = stop
self.pts = pts
self.outmask = outmask
self.max_history = max_history
# Data storage
self.freq_arr = None
self.power_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_power = None
# Twin axis reference for proper cleanup
self.ax3_twin = None
def data_acquisition_thread(self):
#Background thread for continuous data acquisition
while self.running:
try:
# Get scan data
data_bytes = self.tsa.scan(self.start, self.stop, self.pts, self.outmask)
# Convert to arrays
freq_arr, power_arr = convert_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,
'power': power_arr,
'timestamp': datetime.now()
})
time.sleep(0.2) # Small delay to prevent overwhelming the device
except Exception as e:
print(f"Data acquisition error: {e}")
time.sleep(0.5) # Wait a bit before retrying
continue
def start_acquisition(self):
#Start the data acquisition 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 data acquisition thread
self.running = False
if self.data_thread:
self.data_thread.join()
def update_plots(self, frame):
#Update the matplotlib plots with new data
# 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_power = data['power']
# Add to history
self.power_history.append(data['power'])
self.timestamps.append(data['timestamp'])
except queue.Empty:
break
# Clear plots
ax1.clear() # Waterfall
ax2.clear() # Live spectrum
ax3.clear() # Peak tracking
# Clear any existing twin axes completely
if hasattr(self, 'ax3_twin') and self.ax3_twin is not None:
self.ax3_twin.clear()
self.ax3_twin.remove()
self.ax3_twin = None
if self.freq_arr is not None and self.current_power is not None:
# Plot 1: Waterfall (left side - larger)
if len(self.power_history) > 1:
waterfall_data = np.array(list(self.power_history))
# Create time array in reverse order so newest (highest index) appears at top
time_arr = np.arange(len(waterfall_data))
freq_mesh, time_mesh = np.meshgrid(self.freq_arr, time_arr)
im = ax1.pcolormesh(freq_mesh/1e9, time_mesh, waterfall_data,
shading='nearest', cmap='viridis')
ax1.set_xlabel('Frequency (GHz)')
ax1.set_ylabel('Scan Number (newest at top)')
ax1.set_title('Spectrum History (Waterfall)')
# Add colorbar to waterfall plot
if not hasattr(self, 'colorbar_created'):
self.colorbar = plt.colorbar(im, ax=ax1, shrink=0.8)
self.colorbar.set_label('Power (dBm)')
self.colorbar_created = True
# Plot 2: Current Spectrum (top right)
ax2.plot(self.freq_arr/1e9, self.current_power, 'b-', linewidth=1.5)
ax2.set_xlabel('Frequency (GHz)')
ax2.set_ylabel('Power (dBm)')
ax2.set_title('Live Spectrum')
ax2.grid(True, alpha=0.3)
# Set reasonable y-axis limits
if len(self.current_power) > 0:
y_min = np.min(self.current_power) - 5
y_max = np.max(self.current_power) + 5
ax2.set_ylim(y_min, y_max)
# Plot 3: Peak tracking over time (bottom right)
if len(self.power_history) > 1:
peak_powers = [np.max(scan) for scan in self.power_history]
peak_freqs = [self.freq_arr[np.argmax(scan)]/1e9 for scan in self.power_history]
# Plot peak power over time
scan_numbers = list(range(len(peak_powers)))
# Create fresh twin axis for frequency (store reference for proper cleanup)
self.ax3_twin = ax3.twinx()
ax3.plot(scan_numbers, peak_powers, 'r-o', markersize=2,
label='Peak Power', linewidth=1.5)
self.ax3_twin.plot(scan_numbers, peak_freqs, 'g-s', markersize=2,
label='Peak Freq', linewidth=1.5)
ax3.set_xlabel('Scan Number')
ax3.set_ylabel('Peak Power (dBm)', color='r')
self.ax3_twin.set_ylabel('Peak Freq (GHz)', color='g')
ax3.set_title('Peak Tracking')
ax3.grid(True, alpha=0.3)
# Color the y-axis labels to match the lines
ax3.tick_params(axis='y', labelcolor='r', labelsize=8)
self.ax3_twin.tick_params(axis='y', labelcolor='g', labelsize=8)
# Force immediate redraw of the twin axis
self.ax3_twin.relim()
self.ax3_twin.autoscale_view()
# Add timestamp and scan info
if self.timestamps:
scan_count = len(self.timestamps)
time_str = self.timestamps[-1].strftime("%H:%M:%S")
fig.suptitle(f'Live tinySA Spectrum - {time_str} (Scan #{scan_count})',
fontsize=14)
if __name__ == "__main__":
# create a new tinySA object
tsa = tinySA()
# set the return message preferences
tsa.set_verbose(True)
tsa.set_error_byte_return(True)
# attempt to autoconnect
found_bool, connected_bool = tsa.autoconnect()
if not connected_bool:
print("ERROR: could not connect to port")
else:
try:
print("Starting live spectrum measurement...")
print("Close the plot window to stop measurement")
# Scan parameters
start = int(1e9) # 1 GHz
stop = int(3e9) # 3 GHz
pts = 200 # Reduced points for faster updates
outmask = 2 # get measured data
# Create plotter
plotter = LiveSpectrumPlotter(tsa, start, stop, pts, outmask, max_history=30)
# Set up the plot - 2x2 layout with waterfall taking left column
fig = plt.figure(figsize=(14, 10))
# Create grid layout: waterfall on left (spans 2 rows), two plots on right
gs = fig.add_gridspec(2, 2, width_ratios=[2, 1], height_ratios=[1, 1],
hspace=0.3, wspace=0.3)
ax1 = fig.add_subplot(gs[:, 0]) # Waterfall - spans both rows, left column
ax2 = fig.add_subplot(gs[0, 1]) # Live spectrum - top right
ax3 = fig.add_subplot(gs[1, 1]) # Peak tracking - bottom right
# Start data acquisition
plotter.start_acquisition()
# Create animation
ani = animation.FuncAnimation(fig, plotter.update_plots,
interval=300, blit=False)
# Show plot (this blocks until window is closed)
plt.show()
# Cleanup
plotter.stop_acquisition()
tsa.resume()
tsa.disconnect()
print("Live measurement stopped")
except KeyboardInterrupt:
print("\nMeasurement interrupted by user")
tsa.resume()
tsa.disconnect()
except Exception as e:
print(f"Error occurred: {e}")
tsa.resume()
tsa.disconnect()
```
Realtime Waterfall Plot for SCAN Data
#### **Example 6: Finding Peaks in a Frequency Range**
Locating the strongest signal in a span is a common task, and this example (`examples/find_peaks.py`) shows two ways to do it.
The first uses the device's built-in marker peak: `marker_peak(1)` activates marker 1 and parks it on the strongest signal the device sees, returning the marker information. This is the quickest way to find the single largest peak, since the hardware does the work.
The second computes peaks from `scan()` data in Python, which lets you find *multiple* peaks rather than just the strongest one. After collecting the scan, the example walks the data to pick out the top several peaks, blanking a small window around each one it finds so the same signal's shoulder isn't reported twice. The found peaks are printed with their frequencies and power levels, and marked on a plot of the spectrum.
Use the device marker peak when you just need the single strongest signal; use the Python approach when you want to catalog several signals across the span at once. The example also applies the same `:`-artifact handling described in the scan examples above, so malformed firmware values don't skew the results.
Top 3 Peaks in a Frequency Range
### Saving SCAN Data to CSV
```python
# import tinySA_python (tsapython) package
from tsapython import tinySA
# imports FOR THE EXAMPLE
import csv
import numpy as np
def convert_data_to_arrays(start, stop, pts, data):
# using the start and stop frequencies, and the number of points,
freq_arr = np.linspace(start, stop, pts) # note that the decimals might go out to many places.
# you can truncate this because its only used
# for plotting in this example
# As of the Jan. 2024 build in some data returned with SWEEP or SCAN calls there is error data.
# https://groups.io/g/tinysa/topic/tinasa_ultra_sweep_command/104194367
# this shows up as "-:.000000e+01".
# TEMP fix - replace the colon character with a -10. This puts the 'filled in' points around the noise floor.
# more advanced filtering should be applied for actual analysis.
data1 =bytearray(data.replace(b"-:.0", b"-10.0").replace(b":.0", b"10.0"))
# get both values in each row returned (for reference)
#data_arr = [list(map(float, line.split())) for line in data.decode('utf-8').split('\n') if line.strip()]
# get first value in each returned row
data_arr = [float(line.split()[0]) for line in data1.decode('utf-8').split('\n') if line.strip()]
return freq_arr, data_arr
# create a new tinySA object
tsa = tinySA()
# set the return message preferences
tsa.set_verbose(True) #detailed messages
tsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown
# attempt to autoconnect
found_bool, connected_bool = tsa.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 = 450 # sample points
outmask = 2 # get measured data (y axis)
# scan
data_bytes = tsa.scan(start, stop, pts, outmask)
print(data_bytes)
tsa.resume() #resume so screen isn't still frozen
tsa.disconnect()
# processing after disconnect (just for this example)
# convert data to 2 arrays
freq_arr, data_arr = convert_data_to_arrays(start, stop, pts, data_bytes)
# Save the data to CSV
filename = "scan_sample.csv"
# Write out to csv where column 1 is frequency and col 2 is data
with open(filename, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
# Write header row
writer.writerow(['Frequency_Hz', 'Signal_Strength_dBm'])
# Write data rows (frequency, signal strength pairs)
for freq, signal in zip(freq_arr, data_arr):
writer.writerow([f'{freq:.0f}', signal])
print(f"Data saved to {filename}")
print(f"CSV contains {len(freq_arr)} frequency/signal pairs")
print(f"Data saved to {filename}")
```
### Accessing the tinySA Directly
In some cases, this library may not cover all possible command versions, or new features might not be included yet. The tinySA 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.
```python
# import tinySA_python (tsapython) package
from tsapython import tinySA
# create a new tinySA object
tsa = tinySA()
# set the return message preferences
tsa.set_verbose(True) #detailed messages
tsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown
# attempt to autoconnect
found_bool, connected_bool = tsa.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 = 150e6 # 150 MHz
stop = 200e6 # 200 MHz
pts = 450 # for tinySA Ultra
outmask = 1 # get measured data (y axis)
# scan
data_bytes = tsa.command("scan 150e6 200e6 5 2")
print(data_bytes)
tsa.resume() #resume
tsa.disconnect()
```
## List of tinySA 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](#additional-library-commands) section describe the functions in this library.
This section is sorted by the tinySA (Ultra) commands, and includes:
* A brief description of what the command does
* What the original usage looked like
* The tinySA_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:
| | | | | | | |
|-------|-------|-------|-------|-------|-------|-------|
| [abort](#abort) | [actual_freq](#actual_freq) | [agc](#agc) | [attenuate](#attenuate) | [bulk](#bulk) | [calc](#calc) | [caloutput](#caloutput) |
| [capture](#capture) | [clearconfig](#clearconfig) | [color](#color) | [correction](#correction) | [dac](#dac) | [data](#data) | [deviceid](#deviceid) |
| [direct](#direct) | [ext_gain](#ext_gain) | [fill](#fill) | [freq](#freq) | [freq_corr](#freq_corr) | [frequencies](#frequencies) | [help](#help) |
| [hop](#hop) | [if](#if) | [if1](#if1) | [info](#info) | [level](#level) | [levelchange](#levelchange) | [leveloffset](#leveloffset) |
| [line](#line) | [load](#load) | [lna](#lna) | [lna2](#lna2) | [marker](#marker) | [menu](#menu) | [mode](#mode) |
| [modulation](#modulation) | [output](#output) | [pause](#pause) | [rbw](#rbw) | [recall](#recall) | [refresh](#refresh) | [release](#release) |
| [remark](#remark) | [repeat](#repeat) | [reset](#reset) | [restart](#restart) | [resume](#resume) | [save](#save) | [saveconfig](#saveconfig) |
| [scan](#scan) | [scanraw](#scanraw) | [sd_delete](#sd_delete) | [sd_list](#sd_list) | [sd_read](#sd_read) | [selftest](#selftest) | [spur](#spur) |
| [status](#status) | [sweep](#sweep) | [sweeptime](#sweeptime) | [sweep_voltage](#sweep_voltage) | [text](#text) | [threads](#threads) | [touch](#touch) |
| [touchcal](#touchcal) | [touchtest](#touchtest) | [trace](#trace) | [trigger](#trigger) | [ultra](#ultra) | [usart_cfg](#usart_cfg) | [vbat](#vbat) |
| [vbat_offset](#vbat_offset) | [version](#version) | [wait](#wait) | [zero](#zero) | | | |
### **abort**
* **Description:** Sets the abortion enabled status (on/off) or aborts the previous command.
* **Original Usage:** `abort [off|on]`
* **Direct Library Function Call:** `abort(val=None|"off"|"on")`
* **Example Return:** ????
* **Alias Functions:**
* `enable_abort()`
* `disable_abort()`
* `abort_action()`
* **CLI Wrapper Usage:**
* **Notes:** WARNING: DOES NOT ON DEVELOPER'S DUT. When used without parameters the previous command still running will be aborted. Abort must be enabled before using the "abort on" command. Additional error checking has been added with the 'verbose' option.
### **actual_freq**
* **Description:** Sets and gets the frequency correction set by CORRECT FREQUENCY menu in the expert menu settings
* **Original Usage:** `actual_freq [{frequency}]`
* **Direct Library Function Call:** `actual_freq(val=None|Int)`
* **Example Return:** 3000000000
* **Alias Functions:**
* `set_actual_freq(val=Int)`
* `set_actual_freq()`
* **CLI Wrapper Usage:**
* **Notes:** freq in Hz going by the returns. Should be able to set the value with this, according to documentation, but it doesn't appear to set with the development DUT.
### **agc**
* **Description:** Enables/disables the build in Automatic Gain Control
* **Original Usage:** `agc 0..7|auto`
* **Direct Library Function Call:** `agc(val="auto"|0..7)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_agc(val="auto"|Int 0..7)`
* **CLI Wrapper Usage:**
* **Notes:**
### **attenuate**
* **Description:** Sets the internal attenuation
* **Original Usage:** `attenuate [auto|0-31]`
* **Direct Library Function Call:** `attenuate(val="auto"|0..31)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_attenuation(val="auto"|Int 0..31)`
* **CLI Wrapper Usage:**
* **Notes:**
### **bulk**
* **Description:** Sent by tinySA when in auto refresh mode
* **Original Usage:** `bulk`
* **Direct Library Function Call:** `bulk()`
* **Example Return:** `format: "bulk\r\n{X}{Y}{Width}{Height} {Pixeldata}\r\n"`
* **Alias Functions:**
* `get_bulk_data()`
* **CLI Wrapper Usage:**
* **Notes:**
All numbers are binary coded 2 bytes little endian. The pixel data is encoded as 2 bytes per pixel. This is data returned by the device when in AUTO REFRESH mode. NOTE: may need to be paired with a continuous buffer read and dump, which will be tested in the next update
### **calc**
* **Description:** Sets or cancels one of the measurement modes
* **Original Usage:** `calc off|minh|maxh|maxd|aver4|aver16|quasip`
* **Direct Library Function Call:** `calc(val="off"|"minh"|"maxh"|"maxd"|"aver4"|"aver16"|"quasip")`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_calc_off()`
* `set_calc_minh()`
* `set_calc_maxh()`
* `set_calc_maxd()`
* `set_calc_aver4()`
* `set_calc_aver16()`
* `set_calc_quasip()`
* **CLI Wrapper Usage:**
* **Notes:**
* the commands are the same as those listed in the MEASURE menu
* [tinySA Calc Menu](#https://tinysa.org/wiki/pmwiki.php?n=Main.CALC):
* OFF disables any calculation
* MIN HOLD sets the display to hold the minimum value measured. Reset the hold by selecting again. This setting is used to see stable signals that are within the noise
* MAX HOLD sets the display to hold the maximum value measured. Reset the hold by selecting again. This setting can be used for many measurements such as showing the power envelope of a modulated signal.
* MAX DECAY sets the display to hold the maximum value measured for a certain number of scans after which the maximum will start to decay. The default number of scans to hold is 20. This default can be changed in the SETTINGS menu. This setting is used instead of MAX HOLD to reduce the impact of spurious signals
* AVER 4 sets the averaging to new_measurement = old_measurement*3/4+measured_value/4. By default the averaging is linear power averaging
* AVER 16 sets the averaging to new_measurement = old_measurement*15/16+measured_value/16. By default the averaging is linear power averaging
* QUASSIP sets a quasi peak hold mode
* [Official CALC documentation](https://tinysa.org/wiki/pmwiki.php?n=Main.CALC)
### **caloutput**
* **Description:** Disables or sets the caloutput to a specified frequency in MHz. Reference signal.
* **Original Usage:** `caloutput off|30|15|10|4|3|2|1`
* **Library Function :** `cal_output(val="off"|30|15|10|4|3|2|1)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_cal_output_off()`
* `set_cal_output_30()`
* `set_cal_output_15()`
* `set_cal_output_10()`
* `set_cal_output_4()`
* `set_cal_output_3()`
* `set_cal_output_2()`
* `set_cal_output_1()`
* **CLI Wrapper Usage:**
* **Notes:** "controls the build in calibration reference generator. The output of the reference generator is connected to CAL connector. The output frequency can be 1,2,4,10,15 and 30MHz and the output level of the fundamental at 30MHz is -35.6dBm" - From [https://tinysa.org/wiki/pmwiki.php?n=TinySA4.MODE](https://tinysa.org/wiki/pmwiki.php?n=TinySA4.MODE)
### **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:** tinySA original: 320x240, tinySA Ultra and newer: 480x320
### **clearconfig**
* **Description:** Resets the configuration data to factory defaults
* **Original Usage:** `clear config`
* **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.
### **color**
* **Description:** Sets or gets colors of traces and other elements on the spectrum analyzer display. Colors must be in 24-bit RGB color value format.
* **Original Usage:** `color [{id} {rgb24}]`
* **Direct Library Function Call:** `color(ID=None|0..31, RGB=None(default:'0xF8FCF8')|'0x000000'..'0xFFFFFF')`
* **Example Return:**
* If ID='None' used:
`0: 0x000000\r\n 1: 0xF8FCF8\r\n 2: 0x808080\r\n 3: 0xE0E4E0\r\n 4: 0x000000\r\n 5: 0xD0D0D0\r\n 6: 0xF8FC00\r\n 7: 0x40FC40\r\n 8: 0xF800F8\r\n 9: 0xF84040\r\n 10: 0x18E000\r\n 11: 0xF80000\r\n 12: 0x0000F8\r\n 13: 0xF8FCF8\r\n 14: 0x808080\r\n 15: 0x00FC00\r\n 16: 0x808080\r\n 17: 0x000000\r\n 18: 0xF8FCF8\r\n 19: 0x0000F8\r\n 20: 0xF88080\r\n 21: 0x00FC00\r\n 22: 0x888C88\r\n 23: 0xD8DCD8\r\n 24: 0x282828\r\n 25: 0xC0C4C0\r\n 26: 0xF8FCF8\r\n 27: 0x00FC00\r\n 28: 0x00FCF8\r\n 29: 0xF8FC00\r\n 30: 0x000000\r\n 31: 0x000000\r'`
* If ID = '15' used: `0x00FC00`
``
* **Alias Functions:**
* `get_all_colors()`
* `get_marker_color(ID=Int|0..31)`
* `set_marker_color(ID=Int|0..31, col=rgb24)`
* **CLI Wrapper Usage:**
* **Notes:** the rgb24 hex value currently must be passed in as a string
### **correction**
* **Description:** Sets or gets the frequency level correction table
* **Original Usage:** `correction {low|lna|ultra|ultra_lna|direct|direct_lna|harm|harm_lna|out|out_direct|out_adf|out_ultra|off|on} [{0-19} {frequency(Hz)} {value(dB)}]`
* **Direct Library Function Call:** `correction(tableName, slot, freq, val)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* This function is complex enough that it is recommended to use the `command()` function for options not covered by the `correction(...)` library function
* **CLI Wrapper Usage:**
* **Notes:** See [Correction Wiki](https://tinysa.org/wiki/pmwiki.php?n=Main.Correction). The current content of the table is shown by entering `correction low` without any arguments. The data in the table can be modified by specifying the slot number and the new values. There **MUST** be one entry in the low table for 30MHz and the correction for that frequency **MUST** be zero.
* **Future Work:** **Confirm table format across devices**. Value currently limited between -10 and 35, but this needs to be more specific.
### **dac**
* **Description:** Sets or gets the dac value
* **Original Usage:** `dac [0..4095]`
* **Library Function Call(s):** `dac(val=None/Int|0..4095)`
* **Example Return:** `b'usage: dac {value(0-4095)}\r\ncurrent value: 1922\r'`
* **Alias Functions:**
* `set_dac(val=Int|0...4095)`
* `get_dac()`
* **CLI Wrapper Usage:**
* **Notes:**
### **data**
* **Description:** Gets the trace data
* **Original Usage:** `data 0..2`
* **Direct Library Function Call:** `data(val=0|1|2)`
* **Example Return:** `format bytearray(b'7.593750e+00\r\n-8.437500e+01\r\n-8.693750e+01\r\n...\r')`
* **Alias Functions:**
* `get_temporary_data()`
* `get_stored_trace_data()`
* `dump_measurement_data()`
* **CLI Wrapper Usage:**
* **Notes:**
* 0 = temp value, 1 = stored trace, 2 = measurement. strength in decibels (dB)
* `get_temporary_data` not to be confused with `get_temp`, which returns temperature
### **device_id**
* **Description:** Sets or gets a user settable integer number ID that can be use to identify a specific tinySA connected to the PC
* **Original Usage:** `deviceid [{number}]`
* **Direct Library Function Call:** `deviceid(ID=None/Int)`
* **Example Return:** `'deviceid 0\r'`
* **Alias Functions:**
* `get_device_id()`
* `set_device_id(ID=Int|0....)`
* **CLI Wrapper Usage:**
* **Notes:**
### **direct**
* **Description:** Output mode for generating a square wave signal between 830MHz and 1130MHz
* **Original Usage:** `direct {start|stop|on|off} {freq(Hz)}`
* **Direct Library Function Call:** `direct(val, freq=None|Int)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_direct_on()`
* `set_direct_off()`
* `set_direct_start(freq=Int)`
* `set_direct_stop(freq=Int)`
* **CLI Wrapper Usage:**
* **Notes:**
* NOTE: for start/stop, freq must be a positive number. No upper bound is enforced because the valid range is model-dependent; the device rejects out-of-range values.
* might be tinySA Ultra and newer only.
* Related to NORMAL, DIRECT, ADF, and MIXER
* [https://tinysa.org/wiki/pmwiki.php?n=TinySA4.OutputCurveEdit](https://tinysa.org/wiki/pmwiki.php?n=TinySA4.OutputCurveEdit)
### **ext_gain**
* **Description:** Sets the external attenuation/amplification
* **Original Usage:** `ext_gain -100..100`
* **Direct Library Function Call:** `ext_gain(val=Int|-100...100)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_ext_gain(val=Int|-100...100)`
* **CLI Wrapper Usage:**
* **Notes:** * Works in both input and output mode
### **fill**
* **Description:** Sent by tinySA when in auto refresh mode
* **Original Usage:**
* **Direct Library Function Call:**
* **Example Return:** `format: "fill\r\n{X}{Y}{Width}{Height} {Color}\r\n"`
* **Alias Functions:**
* `get_fill_data()`
* **CLI Wrapper Usage:**
* **Notes:** All numbers returned are binary coded 2 bytes little endian. Similar to `bulk`
### **freq**
* **Description:** Pauses the sweep and sets the measurement frequency
* **Original Usage:** `freq {frequency}`
* **Direct Library Function Call:** `freq(val=Int)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_freq(val=Int)`
* **CLI Wrapper Usage:**
* **Notes:** Might need to `resume` the sweep after this. Could be device dependent.
### **freq_corr**
* **Description:** Gets the frequency correction.
* **Original Usage:** `freq_corr`
* **Direct Library Function Call:** `freq_corr()`
* **Example Return:** `b'0 ppb\r'`
* **Alias Functions:**
* `get_frequency_correction()`
* **CLI Wrapper Usage:**
* **Notes:** This command returns the frequency correction, in parts per billion (ppb).
### **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 tiySA help directly, or the library help
* **Original Usage:** `help`
* **Direct Library Function Call:** `help(val=None|0|1)`
* **Example Return:**
* **Alias Functions:**
* `tinySAHelp()`
* **Related Functions:**
* `libraryHelp()`
* **CLI Wrapper Usage:**
* **Notes:** 0 = tinySAHelp(), 1=libraryHelp(). Both functions can also be called directly. libraryHelp() has more information about this library and the inputs.
### **hop**
* **Description:** Measures the input level at each of the indicated frequencies. This is a measurement over a range (ends inclusive), with a specified number of samples.
* **Original Usage:** `hop {start(Hz)} {stop(Hz)} {step(Hz) | points} [outmask]`
* **Direct Library Function Call:** `hop(start=Int, stop=Int, inc=Int, use_pts=Bool)`
* **Example Return:** e
* **Alias Functions:**
* `get_sample_pts(start=Int, stop=Int, inc=Int, use_pts=Bool)`
* **CLI Wrapper Usage:**
* **Notes:** _Ultra only_. From [tinysa-org](https://tinysa-org.translate.goog/wiki/pmwiki.php?n=Main.USBInterface&_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en-US): if the 3rd parameter is below 450 it is assumed to be points, otherwise as step frequency Outmask selects if the frequency (1) or level (2) is output.
### **if**
* **Description:** Sets the intermediate frequency (IF) to automatic or a specific value, 433 Mhz to 435 MHz
* **Original Usage:** `if (0|433M..435M )`
* **Direct Library Function Call:** `set_IF(val=Int|0|433M..435M|'auto')`
* **Example Return:** empty bytearray
* **Alias Functions:**
* None
* **CLI Wrapper Usage:**
* **Notes:** Val input of 0 is 'auto'. Added explicit 'auto' to match other library funcs.
### **if1**
* **Description:** Sets the intermediate frequency (IF) to automatic or a specific value, 975 Mhz to 979 MHz
* **Original Usage:** `if1 (975M..979M )`
* **Direct Library Function Call:** `set_IF1(val=0|975M..979M|'auto')`
* **Example Return:** empty bytearray
* **Alias Functions:**
* None
* **CLI Wrapper Usage:**
* **Notes:** Val input of 0 is 'auto'. Added explicit 'auto' to match other library funcs.
### **info**
* **Description:** Displays various software/firmware and hardware information
* **Original Usage:** `info`
* **Direct Library Function Call:** `info()`
* **Example Return:** `b'tinySA ULTRA\r\n2019-2024 Copyright @Erik Kaashoek\r\n2016-2020 Copyright edy555\r\nSW licensed under GPL. See: https://github.com/erikkaashoek/tinySA\r\nVersion: tinySA4_v1.-143-g864bb27\r\nBuild Time: Jan 10 2024 - 11:14:08\r\nKernel: 4.0.0\r\nCompiler: GCC 7.2.1 20170904 (release) [ARM/embedded-7-branch revision 255204]\r\nArchitecture: ARMv7E-M Core Variant: Cortex-M4F\r\nPort Info: Advanced kernel mode\r\nPlatform:STM32F303xC Analog & DSP\r'`
* **Alias Functions:**
* `get_info()`
* **CLI Wrapper Usage:**
* **Notes:**
### **level**
* **Description:** Sets the output level
* **Original Usage:** `level -76..13`
* **Direct Library Function Call:** `level(val=Int|-76...13)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_level()`
* **CLI Wrapper Usage:**
* **Notes:** Not all values in the range are available. Might be device dependent.
### **levelchange**
* **Description:** Sets the output level delta for low output mode level sweep
* **Original Usage:** `levelchange -70..+70`
* **Direct Library Function Call:** `level_change(val=Int|-70...70)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_level_change()`
* **CLI Wrapper Usage:**
* **Notes:**
### **leveloffset**
* **Description:** Sets or gets the level calibration data
* **Original Usage:** `leveloffset low|high|switch [output] {error}`
* alternative returned information for format: `leveloffset [low|switch|receive_switch|out_switch|lna|harmonic|shift|shift1|shift2|shift3|drive1|drive2|drive3|direct|direct_lna|ultra|ultra_lna|harmonic_lna|adf] {output} [-20..+20]`
* **Direct Library Function Call:** `level_offset(val=low|switch|receive_switch|out_switch|lna|harmonic|shift|shift1|shift2|shift3|drive1|drive2|drive3|direct|direct_lna|ultra|ultra_lna|harmonic_lna|adf, offset=[-20.0...20.0], isOutput=True|False)`
* `isOutput` boolean variable determines if the 'output' argument is included in the statement. See examples below.
* **Example Return:**
* `leveloffset`, with no arguments
* output: `bytearray(b'-8.462500e+01 0.000000000 ... \r\n-8.128125e+01 0.000000000 \r')`
* `leveloffset low -3.0`
* output: `bytearray(b'')`
* `leveloffset low output 0.0`
* output: `bytearray(b'')`
* **Alias Functions:**
* None
* **CLI Wrapper Usage:**
* **Notes:**
* NOT ALL COMBINATIONS ARE VALID.
* Calibration tables:
* `low` - Low frequency mode corrections
* `switch` - Switch-related corrections
* `receive_switch` - Receive switch corrections
* `out_switch` - Output switch corrections
* `lna` - LNA (Low Noise Amplifier) corrections
* `harmonic` - Harmonic mode corrections
* `shift/shift1/shift2/shift3` - Frequency shift corrections
* `drive1/drive2/drive3` - Drive level corrections
* `direct/direct_lna` - Direct mode corrections
* `ultra/ultra_lna` - Ultra mode corrections
* `harmonic_lna` - Harmonic mode with LNA corrections
* `adf` - ADF (frequency synthesizer) corrections
### **line**
* **Description:** Disables the horizontal line or sets it to a specific level.
* **Original Usage:** `line off|{level}`
* **Direct Library Function Call:** `line(val="off"|)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `line_off()`
* `set_line(val=Int|Float)`
* **CLI Wrapper Usage:**
* **Notes:**
### **load**
* **Description:** Loads a previously stored preset to the connected device
* **Original Usage:** `load 0..4`
* **Direct Library Function Call:** `load(val=0|1|2|3|4)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* None
* **CLI Wrapper Usage:**
* **Notes:** 0 is the startup preset
### **lna**
* **Description:** Set LNA usage off/on. The Ultra Plus devices have a 2nd LNA at a higher frequency range.
* **Original Usage:** `lna off|on`
* **Direct Library Function Call:** `lna(val="off"|"on")`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_lna_on()`
* `set_lna_off()`
* **CLI Wrapper Usage:**
* **Notes:** Should not be enabled when AGC is enabled - [tinySA wiki SETTINGS2](https://tinysa.org/wiki/pmwiki.php?n=Main.SETTINGS2)
### **lna2**
* **Description:** Set the second LNA usage off/on. The Ultra Plus devices have a 2nd LNA at a higher frequency range.
* **Original Usage:** `lna2 0..7|auto`
* **Direct Library Function Call:** `lna2(val="auto"|0..7)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_lna2(val="auto"|0..7)`
* **CLI Wrapper Usage:**
* **Notes:**
### **marker**
* **Description:** sets or dumps marker info
* **Original Usage:** `marker {id} on|off|peak|{freq}|{index}`
* **Direct Library Function Call:** `marker(ID=Int|0..4, val="on"|"off"|"peak")`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `marker_on(ID=Int|1..4)`
* `marker_off(ID=Int|1..4)`
* `marker_peak(ID=Int|1..4)`
* `marker_freq(ID=Int|1..4)`
* `marker_index(ID=Int|1..4)`
* **CLI Wrapper Usage:**
* **Notes:** where id=1..4 index=0..num_points-1
Marker levels will use the selected unit Marker peak will activate the marker (if not done already), position the marker on the strongest signal and display the marker info The frequency must be within the selected sweep range mode. Alias functions need error checking.
### **menu**
* **Description:** The menu command can be used to activate any menu item based on the index of the menu item. Numbers start with 1 at the top.
* **Original Usage:** `menu {#} [{#} [{#} [{#}]]]`
* **Direct Library Function Call:** `menu(val=Str)`
* **Example Input:**
* `menu "6 2"` will toggle the waterfall option
* **Example Return:** unknown. depends on menu button 'clicked'
* **Alias Functions:**
* None
* **CLI Wrapper Usage:**
* **Notes:** [tinySA Menu Tree](https://tinysa.org/wiki/pmwiki.php?n=TinySA4.MenuTree) for more information. There's no error checking on this function due to the number of nested menus and buttons.
### **mode**
* **Description:** Sets the mode of the tinySA
* **Original Usage:** `mode low|high input|output`
* **Direct Library Function Call:** `mode(val1="low"|"high", val2="input"|"output")`
* **Example Return:** empty bytearray
* **Alias Functions:** Currently no error checking.
* `set_low_input_mode()`
* `set_low_output_mode()`
* `set_high_input_mode()`
* `set_high_output_mode()`
* **CLI Wrapper Usage:**
* **Notes:** [tinySA Wiki MODE](https://tinysa.org/wiki/pmwiki.php?n=Main.MODE)
* LOW INPUT activates the 0.1-350MHz input mode
* HIGH INPUT activates the 240MHz-960MHz input mode
* LOW OUTPUT activates the 0.1-350MHz output mode
* HIGH OUTPUT activates the 240MHz-960MHz output mode
### **modulation**
* **Description:** Set the modulation in output mode
* **Original Usage:** `modulation off|AM_1kHz|AM_10Hz|NFM|WFM|extern`
* **Direct Library Function Call:** `modulation(val=off|AM_1kHz|AM_10Hz|NFM|WFM|extern)`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_mod_off()`
* `set_mod_AM_1khz()`
* `set_mod_AM_10Hz()`
* `set_mod_NFM()`
* `set_mod_WFM()`
* `set_mod_extern()`
* **CLI Wrapper Usage:**
* **Notes:**
* OFF - Turns modulation off. NO modulation
* AM_1kHz - Set AM modulation 1 kHz
* AM_10Hz - Set AM modulation 10 Hz
* NFM - Narrow FM. sets narrow FM modulation. Width is about 3kHz.
* WFM - wide FM modulation.
* extern - disables the internal LO driving the mixer and enables the high input as mixer LO input. Minimum external LO input frequency is 5MHz.
* [https://tinysa.org/wiki/pmwiki.php?n=Main.MODULATION](https://tinysa.org/wiki/pmwiki.php?n=Main.MODULATION)
### **nf**
* **Description:** get the noise floor in dB
* **Original Usage:** `nf {val=None|??}`
* **Direct Library Function Call:** `nf(val=None)`
* **Example Return:** `b'usage: nf {value}\r\n4.000000000\r'`
* **Alias Functions:**
* `get_nf()`
* **CLI Wrapper Usage:**
* **Notes:**
* The tinySA Ultra can measure, store, and validate the tinySA noise figure (NF). It can also measure amplifier (AMP) NF.
* While it is possible to set this value programmatically, until more documentation is online it is recommended to only GET the nf value.
* "The NF is the degradation in dB of the SNR after the amplifier compared to before the amplifier." - [https://tinysa.org/wiki/pmwiki.php?n=Main.NoiseFactor](https://tinysa.org/wiki/pmwiki.php?n=Main.NoiseFactor)
### **output**
* **Description:** Sets the output mode on or off
* **Original Usage:** `output on|off`
* **Direct Library Function Call:** `output(val="off"|"on")`
* **Example Return:** empty bytearray
* **Alias Functions:**
* `set_output_on()`
* `set_output_off()`
* **CLI Wrapper Usage:**
* **Notes:**
### **pause**
* **Description:** Pauses the sweeping in either input or output mode
* **Original Usage:** `pause`
* **Direct Library Function Call:** `pause()`
* **Example Return:** empty bytearray
* **Alias Functions:**
* None
* **CLI Wrapper Usage:**
* **Notes:**
### **rbw**
* **Description:** sets the rbw to either automatic or a specific value
* **Original Usage:** `rbw auto|3..600` or `rbw 0.2..850|auto`
* **Direct Library Function Call:** `rbw