Initial docs site: Astro/Starlight with caddy-docker-proxy

- Starlight documentation for mcnanovna and mcpositioner
- 19 pages covering tools, prompts, hardware, and tutorials
- Docker deployment with dev/prod modes
- Makefile for docker compose management
- Custom SVG logos and hero illustration
This commit is contained in:
Ryan Malloy 2026-02-04 13:53:21 -07:00
commit e21219be8d
34 changed files with 9296 additions and 0 deletions

8
.env.example Normal file
View File

@ -0,0 +1,8 @@
# Project identifier (prevents container name collisions)
COMPOSE_PROJECT=mcnanovna-docs
# Domain for caddy-docker-proxy
DOMAIN=mcnanovna.l.zmesh.systems
# Mode: prod or dev (used by Makefile to select compose files)
MODE=prod

22
.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
# Dependencies
node_modules/
# Build output
dist/
# Astro
.astro/
# Environment (contains local overrides)
.env.local
.env.*.local
# OS
.DS_Store
Thumbs.db
# Editor
.vscode/
.idea/
*.swp
*.swo

68
Dockerfile Normal file
View File

@ -0,0 +1,68 @@
# Multi-stage Dockerfile for Astro/Starlight docs
# Supports both dev (hot-reload) and prod (static) modes
# =============================================================================
# Stage 1: Base with Node.js
# =============================================================================
FROM node:22-slim AS base
WORKDIR /app
# Disable Astro telemetry
ENV ASTRO_TELEMETRY_DISABLED=1
# =============================================================================
# Stage 2: Install dependencies
# =============================================================================
FROM base AS deps
COPY package.json package-lock.json* ./
RUN npm ci
# =============================================================================
# Stage 3: Development server (hot-reload)
# =============================================================================
FROM base AS dev
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Expose Vite dev server port
EXPOSE 4321
# Dev server with host binding for container access
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
# =============================================================================
# Stage 4: Build static site
# =============================================================================
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# =============================================================================
# Stage 5: Production with Caddy
# =============================================================================
FROM caddy:2-alpine AS prod
# Copy built static files
COPY --from=builder /app/dist /srv
# Simple Caddyfile for static file serving
# (caddy-docker-proxy handles TLS and reverse proxy externally)
COPY <<EOF /etc/caddy/Caddyfile
:80 {
root * /srv
file_server
try_files {path} {path}/ /index.html
encode gzip
# Cache static assets
@static path *.js *.css *.woff2 *.png *.svg *.jpg *.ico
header @static Cache-Control "public, max-age=31536000, immutable"
# Don't cache HTML
@html path *.html /
header @html Cache-Control "no-cache, no-store, must-revalidate"
}
EOF
EXPOSE 80

77
Makefile Normal file
View File

@ -0,0 +1,77 @@
# mcnanovna docs - Makefile for docker compose management
# Usage:
# make up - Start production (static site)
# make dev - Start development (hot-reload)
# make down - Stop containers
# make logs - Follow container logs
# make build - Rebuild container images
# make shell - Open shell in running container
.PHONY: up down logs build rebuild shell dev clean help
# Load environment variables
include .env
export
# Compose command shortcuts
COMPOSE := docker compose
COMPOSE_DEV := docker compose -f docker-compose.yml -f docker-compose.dev.yml
# Default target
help:
@echo "mcnanovna docs - Astro/Starlight documentation site"
@echo ""
@echo "Usage:"
@echo " make up Start production (static Caddy server)"
@echo " make dev Start development (Vite hot-reload)"
@echo " make down Stop all containers"
@echo " make logs Follow container logs"
@echo " make build Build production image"
@echo " make rebuild Force rebuild (no cache)"
@echo " make shell Open shell in running container"
@echo " make clean Remove containers, images, volumes"
@echo ""
@echo "Environment:"
@echo " DOMAIN = $(DOMAIN)"
@echo " MODE = $(MODE)"
# Production mode
up: .env
$(COMPOSE) up -d
$(COMPOSE) logs -f
# Development mode with hot-reload
dev: .env
$(COMPOSE_DEV) up -d --build
$(COMPOSE_DEV) logs -f
# Stop containers
down:
$(COMPOSE) down
$(COMPOSE_DEV) down 2>/dev/null || true
# View logs
logs:
$(COMPOSE) logs -f
# Build image
build:
$(COMPOSE) build
# Rebuild without cache
rebuild:
$(COMPOSE) build --no-cache
# Shell into running container
shell:
$(COMPOSE) exec docs sh
# Clean everything
clean:
$(COMPOSE) down -v --rmi local
$(COMPOSE_DEV) down -v --rmi local 2>/dev/null || true
# Create .env from example if missing
.env:
@echo "Creating .env from .env.example..."
@cp .env.example .env

87
astro.config.mjs Normal file
View File

@ -0,0 +1,87 @@
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
export default defineConfig({
site: 'https://mcnanovna.l.zmesh.systems',
telemetry: false,
devToolbar: { enabled: false },
integrations: [
starlight({
title: 'mcnanovna',
description: 'MCP servers for NanoVNA control and antenna positioner automation',
logo: {
dark: './src/assets/logo-dark.svg',
light: './src/assets/logo-light.svg',
replacesTitle: false,
},
social: {
github: 'https://git.supported.systems/rf/mcnanovna',
},
sidebar: [
{
label: 'Getting Started',
items: [
{ label: 'Introduction', slug: '' },
{ label: 'Quick Start', slug: 'getting-started/quickstart' },
{ label: 'Installation', slug: 'getting-started/installation' },
],
},
{
label: 'mcnanovna',
items: [
{ label: 'Overview', slug: 'mcnanovna/overview' },
{ label: 'Tool Reference', slug: 'mcnanovna/tools' },
{ label: 'Prompts', slug: 'mcnanovna/prompts' },
{ label: 'Web UI', slug: 'mcnanovna/webui' },
],
},
{
label: 'mcpositioner',
items: [
{ label: 'Overview', slug: 'mcpositioner/overview' },
{ label: 'Tool Reference', slug: 'mcpositioner/tools' },
{ label: 'Prompts', slug: 'mcpositioner/prompts' },
],
},
{
label: 'Hardware',
items: [
{ label: 'Positioner Build', slug: 'hardware/positioner-build' },
{ label: 'Wiring Diagram', slug: 'hardware/wiring' },
{ label: 'Firmware', slug: 'hardware/firmware' },
],
},
{
label: 'Tutorials',
items: [
{ label: '3D Pattern Measurement', slug: 'tutorials/pattern-measurement' },
{ label: 'VNA Calibration', slug: 'tutorials/calibration' },
{ label: 'Antenna Analysis', slug: 'tutorials/antenna-analysis' },
],
},
{
label: 'Reference',
items: [
{ label: 'HTTP API (ESP32)', slug: 'reference/http-api' },
{ label: 'Pattern Formats', slug: 'reference/pattern-formats' },
{ label: 'Ham Bands', slug: 'reference/ham-bands' },
],
},
],
customCss: ['./src/styles/custom.css'],
}),
],
vite: {
server: {
host: '0.0.0.0',
// Only configure HMR for reverse proxy when explicitly set
...(process.env.VITE_HMR_HOST && {
hmr: {
host: process.env.VITE_HMR_HOST,
protocol: 'wss',
clientPort: 443,
},
}),
},
},
});

27
docker-compose.dev.yml Normal file
View File

@ -0,0 +1,27 @@
# Development overrides - hot-reload with volume mounts
# Usage: docker compose -f docker-compose.yml -f docker-compose.dev.yml up
services:
docs:
build:
target: dev
volumes:
- .:/app
- /app/node_modules # Anonymous volume to preserve node_modules
environment:
- NODE_ENV=development
- ASTRO_TELEMETRY_DISABLED=1
- VITE_HMR_HOST=${DOMAIN:-mcnanovna.l.zmesh.systems}
labels:
# Override reverse proxy to Vite dev server port
caddy.reverse_proxy: "{{upstreams 4321}}"
# WebSocket support for Vite HMR
caddy.reverse_proxy.flush_interval: "-1"
caddy.reverse_proxy.transport: "http"
caddy.reverse_proxy.transport.read_timeout: "0"
caddy.reverse_proxy.transport.write_timeout: "0"
caddy.reverse_proxy.transport.keepalive: "5m"
caddy.reverse_proxy.transport.keepalive_idle_conns: "10"
caddy.reverse_proxy.stream_timeout: "24h"
caddy.reverse_proxy.stream_close_delay: "5s"

19
docker-compose.yml Normal file
View File

@ -0,0 +1,19 @@
services:
docs:
build:
context: .
target: prod
container_name: ${COMPOSE_PROJECT:-mcnanovna-docs}
restart: unless-stopped
networks:
- caddy
environment:
- ASTRO_TELEMETRY_DISABLED=1
labels:
# caddy-docker-proxy configuration
caddy: ${DOMAIN:-mcnanovna.l.zmesh.systems}
caddy.reverse_proxy: "{{upstreams 80}}"
networks:
caddy:
external: true

6506
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
package.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "mcnanovna-docs",
"version": "2026.02.04",
"description": "Documentation for mcnanovna and mcpositioner MCP servers",
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/starlight": "^0.32.4",
"astro": "^5.17.1",
"sharp": "^0.34.1"
}
}

45
src/assets/hero.svg Normal file
View File

@ -0,0 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300" fill="none">
<!-- VNA Device -->
<rect x="50" y="100" width="120" height="80" rx="8" stroke="#3182ce" stroke-width="2" fill="#1a365d" fill-opacity="0.3"/>
<text x="110" y="130" text-anchor="middle" fill="#63b3ed" font-family="monospace" font-size="10">NanoVNA</text>
<!-- VNA Screen with waveform -->
<rect x="60" y="140" width="100" height="30" rx="2" fill="#0d1117"/>
<polyline points="65,155 75,150 85,160 95,152 105,158 115,153 125,157 135,155 145,156 155,155"
stroke="#4ade80" stroke-width="1.5" fill="none"/>
<!-- USB Connection -->
<line x1="170" y1="140" x2="200" y2="140" stroke="#718096" stroke-width="2" stroke-dasharray="4,2"/>
<!-- Computer/Claude -->
<rect x="200" y="110" width="80" height="60" rx="4" stroke="#3182ce" stroke-width="2" fill="#1a365d" fill-opacity="0.3"/>
<text x="240" y="145" text-anchor="middle" fill="#63b3ed" font-family="monospace" font-size="10">Claude</text>
<!-- WiFi waves to positioner -->
<path d="M 280 140 Q 300 130 320 140" stroke="#718096" stroke-width="1.5" fill="none"/>
<path d="M 285 140 Q 300 135 315 140" stroke="#718096" stroke-width="1.5" fill="none"/>
<path d="M 290 140 Q 300 137 310 140" stroke="#718096" stroke-width="1.5" fill="none"/>
<!-- Positioner -->
<g transform="translate(320, 80)">
<!-- Base -->
<ellipse cx="30" cy="100" rx="35" ry="10" fill="#2d3748"/>
<!-- Vertical axis -->
<rect x="25" y="40" width="10" height="60" fill="#4a5568"/>
<!-- Horizontal arm -->
<rect x="20" y="35" width="40" height="8" fill="#4a5568"/>
<!-- Antenna -->
<line x1="60" y1="20" x2="60" y2="50" stroke="#f6ad55" stroke-width="3"/>
<line x1="50" y1="30" x2="70" y2="30" stroke="#f6ad55" stroke-width="2"/>
</g>
<!-- 3D Pattern sphere hint -->
<circle cx="350" cy="200" r="40" stroke="#3182ce" stroke-width="1" fill="none" stroke-dasharray="3,3"/>
<ellipse cx="350" cy="200" rx="40" ry="15" stroke="#3182ce" stroke-width="1" fill="none"/>
<ellipse cx="350" cy="200" rx="15" ry="40" stroke="#3182ce" stroke-width="1" fill="none"/>
<!-- Labels -->
<text x="110" y="200" text-anchor="middle" fill="#a0aec0" font-family="sans-serif" font-size="9">USB Serial</text>
<text x="300" y="165" text-anchor="middle" fill="#a0aec0" font-family="sans-serif" font-size="9">WiFi HTTP</text>
<text x="350" y="260" text-anchor="middle" fill="#a0aec0" font-family="sans-serif" font-size="9">3D Pattern</text>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

8
src/assets/logo-dark.svg Normal file
View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect x="2" y="8" width="28" height="16" rx="2" stroke="#63b3ed" stroke-width="2"/>
<line x1="6" y1="16" x2="10" y2="16" stroke="#63b3ed" stroke-width="2"/>
<polyline points="10,16 12,12 14,20 16,14 18,18 20,16" stroke="#63b3ed" stroke-width="1.5" fill="none"/>
<line x1="20" y1="16" x2="26" y2="16" stroke="#63b3ed" stroke-width="2"/>
<circle cx="6" cy="16" r="1.5" fill="#63b3ed"/>
<circle cx="26" cy="16" r="1.5" fill="#63b3ed"/>
</svg>

After

Width:  |  Height:  |  Size: 525 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect x="2" y="8" width="28" height="16" rx="2" stroke="#2b6cb0" stroke-width="2"/>
<line x1="6" y1="16" x2="10" y2="16" stroke="#2b6cb0" stroke-width="2"/>
<polyline points="10,16 12,12 14,20 16,14 18,18 20,16" stroke="#2b6cb0" stroke-width="1.5" fill="none"/>
<line x1="20" y1="16" x2="26" y2="16" stroke="#2b6cb0" stroke-width="2"/>
<circle cx="6" cy="16" r="1.5" fill="#2b6cb0"/>
<circle cx="26" cy="16" r="1.5" fill="#2b6cb0"/>
</svg>

After

Width:  |  Height:  |  Size: 525 B

7
src/content.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};

View File

@ -0,0 +1,108 @@
---
title: Installation
description: Detailed installation options for mcnanovna and mcpositioner
---
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
## mcnanovna
### From PyPI (Recommended)
<Tabs>
<TabItem label="Claude Code">
```bash
claude mcp add mcnanovna -- uvx mcnanovna
```
</TabItem>
<TabItem label="Standalone">
```bash
# Run directly with uvx
uvx mcnanovna
# Or install globally
pipx install mcnanovna
mcnanovna
```
</TabItem>
</Tabs>
### From Source
```bash
git clone https://git.supported.systems/rf/mcnanovna.git
cd mcnanovna
uv sync
uv run mcnanovna
```
### With Web UI
The optional 3D radiation pattern viewer requires additional dependencies:
```bash
# From source
uv sync --extra webui
MCNANOVNA_WEB_PORT=8080 uv run mcnanovna
# Open http://localhost:8080 in your browser
```
## mcpositioner
### From PyPI
<Tabs>
<TabItem label="Claude Code">
```bash
claude mcp add mcpositioner -- uvx mcpositioner
```
</TabItem>
<TabItem label="Standalone">
```bash
uvx mcpositioner
```
</TabItem>
</Tabs>
### From Source
```bash
git clone https://git.supported.systems/rf/mcpositioner.git
cd mcpositioner
uv sync
uv run mcpositioner
```
<Aside type="note">
mcpositioner requires the ESP32 positioner hardware. See [Hardware Build](/hardware/positioner-build/) for assembly instructions.
</Aside>
## Environment Variables
### mcnanovna
| Variable | Default | Description |
|----------|---------|-------------|
| `MCNANOVNA_WEB_PORT` | (disabled) | Port for 3D pattern web UI |
### mcpositioner
| Variable | Default | Description |
|----------|---------|-------------|
| `MCPOSITIONER_HOST` | `positioner.local` | ESP32 hostname or IP |
## Both Servers Together
For automated 3D antenna pattern measurement, run both servers:
```bash
# Add both to Claude Code
claude mcp add mcnanovna -- uvx mcnanovna
claude mcp add mcpositioner -- uvx mcpositioner
# Restart Claude Code to load both
claude
```
Then use the `measure_antenna_range` prompt to run automated pattern sweeps.

View File

@ -0,0 +1,70 @@
---
title: Quick Start
description: Get mcnanovna running in 5 minutes
---
import { Steps, Tabs, TabItem, Aside } from '@astrojs/starlight/components';
## Prerequisites
- Claude Code CLI installed
- NanoVNA-H connected via USB
- Python 3.11+ (handled automatically by uvx)
## Installation
<Steps>
1. **Add the MCP server to Claude Code**
```bash
claude mcp add mcnanovna -- uvx mcnanovna
```
2. **Start a new Claude Code session**
```bash
claude
```
3. **Ask Claude to use your VNA**
Try these prompts:
- "Scan my antenna from 144 to 148 MHz"
- "What's the SWR at 145 MHz?"
- "Analyze this filter's frequency response"
- "Capture a screenshot of the VNA display"
</Steps>
<Aside type="tip">
The VNA auto-connects on first tool call. No configuration needed if using default USB settings.
</Aside>
## Verify Connection
Ask Claude: "Get VNA info"
You should see device details like firmware version, serial number, and frequency range.
## Next Steps
- [Install mcpositioner](/mcpositioner/overview/) for automated antenna measurements
- [Run a calibration](/tutorials/calibration/) for accurate measurements
- [Explore all 78 tools](/mcnanovna/tools/)
## Troubleshooting
### VNA not detected
1. Check USB connection
2. Verify device appears as `/dev/ttyACM0` (Linux) or COM port (Windows)
3. Check permissions: `sudo usermod -aG dialout $USER` (Linux)
### Permission denied
On Linux, add your user to the dialout group:
```bash
sudo usermod -aG dialout $USER
# Log out and back in for changes to take effect
```

View File

@ -0,0 +1,166 @@
---
title: Firmware
description: Build and flash the ESP32 positioner firmware
---
import { Steps, Tabs, TabItem, Aside } from '@astrojs/starlight/components';
## Prerequisites
- [PlatformIO](https://platformio.org/) (CLI or VS Code extension)
- USB cable for ESP32
## Build and Flash
<Steps>
1. **Clone the repository**
```bash
git clone https://git.supported.systems/rf/mcpositioner.git
cd mcpositioner/firmware
```
2. **Build the firmware**
```bash
pio run
```
3. **Flash to ESP32**
```bash
pio run -t upload
```
4. **Monitor serial output** (optional)
```bash
pio device monitor
```
</Steps>
## Configuration
Edit `include/config.h` before building:
### WiFi Settings
```cpp
#define WIFI_SSID "your-network"
#define WIFI_PASSWORD "your-password"
```
### Motor Parameters
```cpp
// Steps per revolution (including microstepping)
#define THETA_STEPS_PER_REV 3200 // 200 * 16 microstepping
#define PHI_STEPS_PER_REV 3200
// Gear ratios (if using reduction gearing)
#define THETA_GEAR_RATIO 1.0
#define PHI_GEAR_RATIO 1.0
// Motor current (mA)
#define MOTOR_CURRENT_MA 800
```
### TMC2209 Settings
```cpp
// UART addresses
#define THETA_TMC_ADDR 0x00
#define PHI_TMC_ADDR 0x01
// StallGuard threshold (lower = more sensitive)
#define STALL_VALUE 50
// Homing speed (steps/sec)
#define HOMING_SPEED 500
```
### Pin Assignments
```cpp
// Theta axis
#define THETA_STEP_PIN 16
#define THETA_DIR_PIN 17
#define THETA_EN_PIN 4
// Phi axis
#define PHI_STEP_PIN 21
#define PHI_DIR_PIN 22
#define PHI_EN_PIN 23
// Shared UART
#define TMC_UART_TX 18
#define TMC_UART_RX 19
```
## HTTP API
The firmware exposes these endpoints at `http://positioner.local`:
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/status` | GET | Current position and state |
| `/move` | POST | Absolute move: `{"theta_deg": 90, "phi_deg": 45}` |
| `/move/relative` | POST | Relative move: `{"d_theta": 5, "d_phi": 10}` |
| `/home` | POST | Homing: `{"axis": "both"}` |
| `/stop` | POST | Emergency stop |
| `/config` | GET/POST | Motion parameters |
<Aside type="tip">
The mDNS name `positioner.local` is configurable via `MDNS_HOSTNAME` in config.h.
</Aside>
## Dependencies
The firmware uses these PlatformIO libraries (auto-installed):
| Library | Purpose |
|---------|---------|
| AccelStepper | Smooth acceleration profiles |
| TMCStepper | TMC2209 UART configuration |
| ESPAsyncWebServer | HTTP API |
| ArduinoJson | JSON parsing |
| ESPmDNS | Network discovery |
## Troubleshooting
### Can't connect to WiFi
- Check SSID and password in config.h
- Verify ESP32 is within range
- Check serial monitor for connection status
### mDNS not working
- Some networks block mDNS
- Use IP address directly (shown in serial monitor on boot)
- Set `MCPOSITIONER_HOST` environment variable
### Motors don't respond
- Check UART wiring (TX/RX may be swapped)
- Verify TMC2209 addresses match config
- Check motor power supply
### StallGuard unreliable
- Adjust `STALL_VALUE` (lower = more sensitive)
- Increase motor current
- Decrease homing speed
- Ensure mechanical stops are solid
## OTA Updates
The firmware supports over-the-air updates:
```bash
pio run -t upload --upload-port positioner.local
```
<Aside type="caution">
OTA requires the ESP32 to be on the same network as your computer.
</Aside>

View File

@ -0,0 +1,117 @@
---
title: Positioner Build Guide
description: Build the ESP32 dual-axis antenna positioner
---
import { Steps, Aside } from '@astrojs/starlight/components';
## Bill of Materials
| Qty | Component | Notes |
|-----|-----------|-------|
| 1 | ESP32 DevKit | Any variant with enough GPIO |
| 2 | TMC2209 stepper driver | UART mode, StallGuard support |
| 2 | NEMA 17 stepper motor | 0.9° or 1.8° step angle |
| 1 | 24V power supply | 3A+ for motors |
| 1 | 5V regulator or USB | For ESP32 |
| - | Wire, connectors | See wiring diagram |
<Aside type="tip">
The TMC2209 must be in UART mode, not standalone mode. This enables StallGuard sensorless homing and runtime configuration.
</Aside>
## Mechanical Assembly
The positioner needs two axes of rotation:
1. **Theta axis** (polar, 0-180°): Tilts the antenna from zenith to nadir
2. **Phi axis** (azimuth, 0-360°): Rotates the antenna around vertical axis
### Design Considerations
- **Cable routing**: Ensure cables can handle full rotation without tangling
- **Balance**: Center of gravity should be on the rotation axes
- **Rigidity**: Minimize wobble for accurate measurements
- **Range limits**: Mechanical stops for homing reference
### Mounting Options
| Approach | Pros | Cons |
|----------|------|------|
| 3D printed | Custom fit, cheap | Strength limits |
| Aluminum extrusion | Strong, adjustable | Heavier, more complex |
| PVC pipe | Very cheap, easy | Less precise |
## Electronics Assembly
<Steps>
1. **Mount TMC2209 drivers**
Install both drivers on a breakout board or custom PCB. Ensure proper heatsinking—these drivers can get hot under load.
2. **Wire motor connections**
| TMC2209 Pin | Motor Wire |
|-------------|------------|
| A1 | Coil A+ |
| A2 | Coil A- |
| B1 | Coil B+ |
| B2 | Coil B- |
<Aside type="caution">
Never disconnect motors while powered—the back-EMF can damage the driver.
</Aside>
3. **Wire UART connections**
Both drivers share the same UART bus but have different addresses:
- Theta driver: Address 0x00
- Phi driver: Address 0x01
See [Wiring Diagram](/hardware/wiring/) for full pinout.
4. **Power connections**
- 24V to driver VMOT pins (motor power)
- 5V/3.3V to driver VIO pins (logic power)
- ESP32 powered via USB or separate 5V regulator
5. **Test before mounting**
Flash the firmware and verify both motors respond before final assembly.
</Steps>
## Firmware Setup
See [Firmware](/hardware/firmware/) for build and flash instructions.
## Calibration
After assembly:
1. **Home both axes**: Run `positioner_home(axis="both")`
2. **Verify range**: Move to extremes and check for binding
3. **Tune StallGuard**: Adjust `STALL_VALUE` in `config.h` if homing is unreliable
4. **Set motion parameters**: Use `positioner_config()` to tune speed/accel
## Troubleshooting
### Motor doesn't move
- Check power supply voltage (should be 24V)
- Verify UART communication (TX/RX wiring)
- Check TMC2209 address configuration
### Homing fails (doesn't detect stall)
- Increase motor current in `config.h`
- Decrease homing speed
- Adjust `STALL_VALUE` threshold
### Motors get hot
- Reduce motor current (if torque allows)
- Add heatsinks to TMC2209
- Reduce holding current when idle
### Position drift
- Check mechanical coupling (loose setscrews)
- Verify microstepping is consistent
- Check for missed steps (reduce speed/accel)

View File

@ -0,0 +1,113 @@
---
title: Wiring Diagram
description: ESP32 to TMC2209 to stepper motor connections
---
import { Aside } from '@astrojs/starlight/components';
## Pin Assignments
These are the default pin assignments in `firmware/include/config.h`:
### ESP32 to TMC2209 (Theta Axis)
| ESP32 Pin | TMC2209 Pin | Function |
|-----------|-------------|----------|
| GPIO 16 | STEP | Step pulse |
| GPIO 17 | DIR | Direction |
| GPIO 4 | EN | Enable (active low) |
| GPIO 18 | UART TX | TMC UART |
| GPIO 19 | UART RX | TMC UART |
### ESP32 to TMC2209 (Phi Axis)
| ESP32 Pin | TMC2209 Pin | Function |
|-----------|-------------|----------|
| GPIO 21 | STEP | Step pulse |
| GPIO 22 | DIR | Direction |
| GPIO 23 | EN | Enable (active low) |
| GPIO 18 | UART TX | Shared with theta |
| GPIO 19 | UART RX | Shared with theta |
<Aside type="note">
Both TMC2209 drivers share the same UART bus. They're distinguished by address (0x00 for theta, 0x01 for phi).
</Aside>
## TMC2209 Driver Wiring
### Power
| Pin | Connection |
|-----|------------|
| VMOT | 24V motor supply |
| GND (motor) | 24V ground |
| VIO | 3.3V (from ESP32) |
| GND (logic) | ESP32 ground |
### Motor Outputs
| Pin | Connection |
|-----|------------|
| A1, A2 | Motor coil A |
| B1, B2 | Motor coil B |
### UART Configuration
For UART mode, connect:
- **MS1**: To address selection (see address table)
- **MS2**: To GND for UART mode
- **PDN_UART**: To ESP32 UART TX/RX through 1kΩ resistor
#### TMC2209 Address Selection (MS1 pin)
| Address | MS1 | MS2 |
|---------|-----|-----|
| 0x00 | GND | GND |
| 0x01 | VIO | GND |
| 0x02 | GND | VIO |
| 0x03 | VIO | VIO |
## Schematic
Full KiCad schematics are in the repository:
```
mcpositioner/hardware/
├── positioner.kicad_pro # Project file
├── positioner.kicad_sch # Main schematic
├── positioner.kicad_sym # Custom symbols
└── sym-lib-table # Symbol library table
```
View online: [hardware/ on Gitea](https://git.supported.systems/rf/mcpositioner/src/branch/main/hardware)
## Power Supply Notes
### Motor Power (24V)
- Minimum 3A capacity for two NEMA 17 motors
- Add bulk capacitance (100-470µF) near drivers
- Keep motor power wiring short and thick
### Logic Power (3.3V)
- ESP32's 3.3V output can power both TMC2209 VIO pins
- Current draw is minimal (~10mA per driver)
<Aside type="caution">
Never connect motor power (24V) to logic pins (3.3V). This will destroy the driver and possibly the ESP32.
</Aside>
## Cable Considerations
### Motor Cables
- Use shielded cable if runs are long (>1m)
- Keep motor cables away from sensitive RF connections
- Twisted pairs reduce EMI
### Measurement Cables
- The antenna under test connects to the VNA via coax
- Ensure enough slack for full theta/phi rotation
- Consider using rotary joints for continuous rotation

View File

@ -0,0 +1,59 @@
---
title: mcnanovna
description: MCP servers for NanoVNA control and antenna positioner automation
template: splash
hero:
tagline: Give your AI direct control of RF test equipment
image:
file: ../../assets/hero.svg
actions:
- text: Get Started
link: /getting-started/quickstart/
icon: right-arrow
- text: View on Gitea
link: https://git.supported.systems/rf/mcnanovna
icon: external
variant: minimal
---
import { Card, CardGrid } from '@astrojs/starlight/components';
## What is this?
Two MCP servers that let AI assistants control RF test equipment:
<CardGrid stagger>
<Card title="mcnanovna" icon="document">
Controls NanoVNA-H vector network analyzers over USB serial.
78 tools for sweeps, calibration, analysis, and 3D radiation patterns.
</Card>
<Card title="mcpositioner" icon="setting">
Controls ESP32 dual-axis antenna positioners over WiFi.
5 tools for stepper motor positioning and automated measurement grids.
</Card>
<Card title="Cross-Server Workflows" icon="rocket">
Both servers work together for automated 3D antenna pattern measurement.
The AI orchestrates positioning and VNA measurements across the grid.
</Card>
<Card title="Web UI" icon="laptop">
Optional Three.js 3D viewer for radiation patterns.
Real-time visualization as measurements are taken.
</Card>
</CardGrid>
## Quick Install
```bash
# Add both servers to Claude Code
claude mcp add mcnanovna -- uvx mcnanovna
claude mcp add mcpositioner -- uvx mcpositioner
```
Then ask Claude to analyze your antenna, measure a filter, or run a 3D pattern sweep.
## Hardware
- **NanoVNA-H** or H4 (USB serial, auto-detected)
- **ESP32 positioner** (optional, for automated pattern measurement)
- 2x NEMA 17 steppers + TMC2209 drivers
- Firmware and KiCad schematics included

View File

@ -0,0 +1,85 @@
---
title: mcnanovna Overview
description: MCP server for NanoVNA-H vector network analyzers
---
import { Aside } from '@astrojs/starlight/components';
mcnanovna gives AI assistants direct control of NanoVNA-H vector network analyzers over USB serial. It exposes 78 MCP tools for frequency sweeps, S-parameter measurements, calibration, LCD capture, RF analysis, and 3D antenna radiation pattern visualization.
## Capabilities
### Measurement
- Frequency sweeps with configurable start/stop/points
- S11 (reflection) and S21 (transmission) measurements
- Marker-based frequency analysis
- Real-time data streaming
### Calibration
- Full SOLT calibration workflow
- Save/recall calibration to device flash
- Calibration status verification
### Analysis
- SWR, return loss, impedance calculations
- Filter characterization (cutoffs, bandwidth, Q)
- Crystal parameter extraction
- TDR (time domain reflectometry)
- L/C component identification
- Impedance matching network design
### Radiation Patterns
- Analytical 3D patterns from S11 data
- Support for dipole, monopole, EFHW, loop, patch antennas
- Pattern import from CSV, EMCAR, NEC2, Touchstone S1P
- Optional Three.js web viewer
## Supported Hardware
| Device | Frequency Range | Notes |
|--------|----------------|-------|
| NanoVNA-H | 50 kHz - 900 MHz | Original hardware |
| NanoVNA-H4 | 50 kHz - 1.5 GHz | Extended range |
<Aside type="note">
Other NanoVNA variants using the same USB serial protocol (VID 0x0483, PID 0x5740) should also work.
</Aside>
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Claude Code │
│ │ │
│ MCP Protocol │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ mcnanovna server │ │
│ │ ┌─────────┐ ┌────────────┐ ┌─────────────┐ │ │
│ │ │ tools/ │ │ protocol.py│ │calculations │ │ │
│ │ │ 8 mixins│ │ USB serial│ │ S-param │ │ │
│ │ └────┬────┘ └─────┬──────┘ │ math │ │ │
│ │ │ │ └─────────────┘ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ NanoVNA class │ │ │
│ │ │ (connection lifecycle, auto-reconnect) │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ USB Serial │
│ ▼ │
│ ┌──────────────────┐ │
│ │ NanoVNA-H │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## Connection Lifecycle
1. **First tool call**: Auto-discovers USB device, opens serial at 115200 baud
2. **Idle < 30s**: Trusts existing connection
3. **Idle ≥ 30s**: Sends sync probe to validate; reconnects on failure
4. **Retry**: 2 attempts with 300ms delay on cold/stale ports
No manual connect/disconnect needed—the server manages the connection automatically.

View File

@ -0,0 +1,114 @@
---
title: Prompts
description: Guided workflow prompts for mcnanovna
---
import { Aside } from '@astrojs/starlight/components';
Prompts are pre-built conversation templates that guide the AI through multi-step procedures. They set up context and provide step-by-step instructions for common workflows.
## Available Prompts
### Calibration & Setup
| Prompt | Description |
|--------|-------------|
| `calibrate` | Full SOLT calibration walkthrough |
| `export_touchstone` | Export S-parameters to .s1p/.s2p |
### Antenna Analysis
| Prompt | Description |
|--------|-------------|
| `analyze_antenna` | SWR, impedance, bandwidth analysis |
| `visualize_radiation_pattern` | Generate 3D pattern from S11 |
| `measure_antenna_range` | Automated 3D pattern with positioner |
| `import_pattern` | Import external pattern files |
### Component Analysis
| Prompt | Description |
|--------|-------------|
| `measure_component` | Identify unknown L/C/R |
| `analyze_crystal` | Crystal parameter extraction |
| `analyze_filter_response` | Filter characterization |
| `measure_lc_series` | Series LC resonator |
| `measure_lc_shunt` | Shunt LC resonator |
### Cable & Transmission Line
| Prompt | Description |
|--------|-------------|
| `measure_cable` | Cable characterization |
| `measure_tdr` | Time domain reflectometry |
### Design
| Prompt | Description |
|--------|-------------|
| `impedance_match` | L-network matching design |
| `compare_sweeps` | Before/after comparison |
## Prompt Parameters
Most prompts accept parameters to customize the workflow:
### calibrate
```
calibrate(
band="2m", # Ham band name or "custom"
start_hz=144000000, # Start frequency (overrides band)
stop_hz=148000000, # Stop frequency (overrides band)
points=101, # Sweep points
save_slot=0 # Flash slot for calibration
)
```
### analyze_antenna
```
analyze_antenna(
band="2m", # Ham band name
start_hz=None, # Override start frequency
stop_hz=None, # Override stop frequency
points=101 # Sweep points
)
```
### visualize_radiation_pattern
```
visualize_radiation_pattern(
antenna_type="dipole", # dipole, monopole, efhw, loop, patch, auto
band="2m",
points=101
)
```
<Aside type="tip">
Use `band="custom"` with explicit `start_hz` and `stop_hz` for frequencies outside the ham band presets.
</Aside>
## Ham Band Presets
| Band | Frequency Range |
|------|-----------------|
| 160m | 1.8 - 2.0 MHz |
| 80m | 3.5 - 4.0 MHz |
| 60m | 5.33 - 5.40 MHz |
| 40m | 7.0 - 7.3 MHz |
| 30m | 10.1 - 10.15 MHz |
| 20m | 14.0 - 14.35 MHz |
| 17m | 18.07 - 18.17 MHz |
| 15m | 21.0 - 21.45 MHz |
| 12m | 24.89 - 24.99 MHz |
| 10m | 28.0 - 29.7 MHz |
| 6m | 50 - 54 MHz |
| 2m | 144 - 148 MHz |
| 70cm | 420 - 450 MHz |
| 23cm | 1.24 - 1.30 GHz |
| full | 50 kHz - 900 MHz |
| hf | 1.8 - 30 MHz |
| vhf | 50 - 300 MHz |
| uhf | 300 MHz - 1 GHz |

View File

@ -0,0 +1,159 @@
---
title: Tool Reference
description: All 78 mcnanovna MCP tools
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
## Measurement Tools
| Tool | Description |
|------|-------------|
| `info` | Get device info (firmware, serial, frequency range) |
| `sweep` | Set sweep range (start, stop, points) |
| `scan` | Run measurement scan, return S-parameter data |
| `data` | Get current trace data without new measurement |
| `frequencies` | Get frequency list for current sweep |
| `marker` | Set/read marker position and values |
| `cal` | Run calibration step (open, short, load, thru, isoln, done) |
| `save` | Save calibration to flash slot |
| `recall` | Recall calibration from flash slot |
| `pause` | Pause continuous sweep |
| `resume` | Resume continuous sweep |
## Configuration Tools
| Tool | Description |
|------|-------------|
| `power` | Set/get output power level |
| `bandwidth` | Set/get IF bandwidth |
| `edelay` | Set/get electrical delay compensation |
| `s21offset` | Set/get S21 offset correction |
| `vbat` | Read battery voltage |
| `capture` | Capture LCD screenshot (PNG) |
| `measure` | Set measurement mode |
| `config` | Get full device configuration |
| `saveconfig` | Save configuration to flash |
| `clearconfig` | Reset configuration to defaults |
| `color` | Set/get display colors |
| `freq` | Set single frequency (CW mode) |
| `tcxo` | Set/get TCXO calibration |
| `vbat_offset` | Set/get battery voltage offset |
| `threshold` | Set/get measurement thresholds |
## Display Tools
| Tool | Description |
|------|-------------|
| `trace` | Configure trace display settings |
| `transform` | Enable/configure time domain transform |
| `smooth` | Set trace smoothing |
| `touchcal` | Run touchscreen calibration |
| `touchtest` | Test touchscreen |
| `refresh` | Force display refresh |
| `touch` | Simulate touch press |
| `release` | Simulate touch release |
## Device Tools
| Tool | Description |
|------|-------------|
| `reset` | Reset device |
| `version` | Get firmware version |
| `detect` | Detect and connect to VNA |
| `disconnect` | Close serial connection |
| `raw_command` | Send raw command to device |
| `cw` | Set continuous wave output |
| `sd_list` | List SD card files |
| `sd_read` | Read file from SD card |
| `sd_delete` | Delete file from SD card |
| `time` | Get/set device time |
## Diagnostics Tools
| Tool | Description |
|------|-------------|
| `i2c` | I2C bus diagnostics |
| `si` | Si5351 clock chip status |
| `lcd` | LCD controller info |
| `threads` | FreeRTOS thread status |
| `stat` | System statistics |
| `sample` | Raw ADC samples |
| `test` | Run self-test |
| `gain` | Set/get receiver gain |
| `dump` | Memory dump |
| `port` | Port configuration |
| `offset` | Calibration offsets |
| `dac` | DAC control |
| `usart_cfg` | USART configuration |
| `usart` | USART I/O |
| `band` | Band settings |
## Analysis Tools
| Tool | Description |
|------|-------------|
| `analyze` | Full scan analysis (SWR, Z, return loss) |
| `export_touchstone` | Export to .s1p/.s2p format |
| `export_csv` | Export to CSV format |
| `analyze_filter` | Characterize filter response |
| `analyze_xtal` | Extract crystal parameters |
| `analyze_tdr` | Time domain reflectometry |
| `analyze_component` | Identify unknown L/C/R |
| `analyze_lc_series` | Measure series LC resonator |
| `analyze_lc_shunt` | Measure shunt LC resonator |
| `analyze_lc_match` | Design L-network matching |
| `analyze_s11_resonance` | Find S11 resonance points |
## Radiation Pattern Tools
| Tool | Description |
|------|-------------|
| `radiation_pattern` | Generate 3D pattern from S11 scan |
| `radiation_pattern_from_data` | Generate pattern from known impedance |
| `radiation_pattern_multi` | Multi-frequency pattern comparison |
## Pattern Import Tools
| Tool | Description |
|------|-------------|
| `import_pattern_csv` | Import from CSV file |
| `import_pattern_emcar` | Import from EMCAR vna.dat |
| `import_pattern_nec2` | Import from NEC2 output |
| `import_pattern_s1p` | Import from Touchstone S1P |
| `list_pattern_formats` | List supported import formats |
## Example Usage
<Tabs>
<TabItem label="Basic Scan">
```
User: Scan my antenna from 144 to 148 MHz with 201 points
Claude uses: sweep(144000000, 148000000, 201)
Claude uses: scan(s11=true)
Claude uses: analyze()
```
</TabItem>
<TabItem label="Filter Analysis">
```
User: Analyze this bandpass filter from 1 to 500 MHz
Claude uses: sweep(1000000, 500000000, 201)
Claude uses: analyze_filter()
```
</TabItem>
<TabItem label="Calibration">
```
User: Calibrate for the 2m band
Claude uses: sweep(144000000, 148000000, 101)
Claude uses: cal("load") # user connects load
Claude uses: cal("open") # user connects open
Claude uses: cal("short") # user connects short
Claude uses: cal("thru") # user connects through
Claude uses: cal("done")
Claude uses: save(0)
```
</TabItem>
</Tabs>

View File

@ -0,0 +1,137 @@
---
title: Web UI
description: 3D radiation pattern viewer
---
import { Aside } from '@astrojs/starlight/components';
mcnanovna includes an optional Three.js-based 3D viewer for antenna radiation patterns.
## Enabling the Web UI
Set the `MCNANOVNA_WEB_PORT` environment variable:
```bash
MCNANOVNA_WEB_PORT=8080 uvx mcnanovna
```
Then open http://localhost:8080 in your browser.
<Aside type="note">
The web UI requires optional dependencies. From source: `uv sync --extra webui`
</Aside>
## Features
### 3D Pattern Visualization
- Interactive rotation with mouse drag
- Zoom with scroll wheel
- Gain-mapped color gradient (red = high, blue = low)
- dBi reference rings
### Pattern Sources
1. **From VNA scan**: Run `radiation_pattern` tool
2. **From known impedance**: Use `radiation_pattern_from_data`
3. **File upload**: Load CSV, EMCAR, NEC2, or S1P files
### Controls
| Control | Action |
|---------|--------|
| Left drag | Rotate view |
| Scroll | Zoom in/out |
| Right drag | Pan |
| Double-click | Reset view |
### Side Panel
- Antenna type selector
- Frequency display
- Impedance readout
- Peak gain indicator
- Toggle reference rings
- Toggle axes
## API Endpoints
The web UI provides REST and WebSocket endpoints:
### REST
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/pattern/compute` | POST | Compute pattern from S11 data |
| `/api/pattern/current` | GET | Get current pattern data |
| `/api/status` | GET | Server status |
### WebSocket
Connect to `/ws` for real-time pattern updates during measurement.
```javascript
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onmessage = (event) => {
const pattern = JSON.parse(event.data);
// Update visualization
};
```
## Pattern Format
All patterns use this JSON structure:
```json
{
"antenna_type": "dipole",
"frequency_hz": 145000000,
"theta_deg": [0, 5, 10, ...],
"phi_deg": [0, 10, 20, ...],
"gain_dbi": [-40, -35, -20, ...],
"peak_gain_dbi": 2.15,
"num_points": 6552
}
```
## File Upload
Click "Load File" to import external patterns:
| Format | Extension | Notes |
|--------|-----------|-------|
| CSV | .csv | 2 or 3 columns |
| EMCAR | .dat | Antenna range format |
| NEC2 | .out, .nec | Simulation output |
| S1P | .s1p | Touchstone → analytical |
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Browser │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Three.js Scene │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ │
│ │ │ Pattern Mesh│ │ OrbitControl│ │ Reference │ │ │
│ │ │(gain→color) │ │ (camera) │ │ Rings │ │ │
│ │ └─────────────┘ └─────────────┘ └───────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ HTTP/WebSocket │
│ ▼ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ FastAPI (webui/api.py) │ │
│ │ Pattern compute, WebSocket push │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ Shared NanoVNA │
│ ▼ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ mcnanovna MCP server │ │
│ │ (same process, asyncio.Lock) │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
The web UI runs in a daemon thread sharing the same process as the MCP server. An `asyncio.Lock` prevents concurrent VNA access from web and MCP.

View File

@ -0,0 +1,89 @@
---
title: mcpositioner Overview
description: MCP server for ESP32 dual-axis antenna positioner
---
import { Aside } from '@astrojs/starlight/components';
mcpositioner gives AI assistants control of an ESP32-based dual-axis antenna positioner over WiFi. It drives two NEMA 17 stepper motors via TMC2209 drivers for theta (polar, 0-180°) and phi (azimuth, 0-360°) rotation.
## Purpose
Automated 3D antenna radiation pattern measurement. The positioner physically rotates an antenna under test through a theta/phi grid while a VNA measures transmission (S21) at each position.
## Capabilities
- **Absolute positioning**: Move to any theta/phi coordinate
- **Relative moves**: Offset from current position
- **StallGuard homing**: Sensorless homing using TMC2209 stall detection
- **Emergency stop**: Immediate motor halt
- **Motion tuning**: Configurable speed, acceleration, microstepping
## Hardware Requirements
| Component | Specification |
|-----------|--------------|
| Controller | ESP32 DevKit (any variant) |
| Drivers | 2x TMC2209 in UART mode |
| Motors | 2x NEMA 17 (0.9° or 1.8° step) |
| Power | 24V for motors, 5V for ESP32 |
<Aside type="tip">
Full build instructions and KiCad schematics are in the [Hardware section](/hardware/positioner-build/).
</Aside>
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Claude Code │
│ │ │
│ MCP Protocol │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ mcpositioner server │ │
│ │ ┌─────────────┐ ┌─────────────────────┐ │ │
│ │ │ tools.py │ │ positioner.py │ │ │
│ │ │ 5 tools │─────▶│ httpx client │ │ │
│ │ └─────────────┘ └──────────┬──────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ WiFi HTTP │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ ESP32 Firmware │ │
│ │ ┌─────────┐ ┌──────────────────┐ │ │
│ │ │AccelStep│ │ESPAsyncWebServer │ │ │
│ │ └────┬────┘ └────────┬─────────┘ │ │
│ │ │ HTTP API │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ TMC2209 UART (StallGuard) │ │ │
│ │ └──────────┬──────────────────┘ │ │
│ └─────────────│────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ NEMA 17 Steppers (θ and φ axes) │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## Network Discovery
The ESP32 firmware advertises itself via mDNS as `positioner.local`. Override with the `MCPOSITIONER_HOST` environment variable if your network doesn't support mDNS or you have multiple positioners.
```bash
# Use IP address instead of mDNS
MCPOSITIONER_HOST=192.168.1.100 uvx mcpositioner
```
## Cross-Server Workflow
mcpositioner works with [mcnanovna](/mcnanovna/overview/) for automated pattern measurement:
1. **mcpositioner**: Move antenna to theta/phi position
2. **mcnanovna**: Measure S21 transmission
3. **Repeat** across the measurement grid
4. **Assemble** pattern from collected data
The `measure_pattern_grid` prompt guides this workflow step by step.

View File

@ -0,0 +1,97 @@
---
title: Prompts
description: Guided workflow prompts for mcpositioner
---
import { Aside } from '@astrojs/starlight/components';
## Available Prompts
| Prompt | Description |
|--------|-------------|
| `home_positioner` | Guided homing with safety checks |
| `configure_positioner` | Motion parameter tuning |
| `measure_pattern_grid` | Cross-server 3D pattern measurement |
## home_positioner
Guides through safe positioner homing with pre-flight checks.
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `axis` | string | `"both"` | Which axis to home |
**What it covers:**
1. Verify positioner is reachable
2. Check for obstructions
3. Confirm cables have slack
4. Run StallGuard homing
5. Verify homed state
## configure_positioner
Helps tune motion parameters for your specific setup.
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `speed` | float | None | Target speed (steps/sec) |
| `accel` | float | None | Target acceleration |
| `microstepping` | int | None | Microstep divisor |
**What it covers:**
- Parameter reference table
- Speed vs. measurement quality tradeoffs
- Microstepping guide
- Test move verification
## measure_pattern_grid
The key cross-server workflow for automated 3D antenna pattern measurement.
<Aside type="note">
This prompt requires both mcpositioner AND mcnanovna MCP servers to be running.
</Aside>
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `antenna_type` | string | `"dipole"` | Label for metadata |
| `band` | string | `"2m"` | Ham band name |
| `theta_step` | float | `5.0` | Polar angle step (°) |
| `phi_step` | float | `10.0` | Azimuth step (°) |
| `points` | int | `51` | VNA frequency points |
| `settle_ms` | int | `200` | Settle time after move |
**What it covers:**
1. **Pre-flight checks** on both servers
2. **Homing** the positioner
3. **VNA calibration** reminder
4. **Grid measurement** procedure:
- Serpentine path optimization
- S21 extraction method
- Progress tracking
5. **Pattern assembly** format
6. **Resolution tradeoffs** table
**Grid examples:**
| Step Size | Grid Points | Est. Time | Use Case |
|-----------|------------|-----------|----------|
| 10° × 20° | 342 | ~9 min | Quick survey |
| 5° × 10° | 1332 | ~33 min | Standard |
| 2° × 5° | 6552 | ~164 min | High-resolution |
## Using Prompts
In Claude Code, invoke prompts by name:
```
User: Run the measure_pattern_grid prompt for my Yagi on 70cm
Claude: [Uses measure_pattern_grid prompt with antenna_type="yagi", band="70cm"]
```
The prompt provides step-by-step guidance, and Claude executes the tools from both mcpositioner and mcnanovna as needed.

View File

@ -0,0 +1,151 @@
---
title: Tool Reference
description: All 5 mcpositioner MCP tools
---
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
## Available Tools
| Tool | Description |
|------|-------------|
| `positioner_status` | Get current position and state |
| `positioner_move` | Move to absolute theta/phi position |
| `positioner_home` | Run sensorless homing |
| `positioner_stop` | Emergency stop all motors |
| `positioner_config` | Get/set motion parameters |
## positioner_status
Returns the current state of the positioner.
**Response fields:**
- `theta_deg`: Current polar angle (0-180°)
- `phi_deg`: Current azimuth angle (0-360°)
- `moving`: True if motors are currently moving
- `homed`: True if homing has been completed
**Example:**
```json
{
"theta_deg": 45.0,
"phi_deg": 90.0,
"moving": false,
"homed": true
}
```
## positioner_move
Move to an absolute theta/phi position.
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `theta_deg` | float | required | Target polar angle (0-180°) |
| `phi_deg` | float | required | Target azimuth angle (0-360°) |
| `wait` | bool | `true` | Wait for move to complete |
| `poll_interval_ms` | int | `100` | Status polling interval |
**Example:**
```
positioner_move(theta_deg=90, phi_deg=45, wait=true)
```
<Aside type="caution">
Always home the positioner before making moves. Without homing, the position reference is unknown.
</Aside>
## positioner_home
Run sensorless StallGuard homing on one or both axes.
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `axis` | string | `"both"` | Which axis: `"theta"`, `"phi"`, or `"both"` |
**How it works:**
1. Motors move slowly in the negative direction
2. TMC2209 detects stall when motor hits mechanical stop
3. Position counter is zeroed
4. Motor backs off slightly from the stop
**Example:**
```
positioner_home(axis="both")
```
## positioner_stop
Emergency stop all motors immediately.
**Parameters:** None
**Example:**
```
positioner_stop()
```
<Aside type="tip">
Use this if the antenna is about to collide with something or cables are getting tangled.
</Aside>
## positioner_config
Get or set motion parameters.
**Parameters (all optional):**
| Parameter | Type | Range | Description |
|-----------|------|-------|-------------|
| `speed` | float | 100-5000 | Max speed (steps/sec) |
| `accel` | float | 100-3000 | Acceleration (steps/sec²) |
| `microstepping` | int | 1-256 | Microstep divisor |
**Get current config:**
```
positioner_config()
```
**Set parameters:**
```
positioner_config(speed=2000, accel=1000, microstepping=16)
```
**Tuning guide:**
| Use Case | Speed | Accel | Notes |
|----------|-------|-------|-------|
| Fast survey | 3000 | 2000 | More vibration, use longer settle time |
| Precision | 1000 | 500 | Smoother, less overshoot |
| Heavy antenna | 500 | 300 | Prevents missed steps |
## Example Workflow
<Tabs>
<TabItem label="Basic Movement">
```
# Home first
positioner_home(axis="both")
# Move to measurement position
positioner_move(theta_deg=90, phi_deg=0, wait=true)
# Check position
positioner_status()
```
</TabItem>
<TabItem label="Grid Measurement">
```
# Home and configure
positioner_home(axis="both")
positioner_config(speed=1500, accel=800)
# Measure at multiple points
for theta in [0, 30, 60, 90, 120, 150, 180]:
for phi in [0, 45, 90, 135, 180, 225, 270, 315]:
positioner_move(theta_deg=theta, phi_deg=phi, wait=true)
# ... run VNA measurement here ...
```
</TabItem>
</Tabs>

View File

@ -0,0 +1,92 @@
---
title: Ham Bands
description: Amateur radio frequency band presets
---
mcnanovna prompts accept ham band names as shortcuts for frequency ranges.
## Band Presets
### HF Bands
| Band | Start | Stop | Notes |
|------|-------|------|-------|
| 160m | 1.800 MHz | 2.000 MHz | Top Band |
| 80m | 3.500 MHz | 4.000 MHz | Night propagation |
| 60m | 5.3305 MHz | 5.4035 MHz | Channelized in many countries |
| 40m | 7.000 MHz | 7.300 MHz | Worldwide, day and night |
| 30m | 10.100 MHz | 10.150 MHz | WARC band, CW/data only |
| 20m | 14.000 MHz | 14.350 MHz | Primary DX band |
| 17m | 18.068 MHz | 18.168 MHz | WARC band |
| 15m | 21.000 MHz | 21.450 MHz | Good daytime DX |
| 12m | 24.890 MHz | 24.990 MHz | WARC band |
| 10m | 28.000 MHz | 29.700 MHz | Solar cycle dependent |
### VHF/UHF Bands
| Band | Start | Stop | Notes |
|------|-------|------|-------|
| 6m | 50 MHz | 54 MHz | "Magic Band" - sporadic E |
| 2m | 144 MHz | 148 MHz | Primary VHF band |
| 70cm | 420 MHz | 450 MHz | Primary UHF band |
| 23cm | 1.240 GHz | 1.300 GHz | Microwave |
### Aggregate Ranges
| Name | Start | Stop | Notes |
|------|-------|------|-------|
| full | 50 kHz | 900 MHz | NanoVNA-H full range |
| hf | 1.8 MHz | 30 MHz | All HF bands |
| vhf | 50 MHz | 300 MHz | VHF region |
| uhf | 300 MHz | 1 GHz | UHF region |
## Usage
In prompts:
```
calibrate(band="2m")
analyze_antenna(band="20m")
measure_antenna_range(band="70cm")
```
Or override with explicit frequencies:
```
calibrate(start_hz=144000000, stop_hz=148000000)
```
## NanoVNA Frequency Limits
| Model | Min | Max |
|-------|-----|-----|
| NanoVNA-H | 50 kHz | 900 MHz |
| NanoVNA-H4 | 50 kHz | 1.5 GHz |
Bands outside these ranges can't be measured directly.
## Harmonic Measurements
For bands above the VNA's range, harmonics can sometimes be used:
- 23cm (1.2 GHz) on NanoVNA-H: Measure at 1/3 frequency (400 MHz) and look for harmonic response
This is imprecise but can give a rough idea of antenna behavior.
## Band Plans
These presets cover the full amateur allocation. Actual band plans vary by country and license class. Consult your local regulations for permitted frequencies.
### US Band Plan Example (2m)
| Segment | Start | Stop | Use |
|---------|-------|------|-----|
| CW/SSB | 144.000 | 144.100 | Weak signal |
| SSB | 144.100 | 144.275 | SSB calling |
| Beacons | 144.275 | 144.300 | Propagation beacons |
| FM simplex | 146.400 | 146.580 | FM simplex |
| Repeater inputs | 144.600 | 144.900 | Linked repeaters |
| Repeater outputs | 145.200 | 145.500 | Linked repeaters |
| FM calling | 146.520 | 146.520 | National calling |
For antenna analysis, measure across the full band allocation to see behavior at all frequencies you might use.

View File

@ -0,0 +1,201 @@
---
title: HTTP API (ESP32)
description: ESP32 positioner firmware HTTP endpoints
---
The ESP32 positioner firmware exposes a REST API for position control.
## Base URL
Default: `http://positioner.local`
Override with `MCPOSITIONER_HOST` environment variable.
## Endpoints
### GET /status
Returns current positioner state.
**Response:**
```json
{
"theta_deg": 45.0,
"phi_deg": 90.0,
"moving": false,
"homed": true
}
```
| Field | Type | Description |
|-------|------|-------------|
| `theta_deg` | float | Current polar angle (0-180°) |
| `phi_deg` | float | Current azimuth angle (0-360°) |
| `moving` | bool | True if motors are moving |
| `homed` | bool | True if homing completed |
### POST /move
Move to absolute position.
**Request:**
```json
{
"theta_deg": 90,
"phi_deg": 45
}
```
**Response:**
```json
{
"status": "moving",
"target_theta": 90,
"target_phi": 45
}
```
### POST /move/relative
Move relative to current position.
**Request:**
```json
{
"d_theta": 5,
"d_phi": -10
}
```
**Response:**
```json
{
"status": "moving",
"target_theta": 95,
"target_phi": 80
}
```
### POST /home
Run sensorless homing.
**Request:**
```json
{
"axis": "both"
}
```
Valid values for `axis`: `"theta"`, `"phi"`, `"both"`
**Response:**
```json
{
"status": "homing",
"axis": "both"
}
```
### POST /stop
Emergency stop all motors.
**Request:** (empty body)
**Response:**
```json
{
"status": "stopped"
}
```
### GET /config
Get current motion parameters.
**Response:**
```json
{
"speed": 2000,
"accel": 1000,
"microstepping": 16,
"theta_steps_per_deg": 17.78,
"phi_steps_per_deg": 17.78
}
```
### POST /config
Update motion parameters.
**Request:**
```json
{
"speed": 1500,
"accel": 800
}
```
Only include fields you want to change.
**Response:**
```json
{
"status": "ok",
"speed": 1500,
"accel": 800,
"microstepping": 16
}
```
## Error Handling
Errors return HTTP 4xx/5xx with JSON body:
```json
{
"error": "description of the error"
}
```
Common errors:
| Code | Meaning |
|------|---------|
| 400 | Invalid request (bad JSON, out of range) |
| 503 | Busy (already moving/homing) |
## Example: curl
```bash
# Get status
curl http://positioner.local/status
# Move to position
curl -X POST http://positioner.local/move \
-H "Content-Type: application/json" \
-d '{"theta_deg": 90, "phi_deg": 0}'
# Home both axes
curl -X POST http://positioner.local/home \
-H "Content-Type: application/json" \
-d '{"axis": "both"}'
# Emergency stop
curl -X POST http://positioner.local/stop
```
## Example: Python
```python
import httpx
async def move_positioner(theta: float, phi: float):
async with httpx.AsyncClient() as client:
response = await client.post(
"http://positioner.local/move",
json={"theta_deg": theta, "phi_deg": phi}
)
return response.json()
```

View File

@ -0,0 +1,163 @@
---
title: Pattern Formats
description: Supported antenna pattern file formats
---
mcnanovna can import antenna patterns from several file formats.
## CSV
Comma-separated values with angle and gain data.
### 3-Column Format (Full 3D)
```csv
theta,phi,gain_dbi
0,0,-40
0,10,-40
...
90,0,2.15
90,10,2.15
...
180,0,-40
```
| Column | Description |
|--------|-------------|
| theta | Polar angle (0-180°) |
| phi | Azimuth angle (0-360°) |
| gain_dbi | Gain in dBi |
### 2-Column Format (Single Cut)
```csv
angle,gain
0,-40
45,-10
90,2.15
135,-10
180,-40
```
Single-cut data is synthesized to 3D using a sin(θ) taper.
### Delimiters
Auto-detected: comma (`,`), semicolon (`;`), or tab.
### Headers
Optional. If first row contains non-numeric values, it's treated as a header.
## EMCAR vna.dat
Format from EMCAR antenna range software.
```
# EMCAR antenna pattern measurement
# Frequency: 145 MHz
0 0.5
45 0.8
90 1.0
135 0.8
180 0.5
```
| Column | Description |
|--------|-------------|
| angle | Azimuth angle (degrees) |
| amplitude | Linear amplitude (0-1) |
The gnuplot transform is applied automatically:
- Angle: `(-angle + 90)` rotation
- Gain: `20 * log10(amplitude + 0.01)`
## NEC2 Output
Output from NEC2/NEC4 antenna simulation.
```
- - - RADIATION PATTERNS - - -
- ANGLES - - POWER GAINS - - POLARIZATION -
THETA PHI VERT. HOR. TOTAL AXIAL TILT
DEGREES DEGREES DB DB DB RATIO DEG
0.00 0.00 -999.99 -999.99 -999.99 0.00000 0.00
5.00 0.00 -4.52 -999.99 -4.52 0.00000 90.00
10.00 0.00 1.43 -999.99 1.43 0.00000 90.00
```
### Polarization Selection
| Option | Column |
|--------|--------|
| `total` | TOTAL (default) |
| `vert` | VERT. |
| `hor` | HOR. |
The frequency is auto-detected from the NEC2 header.
## Touchstone S1P
Touchstone S-parameter file with S11 data.
```
! NanoVNA measurement
# Hz S RI R 50
144000000 0.15 -0.08
145000000 0.02 -0.01
146000000 0.12 0.05
```
### Formats
| Option | Data Format |
|--------|-------------|
| RI | Real, Imaginary |
| MA | Magnitude, Angle (degrees) |
| DB | dB Magnitude, Angle (degrees) |
### Processing
1. Find resonance (minimum |S11|)
2. Calculate impedance at resonance
3. Estimate antenna type from impedance
4. Generate analytical pattern
### Antenna Type Override
If auto-detection is wrong, specify the type:
```python
import_pattern_s1p(content, antenna_type="monopole")
```
## Internal Format
All import functions return the same structure:
```python
{
"antenna_type": "dipole",
"frequency_hz": 145000000,
"theta_deg": [0, 5, 10, ...], # List of theta values
"phi_deg": [0, 10, 20, ...], # List of phi values
"gain_dbi": [-40, -35, ...], # Gain at each point
"peak_gain_dbi": 2.15,
"num_points": 6552
}
```
This format is used by:
- Web UI visualization
- `radiation_pattern_multi` comparison
- Export functions
## Interpolation
For irregular measurement grids, IDW (inverse-distance-weighted) interpolation maps data to the standard theta×phi grid:
- Theta: 0° to 180° in 5° steps (default)
- Phi: 0° to 355° in 5° steps (default)
Great-circle distance is used for accurate angular weighting.

View File

@ -0,0 +1,134 @@
---
title: Antenna Analysis
description: Using mcnanovna to analyze antenna performance
---
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
This tutorial covers common antenna analysis tasks using mcnanovna.
## Basic SWR and Impedance
The most common antenna measurement is SWR (Standing Wave Ratio) and impedance at the feedpoint.
Ask Claude: "Analyze my antenna from 144 to 148 MHz"
Claude will:
1. Run a sweep across the band
2. Find the resonant frequency (minimum SWR)
3. Calculate impedance at resonance
4. Report bandwidth where SWR < 2:1
**Example output:**
```
Resonant frequency: 145.2 MHz
SWR at resonance: 1.15:1
Impedance at resonance: 48.5 + j2.3 Ω
Bandwidth (SWR < 2:1): 143.8 - 146.9 MHz (3.1 MHz)
Return loss at resonance: -23.4 dB
```
## Finding Resonance
For antennas with multiple resonances (like a multi-band antenna):
Ask Claude: "Find all resonances from 1 to 30 MHz"
Claude uses `analyze_s11_resonance` to find all points where the antenna is resonant.
## Impedance Matching
If your antenna doesn't match 50Ω, design a matching network:
Ask Claude: "Design a matching network for 35+j25 ohms at 145 MHz"
Claude uses `analyze_lc_match` to compute L-network solutions:
```
Solution 1: Series L (27 nH) + Shunt C (18 pF)
Solution 2: Shunt C (12 pF) + Series L (42 nH)
```
## Antenna Types
<Tabs>
<TabItem label="Dipole">
**Expected characteristics:**
- Resonant impedance: ~73Ω
- Narrow bandwidth
- Figure-8 radiation pattern
Ask Claude: "Analyze my dipole on 20m"
</TabItem>
<TabItem label="Vertical">
**Expected characteristics:**
- Resonant impedance: ~36Ω (ground-mounted)
- Needs matching network or radials
- Omnidirectional pattern
Ask Claude: "Analyze my vertical on 40m"
</TabItem>
<TabItem label="Yagi">
**Expected characteristics:**
- Low impedance at feedpoint (~25Ω)
- Narrow bandwidth
- Directional pattern
Ask Claude: "Analyze my Yagi from 144 to 148 MHz"
</TabItem>
<TabItem label="Loop">
**Expected characteristics:**
- Variable impedance based on size
- High Q (narrow bandwidth)
- Figure-8 pattern (small loop)
Ask Claude: "Analyze my magnetic loop on 40m"
</TabItem>
</Tabs>
## Troubleshooting Antennas
### High SWR everywhere
Possible causes:
- Feedline not connected properly
- Antenna not resonant in measurement range
- Major construction problem
What to check:
- Verify feedline continuity
- Widen the measurement range
- Check physical dimensions
### SWR dip but wrong frequency
The antenna is resonant but not where you want it.
- **Too low**: Antenna is electrically long → shorten
- **Too high**: Antenna is electrically short → lengthen
<Aside type="tip">
For a dipole, each side is approximately λ/4 in length. At 145 MHz, that's about 51 cm per side.
</Aside>
### Good SWR but high reactance
The antenna is resonant but not at 50Ω.
Ask Claude: "Design a matching network for my measured impedance"
### Narrow bandwidth
High-Q antennas (small loops, loaded verticals) have narrow bandwidth. Options:
- Accept it (tune for the portion of band you use)
- Add loading to lower Q
- Use an antenna tuner
## Radiation Patterns
For a quick analytical pattern based on antenna type:
Ask Claude: "Show the radiation pattern for my dipole"
This uses the S11 data to determine resonance and impedance, then generates an idealized 3D pattern.
For measured patterns, see [3D Pattern Measurement](/tutorials/pattern-measurement/).

View File

@ -0,0 +1,138 @@
---
title: VNA Calibration
description: SOLT calibration for accurate measurements
---
import { Steps, Aside } from '@astrojs/starlight/components';
Calibration removes systematic errors from VNA measurements. Without calibration, your readings will include errors from cables, adapters, and the VNA itself.
## When to Calibrate
- First use on a new frequency range
- After changing cables or adapters
- After significant temperature changes
- If measurements look wrong or inconsistent
## What You Need
### Calibration Standards
| Standard | Purpose |
|----------|---------|
| **50Ω Load** | Reference impedance |
| **Open** | Open circuit (can use bare SMA) |
| **Short** | Short circuit |
| **Through** | Connects Port 1 to Port 2 |
<Aside type="tip">
An SMA calibration kit includes precision standards. For casual use, a 50Ω terminator, SMA barrel, and bare connectors work reasonably well.
</Aside>
## Calibration Procedure
<Steps>
1. **Set the sweep range**
Ask Claude: "Set sweep from 144 to 148 MHz with 101 points"
Calibration is frequency-specific. Always calibrate at the frequencies you'll measure.
2. **Connect the LOAD**
Connect the 50Ω load termination to Port 1.
Ask Claude: "Run cal load"
3. **Connect the OPEN**
Remove the load, leave Port 1 open (or use an open standard).
Ask Claude: "Run cal open"
4. **Connect the SHORT**
Connect the short standard to Port 1.
Ask Claude: "Run cal short"
5. **Connect the THROUGH**
Connect Port 1 to Port 2 with a short cable or barrel adapter.
Ask Claude: "Run cal thru"
6. **Optional: Isolation**
Connect 50Ω loads to both ports.
Ask Claude: "Run cal isoln"
This corrects for crosstalk between ports.
7. **Apply calibration**
Ask Claude: "Run cal done"
This computes and applies the error correction coefficients.
8. **Save calibration**
Ask Claude: "Save calibration to slot 0"
Saves to NanoVNA flash for later recall.
</Steps>
## Verification
After calibration, verify it worked:
1. **Load test**: Connect 50Ω load. S11 should be < -40 dB.
2. **Open test**: Leave port open. S11 should be ~0 dB.
3. **Through test**: Connect through. S21 should be ~0 dB, S11 < -40 dB.
## Calibration Slots
The NanoVNA stores calibrations in flash memory:
| Model | Slots |
|-------|-------|
| NanoVNA-H | 0-4 |
| NanoVNA-H4 | 0-6 |
Each slot holds one calibration. Common approach:
- Slot 0: Full range (50 kHz - 900 MHz)
- Slot 1: HF (1.8 - 30 MHz)
- Slot 2: VHF (50 - 300 MHz)
- Slot 3: 2m band (144 - 148 MHz)
- Slot 4: 70cm band (420 - 450 MHz)
## Recalling Calibration
Ask Claude: "Recall calibration from slot 0"
The NanoVNA will use the stored calibration. Note that calibration must match the current sweep settings.
## Troubleshooting
### High return loss with load
- Clean connectors with isopropyl alcohol
- Check for damaged center pin
- Try a different load
### S21 shows high loss through
- Cable may be damaged
- Adapters may be loose
- Recalibrate with the exact cables you'll use
### Calibration doesn't apply
- Frequency range must match
- Try recalibrating
- Check firmware version
<Aside type="caution">
Calibration is only valid for the exact cables and adapters used during calibration. If you change anything in the signal path, recalibrate.
</Aside>

View File

@ -0,0 +1,138 @@
---
title: 3D Pattern Measurement
description: Automated antenna radiation pattern measurement with positioner and VNA
---
import { Steps, Aside } from '@astrojs/starlight/components';
This tutorial walks through measuring a complete 3D antenna radiation pattern using mcpositioner and mcnanovna together.
## Prerequisites
- NanoVNA-H connected via USB
- ESP32 positioner built and on WiFi
- Both MCP servers added to Claude Code:
```bash
claude mcp add mcnanovna -- uvx mcnanovna
claude mcp add mcpositioner -- uvx mcpositioner
```
## Setup
### Physical Arrangement
```
Transmit Antenna
(on VNA Port 1)
│ Line of sight
┌─────────────────────────────────────────┐
│ │
│ Positioner │
│ ┌─────────────────┐ │
│ │ Antenna │ │
│ │ Under Test │◄── VNA Port 2
│ │ (rotates) │ │
│ └────────┬────────┘ │
│ │ │
│ θ axis │
│ │ │
│ ──────────────── │
│ φ axis │
└─────────────────────────────────────────┘
```
- **Transmit antenna**: Fixed, connected to VNA Port 1
- **Antenna under test**: Mounted on positioner, connected to VNA Port 2
- **Distance**: Far-field (at least 2λ, preferably 10λ+)
<Aside type="tip">
For accurate patterns, the transmit antenna should have a known, broad pattern (like a dipole) so it illuminates the AUT evenly across all angles.
</Aside>
## Measurement Procedure
<Steps>
1. **Verify both servers are connected**
Ask Claude: "Check positioner status and VNA info"
Claude will call `positioner_status` and `info` to verify both devices respond.
2. **Home the positioner**
Ask Claude: "Home the positioner on both axes"
This establishes the reference position (theta=0, phi=0).
3. **Calibrate the VNA**
Ask Claude: "Calibrate for the 2m band"
Follow the SOLT calibration procedure. This is critical for accurate S21 measurements.
4. **Run the pattern measurement**
Ask Claude: "Measure a 3D radiation pattern for my dipole antenna on 2m"
Claude uses the `measure_pattern_grid` prompt to guide the measurement:
- Confirms grid resolution (default: 5° theta × 10° phi)
- Moves through each grid point in serpentine order
- Measures S21 at each position
- Assembles the pattern data
5. **View the results**
If the mcnanovna web UI is running (`MCNANOVNA_WEB_PORT=8080`), the pattern renders in 3D automatically.
Or ask Claude: "Show the pattern statistics"
</Steps>
## Resolution Tradeoffs
| Step Size | Grid Points | Time | Use Case |
|-----------|------------|------|----------|
| 10° × 20° | 342 | ~9 min | Quick survey, verify setup |
| 5° × 10° | 1332 | ~33 min | Standard measurement |
| 2° × 5° | 6552 | ~2.7 hr | Publication-quality |
## Pattern Normalization
By default, patterns are **relative**—the peak is set to 0 dBi. This shows the pattern shape but not absolute gain.
For **absolute gain**, measure a reference antenna first:
<Steps>
1. Mount a known-gain antenna (e.g., calibrated dipole at 2.15 dBi)
2. Measure S21 at bore-sight
3. Note the reference S21 value
4. Measure your antenna under test
5. Offset all measurements: `gain_dBi = S21_dB - reference_S21_dB + reference_gain_dBi`
</Steps>
## Troubleshooting
### Pattern looks wrong
- Check that the transmit antenna is properly aimed
- Verify far-field distance (reflections cause errors)
- Recalibrate VNA if temperature changed significantly
### Measurement is noisy
- Increase VNA averaging (use more points)
- Reduce IF bandwidth
- Check cable connections
### Positioner loses position
- Motors may be missing steps—reduce speed
- Check mechanical coupling for slippage
- Increase motor current in firmware
### Web UI not updating
- Check `MCNANOVNA_WEB_PORT` is set
- Verify browser can reach the port
- Check for WebSocket connection errors in browser console

57
src/styles/custom.css Normal file
View File

@ -0,0 +1,57 @@
/* Custom styles for mcnanovna docs */
:root {
--sl-color-accent-low: #1a365d;
--sl-color-accent: #2b6cb0;
--sl-color-accent-high: #4299e1;
--sl-color-white: #fff;
--sl-color-gray-1: #edf2f7;
--sl-color-gray-2: #e2e8f0;
--sl-color-gray-3: #cbd5e0;
--sl-color-gray-4: #a0aec0;
--sl-color-gray-5: #718096;
--sl-color-gray-6: #4a5568;
--sl-color-black: #1a202c;
}
:root[data-theme='dark'] {
--sl-color-accent-low: #1e3a5f;
--sl-color-accent: #3182ce;
--sl-color-accent-high: #63b3ed;
--sl-color-white: #1a202c;
--sl-color-gray-1: #2d3748;
--sl-color-gray-2: #4a5568;
--sl-color-gray-3: #718096;
--sl-color-gray-4: #a0aec0;
--sl-color-gray-5: #cbd5e0;
--sl-color-gray-6: #e2e8f0;
--sl-color-black: #f7fafc;
}
/* Tool reference tables */
.tool-table {
width: 100%;
border-collapse: collapse;
}
.tool-table th,
.tool-table td {
padding: 0.5rem 1rem;
text-align: left;
border-bottom: 1px solid var(--sl-color-gray-3);
}
.tool-table code {
font-size: 0.875rem;
background: var(--sl-color-gray-1);
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
}
/* Hardware diagrams */
.wiring-diagram {
max-width: 100%;
margin: 1.5rem 0;
border-radius: 0.5rem;
border: 1px solid var(--sl-color-gray-3);
}

9
tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}