"use client"; import { useEffect, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { analyzeImpedanceNets, computeImpedance, designAntenna, fetchAntennaReport, fetchImpedanceNets, } from "@/lib/api"; import { PcbUploadButton } from "@/components/project/pcb-upload"; import type { AntennaReport, ImpedanceKind, ImpedanceNetsReport, ImpedanceStackupResult, ImpedanceTraceResult, } from "@/lib/types"; function num(v: string): number { return Number.parseFloat(v); } function fmt(n: number | null | undefined, digits = 2): string { if (n == null || Number.isNaN(n)) return "—"; return n.toFixed(digits); } export function ImpedancePanel({ projectId, hasPcb, onPcbUploaded, }: { projectId: string; hasPcb: boolean; onPcbUploaded?: () => void; }) { const [kind, setKind] = useState("microstrip"); const [h, setH] = useState("0.20"); const [er, setEr] = useState("4.5"); const [t, setT] = useState("0.035"); const [w, setW] = useState("0.35"); const [s, setS] = useState("0.20"); const [targetZ, setTargetZ] = useState(""); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [trace, setTrace] = useState(null); const [stackup, setStackup] = useState(null); const [boardNets, setBoardNets] = useState(null); const [extraNets, setExtraNets] = useState(""); const [antenna, setAntenna] = useState(null); const [f0, setF0] = useState("2440"); const [template, setTemplate] = useState<"ifa" | "meander" | "stub">("ifa"); const [antBusy, setAntBusy] = useState(false); const [antError, setAntError] = useState(null); useEffect(() => { let cancelled = false; fetchImpedanceNets(projectId) .then((r) => { if (!cancelled) setBoardNets(r); }) .catch(() => { if (!cancelled) setBoardNets(null); }); fetchAntennaReport(projectId) .then((r) => { if (!cancelled) setAntenna(r); }) .catch(() => { if (!cancelled) setAntenna(null); }); return () => { cancelled = true; }; }, [projectId]); async function runTrace() { setBusy(true); setError(null); try { const tz = targetZ.trim() === "" ? null : num(targetZ); const result = (await computeImpedance({ mode: "trace", kind, h: num(h), er: num(er), t: num(t), w: tz != null ? null : num(w), s: kind === "microstrip" || kind === "stripline" ? null : num(s), target_z: tz, })) as ImpedanceTraceResult; setTrace(result); if (result.w_mm != null) setW(result.w_mm.toFixed(4)); } catch (e) { setTrace(null); setError(e instanceof Error ? e.message : "Compute failed"); } finally { setBusy(false); } } async function runStackup() { setBusy(true); setError(null); try { const result = (await computeImpedance({ mode: "stackup", h: num(h), er: num(er), t: num(t), s: num(s), })) as ImpedanceStackupResult; setStackup(result); } catch (e) { setStackup(null); setError(e instanceof Error ? e.message : "Stackup failed"); } finally { setBusy(false); } } function downloadDru() { if (!stackup?.kicad_dru) return; const blob = new Blob([stackup.kicad_dru], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "periscope.kicad_dru"; a.click(); URL.revokeObjectURL(url); } async function runSpecifiedNets() { const names = extraNets .split(/[\s,]+/) .map((n) => n.trim()) .filter(Boolean); if (names.length === 0) return; setBusy(true); setError(null); try { setBoardNets(await analyzeImpedanceNets(projectId, names)); } catch (e) { setError(e instanceof Error ? e.message : "Net analysis failed"); } finally { setBusy(false); } } async function runAntennaDesign() { setAntBusy(true); setAntError(null); try { const f0n = f0.trim() === "" ? null : num(f0); const report = await designAntenna(projectId, { f0_mhz: f0n != null && !Number.isNaN(f0n) ? f0n : null, target_z_ohm: 50, h: num(h), er: num(er), t: num(t), template, }); setAntenna(report); } catch (e) { setAntError(e instanceof Error ? e.message : "Antenna design failed"); } finally { setAntBusy(false); } } function copyRecipe() { if (!antenna?.design) return; void navigator.clipboard.writeText(JSON.stringify(antenna.design, null, 2)); } function downloadKicadMod() { const mod = antenna?.design?.geometry?.kicad_mod; const name = antenna?.design?.geometry?.footprint_name ?? "Antenna"; if (!mod) return; const blob = new Blob([mod], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${name}.kicad_mod`; a.click(); URL.revokeObjectURL(url); } const needsGap = kind === "cpw" || kind === "diff"; const design = antenna?.design; return (
Verify antenna / RF feed

{antenna?.marker_help ?? "Matching from IC ANT/RF pins toward ANT* / ANT_FEED. Feed Z0 when PCB nets were analyzed."}

{antenna && antenna.verify.length === 0 && (

No RF ports detected on IC pins (ANT/RF…). Modules with an internal antenna may show nothing here — use Progetta with an ANT* marker.

)} {antenna && antenna.verify.length > 0 && (
{antenna.verify.map((row) => (
{row.ic_ref}.{row.pin} {row.net} {row.topology} {row.status}

{row.detail}

{row.parts.length > 0 && (

Parts: {row.parts.join(", ")}

)} {(row.feed_z0 != null || row.feed_length_mm != null) && (

Feed Z0 {fmt(row.feed_z0)} Ω · {fmt(row.feed_length_mm)} mm {row.marker_ref ? ` · marker ${row.marker_ref}` : ""}

)}
))}
)}
Progetta antenna (ricetta)

Mark the feed join in KiCad (ANT*{" "} footprint or net ANT_FEED). Optional zone net antenna. Choose a template (IFA / meander / stub) to get polylines, SVG preview, and a{" "} .kicad_mod — parametric routing aid, not an EM result.

{!hasPcb && ( )}
{antError &&

{antError}

} {design && (
{design.status} {design.geometry && ( {design.geometry.fit} )} {design.detail}
{design.feed_line && (

Feed microstrip @ {design.feed_line.target_z_ohm} Ω → w ={" "} {fmt(design.feed_line.w_mm, 4)} mm (h={fmt(design.feed_line.h_mm)}, εr={fmt(design.feed_line.er)})

)} {design.radiator?.length_mm_suggest != null && (

λ/4 suggest ≈ {fmt(design.radiator.length_mm_suggest)} mm at{" "} {fmt(design.radiator.f0_mhz, 0)} MHz — {design.radiator.note}

)} {design.geometry?.svg && (
)} {design.geometry && design.geometry.fit !== "need_f0" && (

{design.geometry.template.toUpperCase()} {design.geometry.total_length_mm != null ? ` · path ${fmt(design.geometry.total_length_mm)} mm` : ""} {design.geometry.length_ideal_mm != null ? ` · ideal λ/4 ${fmt(design.geometry.length_ideal_mm)} mm` : ""} {design.geometry.segments.length ? ` · ${design.geometry.segments.length} segment(s)` : ""} {" — "} {design.geometry.note}

)} {design.zone && (

Zone {design.zone.net} on {design.zone.layer} {design.zone.bbox_mm ? ` · bbox [${design.zone.bbox_mm.map((n) => n.toFixed(1)).join(", ")}]` : ""}

)} {design.keepout_checklist.length > 0 && (
    {design.keepout_checklist.map((c) => (
  • {c}
  • ))}
)}
)} Impedance calculator

ImpedenceFinder (Hammerstad–Jensen). The calculator is advice only. With a `.kicad_pcb` and stackup, a pipeline run samples routed signal nets. CPWG is not implemented upstream.

{error &&

{error}

} {trace && (

{trace.z0 != null && <>Z0 = {fmt(trace.z0)} Ω} {trace.zdiff != null && ( <> {" "} Zdiff = {fmt(trace.zdiff)} Ω (odd {fmt(trace.zodd)}, even{" "} {fmt(trace.zeven)}) )} {trace.w_mm != null && <> · w = {fmt(trace.w_mm, 4)} mm}

)}
PCB net Z0 (ImpedenceFinder) {!hasPcb && (

Upload a `.kicad_pcb`, then re-run the pipeline. Power/ground nets are skipped; signal traces with stackup εr/h are sampled.

)} {hasPcb && boardNets?.skipped && (

{boardNets.skipped}

)} {boardNets && boardNets.nets.length > 0 && ( {boardNets.nets.map((row) => ( ))}
Net Z0 avg Ω min–max mm topology
{row.net_name} {row.error ?? fmt(row.z0_avg_ohms)} {row.z0_min_ohms != null ? `${fmt(row.z0_min_ohms)}–${fmt(row.z0_max_ohms)}` : "—"} {fmt(row.length_mm, 2)} {(row.topologies || []).join(", ") || "—"}
)} {hasPcb && (
)}
{stackup && ( Suggested widths (apply in KiCad)
{Object.entries(stackup.targets).map(([k, v]) => (
{k}: w={fmt(v.w_mm, 4)} mm {v.z0 != null && <> · Z0={fmt(v.z0)}} {v.zdiff != null && <> · Zdiff={fmt(v.zdiff)}}
))}
)}
); }