Skip to content

cmwhitehead1/ys1_scanner

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

YS1 RF Toolkit

YardStick One RF toolkit for scanning, capturing, analyzing, and transmitting sub-GHz signals. Ships with an interactive TUI and a web UI.

Features

  • Scan ISM bands (315 / 390 / 433 / 868 / 915 MHz) with RSSI threshold detection
  • Capture packets with preset or custom modulation configs (ASK, 2-FSK, GFSK, MSK)
  • Analyze captures: entropy, preamble detection, byte histograms, Hamming distance, fixed-vs-rolling classification
  • Visualize in multiple formats: bits, hex, hex+ASCII, OOK run-lengths, Manchester decoding
  • Edit captures: delete packets, filter noise by entropy, save as new files, rename
  • Transmit hex payloads or replay captured packets (single or sequence)
  • Spectrum plot — live SVG chart on the web UI Scan tab
  • Two UIs: rich-based TUI for terminal-only workflows, Flask-based web UI for everything else

Requirements

  • Python 3.9 or later
  • YardStick One dongle
  • libusb 1.0 (install via system package manager)
  • rfcat (installed automatically by bootstrap or via pip install -e .[rfcat])

Install

Option A — Quick start (no installation)

git clone <this-repo>
cd ys1-toolkit
python3 ys1_scanner.py

First run triggers the bootstrap: creates a venv at ./.venv/, installs rfcat from GitHub plus rich and flask, re-execs into the venv automatically. This is the path to use if you just want the tool to work without thinking about packaging.

Option B — Proper package install (editable, development)

# On Debian/Ubuntu/Kali:
sudo apt install libusb-1.0-0-dev python3-venv python3-pip

# On macOS:
brew install libusb

# Then:
python3 -m venv .venv
source .venv/bin/activate
pip install -e .[rfcat]

This registers the ys1-scanner command on your PATH, so you can invoke it from anywhere. All code edits take effect immediately — no reinstall needed.

Usage

TUI (default)

sudo ys1-scanner
# or, without installing:
sudo python3 ys1_scanner.py

Web UI

sudo ys1-scanner --web

Browser opens at http://127.0.0.1:8765 automatically. Tabs for Scan / Capture / Analyze / Transmit. Live SSE updates during scan and capture.

Over SSH with port forwarding:

ssh -L 8765:127.0.0.1:8765 user@host
# then on the remote:
sudo ys1-scanner --web --no-browser
# locally, open http://127.0.0.1:8765

CLI (scripting)

# Scan only
sudo ys1-scanner --scan-only --threshold -90

# Capture on a specific frequency
sudo ys1-scanner --freq 318000000 --duration 30 --preset entry-remote

# Custom modulation (overrides preset)
sudo ys1-scanner --freq 318000000 --modulation ASK --drate 1200 --bw 58000

# Analyze a saved capture
ys1-scanner --analyze captures/my_capture.jsonl

# Transmit
sudo ys1-scanner --freq 318000000 --tx a5c3b2d1 --tx-count 5

Run ys1-scanner --help for the full flag list.

Project structure

ys1-toolkit/
├── ys1_scanner.py              # Thin entry point (bootstraps deps, calls ys1.cli.main)
├── pyproject.toml
├── README.md
├── ys1/
│   ├── __init__.py
│   ├── bootstrap.py            # Dependency install / venv re-exec
│   ├── constants.py            # Presets, bands, modulations, thresholds
│   ├── scanner.py              # Scanner class (wraps rfcat)
│   ├── analyzer.py             # PacketAnalyzer class
│   ├── tui.py                  # Interactive TUI (rich)
│   ├── cli.py                  # argparse + dispatch
│   └── web/
│       ├── app.py              # WebApp (Flask orchestrator)
│       ├── routes/
│       │   ├── scan.py         # /api/scan*
│       │   ├── capture.py      # /api/capture*
│       │   ├── analyze.py      # /api/captures*
│       │   └── transmit.py     # /api/transmit
│       └── static/
│           ├── index.html
│           ├── styles.css
│           └── app.js
└── tests/
    ├── conftest.py             # rflib stubs, fixtures
    ├── test_imports.py         # AST-based "no undefined names"
    ├── test_analyzer.py
    ├── test_scanner.py
    ├── test_tui_helpers.py
    ├── test_web_routes.py
    └── test_bootstrap.py

Notes

  • Always run with sudo for USB access to the YardStick One (unless you've configured udev rules to give your user access to /dev/ttyACM*).
  • Captures are saved as JSONL (one packet per line, crash-safe) plus a JSON summary on successful completion. Both go into captures/ by default.
  • The web UI binds to 127.0.0.1 only. It's not accessible from the network. Use SSH port-forwarding if you need remote access.
  • sudo + bootstrap + venv interacts awkwardly: if you sudo python3 ys1_scanner.py on a PEP-668 system, the bootstrap creates the venv as root and re-execs. Subsequent runs work but the venv is root-owned. Simpler to python3 ys1_scanner.py once (unprivileged, to create the venv), then sudo ./.venv/bin/python ys1_scanner.py from then on.

Responsible use

This tool transmits RF on ISM bands. You are responsible for ensuring any transmission complies with local regulations. Replay attacks against RF-controlled access systems (cars, gates, doors) that you don't own are illegal in most jurisdictions. Use it on your own devices.

Testing

The project has a pytest suite covering pure-logic components and the web routes. Tests don't require a YardStick One or any USB hardware — conftest.py stubs rflib so tests run anywhere.

pip install -e .[dev]
pytest                       # all 131 tests, ~1 second
pytest tests/test_imports.py # just the "no undefined names" AST check
pytest -v                    # verbose (lists every test)

What's covered

  • test_imports.py — every module imports cleanly, AND every function body's Name references resolve. This catches refactor regressions where an import gets dropped but the reference is inside a method (so plain import foo doesn't fail). This test would have caught the missing from rich.align import Align that came up after the monolith-to-package split.
  • test_analyzer.pyPacketAnalyzer pure functions: Shannon entropy, preamble detection, Hamming distance, byte histograms, common prefix, end-to-end analyze() contract.
  • test_scanner.pyScanner static methods: the noise filter (locked in after two separate bug-fixes), JSON/JSONL capture file loading, RSSI-to-dBm conversion.
  • test_tui_helpers.py_parse_multiselect (select packets to transmit: "1,3-5,all"), _trim_zeros, _bytes_to_bitstring.
  • test_web_routes.py — Flask test client against every route: path-traversal rejection, 404/409 cases, sibling-file (.jsonl + .json) handling on rename/delete.
  • test_bootstrap.py — PEP 668 detection, Windows vs POSIX venv paths.

Why the AST test exists

Python imports are lazy about function bodies. import foo doesn't check whether foo.bar() references a name that exists — it just loads the module. The NameError only fires when bar() actually runs. This bites hard during refactors: extract a class into a new module, forget one of its import dependencies, tests pass, CI passes, sudo python ys1_scanner.py passes the menu but crashes the moment you hit the scan button.

The AST test walks every function body and checks that every Name reference in Load context resolves to a builtin, module-level define, enclosing function's local, comprehension target, or its own local. If you delete from rich.align import Align from tui.py, the test fails at tui.py:263 undefined name 'Align' in TuiApp._show_header() — pointing at the exact file, line, and method. Costs milliseconds; catches a class of bug that would otherwise only show up in the user's terminal.

About

Sub-GHz RF toolkit for the YardStick One. Scan ISM bands, capture packets across modulations, analyze/visualize OOK patterns, and replay transmissions — from a terminal TUI or a live web UI. Built on rfcat.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors