Add RF antenna verify and design recipe to Impedance tab.

Detect matching toward ANT*/ANT_FEED, compute 50 Ω feed width from stackup, and optional λ/4 length from f0 — before auto-draw or F2 packing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-13 18:57:59 +02:00
co-authored by Cursor
parent 68af0fd432
commit 1d3cf12482
9 changed files with 1055 additions and 29 deletions
@@ -5,13 +5,17 @@ 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,
@@ -49,6 +53,10 @@ export function ImpedancePanel({
const [stackup, setStackup] = useState<ImpedanceStackupResult | null>(null);
const [boardNets, setBoardNets] = useState<ImpedanceNetsReport | null>(null);
const [extraNets, setExtraNets] = useState("");
const [antenna, setAntenna] = useState<AntennaReport | null>(null);
const [f0, setF0] = useState("2440");
const [antBusy, setAntBusy] = useState(false);
const [antError, setAntError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
@@ -59,6 +67,13 @@ export function ImpedancePanel({
.catch(() => {
if (!cancelled) setBoardNets(null);
});
fetchAntennaReport(projectId)
.then((r) => {
if (!cancelled) setAntenna(r);
})
.catch(() => {
if (!cancelled) setAntenna(null);
});
return () => {
cancelled = true;
};
@@ -137,10 +152,173 @@ export function ImpedancePanel({
}
}
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),
});
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));
}
const needsGap = kind === "cpw" || kind === "diff";
const design = antenna?.design;
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-sm">Verify antenna / RF feed</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-sm text-muted-foreground">
{antenna?.marker_help ??
"Matching from IC ANT/RF pins toward ANT* / ANT_FEED. Feed Z0 when PCB nets were analyzed."}
</p>
{antenna && antenna.verify.length === 0 && (
<p className="text-sm text-muted-foreground">
No RF ports detected on IC pins (ANT/RF). Modules with an internal
antenna may show nothing here use Progetta with an ANT* marker.
</p>
)}
{antenna && antenna.verify.length > 0 && (
<div className="space-y-2">
{antenna.verify.map((row) => (
<div
key={`${row.ic_ref}-${row.pin}-${row.net}`}
className="rounded-lg border p-3 text-sm space-y-1"
>
<div className="flex flex-wrap items-center gap-2">
<span className="font-mono font-medium">
{row.ic_ref}.{row.pin}
</span>
<code className="text-xs">{row.net}</code>
<Badge variant="outline">{row.topology}</Badge>
<Badge
variant={row.status === "warning" ? "destructive" : "secondary"}
>
{row.status}
</Badge>
</div>
<p className="text-xs text-muted-foreground">{row.detail}</p>
{row.parts.length > 0 && (
<p className="text-xs text-muted-foreground">
Parts: {row.parts.join(", ")}
</p>
)}
{(row.feed_z0 != null || row.feed_length_mm != null) && (
<p className="text-xs tabular-nums">
Feed Z0 {fmt(row.feed_z0)} Ω · {fmt(row.feed_length_mm)} mm
{row.marker_ref ? ` · marker ${row.marker_ref}` : ""}
</p>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm">Progetta antenna (ricetta)</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-sm text-muted-foreground">
Mark the feed join in KiCad (<code className="text-xs">ANT*</code>{" "}
footprint or net <code className="text-xs">ANT_FEED</code>). Optional
zone net <code className="text-xs">antenna</code>. Auto-draw in the
zone comes later this returns w / Z0 / length to draw by hand.
</p>
{!hasPcb && (
<PcbUploadButton projectId={projectId} onUploaded={onPcbUploaded} />
)}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<label className="space-y-1">
<Label>f0 (MHz)</Label>
<Input value={f0} onChange={(e) => setF0(e.target.value)} placeholder="2440" />
</label>
<label className="space-y-1">
<Label>h (mm)</Label>
<Input value={h} onChange={(e) => setH(e.target.value)} />
</label>
<label className="space-y-1">
<Label>εr</Label>
<Input value={er} onChange={(e) => setEr(e.target.value)} />
</label>
<label className="space-y-1">
<Label>t (mm)</Label>
<Input value={t} onChange={(e) => setT(e.target.value)} />
</label>
</div>
<div className="flex flex-wrap gap-2">
<Button onClick={runAntennaDesign} disabled={antBusy}>
{antBusy ? "Computing…" : "Compute recipe"}
</Button>
<Button
variant="outline"
onClick={copyRecipe}
disabled={!design || design.status !== "ready"}
>
Copy JSON
</Button>
</div>
{antError && <p className="text-sm text-destructive">{antError}</p>}
{design && (
<div className="rounded-lg border p-3 text-sm space-y-2">
<div className="flex flex-wrap gap-2 items-center">
<Badge variant="secondary">{design.status}</Badge>
<span className="text-muted-foreground text-xs">{design.detail}</span>
</div>
{design.feed_line && (
<p className="tabular-nums">
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)})
</p>
)}
{design.radiator?.length_mm_suggest != null && (
<p className="tabular-nums text-xs text-muted-foreground">
λ/4 suggest {fmt(design.radiator.length_mm_suggest)} mm at{" "}
{fmt(design.radiator.f0_mhz, 0)} MHz {design.radiator.note}
</p>
)}
{design.zone && (
<p className="text-xs text-muted-foreground">
Zone {design.zone.net} on {design.zone.layer}
{design.zone.bbox_mm
? ` · bbox [${design.zone.bbox_mm.map((n) => n.toFixed(1)).join(", ")}]`
: ""}
</p>
)}
{design.keepout_checklist.length > 0 && (
<ul className="list-disc pl-4 text-xs text-muted-foreground space-y-0.5">
{design.keepout_checklist.map((c) => (
<li key={c}>{c}</li>
))}
</ul>
)}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm">Impedance calculator</CardTitle>
@@ -297,30 +475,17 @@ export function ImpedancePanel({
<CardTitle className="text-sm">Suggested widths (apply in KiCad)</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-muted-foreground">
<th className="py-1">Target</th>
<th>w mm</th>
<th>s mm</th>
<th>Z</th>
</tr>
</thead>
<tbody>
{Object.entries(stackup.targets).map(([key, row]) => (
<tr key={key} className="border-t border-border">
<td className="py-1">{key}</td>
<td className="tabular-nums">{fmt(row.w_mm, 4)}</td>
<td className="tabular-nums">{fmt(row.s_mm, 4)}</td>
<td className="tabular-nums">
{row.z0 != null ? `${fmt(row.z0)} Ω` : `${fmt(row.zdiff)} Ω diff`}
</td>
</tr>
))}
</tbody>
</table>
<Button variant="outline" onClick={downloadDru}>
Download pinscope.kicad_dru
<div className="grid gap-1 text-sm tabular-nums">
{Object.entries(stackup.targets).map(([k, v]) => (
<div key={k}>
{k}: w={fmt(v.w_mm, 4)} mm
{v.z0 != null && <> · Z0={fmt(v.z0)}</>}
{v.zdiff != null && <> · Zdiff={fmt(v.zdiff)}</>}
</div>
))}
</div>
<Button variant="outline" size="sm" onClick={downloadDru}>
Download .kicad_dru advice
</Button>
</CardContent>
</Card>