{"id":34113975,"url":"https://github.com/lc-linkous/tinysa_python","last_synced_at":"2026-06-05T06:00:37.306Z","repository":{"id":271296378,"uuid":"912609979","full_name":"LC-Linkous/tinySA_python","owner":"LC-Linkous","description":"An unofficial Python API for the tinySA device line (now a PyPI package!)","archived":false,"fork":false,"pushed_at":"2026-06-03T19:16:53.000Z","size":908,"stargazers_count":13,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-03T20:06:15.947Z","etag":null,"topics":["examples-python","pypi-package","spectrum-analyzer","tinysa"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/LC-Linkous.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-01-06T02:58:20.000Z","updated_at":"2026-06-02T00:43:13.000Z","dependencies_parsed_at":"2025-01-06T21:25:49.100Z","dependency_job_id":"edb05c26-db41-4117-abe2-bcd8a1ddc9f2","html_url":"https://github.com/LC-Linkous/tinySA_python","commit_stats":null,"previous_names":["lc-linkous/tinysa_ultra"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/LC-Linkous/tinySA_python","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LC-Linkous%2FtinySA_python","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LC-Linkous%2FtinySA_python/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LC-Linkous%2FtinySA_python/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LC-Linkous%2FtinySA_python/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LC-Linkous","download_url":"https://codeload.github.com/LC-Linkous/tinySA_python/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LC-Linkous%2FtinySA_python/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33932040,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-05T02:00:06.157Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["examples-python","pypi-package","spectrum-analyzer","tinysa"],"created_at":"2025-12-14T19:22:29.435Z","updated_at":"2026-06-05T06:00:37.288Z","avatar_url":"https://github.com/LC-Linkous.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tinySA_python\n\n\u003c!-- Badges. Note on the DOI: 10.5281/zenodo.20546764 is the first-deposit DOI. --\u003e\n \n[![PyPI version](https://badge.fury.io/py/tsapython.svg)](https://badge.fury.io/py/tsapython)\n[![Python versions](https://img.shields.io/pypi/pyversions/tsapython.svg)](https://pypi.org/project/tsapython/)\n[![PyPI - Wheel](https://img.shields.io/pypi/wheel/tsapython.svg)](https://pypi.org/project/tsapython/)\n[![Downloads](https://static.pepy.tech/badge/tsapython)](https://pepy.tech/project/tsapython)\n[![License: GPL v2](https://img.shields.io/badge/License-GPL_v2-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20546764.svg)](https://doi.org/10.5281/zenodo.20546764)\n\n\n## AN UNOFFICIAL Python API for the tinySA Device Series\n\nA Non-GUI Python API for the tinySA series of devices. This repository uses official resources and documentation but is NOT endorsed by the official tinySA product or company. See the [references](#references) section for further reading. See the [official tinySA resources](https://www.tinysa.org/wiki/) for device features.\n\nThis library covers most documented commands for the tinySA device series, and is planned to include configurable, device-specific commands and memory. The documentation (after the examples) is sorted based on the serial command for the device, with some provided usage examples. While some error checking exists in both the device and the library, it is not exhaustive. It is strongly advised to read the official documentation before attempting to script with your tinySA device. Operating the device experimentally or without referencing the official documents runs the risk of **destroying your device**. See the [tinySA First use page](https://tinysa.org/wiki/pmwiki.php?n=Main.FirstUse) for some setup tips and warnings.\n\nThis README provides example code for connecting to the device, scanning and plotting data, saving to CSV, and creating real-time waterfall plots. Examples are not exhaustive. Refer to the [List of tinySA Commands and their Library Commands](#list-of-tinysa-commands-and-their-library-commands) for all of the tested commands for this library. Alias functions have been provided for convenience, but are not exhaustive. \n\nIf you are interested in developing the PyPI package, or making a custom local version, see [Library Development](#library-development) towards the end of this README.\n\n\nThe primary GitHub: [https://github.com/LC-Linkous/tinySA_python](https://github.com/LC-Linkous/tinySA_python)\n\nThe PyPI page: [https://pypi.org/project/tsapython/](https://pypi.org/project/tsapython/)\n\nZenodo archive with DOI: [https://doi.org/10.5281/zenodo.20546764](https://doi.org/10.5281/zenodo.20546764)\n\n## Table of Contents\n* [The tinySA Series of Devices](#the-tinysa-series-of-devices)\n* [Library Usage](#library-usage)\n    * [PyPI Install](#pypi-install)\n    * [Local Install Using UV](#local-install-using-uv)\n* [Requirements](#requirements)\n* [Structure](#structure)\n* [Running Tests](#running-tests)\n* [Error Handling](#error-handling)\n* [Example Implementations](#example-implementations)\n    * [Finding the Serial Port](#finding-the-serial-port)\n        * [Autoconnection with the tinySA_python Library](#autoconnection-with-the-tinysa_python-library)\n        * [Manually Finding a Port on Windows](#manually-finding-a-port-on-windows)\n        * [Manually Finding a Port on Linux](#manually-finding-a-port-on-linux)\n    * [Serial Message Return Format](#serial-message-return-format)\n    * [Connecting and Disconnecting the Device](#connecting-and-disconnecting-the-device)\n    * [Toggle Error Messages](#toggle-error-messages)\n    * [Device and Library Help](#device-and-library-help)\n    * [Setting tinySA Parameters](#setting-tinysa-parameters)\n    * [Getting Data from Active Screen](#getting-data-from-active-screen)\n    * [Saving Screen Images](#saving-screen-images)\n    * [Plotting Data with Matplotlib](#plotting-data-with-matplotlib)\n        * [Example 1: Plot using On-Screen Trace Data and Frequencies](#example-1-plot-using-on-screen-trace-data-and-frequencies)\n        * [Example 2: Plot using Scan Data and Frequencies](#example-2-plot-using-scan-data-and-frequencies)\n        * [Example 3: Plot using SCAN and SCANRAW Data and Calculated Frequencies](#example-3-plot-using-scan-and-scanraw-data-and-calculated-frequencies)\n        * [Example 4: Plot using SCAN And Filters for Artifact Comparison](#example-4-plot-using-scan-and-filters-for-artifact-comparison)\n        * [Example 5: Plot a Waterfall using SCAN and Calculated Frequencies](#example-5-plot-a-waterfall-using-scan-and-calculated-frequencies)\n        * [Example 6: Finding Peaks in a Frequency Range](#example-6-finding-peaks-in-a-frequency-range)\n    * [Saving SCAN Data to CSV](#saving-scan-data-to-csv)\n    * [Accessing the tinySA Directly](#accessing-the-tinysa-directly)\n* [List of tinySA Commands and their Library Commands](#list-of-tinysa-commands-and-their-library-commands)\n* [List of Commands Removed from Library](#list-of-commands-removed-from-library)\n* [Additional Library Functions for Advanced Use](#additional-library-functions-for-advanced-use)\n* [Library Development](#library-development)\n* [Notes for Beginners](#notes-for-beginners)\n    * [Vocab Check](#vocab-check)\n    * [VNA vs. SA vs. LNA vs. SNA vs. SDR vs Signal Generator](#vna-vs-sa-vs-lna-vs-sna-vs-sdr-vs-signal-generator)\n    * [Calibration Setup](#calibration-setup)\n    * [Some General tinySA Notes](#some-general-tinysa-notes)\n* [FAQs](#faqs)\n* [References](#references)\n* [Licensing](#licensing)  \n\n## The tinySA Series of Devices\n\nThe [tinySA line of devices](https://tinysa.org/wiki/pmwiki.php?n=TinySA4.Comparison) are a series of portable and pretty user-friendly devices with both spectrum analyzer and signal generator capabilities. There are four main versions, all of which share the same key features. The Ultra and Ultra Plus versions build off of the original tinySA Basic. They look very similar to the [NanoVNA series](https://nanovna.com/), but are NOT the same device and have different functionalities. They are also made by different people.\n\nThe 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.\n\nOfficial documentation for the tinySA can be found at [https://tinysa.org/](https://tinysa.org/). The official Wiki is going to be more up to date than this repo with new versions and features, and they also have links to GUI-based software (which is also under development). Several community projects exist on GitHub, and some may be official (this is not one of them!).\n\nThere is also a very active tinySA community at [https://groups.io/g/tinysa](https://groups.io/g/tinysa) exploring the device capabilities and its many features. There are in-depth Q\u0026A 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.\n\nThe 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. \n\nImproper usage may destroy your device. \n\n\n\n## Library Usage\n\nThis library is now available via PyPI, local install, or just using the class. We recommend one of the library install options.\n\nSeveral usage examples are provided in the [Example Implementations](#example-implementations) section, including working with the hardware and plotting results with matplotlib. \n\n\n### PyPI Install\n\n\nThe `tsapython` package (from PyPI at [https://pypi.org/project/tsapython/3.0.0/](https://pypi.org/project/tsapython/3.0.0/))  can be installed with:\n\n```python\n\npip install tsapython\n\n```\n\nThe GitHub repository will continue to be named `tinySA_python` to differentiate the working versions and the additional documentation included here. \n\n\n### Local Install Using UV\n\nDeveloping a project, or running something custom? You can pull the code from GitHub and build+install the package locally. \n\n(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)\n\nThis is a summarized version of the instructions at [https://www.sarahglasmacher.com/how-to-build-python-package-uv/](https://www.sarahglasmacher.com/how-to-build-python-package-uv/):\n\n\n```python\n# install UV\npip install uv\n\n# navigate to the tsapython directory\ncd .\\tsapython\n\n# build the package\n# a 'dist' directory should be created in tsapython\nuv build\n\n# install the package locally\npip install dist/tsapython-3.0.0-py3-none-any.whl\n\n```\n\n## Requirements\nThis project requires numpy and pyserial. \nUse 'pip install -r requirements.txt' to install the following dependencies:\n```python\npyserial\nnumpy\n```\nThe above dependencies are only for the API interfacing of the tinySA_python library. Additional dependencies should be installed if you are following the examples in this README. These can be installed with `pip install -r test_requirements.txt`:\n```python\npyserial\nnumpy\nmatplotlib\npyQt5   # Linux OS, some Windows machines\n```\n\nFor anyone unfamiliar with using requirements files, or having issues with the libraries, these can also be installed manually in the terminal (we recommend a Python virtual environment) with:\n\n```python\npip install pyserial numpy matplotlib pyQt5\n```\n`pyQt5` is used with `matplotlib` to draw the figures. It needs to be installed on Linux systems to follow the examples included in tinySA_python, but is not needed on all Windows machines.\n\nIf you are installing the package itself (rather than the loose requirements files), the same optional dependencies are available as extras defined in `pyproject.toml`:\n```python\n# library only (numpy + pyserial)\npip install tsapython\n\n# library + plotting dependencies for the examples\npip install \"tsapython[plotting]\"\n\n# development / running the test suite\npip install -e \".[test]\"\n```\n\n## Previous Versions\n\nRelease history and archived versions of this library are available in a few places:\n\n- **GitHub Releases** — tagged releases with source and notes:\n  [https://github.com/LC-Linkous/tinySA_python/releases](https://github.com/LC-Linkous/tinySA_python/releases)\n  (the 3.0.0 release: [releases/tag/v3.0.0](https://github.com/LC-Linkous/tinySA_python/releases/tag/v3.0.0))\n- **PyPI release history** — every published version, installable with\n  `pip install tsapython==\u003cversion\u003e`:\n  [https://pypi.org/project/tsapython/#history](https://pypi.org/project/tsapython/#history)\n- **Zenodo archive** — a citable, archived snapshot with a DOI:\n  [https://doi.org/10.5281/zenodo.20546764](https://doi.org/10.5281/zenodo.20546764)\n  (DOI `10.5281/zenodo.20546764`)\n\n\n## Structure\nThe `tsapython` library, as it is available on PyPI, is structured as follows:\n```\ntsapython/\n├── .python-version\n├── pyproject.toml\n├── README.md\n├── LICENSE\n├── .gitignore\n├── src/\n│   └── tsapython/\n│       ├── __init__.py\n│       ├── core.py\n│       ├── py.typed\n│       └── _commands/\n│           ├── __init__.py\n│           ├── acquisition.py\n│           ├── calibration.py\n│           ├── display_ui.py\n│           ├── levels_gain.py\n│           ├── markers_traces.py\n│           ├── output_signal.py\n│           ├── presets_config.py\n│           └── system_info.py\n└── tests/\n    ├── __init__.py\n    ├── conftest.py\n    ├── test_smoke.py\n    ├── test_acquisition.py\n    ├── test_calibration.py\n    ├── test_display_ui.py\n    ├── test_levels_gain.py\n    ├── test_markers_traces.py\n    ├── test_output_signal.py\n    ├── test_presets_config.py\n    ├── test_system_info.py\n    ├── test_parsing.py\n    ├── test_hardware.py\n    ├── test_captured_hardware.py\n    ├── collect_samples.py\n    └── fixtures/\n        ├── __init__.py\n        ├── device_responses.py\n        └── captured_responses.py\n```\n\nThe 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.\n\nA `docs` repository for the library will be added later in development for stable releases.\n\nThis 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:\n```\ntinySA_python/\n├── README.md\n├── requirements.txt\n├── test_requirements.txt\n├── media/\n│   └── README images, screenshots\n└── tsapython/\n    ├── .python-version\n    ├── pyproject.toml\n    ├── README.md\n    ├── LICENSE\n    ├── .gitignore\n    ├── examples/\n    │   ├── __init__.py\n    │   ├── complete_workflow.py\n    │   ├── hardware_walkthrough.py\n    │   ├── identifying_serial_ports.py\n    │   ├── using_autoconnect.py\n    │   ├── using_command_func.py\n    │   ├── plotting_scan.py\n    │   ├── plotting_scanraw.py\n    │   ├── plotting_waterfall_realtime.py\n    │   ├── plotting_waterfall_static.py\n    │   ├── save_scan_csv.py\n    │   ├── continuous_scanraw_live.py\n    │   └── continuous_scanraw_collect.py\n    ├── src/\n    │   └── tsapython/\n    │       ├── __init__.py\n    │       ├── core.py\n    │       ├── py.typed\n    │       └── _commands/\n    │           ├── __init__.py\n    │           ├── acquisition.py\n    │           ├── calibration.py\n    │           ├── display_ui.py\n    │           ├── levels_gain.py\n    │           ├── markers_traces.py\n    │           ├── output_signal.py\n    │           ├── presets_config.py\n    │           └── system_info.py\n    └── tests/\n        ├── __init__.py\n        ├── conftest.py\n        ├── test_smoke.py\n        ├── test_acquisition.py\n        ├── test_calibration.py\n        ├── test_display_ui.py\n        ├── test_levels_gain.py\n        ├── test_markers_traces.py\n        ├── test_output_signal.py\n        ├── test_presets_config.py\n        ├── test_system_info.py\n        ├── test_parsing.py\n        ├── test_hardware.py\n        ├── test_captured_hardware.py\n        ├── collect_samples.py\n        └── fixtures/\n            ├── __init__.py\n            ├── device_responses.py\n            └── captured_responses.py\n```\n\n## Running Tests\n\nThis is primarily for development or advanced troubleshooting. These tests are for the API.\n\nThe test suite uses [pytest](https://docs.pytest.org/). Tests must be run from the\n`tsapython` project directory (the one containing `pyproject.toml`), since the pytest\nconfiguration and the `hardware` marker are defined in `pyproject.toml`. Running from a\ndifferent directory will produce an `Unknown pytest.mark.hardware` warning.\n\nInstall the test dependencies first:\n```bash\npip install -e \".[test]\"\n# or, using the requirements file:\npip install pytest pytest-cov\n```\n\nRun the suite (hardware tests self-skip when no device is connected):\n```bash\npython -m pytest\n```\n\n\u003e **Note:** use `python -m pytest`, not `uv run pytest`. Running through `uv` here can\n\u003e create a stray virtual environment inside the project directory and tangle the test\n\u003e environment.\n\nThe suite is split into hardware-free tests and tests that need a connected tinySA.\nThe hardware tests are marked with `@pytest.mark.hardware` and are skipped automatically\nwhen no device is detected:\n```bash\n# run ONLY the hardware-free tests (explicitly skip device tests)\npython -m pytest -m \"not hardware\"\n\n# run ONLY the hardware tests (requires a connected tinySA)\npython -m pytest -m hardware\n```\n\nTo see coverage while testing:\n```bash\npython -m pytest --cov=tsapython --cov-report=term-missing\n```\n\n### Capturing real device responses\n\n`tests/collect_samples.py` is a helper (not a pytest test) that connects to a real device,\nsends a set of read-only commands, and writes their responses to\n`tests/fixtures/captured_responses.py`. The `test_captured_hardware.py` tests then run the\nlibrary's parsing logic against those real captures (these run without a device, since the\nbytes are frozen in the fixture):\n```bash\npython tests/collect_samples.py\n```\n\n\n### Collecting device samples\n`tests/collect_samples.py` is a manual helper (not a pytest test) for capturing real\ndevice responses to use as parsing fixtures. Run it with a tinySA connected:\n```python\n# auto-detect the serial port\npython tests/collect_samples.py\n\n# or specify the port explicitly\npython tests/collect_samples.py --port COM5            # Windows\npython tests/collect_samples.py --port /dev/ttyACM0    # Linux/Mac\n```\n\n### Example scripts\nThe files in `examples/` are runnable demonstrations (not part of the automated test\nsuite) and require a connected device plus the plotting dependencies:\n```python\npip install \"tsapython[plotting]\"     # or: pip install -r test_requirements.txt\npython examples/complete_workflow.py\npython examples/hardware_walkthrough.py\n```\n\n\n## Error Handling\n\nSome 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.\n\nDetailed error messages can be returned by toggling 'verbose' on.\n\nFrom the [official wiki USB Interface page](https://tinysa-org.translate.goog/wiki/pmwiki.php?n=Main.USBInterface\u0026_x_tr_sl=auto\u0026_x_tr_tl=en\u0026_x_tr_hl=en-US):\n\n```\nThere is limited error checking against incorrect parameters or incorrect device mode. \nSome error checking will be integrated as the device configurations are included, \nbut this is not intended to be exhaustive. \n```\n\n\nSome error checking includes:\n\n * 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)\n * Levels are specified in dB(m) and can be specified using a floating point notation. E.g. 10 or 2.5\n * 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)\n\n## Example Implementations\n\nThis 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. \n\n\n### Finding the Serial Port\n\nTo 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. \n\n\n#### Autoconnection with the tinySA_python Library\n\n\nThe 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).\n\n\n```python\n\n# import tinySA_python (tsapython) package\nfrom tsapython import tinySA\n\n# create a new tinySA object    \ntsa = tinySA()\n\n# set the return message preferences \ntsa.set_verbose(True) #detailed messages\ntsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown\n\n\n# attempt to autoconnect\nfound_bool, connected_bool = tsa.autoconnect()\n\n# if port found and connected, then complete task(s) and disconnect\nif connected_bool == True: \n    print(\"device connected\")\n\n    msg = tsa.get_device_id() \n    print(msg)\n    \n\n    tsa.disconnect()\nelse:\n    print(\"ERROR: could not connect to port\")\n\n\n```\n\n\n\n#### Manually Finding a Port on Windows\n1)  Open _Device Manager_, scroll down to _Ports (COM \u0026 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.\n\n2) This uses the pyserial library requirement already installed for this library. It probably also works on Linux systems, but has not been tested yet.\n\n```python\n\nimport serial.tools.list_ports\n\nports = serial.tools.list_ports.comports()\n\nfor port, desc, hwid in ports:\n    print(f\"Port: {port}, Description: {desc}, Hardware ID: {hwid}\")\n\n```\n\nExample output for this method (on Windows) is as follows:\n\n```python\n\nPort: COM4, Description: Standard Serial over Bluetooth link (COM4), Hardware ID: BTHENUM\\{00001101-0000-1000-8000-00805F9B34FB}_LOCALMFG\u00260000\\7\u0026D0D1EE\u00260\u0026000000000000_00000000\nPort: COM3, Description: Standard Serial over Bluetooth link (COM3), Hardware ID: BTHENUM\\{00001101-0000-1000-8000-00805F9B34FB}_LOCALMFG\u00260002\\7\u0026D0D1EE\u00260\u0026B8B3DC31CBA8_C00000000\nPort: COM10, Description: USB Serial Device (COM10), Hardware ID: USB VID:PID=0483:5740 SER=400 LOCATION=1-3\n\n```\n\n\"COM10\" is the port location of the tinySA Ultra that is used in the examples in this README.\n\n\n#### Manually Finding a Port on Linux\n\n```python\n\nimport serial.tools.list_ports\n\nports = serial.tools.list_ports.comports()\n\nfor port, desc, hwid in ports:\n    print(f\"Port: {port}, Description: {desc}, Hardware ID: {hwid}\")\n\n```\n\n```python\n\nPort: /dev/ttyS0, Description: n/a, Hardware ID: n/a\nPort: /dev/ttyS3, Description: n/a, Hardware ID: n/a\nPort: /dev/ttyS2, Description: n/a, Hardware ID: n/a\nPort: /dev/ttyS1, Description: n/a, Hardware ID: n/a\nPort: /dev/ttyACM0, Description: tinySA4, Hardware ID: USB VID:PID=0483:5740 SER=400 LOCATION=3-3:1.0\n\n```\n\nThis method identified the `/dev/ttyACM0`. Now, when attempting to use the autoconnect feature, the following error was initially returned:\n\n```python\n[Errno 13] could not open port /dev/ttyACM0: [Errno 13] Permission denied: '/dev/ttyACM0'\n\n```\n\nThis 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.  \n\n\n\n\n### Serial Message Return Format\n\nThis library returns strings as cleaned byte arrays. The command and first `\\r\\n` pair are removed from the front, and the `ch\u003e` is removed from the end of the tinySA serial return.\n\nThe original message format:\n\n```python\nbytearray(b'deviceid\\r\\ndeviceid 0\\r\\nch\u003e')\n```\n\nCleaned version:\n\n```python\nbytearray(b'deviceid 0\\r')\n```\n\n### Connecting and Disconnecting the Device\n This example shows the process for initializing, opening the serial port, getting device info, and disconnecting.\n\n```python\n\n# import tinySA_python (tsapython) package\nfrom tsapython import tinySA\n\n# create a new tinySA object    \ntsa = tinySA()\n\n# set the return message preferences \ntsa.set_verbose(True) #detailed messages\ntsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown\n\n\n# attempt to connect to previously discovered serial port\nsuccess = tsa.autoconnect()\n\n# if port open, then get device information and disconnect\nif success == False:\n    print(\"ERROR: could not connect to port\")\nelse:\n    msg = tsa.info()\n    print(msg)\n    tsa.disconnect()\n\n```\n\nExample output for this method is as follows:\n\n```python\n\nbytearray(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 \u0026 DSP\\r')\n\n```\n\n\n### Toggle Error Messages\n\nThe following can be used to turn on or off returned error messages.\n\n1) the 'verbose' option. When enabled, detailed messages are printed out. \n\n```python\n# detailed messages are ON\ntsa.set_verbose(True) \n\n# detailed messages are OFF\ntsa.set_verbose(False) \n```\n\n1) the 'errorByte' option. When enabled, if there is an error with the command or configuration, `b'ERROR'` is returned instead of the default `b''`. \n\n```python\n# when an error occurs, b'ERROR' is returned\ntsa.set_error_byte_return(True) \n\n# when an error occurs, the default b'' might be returned\ntsa. set_error_byte_return(False) \n```\n\n### Device and Library Help\n\nThere are three options for help() with this library.\n\n```python\n# the default help function\n# 1 = help for this library, other values call the tinySA device help function \ntsa.help(1)\n\n# calling the library help function directly\ntsa.library_help()\n\n# calling the tinySA device help directly\ntsa.tinySA_help()\n\n```\n\nAll three return a bytearray in the format `bytearray(b'commands:......')`\n\n### Setting tinySA Parameters\n\nMost device parameters are set through their corresponding library functions, documented in the [List of tinySA Commands and their Library Commands](#list-of-tinysa-commands-and-their-library-commands) section below. Each setter follows the same pattern: call the function with the desired value, and the library formats and sends the command. For example:\n\n```python\ntsa.rbw(100)              # set resolution bandwidth to 100 kHz\ntsa.set_sweep_center(96500000)   # set sweep center to 96.5 MHz\ntsa.set_attenuation(10)   # set input attenuation\n```\n\nAcceptable 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.\n\n### Getting Data from Active Screen\n\nSee other sections for the following examples:\n* [Saving Screen Images](#saving-screen-images)\n* [Plotting Data with Matplotlib](#plotting-data-with-matplotlib)\n\nThis example shows several types of common data requests:\n\n```python\n\n# import tinySA_python (tsapython) package\nfrom tsapython import tinySA\n\n# create a new tinySA object    \ntsa = tinySA()\n\n# set the return message preferences \ntsa.set_verbose(True) #detailed messages\ntsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown\n\n\n# attempt to connect to previously discovered serial port\nsuccess = tsa.autoconnect()\n\n# if port open, then complete task(s) and disconnect\nif success == False:\n    print(\"ERROR: could not connect to port\")\nelse:\n   \n    # get current trace data on screen\n    msg = tsa.data(val=2) \n    print(msg)\n\n    # set current device ID\n    msg = tsa.device_id(3) \n    print(msg)\n\n    # get current device ID\n    msg = tsa.device_id() \n    print(msg)\n    \n    # get device information\n    msg = tsa.info() \n    print(msg)\n\n    # pause sweeping\n    msg = tsa.pause() \n    print(msg)\n\n    # resume sweeping\n    msg = tsa.resume() \n    print(msg)\n\n    # get current battery voltage (mV)\n    msg = tsa.vbat() \n    print(msg)\n\n    tsa.disconnect()\n\n\n```\n\n### Saving Screen Images\n \n 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.\n\n This example truncates the last hex value, so a single padding `x00` value has been added. This will eventually be investigated, but it's not hurting the output right now.\n\n```python\n\n# import tinySA_python (tsapython) package\nfrom tsapython import tinySA\n\n\n# imports FOR THE EXAMPLE\nimport numpy as np\nfrom PIL import Image\nimport struct\n\ndef convert_data_to_image(data_bytes, width, height):\n    # this is not a particularly pretty example, and the data_bytes is sometimes a byte short \n\n    # calculate the expected data size\n    expected_size = width * height * 2  # 16 bits per pixel (RGB565), 2 bytes per pixel\n\n    # error checking - brute force, but fine while developing\n    if len(data_bytes) \u003c expected_size:\n        print(f\"Data size is too small. Expected {expected_size} bytes, got {len(data_bytes)} bytes.\")\n        \n        # if the data size is off by 1 byte, add a padding byte\n        if len(data_bytes) == expected_size - 1:\n            print(\"Data size is 1 byte smaller than expected. Adding 1 byte of padding.\")\n             # add a padding byte (0x00) to make the size match\n            data_bytes.append(0) \n        else:\n            return\n\n    elif len(data_bytes) \u003e expected_size:\n        # truncate the data to the expected size (in case it's larger than needed)\n        data_bytes = data_bytes[:expected_size]\n        print(\"Data is larger than the expected size. truncating. check data.\")\n\n    # unpack the byte array to get pixel values (RGB565 format)\n    num_pixels = width * height\n    # unpacking as unsigned shorts (2 bytes each)\n    x = struct.unpack(f\"\u003e{num_pixels}H\", data_bytes)  \n\n    # convert the RGB565 to RGBA\n    arr = np.array(x, dtype=np.uint32)\n    arr = 0xFF000000 + ((arr \u0026 0xF800) \u003e\u003e 8) + ((arr \u0026 0x07E0) \u003c\u003c 5) + ((arr \u0026 0x001F) \u003c\u003c 19)\n\n    # reshape array to match the image dimensions. (height, width) format\n    arr = arr.reshape((height, width)) \n\n    # create the image\n    img = Image.frombuffer('RGBA', (width, height), arr.tobytes(), 'raw', 'RGBA', 0, 1)\n\n    # save the image\n    img.save(\"capture_example.png\")\n\n    # show the image\n    img.show()\n\n# create a new tinySA object    \ntsa = tinySA()\n\n# set the return message preferences \ntsa.set_verbose(True) #detailed messages\ntsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown\n\n\n# attempt to connect to previously discovered serial port\nsuccess = tsa.autoconnect()\n\n# if port closed, then return error message\nif success == False:\n    print(\"ERROR: could not connect to port\")\nelse: # port open, complete task(s) and disconnect\n\n\n    # get the trace data\n    data_bytes = tsa.capture() \n    print(data_bytes)\n    tsa.disconnect()\n\n    # processing after disconnect (just for this example)\n    # test with 480x320 resolution for tinySA Ultra\n    convert_data_to_image(data_bytes, 480, 320)\n\n\n```\n\n\u003cp align=\"center\"\u003e\n        \u003cimg src=\"media/example_screen_capture.png\" alt=\"Capture of On-screen Trace Data\" height=\"350\"\u003e\n\u003c/p\u003e\n   \u003cp align=\"center\"\u003eCapture On-Screen Trace Data of a Frequency Sweep from 100 kHz to 800 kHz\u003c/p\u003e\n\n### Plotting Data with Matplotlib\n\n#### **Example 1: Plot using On-Screen Trace Data and Frequencies**\nThis example plots the last/current sweep of data from the tinySA device. \n`data()` gets the trace data. `frequencies()` gets the frequency values used. \n`byteArrayToNumArray(byteArr)` takes in the returned trace data and frequency \nbytearrays and converts them to arrays that are then plotted using `matplotlib`\n\nThis 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`\n\n```python\n\n# import tinySA_python (tsapython) package\nfrom tsapython import tinySA\n\n\n# import matplotlib FOR THE EXAMPLE\nimport matplotlib.pyplot as plt\n\n# functions used in this example\ndef byteArrayToNumArray(byteArr, enc=\"utf-8\"):\n    # decode the bytearray to a string\n    decodedStr = byteArr.decode(enc)\n    # split the string by newline characters\n    stringVals = decodedStr.splitlines()\n    # convert each value to a float\n    floatVals = [float(val) for val in stringVals]\n    return floatVals\n\n# create a new tinySA object    \ntsa = tinySA()\n\n# set the return message preferences \ntsa.set_verbose(True) #detailed messages\ntsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown\n\n\n# attempt to connect to previously discovered serial port\nsuccess = tsa.autoconnect()\n\n# if port closed, then return error message\nif success == False:\n    print(\"ERROR: could not connect to port\")\nelse: # port open, complete task(s) and disconnect\n\n    # get the trace data\n    data_bytes = tsa.data() \n    print(data_bytes)\n    # get the frequencies used by the last sweep\n    freq_bytes = tsa.frequencies() \n    tsa.disconnect()\n\n    # processing after disconnect (just for this example)\n    dataVals = byteArrayToNumArray(data_bytes)\n    print(len(dataVals))  # length of 450 data points\n\n    freqVals = byteArrayToNumArray(freq_bytes)\n    print(len(freqVals))  # length of 450 data points\n\n    # create the plot\n    plt.plot(freqVals, dataVals)\n\n    # add labels and title\n    plt.xlabel('Frequency (Hz)')\n    plt.ylabel('Measured Data (dBm)')\n    plt.title('tinySA Trace Plot')\n\n    # show the plot\n    plt.show()\n\n```\n\n\u003cp align=\"center\"\u003e\n        \u003cimg src=\"media/example1_plot_SA_data.png\" alt=\"Plot of On-screen Trace Data\" height=\"350\"\u003e\n\u003c/p\u003e\n   \u003cp align=\"center\"\u003ePlotted On-Screen Trace Data of a Frequency Sweep from 100 kHz to 800 MHz\u003c/p\u003e\n\n\n#### **Example 2: Plot using Scan Data and Frequencies**\n\nThis 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`. \n\n\n \n```python\n\n# import tinySA_python (tsapython) package\nfrom tsapython import tinySA\n\n\n# imports FOR THE EXAMPLE\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ndef convert_data_to_arrays(start, stop, pts, data):\n    # using the start and stop frequencies, and the number of points, \n\n    freq_arr = np.linspace(start, stop, pts) # note that the decimals might go out to many places. \n                                                # you can truncate this because it’s only used \n                                                # for plotting in this example\n\n    # As of the Jan. 2024 build in some data returned with SWEEP or SCAN calls there is error data.  \n    # https://groups.io/g/tinysa/topic/tinasa_ultra_sweep_command/104194367  \n    # this shows up as \"-:.000000e+01\".\n    # TEMP fix - replace the colon character with a -10. This puts the 'filled in' points around the noise floor.\n    # more advanced filtering should be applied for actual analysis.\n    data1 =bytearray(data.replace(b\"-:.0\", b\"-10.0\").replace(b\":.0\", b\"10.0\"))\n    \n    # get both values in each row returned (for reference)\n    #data_arr = [list(map(float, line.split())) for line in data.decode('utf-8').split('\\n') if line.strip()] \n   \n    # get first value in each returned row\n    data_arr = [float(line.split()[0]) for line in data1.decode('utf-8').split('\\n') if line.strip()]\n\n    return freq_arr, data_arr\n\n\n# create a new tinySA object    \ntsa = tinySA()\n\n# set the return message preferences \ntsa.set_verbose(True) #detailed messages\ntsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown\n\n\n# attempt to autoconnect\nfound_bool, connected_bool = tsa.autoconnect()\n\n# if port closed, then return error message\nif connected_bool == False:\n    print(\"ERROR: could not connect to port\")\nelse: # if port found and connected, then complete task(s) and disconnect\n\n    # set scan values\n    start = int(1e9)  # 1 GHz\n    stop = int(3e9)   # 3 GHz\n    pts = 450         # sample points\n    outmask = 2       # get measured data (y axis)\n\n    # scan\n    data_bytes = tsa.scan(start, stop, pts, outmask)\n\n    print(data_bytes)\n\n    tsa.resume() #resume so screen isn't still frozen\n\n    tsa.disconnect()\n\n    # processing after disconnect (just for this example)\n\n    # convert data to 2 arrays\n    freq_arr, data_arr = convert_data_to_arrays(start, stop, pts, data_bytes)\n\n    # plot\n    plt.plot(freq_arr, data_arr)\n    plt.xlabel(\"Frequency (Hz)\")\n    plt.ylabel(\"Measured Data (dBm)\")\n    plt.title(\"tinySA Scan Plot\")\n    plt.show()\n    \n```\n\u003cp align=\"center\"\u003e\n        \u003cimg src=\"media/example2_plot_SA_data.png\" alt=\"Plot of Scan Data\" height=\"350\"\u003e\n\u003c/p\u003e\n   \u003cp align=\"center\"\u003ePlotted Scan Data of a Frequency Sweep from 1 GHz  to 3 GHz\u003c/p\u003e\n\n\n#### **Example 3: Plot using SCAN and SCANRAW Data and Calculated Frequencies**\n\nThis 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.\n\nExtra processing needs to be done to get `dBm power` from `scanraw()`.\n\n\nNOTE 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:\n1. Requesting data too quickly after the last read \n    * Expected, as the tinySA needs to resume and re-measure.\n2. Requesting data when the screen is frozen \n    * Mildly expected, user error can trigger this too. Turns out in some situations, the frozen screen is not the same as a `pause`, and there is no data to flush from the buffer because no more data has been taken. This is either a safe error state, a feature of how SCANRAW works, or potentially a bug with the device/firmware/this library. Using the `resume()` function after this will restart measurements.\n3. {UNKNOWN}. There are several conditions that can cause issues, but it's unclear what 'symptoms' go to which problems\n    * On the first few reads after the tinySA has been turned on and operational for at least 1 minute.\n    * After sitting unused for more than a few minutes the returned buffer is \u003c 50% the expected  size or more than 5x the expected size. This is AFTER the flush command. \n\n \n```python\n# import tinySA_python (tsapython) package\nfrom tsapython import tinySA\n\n\n# imports FOR THE EXAMPLE\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport struct\n\ndef convert_data_to_arrays(start, stop, pts, data):\n    # FOR PLOTTING\n    # using the start and stop frequencies, and the number of points, \n\n    freq_arr = np.linspace(start, stop, pts) # note that the decimals might go out to many places. \n                                                # you can truncate this because its only used \n                                                # for plotting in this example\n\n    # As of the Jan. 2024 build in some data returned with SWEEP or SCAN calls there is error data.  \n    # https://groups.io/g/tinysa/topic/tinasa_ultra_sweep_command/104194367  \n    # this shows up as \"-:.000000e+01\".\n    # TEMP fix - replace the colon character with a -10. This puts the 'filled in' points around the noise floor.\n    # more advanced filtering should be applied for actual analysis.\n    data1 =bytearray(data.replace(b\"-:.0\", b\"-10.0\").replace(b\":.0\", b\"10.0\"))\n    \n    # get both values in each row returned (for reference)\n    #data_arr = [list(map(float, line.split())) for line in data.decode('utf-8').split('\\n') if line.strip()] \n   \n    # get first value in each returned row\n    data_arr = [float(line.split()[0]) for line in data1.decode('utf-8').split('\\n') if line.strip()]\n\n    # NOTE: if repeated read errors with utf-8 occur, uncomment the below as an alternative to the\n    # line above. This will show you what value is being returned that caused the problem. It may\n    # indicate a different problem with the serial connection permissions\n\n    # data_arr = []\n    # for i, line in enumerate(data1.decode('utf-8').split('\\n')):\n    #     print(f\"Line {i}: '{line}'\")  # Show the raw line\n    #     line = line.strip()\n    #     if line:\n    #         try:\n    #             value = float(line.split()[0])\n    #             data_arr.append(value)\n    #             # print(f\"  Parsed float: {value}\")\n    #         except ValueError as e:\n    #             print(f\"  Could not convert line to float: {line} — Error: {e}\")\n\n    return freq_arr, data_arr\n\n\n# create a new tinySA object    \ntsa = tinySA()\n\n# set the return message preferences \ntsa.set_verbose(True) #detailed messages\ntsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown\n\n\n# attempt to autoconnect\nfound_bool, connected_bool = tsa.autoconnect()\n\n# if port closed, then return error message\nif connected_bool == False:\n    print(\"ERROR: could not connect to port\")\nelse: # if port found and connected, then complete task(s) and disconnect\n\n    # set scan values\n    start = int(150e6)   # 150 MHz\n    stop = int(500e6)    # 500 MHz\n    pts = 450            # for tinySA Ultra\n    outmask = 2     # get measured data (y axis)\n    # scan raw call - reads until end of stream\n    # this CAN be run in a loop. the limiting factor is time to plot. \n\n    # SCAN\n    scan_data_bytes = tsa.scan(start, stop, pts, outmask)\n\n    # SCAN RAW\n    scanraw_data_bytes = tsa.scan_raw(start, stop, pts, outmask)\n\n\n    # for subsequent reads, the tinySA does freeze while performing SCANRAW\n    # if there's an error, the screen will stay frozen (for reading).\n    # So start it again so new data can be taken\n    tsa.resume()\n\n    # disconnect because we don't need the tinySA to process data\n    tsa.disconnect()\n\n    # process the SCAN data (this is already in dBm)\n    # convert data to 2 arrays for X and Y\n    freq_arr, data_arr = convert_data_to_arrays(start, stop, pts, scan_data_bytes)\n\n    # PROCESS SCANRAW into an array \u0026 reuse the FREQ_ARR value\n    # remove the intro curly brace ({) \n    bin_scanraw = scanraw_data_bytes[1:] #skip the first char because it's the remaining curly brace\n    # use struct.unpack() because of the repeating pattern\n        # \u003c: indicates little-endian byte order, meaning the least significant byte is stored first\n        # 'xH'*pts: a repetition of the format 'xH' once per point.\n        # 'x': represents a pad byte, which is ignored\n        # 'H': represents an unsigned short integer (2 bytes)\n    \n    expected_len = 3 * pts\n    actual_len = len(bin_scanraw)\n    print(f\"Expected length: {expected_len}, Actual length: {actual_len}\")\n    \n    if actual_len == expected_len:\n        # SCANRAW has returned the expected amount of data for the read. \n        # sometimes this function (and not SCAN) does not read the buffer properly\n        # a fix is in progress for LINUX systems. it works fine for Windows\n        processed_scanraw = struct.unpack( '\u003c' + 'xH'*pts, bin_scanraw ) # ignore trailing '}ch\u003e '\n        processed_scanraw = np.array(processed_scanraw, dtype=np.uint16 ).reshape(-1, 1) #unit8 has overflow error\n\n        # CONVERT to dBm Power\n        # take the processed binary data and convert it to dBm. \n        # The equation is from tinySA.org \u0026 official documentation\n        SCALE_FACTOR = 174  # tinySA Basic: 128, tinySA Ultra and newer is 174\n        dBm_data = processed_scanraw / 32 - SCALE_FACTOR\n        print(dBm_data)\n\n        # plot\n        plt.plot(freq_arr, data_arr, label= 'SCAN data')\n        plt.plot(freq_arr, dBm_data, label= 'SCANRAW data')\n        plt.xlabel(\"frequency (hz)\")\n        plt.ylabel(\"measured data (dBm)\")\n        plt.title(\"tinySA SCAN and SCANRAW data\")\n        plt.legend()\n        plt.show()\n    else:\n        print(\"SCANRAW did not return the expected amount of data for the read\")\n\n```\n\u003cp align=\"center\"\u003e\n        \u003cimg src=\"media/example3_plot_SA_data.png\" alt=\"Plot of SCAN and SCANRAW  Data\" height=\"350\"\u003e\n\u003c/p\u003e\n   \u003cp align=\"center\"\u003ePlotted SCAN and SCANRAW Data of a Frequency Sweep from 150 MHz to 500 MHz\u003c/p\u003e\n\n#### **Example 4: Plot using SCAN And Filters for Artifact Comparison**\n\nThis 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.\n\nTo 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.\n \n```python\n# import tinySA_python (tsapython) package\nfrom tsapython import tinySA\n\n# imports FOR THE EXAMPLE\ntry:\n    import numpy as np\n    import matplotlib.pyplot as plt\nexcept ImportError as exc:\n    raise SystemExit(\n        \"This example requires the plotting extra (numpy and matplotlib). \"\n        'Install it with:  pip install \"tsapython[plotting]\"'\n    ) from exc\n\n\ndef parse_scan_levels(data_bytes, fix_artifact=True):\n    # Parse scan(outmask=2) output to an array of dBm levels.\n    # If fix_artifact is True, apply the standard ':' -\u003e 10 substitution so the\n    # data is parseable. (With it False, the malformed rows would raise on\n    # float() -- shown here only to explain why the fix exists.)\n    raw = bytes(data_bytes)\n    if fix_artifact:\n        raw = raw.replace(b\"-:.0\", b\"-10.0\").replace(b\":.0\", b\"10.0\")\n    levels = []\n    for line in bytearray(raw).decode(\"utf-8\").split(\"\\n\"):\n        line = line.strip()\n        if line:\n            levels.append(float(line.split()[0]))\n    return np.array(levels)\n\n\ndef median_filter(x, k=5):\n    # Pure-numpy median filter. k is forced odd. Edge-padded so length is kept.\n    if k % 2 == 0:\n        k += 1\n    pad = k // 2\n    xp = np.pad(x, pad, mode=\"edge\")\n    return np.array([np.median(xp[i:i + k]) for i in range(len(x))])\n\n\ndef moving_average(x, k=5):\n    # Pure-numpy moving average. k is forced odd. Edge-padded so length is kept.\n    if k % 2 == 0:\n        k += 1\n    pad = k // 2\n    xp = np.pad(x, pad, mode=\"edge\")\n    return np.convolve(xp, np.ones(k) / k, mode=\"valid\")\n\n\ndef main():\n    tsa = tinySA()\n    tsa.set_verbose(False)\n    tsa.set_error_byte_return(True)\n\n    found, connected = tsa.autoconnect()\n    if not connected:\n        print(\"ERROR: could not connect to port\")\n        return\n\n    start = int(1e9)     # 1 GHz\n    stop = int(3e9)      # 3 GHz\n    pts = 450\n\n    data_bytes = tsa.scan(start, stop, pts, 2)   # outmask 2 = measured data\n    tsa.resume()\n    tsa.disconnect()\n\n    raw_levels = parse_scan_levels(data_bytes, fix_artifact=True)\n    freqs = np.linspace(start, stop, len(raw_levels))\n\n    # apply the two filters\n    med = median_filter(raw_levels, k=5)\n    avg = moving_average(raw_levels, k=5)\n\n    # report how many artifact-substituted points there were (points sitting at\n    # the -10 substitution value are the likely artifacts)\n    n_artifacts = int(np.sum(np.isclose(raw_levels, -10.0)))\n    print(f\"Scanned {len(raw_levels)} points; \"\n          f\"{n_artifacts} look like ':'-artifact substitutions (~-10 dBm).\")\n\n    # plot all three on one figure\n    plt.figure(figsize=(12, 7))\n    plt.plot(freqs / 1e9, raw_levels, lw=0.8, alpha=0.6,\n             label=\"raw (artifact-substituted, spikes visible)\")\n    plt.plot(freqs / 1e9, med, lw=1.3,\n             label=\"median filter k=5 (removes spikes)\")\n    plt.plot(freqs / 1e9, avg, lw=1.3, alpha=0.8,\n             label=\"moving average k=5 (smears spikes -- cautionary)\")\n    plt.xlabel(\"Frequency (GHz)\")\n    plt.ylabel(\"Power (dBm)\")\n    plt.title(\"tinySA scan: ':' artifact and filtering comparison\")\n    plt.legend()\n    plt.grid(True, alpha=0.3)\n    plt.show()\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\u003cp align=\"center\"\u003e\n        \u003cimg src=\"media/example4_filter_comparison.png\" alt=\"Plot of SCAN artifact and filtering comparison\" height=\"350\"\u003e\n\u003c/p\u003e\n   \u003cp align=\"center\"\u003ePlotted SCAN Artifact and Filtering Comparison\u003c/p\u003e\n\n#### **Example 5: Plot a Waterfall using SCAN and Calculated Frequencies**\n\nThe 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.\n\n\n```python\n# import tinySA_python (tsapython) package\nfrom tsapython import tinySA\n\n\n# imports FOR THE EXAMPLE\nimport csv\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport time\nfrom datetime import datetime\n\ndef convert_data_to_arrays(start, stop, pts, data):\n    # using the start and stop frequencies, and the number of points,\n    freq_arr = np.linspace(start, stop, pts) # note that the decimals might go out to many places.\n                                                # you can truncate this because its only used\n                                                # for plotting in this example\n    # As of the Jan. 2024 build in some data returned with SWEEP or SCAN calls there is error data.  \n    # https://groups.io/g/tinysa/topic/tinasa_ultra_sweep_command/104194367  \n    # this shows up as \"-:.000000e+01\".\n    # TEMP fix - replace the colon character with a -10. This puts the 'filled in' points around the noise floor.\n    # more advanced filtering should be applied for actual analysis.\n   \n    data1 = bytearray(data.replace(b\"-:.0\", b\"-10.0\").replace(b\":.0\", b\"10.0\"))\n   \n    # get both values in each row returned (for reference)\n    #data_arr = [list(map(float, line.split())) for line in data.decode('utf-8').split('\\n') if line.strip()]\n   \n    # get first value in each returned row\n    data_arr = [float(line.split()[0]) for line in data1.decode('utf-8').split('\\n') if line.strip()]\n    return freq_arr, data_arr\n\ndef collect_waterfall_data(tsa, start, stop, pts, outmask, num_scans, scan_interval):\n\n    waterfall_data = []  # 2D array of scan data (time x frequency)\n    timestamps = []\n    freq_arr = None\n    \n    print(f\"Collecting {num_scans} scans with {scan_interval}s intervals...\")\n    \n    for i in range(num_scans):\n        print(f\"Scan {i+1}/{num_scans}\")\n        \n        # Perform scan\n        data_bytes = tsa.scan(start, stop, pts, outmask)\n        \n        # Convert to arrays\n        if freq_arr is None:\n            freq_arr, data_arr = convert_data_to_arrays(start, stop, pts, data_bytes)\n        else:\n            _, data_arr = convert_data_to_arrays(start, stop, pts, data_bytes)\n        \n        # Store data and timestamp\n        waterfall_data.append(data_arr)\n        timestamps.append(datetime.now())\n        \n        # Wait before next scan (except for last scan)\n        if i \u003c num_scans - 1:\n            time.sleep(scan_interval)\n    \n    return freq_arr, np.array(waterfall_data), timestamps\n\ndef plot_waterfall(freq_arr, waterfall_data, timestamps, start, stop):\n    # Create figure with subplots\n    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))\n    \n    # Waterfall plot (main plot)\n    # Create time array for y-axis (scan number or elapsed time)\n    time_arr = np.arange(len(timestamps))\n    \n    # Create meshgrid for pcolormesh\n    freq_mesh, time_mesh = np.meshgrid(freq_arr, time_arr)\n    \n    # Plot waterfall\n    im = ax1.pcolormesh(freq_mesh/1e9, time_mesh, waterfall_data, \n                       shading='nearest', cmap='viridis')\n    \n    ax1.set_xlabel('Frequency (GHz)')\n    ax1.set_ylabel('Scan Number')\n    ax1.set_title(f'Waterfall Plot: {start/1e9:.1f} - {stop/1e9:.1f} GHz')\n    \n    # Add colorbar\n    cbar = plt.colorbar(im, ax=ax1)\n    cbar.set_label('Signal Strength (dBm)')\n    \n    # Latest scan plot (bottom subplot)\n    ax2.plot(freq_arr/1e9, waterfall_data[-1])\n    ax2.set_xlabel('Frequency (GHz)')\n    ax2.set_ylabel('Signal Strength (dBm)')\n    ax2.set_title('Latest Scan')\n    ax2.grid(True, alpha=0.3)\n    \n    plt.tight_layout()\n    return fig\n\n# create a new tinySA object    \ntsa = tinySA()\n# set the return message preferences\ntsa.set_verbose(True) #detailed messages\ntsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown\n\n# attempt to autoconnect\nfound_bool, connected_bool = tsa.autoconnect()\n\n# if port closed, then return error message\nif connected_bool == False:\n    print(\"ERROR: could not connect to port\")\nelse: # if port found and connected, then complete task(s) and disconnect\n    try:\n        # set scan values\n        start = int(1e9)  # 1 GHz\n        stop = int(3e9)   # 3 GHz\n        pts = 450         # sample points\n        outmask = 2       # get measured data (y axis)\n        \n        # waterfall parameters\n        num_scans = 50        # number of scans to collect\n        scan_interval = 0.5   # seconds between scans\n        \n        # collect waterfall data\n        freq_arr, waterfall_data, timestamps = collect_waterfall_data(\n            tsa, start, stop, pts, outmask, num_scans, scan_interval)\n        \n        print(\"Data collection complete!\")\n        \n        # resume and disconnect\n        tsa.resume() #resume so screen isn't still frozen\n        tsa.disconnect()\n        \n        # processing after disconnect\n        print(\"Creating waterfall plot...\")\n        \n        # create waterfall plot\n        fig = plot_waterfall(freq_arr, waterfall_data, timestamps, start, stop)\n        \n        # Save data out to .csv\n        filename = \"waterfall_1_sample.csv\"\n            \n        # Create CSV with frequency headers and time/scan data\n        with open(filename, 'w', newline='') as csvfile:\n            writer = csv.writer(csvfile)\n            \n            # Write header row with frequencies (in Hz)\n            header = ['Scan_Number', 'Timestamp'] + [f'{freq:.0f}' for freq in freq_arr]\n            writer.writerow(header)\n            \n            # Write data rows\n            for i, (scan_data, timestamp) in enumerate(zip(waterfall_data, timestamps)):\n                row = [i+1, timestamp.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]] + scan_data.tolist()\n                writer.writerow(row)\n            \n        print(f\"Data saved to {filename}\")\n        print(f\"CSV contains {len(waterfall_data)} scans with {len(freq_arr)} frequency points each\")\n        \n        # show plot\n        plt.show()\n\n    except KeyboardInterrupt:\n        print(\"\\nScan interrupted by user\")\n        tsa.resume()\n        tsa.disconnect()\n    except Exception as e:\n        print(f\"Error occurred: {e}\")\n        tsa.resume()\n        tsa.disconnect()\n```\n\u003cp align=\"center\"\u003e\n        \u003cimg src=\"media/example5_waterfall_1.png\" alt=\"Static Waterfall Plot for SCAN Data Over 50 Readings\" height=\"350\"\u003e\n\u003c/p\u003e\n   \u003cp align=\"center\"\u003eStatic Waterfall Plot for SCAN Data Over 50 Readings\u003c/p\u003e\n\n\n\n\n\n\nThe second part of the example is a realtime waterfall plot with peak tracking and a sample of the last reading.\n\n```python\n# import tinySA_python (tsapython) package\nfrom tsapython import tinySA\n\n\n# imports FOR THE EXAMPLE\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport matplotlib.animation as animation\nfrom collections import deque\nimport time\nfrom datetime import datetime\nimport threading\nimport queue\n\ndef convert_data_to_arrays(start, stop, pts, data):\n    #Convert the raw tinySA data to frequency and power arrays.\n    # using the start and stop frequencies, and the number of points,\n    freq_arr = np.linspace(start, stop, pts) # note that the decimals might go out to many places.\n                                                # you can truncate this because its only used\n                                                # for plotting in this example\n    # As of the Jan. 2024 build in some data returned with SWEEP or SCAN calls there is error data.  \n    # https://groups.io/g/tinysa/topic/tinasa_ultra_sweep_command/104194367  \n    # this shows up as \"-:.000000e+01\".\n    # TEMP fix - replace the colon character with a -10. This puts the 'filled in' points around the noise floor.\n    # more advanced filtering should be applied for actual analysis.\n   \n    data1 = bytearray(data.replace(b\"-:.0\", b\"-10.0\").replace(b\":.0\", b\"10.0\"))\n    \n    # Get first value in each returned row (power in dBm)\n    try:\n        data_arr = [float(line.split()[0]) for line in data1.decode('utf-8').split('\\n') if line.strip()]\n    except (ValueError, IndexError):\n        # If parsing fails, return zeros\n        data_arr = [0.0] * pts\n    \n    # Ensure data array matches frequency array length\n    if len(data_arr) != pts:\n        # Pad or truncate to match expected points\n        # We do this to visualize what might be going wrong rather than outright throwing an error\n        # -100 is a very low noise floor, especially for a hand held device, so it's not a normal reading\n        if len(data_arr) \u003c pts:\n            data_arr.extend([data_arr[-1] if data_arr else -100.0] * (pts - len(data_arr)))\n        else:\n            data_arr = data_arr[:pts]\n    \n    return freq_arr, np.array(data_arr)\n\nclass LiveSpectrumPlotter:\n    def __init__(self, tsa, start, stop, pts, outmask, max_history=50):\n        self.tsa = tsa\n        self.start = start\n        self.stop = stop\n        self.pts = pts\n        self.outmask = outmask\n        self.max_history = max_history\n        \n        # Data storage\n        self.freq_arr = None\n        self.power_history = deque(maxlen=max_history)\n        self.timestamps = deque(maxlen=max_history)\n        \n        # Threading for data acquisition\n        self.data_queue = queue.Queue()\n        self.running = False\n        self.data_thread = None\n        \n        # Current data for single-trace plots\n        self.current_power = None\n        \n        # Twin axis reference for proper cleanup\n        self.ax3_twin = None\n        \n    def data_acquisition_thread(self):\n        #Background thread for continuous data acquisition\n        while self.running:\n            try:\n                # Get scan data\n                data_bytes = self.tsa.scan(self.start, self.stop, self.pts, self.outmask)\n                \n                # Convert to arrays\n                freq_arr, power_arr = convert_data_to_arrays(\n                    self.start, self.stop, self.pts, data_bytes)\n                \n                # Put data in queue for main thread\n                self.data_queue.put({\n                    'freq': freq_arr,\n                    'power': power_arr,\n                    'timestamp': datetime.now()\n                })\n                \n                time.sleep(0.2)  # Small delay to prevent overwhelming the device\n                \n            except Exception as e:\n                print(f\"Data acquisition error: {e}\")\n                time.sleep(0.5)  # Wait a bit before retrying\n                continue\n    \n    def start_acquisition(self):\n        #Start the data acquisition thread\n        self.running = True\n        self.data_thread = threading.Thread(target=self.data_acquisition_thread)\n        self.data_thread.daemon = True\n        self.data_thread.start()\n    \n    def stop_acquisition(self):\n        #Stop the data acquisition thread\n        self.running = False\n        if self.data_thread:\n            self.data_thread.join()\n    \n    def update_plots(self, frame):\n        #Update the matplotlib plots with new data\n        \n        # Get all available data from queue\n        while not self.data_queue.empty():\n            try:\n                data = self.data_queue.get_nowait()\n                \n                # Store frequency array (first time only)\n                if self.freq_arr is None:\n                    self.freq_arr = data['freq']\n                \n                # Update current data\n                self.current_power = data['power']\n                \n                # Add to history\n                self.power_history.append(data['power'])\n                self.timestamps.append(data['timestamp'])\n                \n            except queue.Empty:\n                break\n        \n        # Clear plots\n        ax1.clear()  # Waterfall\n        ax2.clear()  # Live spectrum \n        ax3.clear()  # Peak tracking\n        \n        # Clear any existing twin axes completely\n        if hasattr(self, 'ax3_twin') and self.ax3_twin is not None:\n            self.ax3_twin.clear()\n            self.ax3_twin.remove()\n            self.ax3_twin = None\n        \n        if self.freq_arr is not None and self.current_power is not None:\n            # Plot 1: Waterfall (left side - larger)\n            if len(self.power_history) \u003e 1:\n                waterfall_data = np.array(list(self.power_history))\n                # Create time array in reverse order so newest (highest index) appears at top\n                time_arr = np.arange(len(waterfall_data))\n                freq_mesh, time_mesh = np.meshgrid(self.freq_arr, time_arr)\n                \n                im = ax1.pcolormesh(freq_mesh/1e9, time_mesh, waterfall_data, \n                                   shading='nearest', cmap='viridis')\n                ax1.set_xlabel('Frequency (GHz)')\n                ax1.set_ylabel('Scan Number (newest at top)')\n                ax1.set_title('Spectrum History (Waterfall)')\n                \n                # Add colorbar to waterfall plot\n                if not hasattr(self, 'colorbar_created'):\n                    self.colorbar = plt.colorbar(im, ax=ax1, shrink=0.8)\n                    self.colorbar.set_label('Power (dBm)')\n                    self.colorbar_created = True\n            \n            # Plot 2: Current Spectrum (top right)\n            ax2.plot(self.freq_arr/1e9, self.current_power, 'b-', linewidth=1.5)\n            ax2.set_xlabel('Frequency (GHz)')\n            ax2.set_ylabel('Power (dBm)')\n            ax2.set_title('Live Spectrum')\n            ax2.grid(True, alpha=0.3)\n            \n            # Set reasonable y-axis limits\n            if len(self.current_power) \u003e 0:\n                y_min = np.min(self.current_power) - 5\n                y_max = np.max(self.current_power) + 5\n                ax2.set_ylim(y_min, y_max)\n            \n            # Plot 3: Peak tracking over time (bottom right)\n            if len(self.power_history) \u003e 1:\n                peak_powers = [np.max(scan) for scan in self.power_history]\n                peak_freqs = [self.freq_arr[np.argmax(scan)]/1e9 for scan in self.power_history]\n                \n                # Plot peak power over time\n                scan_numbers = list(range(len(peak_powers)))\n                \n                # Create fresh twin axis for frequency (store reference for proper cleanup)\n                self.ax3_twin = ax3.twinx()\n                \n                ax3.plot(scan_numbers, peak_powers, 'r-o', markersize=2, \n                        label='Peak Power', linewidth=1.5)\n                self.ax3_twin.plot(scan_numbers, peak_freqs, 'g-s', markersize=2, \n                                   label='Peak Freq', linewidth=1.5)\n                \n                ax3.set_xlabel('Scan Number')\n                ax3.set_ylabel('Peak Power (dBm)', color='r')\n                self.ax3_twin.set_ylabel('Peak Freq (GHz)', color='g')\n                ax3.set_title('Peak Tracking')\n                ax3.grid(True, alpha=0.3)\n                \n                # Color the y-axis labels to match the lines\n                ax3.tick_params(axis='y', labelcolor='r', labelsize=8)\n                self.ax3_twin.tick_params(axis='y', labelcolor='g', labelsize=8)\n                \n                # Force immediate redraw of the twin axis\n                self.ax3_twin.relim()\n                self.ax3_twin.autoscale_view()\n        \n        # Add timestamp and scan info\n        if self.timestamps:\n            scan_count = len(self.timestamps)\n            time_str = self.timestamps[-1].strftime(\"%H:%M:%S\")\n            fig.suptitle(f'Live tinySA Spectrum - {time_str} (Scan #{scan_count})', \n                        fontsize=14)\n\n\nif __name__ == \"__main__\":\n    # create a new tinySA object    \n    tsa = tinySA()\n    # set the return message preferences\n    tsa.set_verbose(True)\n    tsa.set_error_byte_return(True)\n\n    # attempt to autoconnect\n    found_bool, connected_bool = tsa.autoconnect()\n\n    if not connected_bool:\n        print(\"ERROR: could not connect to port\")\n    else:\n        try:\n            print(\"Starting live spectrum measurement...\")\n            print(\"Close the plot window to stop measurement\")\n            \n            # Scan parameters\n            start = int(1e9)  # 1 GHz\n            stop = int(3e9)   # 3 GHz\n            pts = 200         # Reduced points for faster updates\n            outmask = 2       # get measured data\n            \n            # Create plotter\n            plotter = LiveSpectrumPlotter(tsa, start, stop, pts, outmask, max_history=30)\n            \n            # Set up the plot - 2x2 layout with waterfall taking left column\n            fig = plt.figure(figsize=(14, 10))\n            \n            # Create grid layout: waterfall on left (spans 2 rows), two plots on right\n            gs = fig.add_gridspec(2, 2, width_ratios=[2, 1], height_ratios=[1, 1],\n                                hspace=0.3, wspace=0.3)\n            \n            ax1 = fig.add_subplot(gs[:, 0])  # Waterfall - spans both rows, left column\n            ax2 = fig.add_subplot(gs[0, 1])  # Live spectrum - top right\n            ax3 = fig.add_subplot(gs[1, 1])  # Peak tracking - bottom right\n            \n            # Start data acquisition\n            plotter.start_acquisition()\n            \n            # Create animation\n            ani = animation.FuncAnimation(fig, plotter.update_plots, \n                                        interval=300, blit=False)\n            \n            # Show plot (this blocks until window is closed)\n            plt.show()\n            \n            # Cleanup\n            plotter.stop_acquisition()\n            tsa.resume()\n            tsa.disconnect()\n            \n            print(\"Live measurement stopped\")\n            \n        except KeyboardInterrupt:\n            print(\"\\nMeasurement interrupted by user\")\n            tsa.resume()\n            tsa.disconnect()\n        except Exception as e:\n            print(f\"Error occurred: {e}\")\n            tsa.resume()\n            tsa.disconnect()\n\n```\n\n\n\u003cp align=\"center\"\u003e\n        \u003cimg src=\"media/example6_waterfall_realtime.png\" alt=\"Realtime Waterfall Plot for SCAN Data\" height=\"350\"\u003e\n\u003c/p\u003e\n   \u003cp align=\"center\"\u003eRealtime Waterfall Plot for SCAN Data\u003c/p\u003e\n\n\n#### **Example 6: Finding Peaks in a Frequency Range**\n\nLocating the strongest signal in a span is a common task, and this example (`examples/find_peaks.py`) shows two ways to do it.\n\nThe 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.\n\nThe 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.\n\nUse 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.\n\n\u003cp align=\"center\"\u003e\n        \u003cimg src=\"media/example7_find_peaks.png\" alt=\"Top 3 peaks of a signal read\" height=\"350\"\u003e\n\u003c/p\u003e\n   \u003cp align=\"center\"\u003eTop 3 Peaks in a Frequency Range\u003c/p\u003e\n\n\n\n### Saving SCAN Data to CSV\n\n```python\n\n# import tinySA_python (tsapython) package\nfrom tsapython import tinySA\n\n\n# imports FOR THE EXAMPLE\nimport csv\nimport numpy as np\n\ndef convert_data_to_arrays(start, stop, pts, data):\n    # using the start and stop frequencies, and the number of points, \n\n    freq_arr = np.linspace(start, stop, pts) # note that the decimals might go out to many places. \n                                                # you can truncate this because its only used \n                                                # for plotting in this example\n\n    # As of the Jan. 2024 build in some data returned with SWEEP or SCAN calls there is error data.  \n    # https://groups.io/g/tinysa/topic/tinasa_ultra_sweep_command/104194367  \n    # this shows up as \"-:.000000e+01\".\n    # TEMP fix - replace the colon character with a -10. This puts the 'filled in' points around the noise floor.\n    # more advanced filtering should be applied for actual analysis.\n    \n    data1 =bytearray(data.replace(b\"-:.0\", b\"-10.0\").replace(b\":.0\", b\"10.0\"))\n    \n    # get both values in each row returned (for reference)\n    #data_arr = [list(map(float, line.split())) for line in data.decode('utf-8').split('\\n') if line.strip()] \n   \n    # get first value in each returned row\n    data_arr = [float(line.split()[0]) for line in data1.decode('utf-8').split('\\n') if line.strip()]\n\n    return freq_arr, data_arr\n\n\n# create a new tinySA object    \ntsa = tinySA()\n\n# set the return message preferences \ntsa.set_verbose(True) #detailed messages\ntsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown\n\n# attempt to autoconnect\nfound_bool, connected_bool = tsa.autoconnect()\n\n# if port closed, then return error message\nif connected_bool == False:\n    print(\"ERROR: could not connect to port\")\nelse: # if port found and connected, then complete task(s) and disconnect\n    # set scan values\n    start = int(1e9)  # 1 GHz\n    stop = int(3e9)   # 3 GHz\n    pts = 450         # sample points\n    outmask = 2       # get measured data (y axis)\n\n    # scan\n    data_bytes = tsa.scan(start, stop, pts, outmask)\n\n    print(data_bytes)\n\n    tsa.resume() #resume so screen isn't still frozen\n\n    tsa.disconnect()\n\n    # processing after disconnect (just for this example)\n\n    # convert data to 2 arrays\n    freq_arr, data_arr = convert_data_to_arrays(start, stop, pts, data_bytes)\n\n\n    # Save the data to CSV\n    filename = \"scan_sample.csv\"\n        \n    # Write out to csv where column 1 is frequency and col 2 is data\n    with open(filename, 'w', newline='') as csvfile:\n        writer = csv.writer(csvfile)\n        \n        # Write header row\n        writer.writerow(['Frequency_Hz', 'Signal_Strength_dBm'])\n        \n        # Write data rows (frequency, signal strength pairs)\n        for freq, signal in zip(freq_arr, data_arr):\n            writer.writerow([f'{freq:.0f}', signal])\n    \n    print(f\"Data saved to {filename}\")\n    print(f\"CSV contains {len(freq_arr)} frequency/signal pairs\")\n\n\n    print(f\"Data saved to {filename}\")\n\n\n```\n\n\n\n### Accessing the tinySA Directly\n\nIn 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. \n\n```python\n# import tinySA_python (tsapython) package\nfrom tsapython import tinySA\n\n\n# create a new tinySA object    \ntsa = tinySA()\n\n# set the return message preferences \ntsa.set_verbose(True) #detailed messages\ntsa.set_error_byte_return(True) #get explicit b'ERROR' if error thrown\n\n\n# attempt to autoconnect\nfound_bool, connected_bool = tsa.autoconnect()\n\n# if port closed, then return error message\nif connected_bool == False:\n    print(\"ERROR: could not connect to port\")\nelse: # if port found and connected, then complete task(s) and disconnect\n\n    # set scan values\n    start = 150e6   # 150 MHz\n    stop = 200e6    # 200 MHz\n    pts = 450       # for tinySA Ultra\n    outmask = 1     # get measured data (y axis)\n\n    # scan\n    data_bytes = tsa.command(\"scan 150e6 200e6 5 2\")\n\n    print(data_bytes)\n\n    tsa.resume() #resume \n\n    tsa.disconnect()\n\n```\n\n\n\n## List of tinySA Commands and their Library Commands\n\nLibrary functions are organized based on the command passed to the device. For example, any functions with shortcuts for using the `sweep` command will be grouped under `sweep`. This list and the following list in the [Additional Library Commands](#additional-library-commands) section describe the functions in this library. \n\nThis section is sorted by the tinySA (Ultra) commands, and includes:\n* A brief description of what the command does\n* What the original usage looked like\n* The tinySA_python function call, or calls if multiple options exist \n* Example return, or example format of return\n* Any additional notes about the usage\n\nAll of the listed commands are included in this API to some degree, but error checking may be incomplete.\n\nQuick Link Table:\n|  |   |     |   |       |      |      |\n|-------|-------|-------|-------|-------|-------|-------|\n| [abort](#abort)   | [actual_freq](#actual_freq)  | [agc](#agc)      | [attenuate](#attenuate)  | [bulk](#bulk)       | [calc](#calc)        | [caloutput](#caloutput) |\n| [capture](#capture) | [clearconfig](#clearconfig) | [color](#color)   | [correction](#correction) | [dac](#dac)        | [data](#data)        | [deviceid](#deviceid)  |\n| [direct](#direct) | [ext_gain](#ext_gain)    | [fill](#fill)       | [freq](#freq)        | [freq_corr](#freq_corr) | [frequencies](#frequencies) | [help](#help)  |\n| [hop](#hop)            | [if](#if)           | [if1](#if1)          | [info](#info)     | [level](#level)             | [levelchange](#levelchange) | [leveloffset](#leveloffset) |\n| [line](#line) | [load](#load)   | [lna](#lna)          | [lna2](#lna2)     | [marker](#marker)           | [menu](#menu)     | [mode](#mode)           |\n| [modulation](#modulation) | [output](#output)  | [pause](#pause)   | [rbw](#rbw)                 | [recall](#recall) | [refresh](#refresh)     | [release](#release) |\n| [remark](#remark)    | [repeat](#repeat) | [reset](#reset)             | [restart](#restart) | [resume](#resume)      | [save](#save)       | [saveconfig](#saveconfig) |\n| [scan](#scan)     | [scanraw](#scanraw)         | [sd_delete](#sd_delete) | [sd_list](#sd_list)   | [sd_read](#sd_read) | [selftest](#selftest) | [spur](#spur)     |\n| [status](#status)           | [sweep](#sweep)   | [sweeptime](#sweeptime) | [sweep_voltage](#sweep_voltage) | [text](#text)   | [threads](#threads) | [touch](#touch)             |\n| [touchcal](#touchcal) | [touchtest](#touchtest) | [trace](#trace)     | [trigger](#trigger)  | [ultra](#ultra)   | [usart_cfg](#usart_cfg)     | [vbat](#vbat)     |\n| [vbat_offset](#vbat_offset) | [version](#version) | [wait](#wait)        | [zero](#zero)     |                         |                     |                      |\n\n\n### **abort**\n* **Description:** Sets the abortion enabled status (on/off) or aborts the previous command.\n* **Original Usage:** `abort [off|on]`\n* **Direct Library Function Call:** `abort(val=None|\"off\"|\"on\")` \n* **Example Return:** ????\n* **Alias Functions:**\n    * `enable_abort()`\n    * `disable_abort()`\n    * `abort_action()`\n* **CLI Wrapper Usage:**\n* **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. \n\n### **actual_freq**\n* **Description:**  Sets and gets the frequency correction set by CORRECT FREQUENCY menu in the expert menu settings\n* **Original Usage:** `actual_freq [{frequency}]`\n* **Direct Library Function Call:** `actual_freq(val=None|Int)`\n* **Example Return:** 3000000000\n* **Alias Functions:**\n    * `set_actual_freq(val=Int)`\n    * `set_actual_freq()`\n* **CLI Wrapper Usage:**\n* **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. \n\n\n### **agc**\n* **Description:**  Enables/disables the build in Automatic Gain Control\n* **Original Usage:** `agc 0..7|auto`\n* **Direct Library Function Call:** `agc(val=\"auto\"|0..7)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_agc(val=\"auto\"|Int 0..7)`\n* **CLI Wrapper Usage:**\n* **Notes:** \n\n\n### **attenuate**\n* **Description:** Sets the internal attenuation\n* **Original Usage:** `attenuate [auto|0-31]`\n* **Direct Library Function Call:** `attenuate(val=\"auto\"|0..31)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_attenuation(val=\"auto\"|Int 0..31)`\n* **CLI Wrapper Usage:**\n* **Notes:** \n\n\n### **bulk**\n* **Description:** Sent by tinySA when in auto refresh mode\n* **Original Usage:** `bulk`\n* **Direct Library Function Call:** `bulk()`\n* **Example Return:** `format: \"bulk\\r\\n{X}{Y}{Width}{Height} {Pixeldata}\\r\\n\"`\n* **Alias Functions:**\n    *  `get_bulk_data()`\n* **CLI Wrapper Usage:**\n* **Notes:** \n 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           \n            \n\n### **calc**\n* **Description:** Sets or cancels one of the measurement modes\n* **Original Usage:** `calc off|minh|maxh|maxd|aver4|aver16|quasip`\n* **Direct Library Function Call:** `calc(val=\"off\"|\"minh\"|\"maxh\"|\"maxd\"|\"aver4\"|\"aver16\"|\"quasip\")`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_calc_off()`\n    * `set_calc_minh()` \n    * `set_calc_maxh()` \n    * `set_calc_maxd()` \n    * `set_calc_aver4()` \n    * `set_calc_aver16()` \n    * `set_calc_quasip()` \n* **CLI Wrapper Usage:**\n* **Notes:** \n  * the commands are the same as those listed in the MEASURE menu\n  * [tinySA Calc Menu](#https://tinysa.org/wiki/pmwiki.php?n=Main.CALC):\n    * OFF disables any calculation \n    * 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 \n    * 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. \n    * 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 \n    * AVER 4 sets the averaging to new_measurement = old_measurement*3/4+measured_value/4. By default the averaging is linear power averaging \n    * AVER 16 sets the averaging to new_measurement = old_measurement*15/16+measured_value/16. By default the averaging is linear power averaging \n    * QUASSIP sets a quasi peak hold mode\n    * [Official CALC documentation](https://tinysa.org/wiki/pmwiki.php?n=Main.CALC)\n\n\n### **caloutput**\n* **Description:** Disables or sets the caloutput to a specified frequency in MHz. Reference signal.\n* **Original Usage:** `caloutput off|30|15|10|4|3|2|1`\n* **Library Function :**  `cal_output(val=\"off\"|30|15|10|4|3|2|1)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_cal_output_off()`\n    * `set_cal_output_30()`\n    * `set_cal_output_15()`\n    * `set_cal_output_10()`\n    * `set_cal_output_4()`\n    * `set_cal_output_3()`\n    * `set_cal_output_2()`\n    * `set_cal_output_1()`\n* **CLI Wrapper Usage:**\n* **Notes:** \"controls the build in calibration reference generator. The output of the reference generator is connected to CAL connector. The output frequency can be 1,2,4,10,15 and 30MHz and the output level of the fundamental at 30MHz is -35.6dBm\" - From [https://tinysa.org/wiki/pmwiki.php?n=TinySA4.MODE](https://tinysa.org/wiki/pmwiki.php?n=TinySA4.MODE)\n\n\n### **capture**\n* **Description:** Requests a screen dump to be sent in binary format of HEIGHTxWIDTH pixels of each 2 bytes\n* **Original Usage:** `capture`\n* **Direct Library Function Call:** `capture()`\n* **Example Return:** `format:'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\...x00\\x00\\x00'`\n* **Alias Functions:**\n    * `capture_screen()`\n* **CLI Wrapper Usage:**\n* **Notes:** tinySA original: 320x240, tinySA Ultra and newer: 480x320  \n\n\n### **clearconfig**\n* **Description:** Resets the configuration data to factory defaults\n* **Original Usage:** `clear config`\n* **Direct Library Function Call:** `clear_config()`\n* **Example Return:** `b'Config and all calibration data cleared. \\r\\n Do reset manually to take effect. Then do touch calibration and save.\\r'`\n* **Alias Functions:**\n    * `clear_and_reset()`\n* **CLI Wrapper Usage:**\n* **Notes:** Requires password '1234'. Hardcoded. Other functions need to be used with this to complete the process.\n\n\n### **color**\n* **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.\n* **Original Usage:** `color [{id} {rgb24}]`\n* **Direct Library Function Call:** `color(ID=None|0..31, RGB=None(default:'0xF8FCF8')|'0x000000'..'0xFFFFFF')`\n* **Example Return:** \n   * If ID='None' used:  \n`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'`\n   * If ID = '15' used: `0x00FC00`\n``\n* **Alias Functions:**\n    * `get_all_colors()`\n    * `get_marker_color(ID=Int|0..31)`\n    * `set_marker_color(ID=Int|0..31, col=rgb24)`\n* **CLI Wrapper Usage:**\n* **Notes:**  the rgb24 hex value currently must be passed in as a string\n\n\n### **correction**\n* **Description:** Sets or gets the frequency level correction table\n* **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)}]`\n* **Direct Library Function Call:** `correction(tableName, slot, freq, val)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * This function is complex enough that it is recommended to use the `command()` function for options not covered by the `correction(...)` library function\n* **CLI Wrapper Usage:**\n* **Notes:**  See [Correction Wiki](https://tinysa.org/wiki/pmwiki.php?n=Main.Correction). The current content of the table is shown by entering `correction low` without any arguments. The data in the table can be modified by specifying the slot number and the new values. There **MUST** be one entry in the low table for 30MHz and the correction for that frequency **MUST** be zero. \n* **Future Work:** **Confirm table format across devices**. Value currently limited between -10 and 35, but this needs to be more specific.\n\n\n### **dac**\n* **Description:** Sets or gets the dac value\n* **Original Usage:** `dac [0..4095]`\n* **Library Function Call(s):** `dac(val=None/Int|0..4095)`\n* **Example Return:** `b'usage: dac {value(0-4095)}\\r\\ncurrent value: 1922\\r'`\n* **Alias Functions:**\n    * `set_dac(val=Int|0...4095)`\n    * `get_dac()`\n* **CLI Wrapper Usage:**\n* **Notes:** \n\n### **data**\n* **Description:** Gets the trace data\n* **Original Usage:** `data 0..2`\n* **Direct Library Function Call:** `data(val=0|1|2)`\n* **Example Return:** `format bytearray(b'7.593750e+00\\r\\n-8.437500e+01\\r\\n-8.693750e+01\\r\\n...\\r')`\n* **Alias Functions:**\n    * `get_temporary_data()` \n    * `get_stored_trace_data()`\n    * `dump_measurement_data()`\n* **CLI Wrapper Usage:**\n* **Notes:**   \n    * 0 = temp value, 1 = stored trace, 2 = measurement. strength in decibels (dB) \n    * `get_temporary_data` not to be confused with `get_temp`, which returns temperature\n       \n          \n### **device_id**\n* **Description:** Sets or gets a user settable integer number ID that can be use to identify a specific tinySA connected to the PC\n* **Original Usage:** `deviceid [{number}]`\n* **Direct Library Function Call:** `deviceid(ID=None/Int)`\n* **Example Return:** `'deviceid 0\\r'`\n* **Alias Functions:**\n    * `get_device_id()`\n    * `set_device_id(ID=Int|0....)`\n* **CLI Wrapper Usage:**\n* **Notes:** \n\n\n### **direct**\n* **Description:** Output mode for generating a square wave signal between 830MHz and 1130MHz\n* **Original Usage:** `direct {start|stop|on|off} {freq(Hz)}`\n* **Direct Library Function Call:** `direct(val, freq=None|Int)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_direct_on()`\n    * `set_direct_off()`\n    * `set_direct_start(freq=Int)`\n    * `set_direct_stop(freq=Int)`\n* **CLI Wrapper Usage:**\n* **Notes:** \n    * 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.\n    * might be tinySA Ultra and newer only.\n    * Related to NORMAL, DIRECT, ADF, and MIXER\n    * [https://tinysa.org/wiki/pmwiki.php?n=TinySA4.OutputCurveEdit](https://tinysa.org/wiki/pmwiki.php?n=TinySA4.OutputCurveEdit)\n\n\n### **ext_gain**\n* **Description:** Sets the external attenuation/amplification\n* **Original Usage:** `ext_gain -100..100`\n* **Direct Library Function Call:** `ext_gain(val=Int|-100...100)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_ext_gain(val=Int|-100...100)`\n* **CLI Wrapper Usage:**\n* **Notes:** * Works in both input and output mode\n\n\n### **fill**\n* **Description:** Sent by tinySA when in auto refresh mode\n* **Original Usage:**\n* **Direct Library Function Call:**\n* **Example Return:** `format: \"fill\\r\\n{X}{Y}{Width}{Height} {Color}\\r\\n\"`\n* **Alias Functions:**\n    * `get_fill_data()`\n* **CLI Wrapper Usage:**\n* **Notes:**  All numbers returned are binary coded 2 bytes little endian. Similar to `bulk`\n\n\n### **freq**\n* **Description:** Pauses the sweep and sets the measurement frequency\n* **Original Usage:** `freq {frequency}`\n* **Direct Library Function Call:** `freq(val=Int)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_freq(val=Int)`\n* **CLI Wrapper Usage:**\n* **Notes:** Might need to `resume` the sweep after this. Could be device dependent.   \n\n\n### **freq_corr**\n* **Description:** Gets the frequency correction.\n* **Original Usage:** `freq_corr`\n* **Direct Library Function Call:**  `freq_corr()`\n* **Example Return:** `b'0 ppb\\r'`\n* **Alias Functions:**\n    * `get_frequency_correction()`\n* **CLI Wrapper Usage:**\n* **Notes:**  This command returns the frequency correction, in parts per billion (ppb).\n\n\n\n### **frequencies**\n* **Description:** Gets the frequencies used by the last sweep\n* **Original Usage:** `frequencies`\n* **Direct Library Function Call:**  `frequencies()`\n* **Example Return:**  `b'1500000000\\r\\n... \\r\\n3000000000\\r'`\n* **Alias Functions:**\n    * `get_last_freqs()`\n* **CLI Wrapper Usage:**\n* **Notes:**   \n\n\n### **help**\n* **Description:** Gets a list of the available commands. Can be used to call tiySA help directly, or the library help\n* **Original Usage:** `help`\n* **Direct Library Function Call:** `help(val=None|0|1)`\n* **Example Return:**   \n* **Alias Functions:**\n    * `tinySAHelp()`\n* **Related Functions:**\n    * `libraryHelp()` \n* **CLI Wrapper Usage:**\n* **Notes:**  0 = tinySAHelp(), 1=libraryHelp(). Both functions can also be called directly. libraryHelp() has more information about this library and the inputs. \n\n\n### **hop**\n* **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.\n* **Original Usage:** `hop {start(Hz)} {stop(Hz)} {step(Hz) | points}  [outmask]`\n* **Direct Library Function Call:** `hop(start=Int, stop=Int, inc=Int, use_pts=Bool)`\n* **Example Return:** e\n* **Alias Functions:**\n    * `get_sample_pts(start=Int, stop=Int, inc=Int, use_pts=Bool)`\n* **CLI Wrapper Usage:**\n* **Notes:**  _Ultra only_. From [tinysa-org](https://tinysa-org.translate.goog/wiki/pmwiki.php?n=Main.USBInterface\u0026_x_tr_sl=auto\u0026_x_tr_tl=en\u0026_x_tr_hl=en-US): if the 3rd parameter is below 450 it is assumed to be points, otherwise as step frequency Outmask selects if the frequency (1) or level (2) is output. \n\n\n### **if**\n* **Description:** Sets the intermediate frequency (IF) to automatic or a specific value, 433 Mhz to 435 MHz \n* **Original Usage:** `if (0|433M..435M )`\n* **Direct Library Function Call:** `set_IF(val=Int|0|433M..435M|'auto')`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * None\n* **CLI Wrapper Usage:**\n* **Notes:**  Val input of 0 is 'auto'. Added explicit 'auto' to match other library funcs.\n\n### **if1**\n* **Description:** Sets the intermediate frequency (IF) to automatic or a specific value, 975 Mhz to 979 MHz \n* **Original Usage:** `if1 (975M..979M )`\n* **Direct Library Function Call:** `set_IF1(val=0|975M..979M|'auto')`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * None\n* **CLI Wrapper Usage:**\n* **Notes:**  Val input of 0 is 'auto'. Added explicit 'auto' to match other library funcs.\n\n### **info**\n* **Description:** Displays various software/firmware and hardware information\n* **Original Usage:** `info`\n* **Direct Library Function Call:** `info()`\n* **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 \u0026 DSP\\r'`\n* **Alias Functions:**\n    * `get_info()`\n* **CLI Wrapper Usage:**\n* **Notes:** \n\n### **level**\n* **Description:** Sets the output level\n* **Original Usage:** `level -76..13`\n* **Direct Library Function Call:** `level(val=Int|-76...13)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_level()`\n* **CLI Wrapper Usage:**\n* **Notes:** Not all values in the range are available.  Might be device dependent. \n\n### **levelchange**\n* **Description:** Sets the output level delta for low output mode level sweep\n* **Original Usage:** `levelchange -70..+70`\n* **Direct Library Function Call:** `level_change(val=Int|-70...70)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_level_change()`\n* **CLI Wrapper Usage:**\n* **Notes:**  \n\n### **leveloffset**\n* **Description:** Sets or gets the level calibration data\n* **Original Usage:** `leveloffset low|high|switch [output] {error}`\n    * 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]`\n* **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)`\n    * `isOutput` boolean variable determines if the 'output' argument is included in the statement. See examples below.\n* **Example Return:**\n    * `leveloffset`, with no arguments\n        * output: `bytearray(b'-8.462500e+01 0.000000000 ... \\r\\n-8.128125e+01 0.000000000 \\r')`\n    * `leveloffset low -3.0`\n        * output: `bytearray(b'')`\n    * `leveloffset low output 0.0`\n        * output: `bytearray(b'')`\n* **Alias Functions:**\n    * None\n* **CLI Wrapper Usage:**\n* **Notes:**  \n    * NOT ALL COMBINATIONS ARE VALID.\n    * Calibration tables:\n        * `low` - Low frequency mode corrections\n        * `switch` - Switch-related corrections\n        * `receive_switch` - Receive switch corrections\n        * `out_switch` - Output switch corrections\n        * `lna` - LNA (Low Noise Amplifier) corrections\n        * `harmonic` - Harmonic mode corrections\n        * `shift/shift1/shift2/shift3` - Frequency shift corrections\n        * `drive1/drive2/drive3` - Drive level corrections\n        * `direct/direct_lna` - Direct mode corrections\n        * `ultra/ultra_lna` - Ultra mode corrections\n        * `harmonic_lna` - Harmonic mode with LNA corrections\n        * `adf` - ADF (frequency synthesizer) corrections\n\n### **line**\n* **Description:** Disables the horizontal line or sets it to a specific level.\n* **Original Usage:**  `line off|{level}` \n* **Direct Library Function Call:** `line(val=\"off\"|)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `line_off()`\n    * `set_line(val=Int|Float)`\n* **CLI Wrapper Usage:**\n* **Notes:**   \n\n### **load**\n* **Description:** Loads a previously stored preset to the connected device\n* **Original Usage:** `load 0..4`\n* **Direct Library Function Call:** `load(val=0|1|2|3|4)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * None\n* **CLI Wrapper Usage:**\n* **Notes:**  0 is the startup preset\n\n### **lna**\n* **Description:** Set LNA usage off/on. The Ultra Plus devices have a 2nd LNA at a higher frequency range.\n* **Original Usage:** `lna off|on` \n* **Direct Library Function Call:** `lna(val=\"off\"|\"on\")`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_lna_on()`\n    * `set_lna_off()`\n* **CLI Wrapper Usage:**\n* **Notes:**  Should not be enabled when AGC is enabled - [tinySA wiki SETTINGS2](https://tinysa.org/wiki/pmwiki.php?n=Main.SETTINGS2)\n\n\n### **lna2**\n* **Description:** Set the second LNA usage off/on. The Ultra Plus devices have a 2nd LNA at a higher frequency range.\n* **Original Usage:** `lna2 0..7|auto`\n* **Direct Library Function Call:** `lna2(val=\"auto\"|0..7)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_lna2(val=\"auto\"|0..7)`\n* **CLI Wrapper Usage:**\n* **Notes:** \n\n\n\n### **marker**\n* **Description:** sets or dumps marker info\n* **Original Usage:**  `marker {id} on|off|peak|{freq}|{index}`\n* **Direct Library Function Call:** `marker(ID=Int|0..4, val=\"on\"|\"off\"|\"peak\")`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `marker_on(ID=Int|1..4)`\n    * `marker_off(ID=Int|1..4)`\n    * `marker_peak(ID=Int|1..4)`\n    * `marker_freq(ID=Int|1..4)`\n    * `marker_index(ID=Int|1..4)`\n* **CLI Wrapper Usage:**\n* **Notes:**   where id=1..4 index=0..num_points-1\nMarker 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. \n\n\n### **menu**\n* **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. \n* **Original Usage:** `menu {#} [{#} [{#} [{#}]]]`\n* **Direct Library Function Call:** `menu(val=Str)`\n* **Example Input:**\n    * `menu \"6 2\"` will toggle the waterfall option \n* **Example Return:** unknown. depends on menu button 'clicked'\n* **Alias Functions:**\n    * None\n* **CLI Wrapper Usage:**\n* **Notes:**  [tinySA Menu Tree](https://tinysa.org/wiki/pmwiki.php?n=TinySA4.MenuTree) for more information. There's no error checking on this function due to the number of nested menus and buttons. \n\n### **mode**\n* **Description:** Sets the mode of the tinySA\n* **Original Usage:** `mode low|high input|output`\n* **Direct Library Function Call:** `mode(val1=\"low\"|\"high\", val2=\"input\"|\"output\")`\n* **Example Return:** empty bytearray\n* **Alias Functions:** Currently no error checking.\n    * `set_low_input_mode()`\n    * `set_low_output_mode()`\n    * `set_high_input_mode()`\n    * `set_high_output_mode()`\n* **CLI Wrapper Usage:**\n* **Notes:**  [tinySA Wiki MODE](https://tinysa.org/wiki/pmwiki.php?n=Main.MODE)\n    * LOW INPUT activates the 0.1-350MHz input mode\n    * HIGH INPUT activates the 240MHz-960MHz input mode\n    * LOW OUTPUT activates the 0.1-350MHz output mode\n    * HIGH OUTPUT activates the 240MHz-960MHz output mode \n\n\n### **modulation**\n* **Description:** Set the modulation in output mode\n* **Original Usage:** `modulation off|AM_1kHz|AM_10Hz|NFM|WFM|extern`\n* **Direct Library Function Call:** `modulation(val=off|AM_1kHz|AM_10Hz|NFM|WFM|extern)`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_mod_off()`\n    * `set_mod_AM_1khz()`\n    * `set_mod_AM_10Hz()`\n    * `set_mod_NFM()`\n    * `set_mod_WFM()`\n    * `set_mod_extern()`\n* **CLI Wrapper Usage:**\n* **Notes:** \n    * OFF -  Turns modulation off. NO modulation \n    * AM_1kHz - Set AM modulation 1 kHz\n    * AM_10Hz - Set AM modulation 10 Hz\n    * NFM - Narrow FM. sets narrow FM modulation. Width is about 3kHz. \n    * WFM - wide FM modulation. \n    * extern - disables the internal LO driving the mixer and enables the high input as mixer LO input. Minimum external LO input frequency is 5MHz. \n    * [https://tinysa.org/wiki/pmwiki.php?n=Main.MODULATION](https://tinysa.org/wiki/pmwiki.php?n=Main.MODULATION)\n\n\n\n### **nf**\n* **Description:** get the noise floor in dB\n* **Original Usage:** `nf {val=None|??}`\n* **Direct Library Function Call:** `nf(val=None)`\n* **Example Return:** `b'usage: nf {value}\\r\\n4.000000000\\r'`\n* **Alias Functions:**\n    * `get_nf()`\n* **CLI Wrapper Usage:**\n* **Notes:** \n* The tinySA Ultra can measure, store, and validate the tinySA noise figure (NF). It can also measure amplifier (AMP) NF. \n* While it is possible to set this value programmatically, until more documentation is online it is recommended to only GET the nf value. \n* \"The NF is the degradation in dB of the SNR after the amplifier compared to before the amplifier.\" - [https://tinysa.org/wiki/pmwiki.php?n=Main.NoiseFactor](https://tinysa.org/wiki/pmwiki.php?n=Main.NoiseFactor)\n\n\n### **output**\n* **Description:** Sets the output mode on or off\n* **Original Usage:** `output on|off`\n* **Direct Library Function Call:** `output(val=\"off\"|\"on\")`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * `set_output_on()`\n    * `set_output_off()`\n* **CLI Wrapper Usage:**\n* **Notes:** \n\n\n### **pause**\n* **Description:** Pauses the sweeping in either input or output mode\n* **Original Usage:** `pause`\n* **Direct Library Function Call:** `pause()`\n* **Example Return:** empty bytearray\n* **Alias Functions:**\n    * None\n* **CLI Wrapper Usage:**\n* **Notes:** \n\n\n### **rbw**\n* **Description:** sets the rbw to either automatic or a specific value\n* **Original Usage:** `rbw auto|3..600` or `rbw 0.2..850|auto`\n* **Direct Library Function Call:** `rbw","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flc-linkous%2Ftinysa_python","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flc-linkous%2Ftinysa_python","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flc-linkous%2Ftinysa_python/lists"}