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 section for further reading. See the official tinySA resources 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 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 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 towards the end of this README.
The primary GitHub: https://github.com/LC-Linkous/tinySA_python
The PyPI page: https://pypi.org/project/tsapython/
Zenodo archive with DOI: https://doi.org/10.5281/zenodo.20546764
Web UI built on this library: https://lc-linkous.github.io/tinysa-web/ (source: LC-Linkous/tinysa-web). The web UI tracks the latest stable library release; its 4.0.0 update follows this release.
- The tinySA Series of Devices
- Library Usage
- Requirements
- Structure
- Running Tests
- Error Handling
- Logging
- Upgrading to 4.0.0
- Device Profiles
- RF Output Setting
- Firmware Compatibility
- Change Summary
- Deprecated Functions
- Example Implementations
- Finding the Serial Port
- Serial Message Return Format
- Connecting and Disconnecting the Device
- Toggle Error Messages
- Device and Library Help
- Setting tinySA Parameters
- Getting Data from Active Screen
- Saving Screen Images
- Plotting Data with Matplotlib
- Example 1: Plot using On-Screen Trace Data and Frequencies
- Example 2: Plot using Scan Data and Frequencies
- Example 3: Plot using SCAN and SCANRAW Data and Calculated Frequencies
- Example 4: Plot using SCAN And Filters for Artifact Comparison
- Example 5: Plot a Waterfall using SCAN and Calculated Frequencies
- Example 6: Finding Peaks in a Frequency Range
- Saving SCAN Data to CSV
- Accessing the tinySA Directly
- List of tinySA Commands and their Library Commands
- List of Commands Removed from Library
- Additional Library Functions for Advanced Use
- Library Development
- Notes for Beginners
- FAQs
- References
- Licensing
The tinySA line of devices 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, 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/. 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 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.
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 section, including working with the hardware and plotting results with matplotlib.
The tsapython package (from PyPI at https://pypi.org/project/tsapython/) can be installed with:
pip install tsapython
The GitHub repository will continue to be named tinySA_python to differentiate the working versions and the additional documentation included here.
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/:
# 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
The library itself depends only on pyserial. The examples in this
README additionally use matplotlib (and pyQt5 on Linux, where it provides the
GUI backend for the figures; it is not needed on all Windows machines).
All dependencies are declared in pyproject.toml, with the example dependencies
available as the plotting extra:
# library only (pyserial is its one dependency)
pip install tsapython
# library + numpy and matplotlib for the examples/ scripts
pip install "tsapython[plotting]"
# development / running the test suite
pip install -e ".[test]"If you prefer installing the libraries manually (we recommend a Python virtual environment):
pip install pyserial numpy matplotlib pyQt5Release 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 (the 3.0.0 release: releases/tag/v3.0.0)
- PyPI release history — every published version, installable with
pip install tsapython==<version>: https://pypi.org/project/tsapython/#history - Zenodo archive — a citable, archived snapshot with a DOI:
https://doi.org/10.5281/zenodo.20546764
(DOI
10.5281/zenodo.20546764)
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
│ ├── _host.py
│ ├── _identify.py
│ ├── _log.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
├── diagnose_reset.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_reader.py
├── test_log.py
├── test_identify.py
├── test_firmware_compat.py
├── test_batch2.py
├── test_hardware.py
├── test_captured_hardware.py
├── collect_samples.py
├── probe_firmware_returns.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
├── media/
│ └── README images, screenshots
└── tsapython/
├── .python-version
├── pyproject.toml
├── README.md
├── LICENSE
├── .gitignore
├── examples/
│ ├── complete_workflow.py
│ ├── hardware_walkthrough.py
│ ├── identifying_serial_ports.py
│ ├── using_autoconnect.py
│ ├── using_command_func.py
│ ├── filtering_scan_artifacts.py
│ ├── find_peaks.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
│ ├── _host.py
│ ├── _identify.py
│ ├── _log.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
├── diagnose_reset.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_reader.py
├── test_log.py
├── test_identify.py
├── test_firmware_compat.py
├── test_batch2.py
├── test_hardware.py
├── test_captured_hardware.py
├── collect_samples.py
├── probe_firmware_returns.py
├── probe_firmware_returns.py
└── fixtures/
├── __init__.py
├── device_responses.py
└── captured_responses.py
This is primarily for development or advanced troubleshooting. These tests are for the API.
The test suite uses pytest. 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.
This is a configured uv project with a committed
uv.lock, so the recommended workflow is:
uv sync # installs pyserial + the dev group, editable-installs tsapython
uv run pytest # hardware tests self-skip when no device is connectedRun everything through uv run so the synced environment is used. (If you are
not using uv, pip install -e ".[test]" followed by python -m pytest also
works.)
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:
# run ONLY the hardware-free tests (explicitly skip device tests)
uv run pytest -m "not hardware"
# run ONLY the hardware tests (requires a connected tinySA)
uv run pytest -m hardwareTo see coverage while testing:
uv run pytest --cov=tsapython --cov-report=term-missingContinuous integration runs the hardware-free suite on Windows, Linux, and macOS
across Python 3.10–3.13 on every push and pull request, along with lint and
type checks (uv run ruff check . and uv run mypy — the package ships
py.typed, and the annotations are enforced) and a package build check.
For example:
uv sync
uv run pytest -m "not hardware"
uv run ruff check .
uv run mypy
tests/probe_firmware_returns.py is a manual helper that asks a connected tinySA what its firmware accepts: it harvests every command's usage line, sends old and new argument forms, calls the library functions, and writes probe_results.json and probe_results.md. Nothing in the default run touches the RF output or writes to the device. Use it before and after a firmware update, or to report a board the library has not been verified on:
uv run python tests/probe_firmware_returns.py --backup --capture --out probe_results/my_unit
uv run python tests/probe_firmware_returns.py --help--backup adds a read-only dump of calibration and configuration values, worth keeping before a firmware update (an update can reset stored configuration). --generator adds signal generator probes with the output kept muted.
tests/collect_samples.py is a manual 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). Run it with a tinySA connected:
# auto-detect the serial port
uv run python tests/collect_samples.py
# or specify the port explicitly
uv run python tests/collect_samples.py --port COM5 # Windows
uv run python tests/collect_samples.py --port /dev/ttyACM0 # Linux/MacThe files in examples/ are runnable demonstrations (not part of the automated test
suite) and require a connected device plus the plotting dependencies:
pip install "tsapython[plotting]"
python examples/complete_workflow.py
python examples/hardware_walkthrough.pySome 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.
Since 4.0.0, a command the device rejects also returns the error value. If the firmware answers a setter with a usage: line (bad argument), name? (a command this firmware does not have) or a refusal such as usb disk mode active, the call returns b'' (or b'ERROR' with set_error_byte_return(True)) instead of the device's text. Where the library can tell in advance that the device will refuse (a word the model lacks, a command missing from its help list, a number above the model's marker count), nothing is sent at all. command() is the exception: it always returns exactly what the device printed. The reason for every refusal is printed when verbose is on and written to the log file.
From the official wiki USB Interface page:
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)
4.0.0 changes what some calls return. Working scripts need these checks:
| If your script... | In 3.x | In 4.0.0 | What to do |
|---|---|---|---|
switches the generator output on (output("on"), set_output_on(), cal_output(30) ...) |
Sent immediately | Refused and nothing sent until set_output_allowed(True) has been called |
Add tsa.set_output_allowed(True) once, before the first output("on"). See RF Output Setting |
| reads the return value of a setter to see the device's usage text | Device text came back | The error value comes back (b'', or b'ERROR' with the flag on) |
Use command() for the raw text, or turn on the log to see the reason |
sends a command the connected device does not have, or a word its model lacks (mode high on an Ultra, hop on a Basic) |
Sent; the device printed usage: or name? |
Refused before sending; error value | Check tsa.profile and tsa.deviceCommands, or use command() |
calls scan_raw(..., unbuf=2) or 3 |
Sent (and on firmware v1.4-177 or later left the device streaming) | Refused; error value | Use unbuf=0 or 1, continuous_scanraw(), or the new scan_raw_stream() |
| relies on a call waiting forever for a slow device | Waited indefinitely | Returns None after 120 s |
Pass tinySA(serial_timeout=None) or raise the value; see Serial read timeout |
calls connect() or autoconnect() on a device that is not a tinySA |
Opened the port | Opens the port, sends version, info, help, keeps the default profile with a warning |
Set tsa.identifyOnConnect = False to skip the three commands |
uses select_existing_device() |
Did nothing | Selects the profile by name | Nothing; it now works |
uses set_mod_AM_1khz(), set_mod_AM_10Hz() or trace_clear() |
Deprecated, callable | Deprecated, callable | Move to the replacements when convenient |
Nothing was renamed or removed. The firmware floor is v1.4-182 (October 2024); older firmware connects with a warning in verbose mode and a firmware-below-floor event in the log.
Since 4.0.0 the library knows which tinySA it is talking to and uses that model's limits and command words.
tsa = tinySA()
tsa.autoconnect() # sends version, info, help and picks the profile
print(tsa.profile.name) # 'tinySA Ultra (ZS405)'
print(tsa.profile.verified) # 'hardware' or 'source'
print(tsa.firmwareBuild) # (1, 4, 143)
print(sorted(tsa.deviceCommands)) # the device's own help list| Function or attribute | Purpose |
|---|---|
identify() |
Re-run the three identification commands. Returns a dict with family, board, build, commands, profile |
identifyOnConnect |
True by default. Set False before connect() to skip identification |
select_existing_device("BASIC" | "ZS304" | "ULTRA" | "ZS405" | "ULTRA+" | "ZS406" | "ZS407") |
Choose the profile by name, for scripts that must not query the device or want to override detection |
profile / get_profile() |
The active DeviceProfile: max_points, markers, traces, screen_width, screen_height, rbw_khz, levelchange_db, level_low_output, level_high_output, if_hz, scanraw_offset, the word lists, commands |
profileDetected |
True if the profile came from the device, False if selected or default |
deviceCommands |
The command set from the device's help reply, or None if not read. When present it decides which commands are sent |
firmwareBuild |
(major, minor, commits) from the version reply, or None for self-built firmware |
Before identification, or when the device is not recognised, the profile is DEFAULT: the Ultra family's words with the wide 3.x limits.
What the profile gates:
- commands the model does not have (
hop,lna,lna2,agc,if1,avoid,nf,time,usbdisk, SD card commands on the Basic); - commands this firmware build does not list in
help(waitscan,channel,avoid,usbdiskon older firmware); - words the model lacks:
mode highon the Ultra family;spur auto,output normal|mixer,calc log|lin,modulation fmon the Basic;modulation nfm|wfm|externon the Ultra family; - the
ifrange per board,levelandlevelchangeranges per family, marker and trace counts.
Since 4.0.0 the generator output cannot be switched on by accident. output("on"), set_output_on(), and cal_output() with any value other than "off" return the error value and send nothing until the setting is on:
tsa.set_output_allowed(True) # sends nothing to the device; only permits the next step
tsa.setup_generator(10e6, -30, "am", 1000) # output off -> mode -> level -> frequency -> modulation
tsa.output("on")
...
tsa.output("off") # always allowedset_output_allowed(allowed)/get_output_allowed(): off by default on every newtinySA().setup_generator(freq_hz, level_dbm, modulation_type="off", modulation_rate_hz=None, high=False)applies the generator settings in the order the firmware needs and leaves the output off. The level is set after the mode because the firmware resets the level to maximum on every mode change.high=Trueselects the Basic's high output mode. ReturnsTrueif every step was accepted.set_generator(level_dbm=None, freq_hz=None, modulation_type=None, modulation_rate_hz=None, depth_percent=None, deviation_hz=None)changes settings once in output mode, with the output on or off. Only the arguments given are sent.
Why those two helpers pause the sweep, and why the library waits briefly after output and modulation. In output mode, level, sweep, rbw, attenuate, pause, resume and mode are run by the firmware's sweep thread between sweeps, and the generator loop only yields to them when it is asked to; while the sweep is paused the firmware runs them at once. output, modulation, caloutput and leveloffset run in the shell thread and return immediately, but each one leaves a redraw request that the sweep thread clears a few milliseconds later. A between-sweeps command queued inside that window loses its wake-up, the generator loop never looks at it, and the device answers Command timeout about 20 seconds later (seen on a ZS405 running v1.4-252; the dropped command is never executed). The library therefore waits tinySA.SETTLE_AFTER_IMMEDIATE_SETTER_S (0.4 s; on a ZS405 a command 150 ms after the setter is dropped, 230 ms after it is served) after those four commands, and if a command still comes back with Command timeout it is sent once more before the error value is returned. If you send raw commands yourself in output mode, keep the same habits: pause(), the settings, resume(), and a short gap after output/modulation.
- The device cannot report whether its output is on, so the library does not track it either; keep
output("off")at the end of your scripts.
The library can write a log file of every command it handles: what was sent, what came back, and why a call was refused. Logging is off by default, and the library keeps nothing in memory. Each record is written and flushed to the file as it happens.
from tsapython import tinySA
tsa = tinySA()
tsa.autoconnect()
tsa.set_log_file("session.jsonl") # level="commands" is the default
# tsa.set_log_file("session.jsonl", level="data")
tsa.rbw(30)
tsa.calc("quasip")
tsa.attenuate(40) # refused by the library; logged with the reason
tsa.close_log_file()
tsa.disconnect()set_log_file(path, level="commands")opens the file for appending and returnsTrueif it could be opened. If a device is connected, it is asked forversionandinfoso the log can name the board and firmware.close_log_file()stops logging.get_log_file()returns the path of the open log, orNone.
File format: JSON Lines. One JSON object per line, UTF-8. Times are ISO 8601 in UTC with milliseconds.
| Level | Reply text | Binary replies (scanraw, capture) |
|---|---|---|
commands |
First 256 characters, with the full length recorded | Length only |
data |
Complete | Complete, base64 in reply_b64 |
Every record has t (the time) and type:
type |
When | Fields |
|---|---|---|
session |
First record after set_log_file(), and again after each connect() |
log_format, library, python, platform, port, version_reply, info_first_line, family, board, build |
command |
Every library call that sends a command, or is refused before sending | seq, function, sent, status, elapsed_ms, reply, reply_len, truncated, reply_b64 (level data, binary replies only), reason |
event |
connect, disconnect, log-closed |
event, detail |
status is one of:
| Value | Meaning |
|---|---|
ok |
The device replied normally |
usage |
The device printed a usage line: it rejected the argument |
unknown-command |
The device printed name?: this firmware does not have the command |
device-refused |
The device printed a refusal, for example usb disk mode active |
timeout |
No prompt from the device inside the time set with set_serial_timeout() |
read-failed |
The serial read failed |
blocked |
The library refused the call and sent nothing; reason says why |
Example (level commands):
{"t":"2026-10-09T19:22:04.118Z","type":"session","log_format":1,"library":"3.0.1","python":"3.12.4","platform":"Windows-11","port":"COM7","version_reply":"tinySA4_v1.4-143-g864bb27\r\nHW Version:V0.4.5.1.1","info_first_line":"tinySA ULTRA","family":"ultra","board":"ZS405","build":[1,4,143]}
{"t":"2026-10-09T19:22:04.530Z","type":"command","seq":1,"function":"rbw","sent":"rbw 30","status":"ok","elapsed_ms":41,"reply":"","reply_len":0,"truncated":false,"reason":""}
{"t":"2026-10-09T19:22:04.611Z","type":"command","seq":2,"function":"mode","sent":"mode high input","status":"usage","elapsed_ms":38,"reply":"usage: mode [low] input|output\r","reply_len":31,"truncated":false,"reason":"device rejected the argument"}
{"t":"2026-10-09T19:22:04.650Z","type":"command","seq":3,"function":"attenuate","sent":"","status":"blocked","elapsed_ms":0,"reply":"","reply_len":0,"truncated":false,"reason":"ERROR: attenuate() takes vals [0 - 31]|\"auto\""}The log is the way to find out why a call returned b'' or b'ERROR'. In the example, mode("high", "input") returns the error value (b'', or b'ERROR' with set_error_byte_return(True)) and get_last_status() says "usage"; the log records the device's text and the reason. (Before 4.0.0 the usage text itself was returned.)
A bare command, or one with ?, is a query. The device answers a query with a usage line that includes the current value, so those are logged as ok.
The library waits up to 120 seconds for the device prompt (tinySA.DEFAULT_SERIAL_TIMEOUT). The firmware's longest sweep time is 60 s, so any single reply completes well inside that. Three ways to change it:
tinySA.DEFAULT_SERIAL_TIMEOUT = 30 # every instance created afterwards
tsa = tinySA(serial_timeout=30) # one instance
tsa.set_serial_timeout(30) # at any time; None waits indefinitely (the 3.x behaviour)On a timeout the call returns None, which is the value a failed serial read already returned, and the log records a timeout status.
The tinySA firmware has changed its serial commands over time, and the Basic and the Ultra family do not accept the same words. This section records what the library knows about those differences.
Everything here was read from the tinySA firmware source (v1.4-244, September 2026). It was checked on hardware against one device, a tinySA Ultra ZS405, twice: on 2026-10-17 running tinySA4_v1.4-143 (36 of 36 probe results matched the source reading) and on 2026-10-18 running tinySA4_v1.4-252 (every build-dependent prediction confirmed, and the signal-generator sequence verified with the hardware_output tests). The Basic and the Ultra+ boards are still from the source only.
Firmware versions are a commit count after the v1.4 tag, so v1.4-143 is older than v1.4-244. The version command prints it, and the log file records it as build.
The firmware has a timeout of its own. From v1.4-222 the device prints Command timeout (and no prompt) when a command that runs between sweeps is not picked up within 20 s. The library treats that line as the end of the reply, so the call returns the error value after about 20 s rather than waiting out the read timeout; in the log the record has status: device-refused and its reason names Command timeout. A record for a read that timed out keeps whatever bytes did arrive in its reason field, which is the first thing to look at when a command goes unanswered.
tsa.rbw(30)
tsa.get_last_status() # 'ok'
tsa.level(50) # out of range for the model: refused by the library, nothing sent
tsa.get_last_status() # 'blocked'
tsa.command("nosuch")
tsa.get_last_status() # 'unknown-command'With the default error mode, an accepted setter and a rejected one both return b''. get_last_status() returns the same word the log file records for the command: ok, blocked (a library check refused it before sending), usage (the device rejected the argument), unknown-command (this firmware lacks it), device-refused (usb disk mode active, Command timeout), timeout (no prompt within the serial read timeout) or read-failed. set_error_byte_return(True) remains the other way: refusals return b'ERROR'.
| Device | Profile name | version reply |
profile.verified |
|---|---|---|---|
| tinySA Basic (ZS304) | BASIC |
tinySA_..., no HW Version line |
source |
| tinySA Ultra (ZS405) | ZS405 |
tinySA4_..., HW Version:V0.4.5.1 or V0.4.5.1.1 |
hardware (v1.4-143 and v1.4-252, 2026-10-18) |
| tinySA Ultra+ (ZS406) | ZS406 |
tinySA4_..., HW Version:V0.4.6 |
source |
| tinySA Ultra+ (ZS407) | ZS407 |
tinySA4_..., HW Version:V0.5.4 (V0.5.2, V0.5.3, V0.5.3.2 on older firmware) |
source |
The oldest firmware the library is written for is v1.4-182 (October 2024). The firmware's own help reply decides which commands are sent, so older firmware still works for the commands it has; the library warns once at connect.
If you own a Basic or an Ultra+, running tests/probe_firmware_returns.py and attaching the result to an issue is the way to move its profile from source to hardware.
The three Ultra boards run the same firmware and accept the same commands. Only values differ:
| Ultra (ZS405) | Ultra+ (ZS406) | Ultra+ (ZS407) | |
|---|---|---|---|
if range |
975M to 979M | 1067M to 1073M | 1067M to 1073M |
| Top of the normal range (ultra mode off) | 800 MHz | 900 MHz | 900 MHz |
| Top of the range with ultra mode on, before harmonic mode | about 5.33 GHz | about 5.42 GHz | about 7.37 GHz |
Changes made before the library existed. The library's command words originally followed the tinySA wiki, which describes older firmware than any Ultra shipped with.
| The library sent | Firmware accepted it until | What replaced it | The library now |
|---|---|---|---|
modulation AM_1kHz|AM_10Hz|NFM|WFM |
Nov 2020 | am|nfm|wfm|extern (Basic); am|fm|freq|depth|deviation (Ultra family, Aug 2022) |
Accepts the current words; translates the old ones |
calc quasip |
May 2022 | quasi |
Sends quasi for either word |
trace ... clear |
Jul 2021 | copy|freeze|subtract|view|value |
trace_clear() is deprecated |
mode high ... on the Ultra |
Oct 2022 at the latest | mode [low] input|output |
Unchanged: still sent, and rejected by an Ultra |
levelchange limited to ±70, if limited to 433M to 435M |
Sep 2022 (Ultra) | ±90; a range per board | Accepts ±90 and all three if ranges |
Changes since v1.4-143 (January 2024).
| Build | Date | Change | Devices | The library |
|---|---|---|---|---|
| 166 | 2024-05-14 | abort [off|on] added |
All | abort() |
| 176 | 2024-08-09 | restart {seconds} added |
Ultra family | restart() |
| 177 | 2024-08-12 | scanraw 4th argument becomes a bit field: 1 unbuffered, 2 repeat until interrupted. Before, any non-zero value meant unbuffered |
All | scan_raw() reads one frame. Use unbuf 0 or 1 |
| 178 | 2024-08-17 | Ultra+ ZS406 recognised | Ultra family | Identified in the log |
| 183 | 2024-10-20 | if range 1067M to 1073M on the Ultra+ |
Ultra+ | Accepted by set_IF() |
| 188 | 2024-12-02 | ZS407 reports V0.5.4 |
Ultra+ | Identified in the log |
| 190 | 2024-12-20 | Board name added to the first info line |
Ultra family | Identified in the log |
| 193 | 2025-01-08 | leveloffset names shift4, direct_output, low_output |
Ultra family | Accepted by level_offset() |
| 194 | 2025-01-22 | avoid auto|off|on|dump added |
Ultra family | Not wrapped yet; use command() |
| 216 | 2025-11-05 | waitscan [{scans}] added |
All | Not wrapped yet; use command() |
| 222 | 2025-12-30 | A stuck command prints Command timeout after about 20 seconds |
All | Logged as device-refused |
| 223 | 2026-03-23 | channel [1-3] added |
All | Not wrapped yet; use command() |
| 228 | 2026-07-11 | scan refuses fewer than 2 points |
All | Logged as device-refused |
| 230 | 2026-07-14 | Number formatting fixed. Before, an exact power of ten printed with : as its leading digit (-100.0 as -:.000000e+01) |
All | See the note below |
| 235 | 2026-08-12 | trace dBV unit added |
All | Accepted by trace_units() |
| 244 | 2026-09-26 | usbdisk off|on added; other commands answer usb disk mode active while it is on |
Ultra family | Not wrapped yet; logged as device-refused |
The : in scan data. In firmware before v1.4-230, : in a number is the digit after 9: it stands for a leading 10. -:.000000e+01 is 10.000000e+01 with a minus sign, which is exactly -100.0. The scan examples in this README replace such values with a noise-floor number; the true value can be recovered by reading : as 10.
Finding out what a device accepts. The device prints its own command list with help, and most commands print their accepted arguments when sent with ?:
print(tsa.tinySA_help())
print(tsa.command("modulation ?"))| Basic (ZS304) | Ultra family (ZS405, ZS406, ZS407) | |
|---|---|---|
| Ultra-only commands | Not present | hop, lna, lna2, agc, if1, freq_corr, avoid, direct, ultra, nf, time, restart, usbdisk, k (temperature) |
SD card commands (sd_list, sd_read, sd_delete) |
Not present | Present |
mode |
low|high input|output |
[low] input|output |
modulation |
off|am|nfm|wfm|extern|freq |
off|am|fm|freq|depth|deviation |
output |
on|off |
adds normal|mixer |
level range |
-76 to -6 (low output), -38 to 13 (high output) | -76 to -6 |
spur |
off|on |
adds auto |
calc |
off|minh|maxh|maxd|aver4|aver16|aver|quasi |
adds log|lin |
levelchange |
±70 | ±90 |
rbw upper limit |
600 kHz | 850 kHz |
if range |
433M to 435M | By board (see above) |
| Points, markers, traces, screen | 290, 4, 3, 320x240 | 450, 8, 4, 480x320 |
scanraw offset |
128 | 174 |
Since 4.0.0 the device profile applies these differences: a word the connected model lacks is refused before it is sent.
Calls that worked before behave the same. These are fixes for calls that failed or were ignored by the device, and additions. Verified on a tinySA Ultra ZS405 (firmware v1.4-143, and v1.4-252 for 4.0.0); other boards from firmware source.
- Fixed:
hop()withoutmask=3raised an exception.wait()could not send the bare command.attenuate(31)andcolor(31, ...)were refused.trace_freeze()sent a command the firmware ignores.capture()could return a short image.tsapython.__version__reported2.0.0. - Signal generator:
modulation()takes the words current firmware accepts, with newmodulation_freq(),modulation_depth()andmodulation_deviation().level()andlevel_change()take decimals, andlevel_change()goes to ±90.output()takesnormalandmixer. - Wider arguments:
rbw()andtrigger_level()take decimals;calc()takesaver,quasi,log,lin;spur()takesauto;trace_units()takesdBV;set_IF()takes the Ultra and Ultra+ ranges;scan()allows a zero span;scan_raw()allowsunbuf=0. - New: the log file,
set_serial_timeout(), getters for RBW, attenuation, sweep time, IF, DAC, noise figure, calc mode and sweep range,decode_number()for the:artifact, and the Firmware Compatibility tables. - New commands:
waitscan(),channel(),avoid(),usbdisk(),time(),correction_reset(),sweep_mode(),marker_mode(),marker_all_off(),marker_read(),resume(count),sd_list(pattern). - SD card:
sd_read()returns exactly the file's bytes (the 4-byte size prefix is consumed and a>byte no longer ends the read);list_sd_files(),save_sd_file()andsave_firmware_dump()copy files, including a firmware backup, to the computer. - Reader: a reply is complete at the
ch>prompt, not at the first>byte, so text such asusage: deviceid [<number>]is no longer cut short. - Tests:
tests/probe_firmware_returns.py, a manual probe that asks a connected device what its firmware accepts and writes a JSON/markdown report; seven hardware set-and-read-back tests. - Examples: raw command strings no longer use e-notation, which the firmware reads incorrectly (
150e6is read as 150 Hz).
See Upgrading to 4.0.0 for what to change in a script.
- Device rejections return the error value. A
usage:line,name?or a device refusal is no longer returned as data.command()keeps returning the raw text. - RF output setting.
set_output_allowed(True)is required beforeoutput("on"),set_output_on()orcal_output()will send anything.setup_generator()applies generator settings in the safe order with the output off. - Device profiles. The library identifies the device at connect (
version,info,help), selects a profile (BASIC,ZS405,ZS406,ZS407), and refuses commands and words the connected device does not have.select_existing_device()now works.identify(),identifyOnConnect,profile,deviceCommands,firmwareBuildare new. scan_raw()acceptsunbuf0 or 1 only.scan_raw_stream()drives the device-side continuous scan (firmware v1.4-177 or later; verified on hardware). A short or timed-outscanrawframe returnsNoneinstead of partial data.get_last_status()says how the last command ended (ok,blocked,usage,unknown-command,device-refused,timeout,read-failed) whatever it returned, so a script in the default error mode can tell an accepted setter from a rejected one.setup_generator()andset_generator()use it.- Command length. Commands longer than 47 bytes (
tinySA.MAX_COMMAND_LENGTH, the firmware's command line) are refused before sending. - Serial read timeout defaults to 120 s (
tinySA.DEFAULT_SERIAL_TIMEOUT,tinySA(serial_timeout=...),set_serial_timeout());Nonerestores the 3.x behaviour. - Firmware floor v1.4-182; older firmware connects with a warning.
These functions name commands that no supported tinySA firmware accepts. They are still in the library and still callable, and their entries in the command list below are kept; since 4.0.0 each call also emits a DeprecationWarning (silent by default outside tests; python -W error::DeprecationWarning turns it into an error). They will be removed in a later major version.
| Function | Firmware stopped accepting it | What it does now | Use instead |
|---|---|---|---|
set_mod_AM_1khz() |
Nov 2020 (modulation AM_1kHz) |
Sends modulation am, then modulation freq 1000 |
modulation("am") and modulation_freq(1000) |
set_mod_AM_10Hz() |
Nov 2020 (modulation AM_10Hz) |
Sends modulation am, then modulation freq 10 |
modulation("am") and modulation_freq(10) |
trace_clear() |
Jul 2021 (trace ... clear) |
Sends trace {n} clear, which current firmware ignores |
No replacement in current firmware |
Three related names are not deprecated, because the Basic still has the feature: set_mod_NFM(), set_mod_WFM() and set_calc_quasip() now send the lower-case or renamed word that current firmware expects.
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.
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.
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).
# 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")
-
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.
-
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: 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.
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}")
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:
[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.
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:
bytearray(b'deviceid\r\ndeviceid 0\r\nch>')
Cleaned version:
bytearray(b'deviceid 0\r')
This example shows the process for initializing, opening the serial port, getting device info, and disconnecting.
# 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:
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')
The following can be used to turn on or off returned error messages.
- the 'verbose' option. When enabled, detailed messages are printed out.
# detailed messages are ON
tsa.set_verbose(True)
# detailed messages are OFF
tsa.set_verbose(False) - the 'errorByte' option. When enabled, if there is an error with the command or configuration,
b'ERROR'is returned instead of the defaultb''.
# 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) There are three options for help() with this library.
# 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:......')
Most device parameters are set through their corresponding library functions, documented in the 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:
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 attenuationAcceptable 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.
See other sections for the following examples:
This example shows several types of common data requests:
# 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()
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.
capture() returns exactly width * height * 2 bytes. Earlier versions of the library returned one byte too few (and could stop early at a pixel byte of 0x3E), which is why this example checks the size and pads it. The check is harmless and has been left in.
# 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
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
# 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
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.
# 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
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:
- Requesting data too quickly after the last read
- Expected, as the tinySA needs to resume and re-measure.
- 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 theresume()function after this will restart measurements.
- Mildly expected, user error can trigger this too. Turns out in some situations, the frozen screen is not the same as a
- {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.
# 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
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.
# 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
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.
# 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.
# 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
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
# 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}")
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.
# 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
# NOTE: raw command strings go to the device unchanged. The firmware reads
# '150M' or '150000000', but NOT '150e6' (that is read as 150 Hz).
data_bytes = tsa.command("scan 150M 200M 5 2")
print(data_bytes)
tsa.resume() #resume
tsa.disconnect()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 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:
- 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()
- 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.
- 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()
- 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.
- 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)
- Notes:
- 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)
- Notes:
- 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()
- 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
- Description: Sets or cancels one of the measurement modes
- Original Usage:
calc off|minh|maxh|maxd|aver4|aver16|aver|quasi(Basic), pluslog|linon the Ultra family - Direct Library Function Call:
calc(val="off"|"minh"|"maxh"|"maxd"|"aver4"|"aver16"|"aver"|"quasi"|"log"|"lin") - 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_aver()set_calc_quasi()set_calc_quasip()- older name, sendscalc quasi
- Notes:
- The quasi peak keyword was
quasipin tinySA firmware until May 2022 and isquasisince.calc("quasip")is still accepted and is sent asquasi. logandlinselect logarithmic or linear averaging (Ultra family only).- the commands are the same as those listed in the MEASURE menu
- tinySA Calc Menu:
- 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
- The quasi peak keyword was
- 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()
- 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
- 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()
- Notes: tinySA original: 320x240, tinySA Ultra and newer: 480x320
- The image is read by length, and exactly
screenWidth * screenHeight * 2bytes are returned. Earlier versions read up to the first>byte (0x3E), which could cut the image short when a pixel contained that value, and returned one byte too few. - The library is set for a 480x320 screen by default. On a Basic the full 320x240 image is still returned, after a short pause, with a warning when verbose is on.
- The image is read by length, and exactly
- 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()
- Notes: Requires password '1234'. Hardcoded. Other functions need to be used with this to complete the process.
- 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``
- If ID='None' used:
- Alias Functions:
get_all_colors()get_marker_color(ID=Int|0..31)set_marker_color(ID=Int|0..31, col=rgb24)
- Notes: the rgb24 hex value currently must be passed in as a string
- 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 thecorrection(...)library function
- This function is complex enough that it is recommended to use the
- Notes: See Correction Wiki. The current content of the table is shown by entering
correction lowwithout 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.
- 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()
- Notes:
- 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()
- Notes:
- 0 = temp value, 1 = stored trace, 2 = measurement. strength in decibels (dB)
get_temporary_datanot to be confused withget_temp, which returns temperature
- 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....)
- Notes:
- 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)
- 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
- 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)
- Notes: * Works in both input and output mode
- 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()
- Notes: All numbers returned are binary coded 2 bytes little endian. Similar to
bulk
- 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)
- Notes: Might need to
resumethe sweep after this. Could be device dependent.
- 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()
- Notes: This command returns the frequency correction, in parts per billion (ppb).
- 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()
- Notes:
- 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()
- Notes: 0 = tinySAHelp(), 1=libraryHelp(). Both functions can also be called directly. libraryHelp() has more information about this library and the inputs.
- 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, outmask=None|1|2|3) - Example Return: the measured levels as text. Bad arguments return the error value (4.0.0; they used to return
None, which is what a timeout returns) - Alias Functions:
get_sample_pts(start=Int, stop=Int, pts=Int)
- Notes: Ultra only. From tinysa-org: 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.
- Description: Sets the intermediate frequency (IF) to automatic or a specific value. The range depends on the board.
- Original Usage:
if (0|433M..435M )(Basic),if {975M..979M}(Ultra ZS405),if {1067M..1073M}(Ultra+ ZS406, ZS407) - Direct Library Function Call:
set_IF(val=0|'auto'|Int in one of the three ranges, in Hz) - Example Return: empty bytearray
- Alias Functions:
- None
- Notes: Val input of 0 is 'auto'. Added explicit 'auto' to match other library funcs. The library accepts a value in any of the three ranges; the device rejects one that is outside the range of its own board.
- 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
- Notes: Val input of 0 is 'auto'. Added explicit 'auto' to match other library funcs.
- 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()
- Notes:
- Description: Sets the output level
- Original Usage:
level -76..-6(low output mode),level -38..13(high output mode, Basic only) - Direct Library Function Call:
level(val=Int|Float, -76...13) - Example Return: empty bytearray
- Alias Functions:
set_level()
- Notes: Decimals are allowed (
level(-30.5)). The device clamps a value outside the range of its current output mode to the nearest limit; it does not reject it. After entering output mode the level is at the top of the range, so set the level before switching the output on.
- Description: Sets the output level delta for low output mode level sweep
- Original Usage:
levelchange -70..+70(Basic),levelchange -90..+90(Ultra family) - Direct Library Function Call:
level_change(val=Int|Float, -90...90) - Example Return: empty bytearray
- Alias Functions:
set_level_change()
- Notes:
- 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]
- alternative returned information for format:
- 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)isOutputboolean 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')
- output:
leveloffset low -3.0- output:
bytearray(b'')
- output:
leveloffset low output 0.0- output:
bytearray(b'')
- output:
- Alias Functions:
- None
- Notes:
- NOT ALL COMBINATIONS ARE VALID.
- Calibration tables:
low- Low frequency mode correctionsswitch- Switch-related correctionsreceive_switch- Receive switch correctionsout_switch- Output switch correctionslna- LNA (Low Noise Amplifier) correctionsharmonic- Harmonic mode correctionsshift/shift1/shift2/shift3- Frequency shift correctionsdrive1/drive2/drive3- Drive level correctionsdirect/direct_lna- Direct mode correctionsultra/ultra_lna- Ultra mode correctionsharmonic_lna- Harmonic mode with LNA correctionsadf- ADF (frequency synthesizer) corrections
- 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)
- Notes:
- 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
- Notes: 0 is the startup preset
- 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()
- Notes: Should not be enabled when AGC is enabled - tinySA wiki SETTINGS2
- 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)
- Notes:
- Description: sets or dumps marker info
- Original Usage:
marker {id} on|off|peak|{freq}|{index} - Direct Library Function Call:
marker(ID=Int|1..8, val="on"|"off"|"peak"|freq|index) - Example Return: empty bytearray
- Alias Functions:
marker_on(ID),marker_off(ID),marker_peak(ID),marker_freq(ID, Hz),marker_index(ID, index)marker_mode(ID, mode, val)-modeis"delta"(val: reference marker number or"off"),"noise","tracking","trace_aver"(val:"on"|"off") or"trace"(val: trace number)marker_all_off()- sendsmarker offmarker_read(ID=None)- dumps{id} {index} {frequency} {value}for every enabled marker, or one
- Notes: where id=1..4 on the Basic, 1..8 on the Ultra family; 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.
- 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
- Notes: tinySA Menu Tree for more information. There's no error checking on this function due to the number of nested menus and buttons.
- Description: Sets the mode of the tinySA
- Original Usage:
mode low|high input|output(Basic),mode [low] input|output(Ultra family) - 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()- Basic onlyset_high_output_mode()- Basic only
- Notes: tinySA Wiki 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
- The Ultra family has no high mode.
mode high ...is answered there with a usage line and the mode does not change. - The device switches the output off whenever the mode actually changes.
- Description: Set the modulation in output mode
- Original Usage:
modulation off|am|fm|freq|depth|deviation(Ultra family),modulation off|am|nfm|wfm|extern|freq(Basic) - Direct Library Function Call:
modulation(val="off"|"am"|"fm"|"nfm"|"wfm"|"extern") - Example Return: empty bytearray
- Related Functions:
modulation_freq(val=1..10000)- modulation rate in Hzmodulation_depth(val=0..100)- AM depth in percent, Ultra family onlymodulation_deviation(val=100..300000)- FM deviation in Hz, Ultra family only
- Alias Functions:
set_mod_off()set_mod_AM()set_mod_FM()- Ultra family onlyset_mod_NFM()- Basic onlyset_mod_WFM()- Basic onlyset_mod_extern()- Basic onlyset_mod_AM_1khz(),set_mod_AM_10Hz()- see Deprecated Functions
- Notes:
- off - Turns modulation off. NO modulation
- am - AM modulation. Set the rate with
modulation_freq()and, on the Ultra family, the depth withmodulation_depth() - fm - FM modulation (Ultra family). Set the rate with
modulation_freq()and the deviation withmodulation_deviation() - nfm - Narrow FM (Basic). Width is about 3kHz.
- wfm - wide FM modulation (Basic).
- extern - (Basic) disables the internal LO driving the mixer and enables the high input as mixer LO input. Minimum external LO input frequency is 5MHz.
- The keywords
AM_1kHz,AM_10Hz,NFMandWFMbelong to tinySA firmware up to November 2020. Current firmware matches keywords case-sensitively and rejects them. The library still accepts them and sends the current equivalent:AM_1kHzbecomesmodulation amthenmodulation freq 1000;NFMbecomesmodulation nfm. - https://tinysa.org/wiki/pmwiki.php?n=Main.MODULATION
- 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()
- 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
- Description: Sets the output on or off. On the Ultra family it also selects the output path.
- Original Usage:
output on|off(Basic),output on|off|normal|mixer(Ultra family) - Direct Library Function Call:
output(val="off"|"on"|"normal"|"mixer") - Example Return: empty bytearray
- Alias Functions:
set_output_on()set_output_off()set_output_path_normal()- Ultra family onlyset_output_path_mixer()- Ultra family only
- Notes:
normalandmixerchoose the output path and do not switch the output on or off. The device cannot report whether its output is on.
- 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
- Notes:
- Description: sets the rbw to either automatic or a specific value
- Original Usage:
rbw auto|3..600orrbw 0.2..850|auto - Direct Library Function Call:
rbw(val="auto"|Int|Float) - Example Return: empty bytearray
- Alias Functions:
set_rbw_auto()
- Notes:
- The number specifies the target rbw in kHz.
- Frequencies listed in tinySA Basic documentation: 3 kHz, 10 kHz, 30 kHz, 100 kHz, 300 kHz, 600 kHz. https://tinysa.org/wiki/pmwiki.php?n=Main.RBW
- Frequencies listed in the tinySA Ultra documentation: auto, 200Hz, 1kHz 3 kHz, 10 kHz, 30 kHz, 100 kHz, 300 kHz, 600 kHz 850 kHz. https://tinysa.org/wiki/pmwiki.php?n=TinySA4.RBW
- If you set a value not in the list, the tinySA (NOT this library) will default to the next highest value.
- Decimals are allowed:
rbw(0.3)asks for 300 Hz. rbw()with no argument setsauto. It does not read the current value.
- Description: Loads a previously stored preset from the device
- Original Usage:
recall 0..4 - Direct Library Function Call:
recal(val=0|1|2|3|4) - Example Return: empty bytearray
- Alias Functions:
- None
- Notes: Same functionality as
load(). 0 is the startup preset.
- Description: Enables or disables the auto refresh mode
- Original Usage:
refresh on|off - Direct Library Function Call:
refresh(val="off"|"on") - Example Return: empty bytearray
- Alias Functions:
refresh_on()refresh_off()
- Notes:
- Description: Triggers a signal for the removal/release of the touch screen
- Original Usage:
release - Direct Library Function Call:
release() - Example Return: empty bytearray
- Alias Functions:
- None
- Notes:
- Description: does nothing, per official documentation
- Original Usage:
remark [use any text] - Direct Library Function Call:
remark(val=Str) - Example Return: empty bytearray
- Alias Functions:
- None
- Notes:
- https://tinysa.org/wiki/pmwiki.php?n=Main.USBInterface
- potential use: adding comments to scripts and denoting that the line does nothing with
remarkas the first word.
- Description: Sets the number of (re)measurements that should be taken at every frequency
- Original Usage:
repeat 1..1000 - Direct Library Function Call:
repeat(val=Int|1..1000) - Example Return: empty bytearray
- Alias Functions:
- None
- Notes: increasing the repeat reduces the noise per frequency, repeat 1 is the normal scanning mode. Has int(val) conversion.
- Description: Resets the tinySA
- Original Usage:
reset - Direct Library Function Call:
reset() - Example Return:
None. The device reboots and the USB port disappears before any reply, so there is nothing to read; since 4.0.0 the resultingSerialExceptionis caught andNonereturned instead of being raised. - Alias Functions:
reset_device()
- Notes: Disconnects the serial too. Call
disconnect()(or let the object go) andconnect()/autoconnect()again once the device has re-enumerated.
- 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()
- Notes:
- Has not worked in testing on development DUT, but appears to work on some devices online.
- 0 seconds stops the restarting process.
- Description: Resumes the sweeping in either input or output mode
- Original Usage:
resume [{count}] - Direct Library Function Call:
resume(count=None|Int)- with a count the device sweeps that many times, then pauses - Example Return: empty bytearray
- Alias Functions:
- None
- Notes:
- Description: Saves the current setting to a preset
- Original Usage:
save 0..4 - Direct Library Function Call:
save(val=0..4) - Example Return: empty bytearray
- Alias Functions:
- None
- Notes: where 0 is the startup preset
- Description: Saves the device configuration data
- Original Usage:
saveconfig - Direct Library Function Call:
save_config() - Example Return: empty bytearray
- Alias Functions:
- None
- Notes: Takes no arguments.
- 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:
- Example arg:
scan 0 2e6 5 1 - Results:
bytearray(b'0 \r\n1 \r\n1 \r\n2 \r\n2 \r') - Example arg:
scan 0 2e6 5 2 - Results:
bytearray(b'5.843750e+00 0.000000000 \r\n5.343750e+00 0.000000000 \r\n4.843750e+00 0.000000000 \r\n4.843750e+00 0.000000000 \r\n4.843750e+00 0.000000000 \r') - Example arg:
scan 0 2e6 5 3 - Results:
bytearray(b'0 5.375000e+00 0.000000000 \r\n1 5.875000e+00 0.000000000 \r\n1 5.375000e+00 0.000000000 \r\n2 5.375000e+00 0.000000000 \r\n2 5.375000e+00 0.000000000 \r') - Example arg:
scan 0 2e6 5 4 - Results:
bytearray(b'5.343750e+00 0.000000000 \r\n5.843750e+00 0.000000000 \r\n5.843750e+00 0.000000000 \r\n5.343750e+00 0.000000000 \r\n5.843750e+00 0.000000000 \r')
- Example arg:
- Alias Functions:
- None, but see
plotting_scan.pyexample
- None, but see
- Notes:
[points]is the number of points in the scan. The MAX points is device dependent. Basic is 290, Ultra is 450 .[outmask]1=frequencies, 2=measured data, 4=stored data. 3 not in documentation, but appears to blend 1 and 2.- The outmask is a bit field: 1=frequencies, 2=measured data, 4=stored data, 8=temporary data. 3 is 1 and 2 together.
startmay equalstop(a zero-span scan).- The example args above were captured with
2e6typed as the stop frequency. The firmware does not read e-notation, so the device scanned 0 Hz to 2 Hz, which is why the returned frequencies are 0, 1, 1, 2, 2. Use2000000or2Min raw command strings. - tinySA firmware before v1.4-230 (July 2026) prints a value that is an exact power of ten with
:as its leading digit, for example-:.000000e+01for exactly -100.0. See Firmware Compatibility.
- Description: Performs a scan of unlimited amount of points and sends the data in binary form
- Original Usage:
scanraw {start(Hz)} {stop(Hz)} [points][option]orscanraw {start(Hz)} {stop(Hz)} [points] [unbuffered]depending on the source - Direct Library Function Call:
scan_raw(start=Int|Float, stop=Int|Float, pts=Int|Float, unbuf=0|1) - Example Return:
- Raw, unprocessed return for 15 pts:
b'scanraw 150000000 200000000 15 2\r\n{x"\nx3\nx4\nx\x15\nx6\nx\x07\nx)\nxj\nx\xfb\txm\nx]\nxO\nxp\nx\xb2\x0bx3\x0c}ch>' - Example arg:
scanraw 150M 200M 5 1 - Results:
bytearray(b'{xS\nxd\nx\x98\nx]\nx\x02\x0c') - Example arg:
scanraw 150M 200M 2 - Results:
bytearray(b'{xs\nxu\nx\x8a\nx^\nx\xf2\x0b')
- Raw, unprocessed return for 15 pts:
- Alias Functions:
- None, but see
plotting_scanraw.pyexample
- None, but see
- Notes:
- WARNING: the screen will not match the data output during this function. That is expected.
unbuf: 0 = buffered, 1 = unbuffered (each point is sent as it is measured). From tinySA firmware v1.4-177 (August 2024) the argument is a bit field, and 2 or 3 make the device repeat the sweep until it is interrupted.scan_raw()reads one frame, so use 0 or 1. 2 and 3 are still accepted for now because earlier versions accepted them.- "The measured data is the level in dBm and is send as '{' ('x' MSB LSB)*points '}'. To get the dBm level from the 16 bit data, divide by 32 and subtract 128 for the tinySA and 174 for the tinySA Ultra. The option, when present, can be either 0,1,2 or 3 being the sum of 1=unbuffered and 2=continuous." - https://tinysa.org/wiki/pmwiki.php?n=Main.USBInterface
- Description: Deletes a specific file on the sd card
- Original Usage:
sd_delete {filename} - Direct Library Function Call:
sd_delete(val=Str) - Example Return:
- For successful delete:
bytearray(b'delete: -0.bmp OK\r' - If a file is not deleted:
bytearray(b'')
- For successful delete:
- Alias Functions:
- None
- Notes:
- Description: Displays list of filenames with extension and sizes
- Original Usage:
sd_list [{pattern}] - Direct Library Function Call:
sd_list(pattern=None|Str)e.g.sd_list("*.bin") - Example Return: format example:
-0.bmp 307322 - Alias Functions:
list_sd_files(pattern=None)- the same, parsed into[(filename, size_bytes), ...]
- Notes: Ultra family only. A missing card returns
err: (3) no card(an empty list fromlist_sd_files()).
- Description: Reads a specific file on the sd card
- Original Usage:
sd_read {filename} - Direct Library Function Call:
sd_read(val=Str) - Example Return:
- The file's bytes, exactly as stored on the card:
bytearray(b'BMz\xb0\x04\x00\x00\x00......') - If file does not exist on SD card, then
bytearray(b'err: (4) no file\r')will be returned.
- The file's bytes, exactly as stored on the card:
- Alias Functions:
save_sd_file(filename, path)- copies one file from the card to the computer; returns the byte count, or -1save_firmware_dump(path, filename=None)- see the note below
- Notes:
- The device sends a 4-byte little-endian file size, then the file.
sd_read()reads exactly that many bytes and returns only the file; in earlier versions the size prefix was returned as part of the data and the read could stop early at a>byte. - Firmware backup. The Ultra family can write its own firmware to the SD card: on the device, CONFIG > MORE > EXPERT CONFIG > DUMP FIRMWARE writes a
.binof the whole flash (262144 bytes). There is no serial command for the dump itself.save_firmware_dump("backup.bin")then copies the largest.binon the card to the computer, and warns if the size is not a full flash image. Flashing that image back is done with the normal DFU procedure from the tinySA wiki, not with this library.
- The device sends a 4-byte little-endian file size, then the file.
- Description: performs one or all selftests.
- Original Usage:
selftest 0 0..9,selftest (1-3) [arg] - Direct Library Function Call:
selftest(val=0..9) - Example Return: empty bytearray
- Alias Functions:
all_self_tests()
- Notes:
- MUST have cable connected to RF and CAL.
- The prompt for TOUCH still happens after
- 0 runs all self tests.
- Upper number of tests is likely tied to whatever the device actually has. Limited documentation.
- Other integers appear to rerun specific tests, but there is no confirmation on the screen or returned information.
- Description: Enables or disables spur reduction
- Original Usage:
spur on|off(Basic),spur off|on|auto(Ultra family) - Direct Library Function Call:
spur(val="off"|"on"|"auto") - Example Return: empty bytearray
- Alias Functions:
spur_on()spur_off()spur_auto()- Ultra family only
- Notes:
- Description: Displays the current device status (paused/resumed)
- Original Usage:
status - Direct Library Function Call:
status() - Example Return:
b'Resumed' - Alias Functions:
get_status()
- Notes:
- Description: Set sweep boundaries or execute a sweep
- Original Usage:
sweep [(start|stop|center|span|cw {frequency}) | ({start(Hz)} {stop(Hz)} [0..MAX PTS] ) ] - Direct Library Function Call:
config_sweep(argName=start|stop|center|span|cw, val=Int|Float)ANDperform_sweep(start, stop, pts) - Example Return:
- empty bytearray
b'' - bytearray(b'0 800000000 450\r')
- empty bytearray
- Alias Functions:
get_sweep_params()set_sweep_start(val=FREQ)set_sweep_stop(val=FREQ)set_sweep_center(val=FREQ)set_sweep_span(val=Int|Float)set_sweep_cw(val=Int|Float)run_sweep(start=FREQ, stop=FREQ, pts=INT)sweep_mode(val="normal"|"precise"|"fast"|"noise"|"go"|"abort")get_sweep_range()-(start_hz, stop_hz, points)parsed from the baresweepreply
- Notes: sweep without arguments lists the current sweep settings, the frequencies specified should be within the permissible range. The sweep commands apply both to input and output modes. MAX PTS is device dependent; 290 for tinySA Basic and 450 for tinySA Ultra and newer
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).sweep {start(Hz)} {stop(Hz)} [0..MAX PTS]: sets the start and stop frequencies, and optionally the number of points in the sweep
- Description: Sets the time between each sweep.
- Original Usage:
sweeptime {time(Seconds)} - Direct Library Function Call:
- Example Return: empty bytearray
- Alias Functions:
- None
- Notes: the time specified may end in a letter where m=milli and u=micro
- Description: Get the temperature from the device
- Original Usage:
k - Direct Library Function Call:
temp() - Example Return:
bytearray(b'43.25\r') - Alias Functions:
get_temp()
- Notes: single letter command. might not work on tinySA Basic model.
- Description: specifies the text entry for the active keypad
- Original Usage:
text keypadtext - Direct Library Function Call:
text(val="") - Example Return: empty bytearray
- Alias Functions:
- None
- Notes: where keypadtext is the text used. Example: text 12M. Currently does not work for entering file names.
- Description: lists information of the threads in the tinySA
- Original Usage:
threads - Direct Library Function Call:
threads() - Example Return:
b'stklimit| |stk free| addr|refs|prio| state| name\r\n20000200|2000054C|00000248|200016A8| 0| 128| CURRENT| main\r\n20001530|2000157C|0000008C|20001650| 0| 1| READY| idle\r\n200053D8|200056C4|00000250|200059B0| 0| 127| READY| sweep\r' - Alias Functions:
- None
- Notes:
- Description: sends the coordinates of a touch
- Original Usage:
touch {X coordinate} {Y coordinate} - Direct Library Function Call:
touch(x=0,y=0) - Example Return: empty bytearray
- Alias Functions:
perform_touch(x=Int, y=Int)
- Notes: The upper left corner of the screen is "0 0"
- Description: starts the touch calibration
- Original Usage:
touchcal - Direct Library Function Call:
touch_cal() - Example Return: empty bytearray
- Alias Functions:
start_touch_cal()
- Notes:
- Description: starts the touch test
- Original Usage:
touchtest - Direct Library Function Call:
touch_test() - Example Return: empty bytearray
- Alias Functions:
start_touch_test()
- Notes: instructions on screen "touch panel, draw lines, press button to complete"
- Description: displays all or one trace information or sets trace related information. INCOMPLETE due to how many combinations are possible.
- Original Usage:
- from help message return
bytearray(b'trace {dBm|dBmV|dBuV|RAW|V|Vpp|W}\r\ntrace {scale|reflevel} auto|{value}\r\ntrace [{trace#}] value\r\ntrace [{trace#}] {copy|freeze|subtract|view|value} {trace#}|off|on|[{index} {value}]\r') - general structure for commands:
trace [{trace#}] {copy|freeze|subtract|view|value} {trace#}|off|on|[{index} {value}] - from earlier documentation:
trace [{0..2} | dBm|dBmV|dBuV| V|W |store|clear|subtract | (scale|reflevel) auto|{level}]
- from help message return
- Direct Library Function Call:
trace_toggle(ID=Int|0..2..4, val="on"|"off")trace_select(ID=Int|0..2..4)trace_units(ID=Int, val="dBm"|"dBmV"|"dBuV"|"RAW"|"V"|"Vpp"|"W"|"dBV")-dBVneeds tinySA firmware v1.4-235 or later; older firmware ignores it without an errortrace_scale(val="auto"|Int|Float)trace_reflevel(val="auto"|Int|Float)trace_value(ID=Int)trace_subtract(ID1=Int, ID2=Int)trace_copy(ID1=Int, ID2=Int)trace_freeze(ID=Int, val="on"|"off")andtrace_unfreeze(ID=Int)- the device needs theon/offword;trace 2 freezealone is ignored by the firmwaretrace_action(ID=Int, val="copy"|"freeze"|"subtract"|"view"|"value", arg=None|trace#|"on"|"off")trace_clear(val)- see Deprecated Functions
- Example Return:
- empty bytearray
b'' trace_select(1):bytearray(b'1: dBm 0.000000000 10.000000000 \r')
trace_value(1):bytearray(b'trace 1 value 0 -91.13\r\ntrace 1 value 1 -92.59\r\ntrace 1 value 2 -93.09\r\ntrace 1 value 3 -89.59.....\r\ntrace 1 value 448 -84.78\r\ntrace 1 value 449 -85.25\r')(returns MAX POINTS number of readings. PTS not currently settable)
- empty bytearray
- Alias Functions:
- None, see direct library function calls
- it is also suggested to use the
command()function to perform more complex actions because this is a complicated command structure
- Notes: For readability, this command was split into multiple functions initially rather than using complex alias functions. There is a mismatch of information of commands between versions, so this library uses the documentation returned by the device.
trace_select(): tinySA Ultra has 4 traces to choose from. Other devices may have other numbers of traces.trace_reflevel(...): adjusts the reference level of a trace. Levels are specified in dB(m) and can be specified using a floating point notation. E.g. 10 or 2.5 https://tinysa.org/wiki/pmwiki.php?n=Main.USBInterface
- Description: sets the trigger type or level
- Original Usage:
trigger auto|normal|single|{level(dBm)}ortrigger {value}\r\ntrigger {auto|normal|single}\r - Direct Library Function Call:
trigger(val="auto"|"normal"|"single"|None, freq=None|Int|Float|"-52.5") - Example Return: empty bytearray. The level may be a number or a numeric string (4.0.0;
trigger_level("-52.5")used to be refused). - Alias Functions:
trigger_auto()trigger_normal()trigger_single()trigger_level(FREQ)
- Notes: the trigger level is always set in dBm
- Description: turn on/off/config tiny SA ultra mode features
- Original Usage:
ultra off|on|auto|start|harm {freq} - Direct Library Function Call:
ultra(val="on"|"off") - Example Return: empty bytearray
- Alias Functions:
set_ultra_on()set_ultra_off()set_ultra_auto()set_ultra_start(val=FREQ)set_ultra_harmonic(val=FREQ)
- Notes:
- ultra on: enable ultra mode
- ultra off {freq}: disable ultra mode
- ultra auto: let the tinySA decide
- ultra start {freq}: set ultra start frequency to a specific frequency in MHz
- ultra harm {freq}: enables harmonic mode and set the frequency to which the harmonic filter is tuned. NO ERROR CHECKING FOR THIS in library
- Description: Get port current baud rate/ gets the current serial config
- Original Usage:
usart_cfg - Direct Library Function Call:
usart_cfg() - Example Return:
b'Serial: 115200 baud\r' - Alias Functions:
usart_cfg()
- Notes: default is 115,200
- Description: Get the current battery voltage
- Original Usage:
vbat - Direct Library Function Call:
- Example Return: empty bytearray
- Alias Functions:
get_vbat()
- Notes:
- Description: Sets or gets the battery offset value
- Original Usage:
vbat_offset [{0..4095}] - Direct Library Function Call:
vbat_offset(val=None|0..4095) - Example Return:
b'300\r' - Alias Functions:
get_vbat_offset()set_vbat_offset(val=Int)
- Notes:
- Description: returns the version text
- Original Usage:
version - Direct Library Function Call:
version() - Example Return: empty bytearray
- Alias Functions:
get_version()
- Notes:
- Description: wait for a single sweep to finish and pauses sweep or waits for specified number of seconds
- Original Usage:
wait [{seconds}] - Direct Library Function Call:
wait(val=None|Int|"Int") - Example Return: empty bytearray. A numeric string (
"5") is accepted; anything that is not a positive whole number returns the error value (4.0.0; it used to raiseTypeError). - Alias Functions:
- None
- Notes: Without an argument, this puts device in the WAIT state and it needs to manually RESUME.
wait()now sends the bare command; in earlier versions the default argument was rejected andwait(None)was needed.
- Description: get or set the zero offset in dBm
- Original Usage:
zero {level}\r\n174dBm - Direct Library Function Call:
zero(val=None|Int) - Example Return: empty bytearray
- Alias Functions:
get_zero_offset()- the raw replyget_scanraw_offset()- the value as an integer: 174 on the Ultra family, 128 on the Basic. This is the offset to subtract when decodingscanrawdata
- Notes: If unfamiliar with device and operation, DO NOT CHANGE THIS VALUE.
Full list of help commands
commands: freq time dac nf saveconfig clearconfig zero sweep pause resume wait repeat status caloutput save recall trace trigger marker line usart_cfg vbat_offset color if if1 lna2 agc actual_freq freq_corr attenuate level sweeptime leveloffset levelchange modulation rbw mode spur lna direct ultra load ext_gain output deviceid correction calc menu text remark
Other commands: version reset data frequencies scan hop scanraw test touchcal touchtest usart capture refresh touch release vbat help info selftest sd_list sd_read sd_delete threadsThe firmware has no dedicated "get" commands, but most setters print their current value when sent with ?. These helpers send that query and parse the value. They return None when the device gave no value (for example an older firmware that lacks the command).
| Function | Returns | Device query |
|---|---|---|
get_rbw() |
kHz, e.g. 10.0, 0.3 |
rbw ? |
get_attenuation() |
dB | attenuate ? |
get_sweep_time() |
seconds | sweeptime ? |
get_IF() |
Hz | if ? |
get_scanraw_offset() |
174 or 128 | zero ? |
get_noise_figure() |
dB | nf ? |
get_dac_value() |
0..4095 | dac ? |
get_calc() |
OFF, MINH, MAXH, MAXD, AVER4, A16, AVER, QUASI... |
calc ? |
get_sweep_range() |
(start_hz, stop_hz, points) |
sweep |
Note that rbw() and attenuate() with no argument still set auto, as they always have.
tinySA.decode_number(text) parses one number from a device reply, including the : artifact of firmware before v1.4-230 (-:.000000e+01 is exactly -100.0).
These wrap commands that exist only in newer firmware. On older firmware the device answers name?, which comes back as the reply text (and as unknown-command in the log).
| Function | Device command | Needs firmware |
|---|---|---|
waitscan(count=1) |
waitscan {count} |
v1.4-216 |
channel(val=None|1..3) |
channel [1-3] |
v1.4-223 |
avoid(val="auto"|"off"|"on"|"dump") |
avoid ... |
v1.4-194, Ultra family |
usbdisk(val=None|"on"|"off") |
usbdisk ... |
v1.4-244, Ultra family. While on, most other commands answer usb disk mode active |
time(field=None, val=None) |
time / time y|m|d|h|min|sec {0-99} |
Ultra family |
correction_reset(table) |
correction {table} reset |
any |
sweep_mode(val) |
sweep normal|precise|fast|noise|go|abort |
any |
marker_mode(ID, mode, val), marker_all_off(), marker_read(ID=None) |
marker ... |
any |
resume(count) |
resume {count} |
any |
sd_list(pattern), list_sd_files(), save_sd_file(), save_firmware_dump() |
sd_list, sd_read |
Ultra family |
- Description: Sets the sweep voltage
- Original Usage:
sweep_voltage {0-3.3} - Direct Library Function Call:
sweep_voltage() - Example Return: empty bytearray
- Notes: device does not recognize this. Will revisit after next firmware update
- Description: override library functions to run commands on the tinySA 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 150M 200M 5 2") - The string is sent unchanged. The firmware accepts
150000000,150M,0.15Gor150000k, but not e-notation:150e6is read as 150 Hz. - 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:
- Example Return: command dependent
- Alias Functions:
- None
- Notes: If unfamiliar with device and operation, DO NOT USE THIS. There is no error checking and you will be interfacing with the tinySA device directly.
This library (and the PyPI package) have been developed through experimental usage and some existing references. The primary function of the library is to make it more accessible for developers to access the tinySA device lines with a non-GUI based Python interface for custom programs or scripts.
Collaboration on this repository is welcome. Development is active, but may be a little slow at times. Please submit issues and comments on GitHub, NOT the tinySA community page. This repository exists and operates separately of the official tinySA product.
CONTRIBUTING.md has the development setup, the checks a pull request must pass, how to add a command or a device profile, the hardware test layers, and the release steps. The short version:
cd tsapython
uv sync
uv run pytest # hardware-free suite; hardware tests self-skip
uv run ruff check . && uv run mypyHow the code is organised:
| Where | What |
|---|---|
src/tsapython/core.py |
The tinySA class: serial port, tinySA_serial() (the one place bytes are written and read), the log file, identification and profiles, the getters |
src/tsapython/_commands/*.py |
One mixin per command group; every device command is a method here |
src/tsapython/_profiles.py |
What each model accepts (BASIC, ZS405, ZS406, ZS407), read from the firmware source |
src/tsapython/_identify.py |
Pure text helpers: which device sent this version/info, and did the device accept or reject the command |
src/tsapython/_log.py |
The JSON Lines log writer |
tests/ |
Hardware-free tests (CI), hardware and hardware_output tests, the probe script, the bench script, captured fixtures |
Test layers and when each runs:
| Command | Needs | Runs |
|---|---|---|
uv run pytest |
Nothing | Every push (CI), 600+ tests, coverage gate 85% (currently about 97%) |
uv run pytest -m hardware |
A tinySA on USB | Before a release; nothing switches the output on |
TSAPYTHON_OUTPUT_OK=1 uv run pytest -m hardware_output |
A load or attenuator on the RF port | Before a release; proves generator settings are accepted |
uv run python tests/bench_generator.py --path-loss <dB> |
Attenuator and a receiver | Once per release; the only proof of what the generator emits |
uv run python tests/probe_firmware_returns.py |
A tinySA on USB | Before and after a firmware update, and for reporting an unverified board |
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:
- The tinySA wiki
- The getting started first use page
- Frequently asked questions (FAQs) can be found on the Wiki FAQs page
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.
- 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.
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 API interfacing, 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, but the tinySA (currently) has expanded features for generating signals. This library was made for the tinySA line of devices. There might be some compatibility with the NanoVNA, but this is not currently supported or under development. To avoid confusion, there is a nanoVNA_python library that focuses on the NanoVNA.
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 values 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.
Some tips:
- The cable MUST be connected between the two ports BEFORE starting the calibration.
- The cable should be finger tight. If the connector will not turn, there's a high risk of cross threading if it's forced. It's better to try again than to need to buy and install new connectors.
- If you have any RF equipment nearby (including hand-held radios), avoid transmitting during calibration or self-tests. Depending on the signal strength, this could cause issues with the results.
These are notes collected from various references as this README documentation is built out. Some are obvious, some were not at the time.
- Screen size of 2.8 inches
- Points displayed per scan: 290 max
- Low input frequency: 100 kHz - 350 MHz
- High input: 240 MHz - 960 MHz
- No SD card storage
-
Screen size of 4 inches
-
Points displayed per scan: 450 max
-
Low input frequency: 100k Hz-800 MHz, but Ultra mode and harmonic mode changes this
-
High input: Ultra mode and harmonic mode impact this
-
Has SD card storage option
-
In the tinySA Ultra mode, there are four output modes: Normal (below 830 MHz), Direct (between 830MHz and 1130MHz), Ultra, and Scan.
-
Ultra Mode: A special operational mode on the Ultra and Ultra Plus devices that allows users to access a broader frequency range, up to 6 GHz for the tinySA Ultra. Comes with an increased scan time.
-
Harmonic Mode: A mode for measuring the harmonics of a fundamental signal instead of displaying the entire spectrum.
-
There are two versions of the Ultra Plus: the ZS406 and the ZS 407. They have different frequency ranges. Refer to official documentation for differences.
-
Screen size of 4 inches
-
Points displayed per scan: 450 max
-
Low input frequency: 100k Hz-900 MHz, but Ultra mode and harmonic mode changes this
-
High input: Ultra mode and harmonic mode impact this
-
Has SD card storage option
-
In the tinySA Ultra mode, there are four output modes: Normal (below 830 MHz), Direct (between 830MHz and 1130MHz), Ultra, and Scan.
-
Ultra Mode: A special operational mode on the Ultra and Ultra Plus devices that allows users to access a broader frequency range, up to 5.3 or 7.3 GHz for the tinySA Ultra Plus (depends on the model). Comes with an increased scan time.
-
Harmonic Mode: A mode for measuring the harmonics of a fundamental signal instead of displaying the entire spectrum.
The tinySA_python library (named for the GitHub repository) is a collection of documentation, examples, and the tsapython package. Further expansion for other python packages is planned to include advanced usage functionality. The tinySA_python repository is intended for beginners and those looking for a simplified way of interacting with their tinySA device. The library itself does not use a graphical user interface (GUI), but it can be used with visualization tools to plot collected data.
The Library Usage section includes information on how to download the package with pip, or installing a version locally from the repository.
This library is updated in spurts. June-August are going to be the most active development months, with limited updates otherwise. Development has slowed as the project stabilizes, but sometimes new features are proposed or needed. Bug fixes will be addressed as they are brought up.
-
https://groups.io/g/tinysa/topic/tinysa_screen_capture_using/82218670
-
The firmware on GitHub at https://github.com/erikkaashoek/tinySA
-
Websites that have been trawled through for random bits of information:
- https://www.passion-radio.fr/
- They have a 'TinySA Menu Tree' PDF doc that has been very useful
- The main TinySA GitHub
- Spectrum Analyzer How-To Guide: What They Are, What They Measure, & How to Use Them
- https://www.passion-radio.fr/
-
Hardware, S-parameters and 2 port networks:
-
Python Packaging and Library Setup:
-
A running list of some other cool tinySA related projects:
- https://github.com/g4ixt/QtTinySA
- https://github.com/Ho-Ro/nanovna-tools
- Has interfacing with the tinySA AND NanoVNA!
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 tinySA 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 while under development).
This software is UNOFFICIAL, meaning that the tinySA team does not offer tech support for it, does not maintain it, and has no responsibility for any of the contents.







