Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Dependencies
node_modules/
**/node_modules/

# Environment files (contain secrets/config)
.env
backend/.env

# Build artifacts
dist/
build/

# Logs
*.log
npm-debug.log*

# OS files
.DS_Store
Thumbs.db

# Editor
.vscode/
.idea/
*.swp
*.swo

# Package lock (we keep package-lock.json for reproducibility)
# package-lock.json
164 changes: 162 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,165 @@
# Mr. Wiggles 🌀

Real-time wireless signal fox hunting sonar visualizer.
> Real-time wireless signal fox hunting sonar visualizer

*Under construction - full starter project incoming*
Mr. Wiggles turns your SDR antenna into a visual signal radar.
It displays animated crescent ripples on a black-canvas sonar view that show **RSSI**, **Direction of Arrival (DoA)**, and **frequency** of detected wireless signals in real time.

---

## Features

| Feature | Description |
|---------|-------------|
| 🌊 Crescent ripples | Arc waves emanate from centre in the DoA direction |
| 🎵 Beat waves | Sinusoidal oscillations along each crescent show frequency |
| 📶 RSSI → thickness | Stronger signal = thicker, brighter crescent |
| 📍 Located indicator | Glowing circle + banner when RSSI threshold is reached |
| 📡 Interactive menu | Dropdown to pick which signal to hunt |
| 🎮 Demo mode | Runs synthetic data with no hardware required |
| 🔌 Hardware SDR | RTL-SDR & HackRF support via `rtl_power` / `hackrf_sweep` |
| 🔄 WebSocket | Real-time 60 Hz data push to the browser |
| 🖥 240 Hz canvas | Silky-smooth animation via `requestAnimationFrame` |

---

## Quick Start

### Prerequisites

- Node.js 18+
- (Optional) RTL-SDR or HackRF hardware

### 1. Clone and install

```bash
git clone https://github.com/GG-93/mr-wiggles.git
cd mr-wiggles
npm install
```

### 2. Configure

```bash
cp backend/.env.example backend/.env
# Edit backend/.env – set DEMO_MODE=true for testing without hardware
```

### 3. Run

```bash
npm start
```

Open **http://localhost:3000** in your browser.

---

## Demo Mode

Demo mode generates synthetic signal data so you can explore the visualiser without
any hardware. It is enabled by default (`DEMO_MODE=true` in `.env`).

Five virtual Wi-Fi signals are simulated with:
- Slowly drifting DoA
- Fluctuating RSSI (simulates movement)
- Random transmission bursts

---

## Hardware Setup (RTL-SDR / HackRF)

```bash
sudo ./scripts/install-sdr.sh
```

The script installs OS drivers, udev rules, and blacklists conflicting kernel modules.
After installation set `DEMO_MODE=false` in `backend/.env`.

---

## Project Structure

```
mr-wiggles/
├── backend/
│ ├── src/
│ │ ├── index.js Main server (Express + WebSocket)
│ │ ├── sdr/
│ │ │ ├── demoSDR.js Synthetic signal generator
│ │ │ └── hardwareSDR.js RTL-SDR / HackRF interface
│ │ ├── processors/
│ │ │ └── signalManager.js Smoothing, threshold detection
│ │ └── utils/
│ │ ├── wsServer.js WebSocket broadcast server
│ │ └── helpers.js Math helpers (EMA, RSSI→strength, etc.)
│ ├── package.json
│ └── .env.example
├── frontend/
│ ├── index.html
│ ├── js/
│ │ ├── app.js Main controller
│ │ ├── renderer.js Canvas animation engine
│ │ └── wsClient.js WebSocket client (auto-reconnect)
│ └── css/
│ └── style.css
├── scripts/
│ └── install-sdr.sh SDR driver installer (Linux)
├── docs/
│ └── extending-antennas.md Guide: add new SDR backends
├── package.json Root workspace
└── README.md
```

---

## Visual Parameter Mapping

| Signal Property | Visual Effect |
|-----------------|---------------|
| RSSI (dBm) | Crescent line thickness + brightness |
| Direction of Arrival (°) | Crescent emanation angle from centre |
| Frequency (MHz) | Ripple travel speed + beat wave frequency |
| Active transmission | Beat wave animation intensity |
| RSSI ≥ threshold | Green glowing circle + "Signal Located!" banner |

---

## Configuration Reference (`backend/.env`)

| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `3000` | HTTP server port |
| `DEMO_MODE` | `true` | Use synthetic data |
| `DEMO_SIGNAL_COUNT` | `5` | Number of virtual signals |
| `DEMO_UPDATE_RATE_HZ` | `60` | Demo frame rate |
| `SDR_TYPE` | `auto` | `rtlsdr`, `hackrf`, or `auto` |
| `SDR_CENTER_FREQ` | `2437000000` | Centre frequency (Hz) |
| `SDR_GAIN` | `20` | SDR gain (dB) |
| `RSSI_THRESHOLD` | `-60` | dBm threshold for "located" |
| `DOA_SMOOTHING` | `0.3` | EMA alpha for DoA (0–1) |
| `WS_BROADCAST_RATE_HZ` | `60` | WebSocket update rate |

---

## REST API

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/status` | Server status and mode |
| `GET` | `/api/signals` | Current signal snapshot |
| `POST` | `/api/target` | Set hunt target `{ "id": "..." }` |

WebSocket endpoint: `ws://localhost:3000/ws`

---

## Extending

See **[docs/extending-antennas.md](docs/extending-antennas.md)** for a complete guide on adding new SDR hardware backends, antenna types, and protocols (Bluetooth, Zigbee, LoRa, ADS-B, and more).

---

## License

MIT
40 changes: 40 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Mr. Wiggles Backend Configuration

# Server
PORT=3000
HOST=localhost

# Demo mode – synthetic signals (no hardware required)
# Set to false to use real Wi-Fi / BLE / ESP32 scanning
DEMO_MODE=true
DEMO_SIGNAL_COUNT=5
DEMO_UPDATE_RATE_HZ=60

# ── Wi-Fi Scanner ────────────────────────────────────────────────────────────
# Scans using built-in OS commands (nmcli / iwlist / airport / netsh).
# No additional packages required.
WIFI_SCAN_INTERVAL_MS=4000

# ── BLE Scanner ──────────────────────────────────────────────────────────────
# Requires: npm install @abandonware/noble (inside backend/)
# Linux also needs: sudo apt-get install bluetooth bluez libbluetooth-dev libudev-dev
# Set to false to disable BLE scanning even if noble is installed.
ENABLE_BLE=true

# ── ESP32 Serial Bridge (optional) ───────────────────────────────────────────
# Connect an ESP32 via USB and flash it with the Mr. Wiggles firmware.
# Leave ESP32_PORT blank (or unset) to disable.
# Linux example: /dev/ttyUSB0 or /dev/ttyACM0
# macOS example: /dev/cu.usbserial-0001
# Windows example: COM3
# Requires: npm install serialport (inside backend/)
# ESP32_PORT=/dev/ttyUSB0
ESP32_BAUD=115200

# ── Signal Detection ─────────────────────────────────────────────────────────
# Signals at or above RSSI_THRESHOLD are flagged as "located" (green circle).
RSSI_THRESHOLD=-60
DOA_SMOOTHING=0.3

# ── WebSocket ────────────────────────────────────────────────────────────────
WS_BROADCAST_RATE_HZ=60
24 changes: 24 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "mr-wiggles-backend",
"version": "1.0.0",
"description": "Mr. Wiggles backend - Wi-Fi, Bluetooth, and BLE signal capture and WebSocket streaming",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js",
"test": "node --test src/**/*.test.js 2>/dev/null || echo 'No tests found'"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"ws": "^8.17.1"
},
"optionalDependencies": {
"@abandonware/noble": "^1.9.2-26",
"serialport": "^13.0.0"
},
"engines": {
"node": ">=18.0.0"
}
}
86 changes: 86 additions & 0 deletions backend/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict';

require('dotenv').config();
const http = require('http');
const express = require('express');
const cors = require('cors');
const path = require('path');
const WebSocketServer = require('./utils/wsServer');
const SignalManager = require('./processors/signalManager');
const DemoSDR = require('./sdr/demoSDR');
const NativeScanner = require('./sdr/nativeScanner');

const PORT = parseInt(process.env.PORT || '3000', 10);
const HOST = process.env.HOST || 'localhost';
const DEMO_MODE = process.env.DEMO_MODE !== 'false';

// ── Express app ──────────────────────────────────────────────────────────────
const app = express();
app.use(cors());
app.use(express.json());

// Serve frontend static files
const frontendDir = path.resolve(__dirname, '../../frontend');
app.use(express.static(frontendDir));

// REST endpoints
app.get('/api/status', (_req, res) => {
res.json({
mode: DEMO_MODE ? 'demo' : 'hardware',
version: '1.0.0',
uptime: Math.floor(process.uptime()),
});
});

app.get('/api/signals', (_req, res) => {
res.json(signalManager.getSignals());
});

app.post('/api/target', (req, res) => {
const { id } = req.body;
if (!id) return res.status(400).json({ error: 'id required' });
signalManager.setTarget(id);
res.json({ ok: true, target: id });
});

// ── HTTP + WebSocket server ──────────────────────────────────────────────────
const server = http.createServer(app);
const wss = new WebSocketServer(server);

// ── Signal manager ───────────────────────────────────────────────────────────
const signalManager = new SignalManager();

signalManager.on('update', (payload) => {
wss.broadcast(payload);
});

// ── SDR backend ──────────────────────────────────────────────────────────────
const sdr = DEMO_MODE ? new DemoSDR() : new NativeScanner();

sdr.on('frame', (frame) => {
signalManager.process(frame);
});

sdr.on('error', (err) => {
console.error('[SDR] Error:', err.message);
});

// ── Boot ─────────────────────────────────────────────────────────────────────
server.listen(PORT, HOST, () => {
console.log(`Mr. Wiggles backend running at http://${HOST}:${PORT}`);
console.log(`Mode: ${DEMO_MODE ? 'DEMO (synthetic data)' : 'LIVE (WiFi + BLE + ESP32)'}`);
console.log(`Frontend: http://${HOST}:${PORT}`);
sdr.start();
});

// Graceful shutdown
process.on('SIGINT', () => {
console.log('\nShutting down...');
sdr.stop();
server.close(() => process.exit(0));
});

process.on('SIGTERM', () => {
sdr.stop();
server.close(() => process.exit(0));
});
Loading