Ryan Malloy 646c92324d 3D antenna radiation pattern visualization: analytical models + Three.js web UI
Add analytical radiation pattern models for 5 antenna types (dipole, monopole,
EFHW, loop, patch) driven by S11 impedance measurements. Pure Python math with
closed-form far-field equations — no numpy or simulation dependencies.

New MCP tools:
- radiation_pattern: scan S11 → find resonance → compute 3D pattern
- radiation_pattern_from_data: compute from known impedance (no hardware)
- radiation_pattern_multi: patterns across a frequency band for animation

Web UI (opt-in via MCNANOVNA_WEB_PORT env var):
- Three.js gain-mapped sphere with OrbitControls
- Surface/wireframe/plane cut display modes with teal→amber color ramp
- Smith chart overlay, dBi reference rings, E/H plane cross-sections
- Real-time WebSocket push on new pattern computation
- FastAPI backend shares process with MCP server, zero new core deps

Frontend: Vite + TypeScript + Three.js, built assets committed to webui/static/.
Optional dependencies: fastapi + uvicorn via pip install mcnanovna[webui].
2026-01-31 15:27:19 -07:00

96 lines
2.5 KiB
TypeScript

import type { PatternData, ComputeParams, ScanParams } from './types';
const BASE_URL = '';
export async function fetchPattern(params: ComputeParams): Promise<PatternData> {
const resp = await fetch(`${BASE_URL}/api/pattern/compute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!resp.ok) {
const body = await resp.text();
throw new Error(`Compute failed (${resp.status}): ${body}`);
}
return resp.json();
}
export async function scanPattern(params: ScanParams): Promise<PatternData> {
const resp = await fetch(`${BASE_URL}/api/pattern`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!resp.ok) {
const body = await resp.text();
throw new Error(`Scan failed (${resp.status}): ${body}`);
}
return resp.json();
}
export async function getStatus(): Promise<{ connected: boolean; device?: string }> {
const resp = await fetch(`${BASE_URL}/api/status`);
if (!resp.ok) throw new Error(`Status check failed (${resp.status})`);
return resp.json();
}
export async function getBands(): Promise<Record<string, { start_hz: number; stop_hz: number }>> {
const resp = await fetch(`${BASE_URL}/api/bands`);
if (!resp.ok) throw new Error(`Bands fetch failed (${resp.status})`);
return resp.json();
}
export type PatternCallback = (data: PatternData) => void;
export type StatusCallback = (connected: boolean) => void;
export function connectWebSocket(
onPattern: PatternCallback,
onStatus: StatusCallback
): { close: () => void } {
let ws: WebSocket | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let closed = false;
function connect() {
if (closed) return;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = `${protocol}//${window.location.host}/ws/pattern`;
ws = new WebSocket(url);
ws.onopen = () => {
onStatus(true);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as PatternData;
onPattern(data);
} catch {
// non-pattern message, ignore
}
};
ws.onclose = () => {
onStatus(false);
if (!closed) {
reconnectTimer = setTimeout(connect, 3000);
}
};
ws.onerror = () => {
ws?.close();
};
}
connect();
return {
close() {
closed = true;
if (reconnectTimer) clearTimeout(reconnectTimer);
ws?.close();
},
};
}