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].
96 lines
2.5 KiB
TypeScript
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();
|
|
},
|
|
};
|
|
}
|