Devesh36/key-finder
A compact BLE key finder for keys, bags and remotes: replaceable CR2032, piezo alert, status LED and pair button in a 38 mm black keyring enclosure. Includes routed PCB, enclosed/exploded assembly and BOM. Firmware/app and physical qualification remain separate.
- Version
- 0.1.2
- License
- unset
- Stars
- 0
index.circuit.tsx
PCB
Schematic
import "tscircuit"
import {manualRouting} from "./manual-routing"
import {padOffsets} from "./pad-offsets"
import { Fragment } from "react"
import { ISP1807_LR_RS, pinLabels } from "./imports/ISP1807_LR_RS"
import { MY_2032_16 } from "./imports/MY_2032_16"
import { A_1TS026A_1600_0553_CT } from "./imports/A_1TS026A_1600_0553_CT"
import { XL_1608SURC_06 } from "./imports/XL_1608SURC_06"
// Native outline: 34 mm circle with a flat above the battery, leaving room for
// the enclosure's keyring eye. Rounded coordinates avoid scientific notation.
export const outline = Array.from({length:128},(_,i)=>{
const a=i*Math.PI/64
return {x:+(17*Math.cos(a)).toFixed(5),y:+Math.min(10.7,17*Math.sin(a)).toFixed(5)}
}).filter((p,i,a)=>i===0 || p.x!==a[i-1].x || p.y!==a[i-1].y)
const loc=(pcbX:number,pcbY:number,schX:number,schY:number,schSectionName:string)=>({pcbX:`${pcbX}mm`,pcbY:`${pcbY}mm`,schX:schX*0.45,schY:schY*0.45,schSectionName,schSheetName:"main"})
const grounds=[1,7,14,16,18,21,23,24,25,31]
const used=[...grounds,13,20,22,26,28,30,32,34,36,38]
export const nets:Record<string,string[]>={
VBAT:["BT1.pin1","BT1.pin2","U1.pin26","C1.pin1","C2.pin1","R4.pin1","R5.pin1","TP1.pin1"],
GND:["BT1.pin3",...grounds.map(p=>`U1.pin${p}`),"C1.pin2","C2.pin2","LED1.pin1","SW1.pin3","SW1.pin4","TP2.pin1"],
PIEZO_A:["U1.pin34","R1.pin1"], PIEZO_A_OUT:["R1.pin2","BZ1.pin1"],
PIEZO_B:["U1.pin36","R2.pin1"], PIEZO_B_OUT:["R2.pin2","BZ1.pin2"],
STATUS:["U1.pin32","R3.pin1"], LED_A:["R3.pin2","LED1.pin2"],
PAIR_N:["U1.pin38","R4.pin2","SW1.pin1","SW1.pin2"],
RESET_N:["U1.pin13","R5.pin2","TP5.pin1"],
SWDIO:["U1.pin28","TP3.pin1"], SWDCLK:["U1.pin30","TP4.pin1"],
}
type P={x:number,y:number,layer?:"top"|"bottom",via?:boolean,fromLayer?:"top"|"bottom",toLayer?:"top"|"bottom"}
const pt=(x:number,y:number):P=>({x,y})
const down=(x:number,y:number):P=>({x,y,via:true,fromLayer:"top",toLayer:"bottom"})
const up=(x:number,y:number):P=>({x,y,via:true,fromLayer:"bottom",toLayer:"top"})
const manualRoutes:[string,string,string,[number,number],P[]][]=[
["PIEZO_A_DRIVE",".U1 > .pin34",".R1 > .pin1",[0,-10.7],[pt(1.950085,-5.6),pt(0.975,-4.5)]],
["PIEZO_B_DRIVE",".U1 > .pin36",".R2 > .pin1",[0,-10.7],[pt(1.300099,-5.95),pt(-2.325,-5.95),pt(-2.325,-4.5)]],
["LED_DRIVE",".U1 > .pin32",".R3 > .pin1",[0,-10.7],[pt(2.600071,-5.4),pt(4.275,-5.4),pt(5.175,-4.5)]],
["LED_LIMITED",".R3 > .pin2",".LED1 > .pin2",[6,-4.5],[pt(10.75,-4.5),pt(10.75,-1)]],
["PIEZO_A_LEAD",".R1 > .pin2",".BZ1 > .pin1",[1.8,-4.5],[pt(2.625,-2.8),pt(-8.5,-2.8),pt(-8.5,2)]],
["PIEZO_B_LEAD",".R2 > .pin2",".BZ1 > .pin2",[-1.5,-4.5],[down(-0.675,-5.5),pt(-5.5,-5.5),up(-5.5,2)]],
["PAIR_SIGNAL",".U1 > .pin38",".R4 > .pin2",[0,-10.7],[down(0.650113,-6.35),pt(8,-6.35),up(8,3),pt(6.825,3)]],
["PAIR_SWITCH",".R4 > .pin2",".SW1 > .pin1",[6,3],[pt(7.5,4.375031),pt(8.675009,4.375031)]],
["PAIR_CONTACTS",".SW1 > .pin1",".SW1 > .pin2",[10,4],[pt(8.675009,3.624969)]],
["RESET_PAD",".R5 > .pin2",".TP5 > .pin1",[-6,-6.5],[down(-5.175,-6.5),pt(-5.175,-6),pt(-14,-6),pt(-14,7.8),pt(3,7.8),up(3,5)]],
["SWD_DATA",".U1 > .pin28",".TP3 > .pin1",[0,-10.7],[pt(13.7,-8.749915),pt(13.7,7),pt(-3,7),pt(-3,5)]],
["SWD_CLOCK",".U1 > .pin30",".TP4 > .pin1",[0,-10.7],[pt(4.5,-8.099929),pt(4.5,-6.2),pt(12.8,-6.2),pt(12.8,6.3),pt(0,6.3),pt(0,5)]],
]
export default function KeyFinderBoard(){return <board title="Key Finder" outline={outline} width={34} height={34}
thickness={1} layers={4} isViaInPadAllowed doubleSidedAssembly solderMaskColor="black" schTraceAutoLabelEnabled
autorouter={{local:true,algorithmFn:manualRouting,traceClearance:0.15,allowViaInPad:true}} minTraceWidth={0.15} minBoardEdgeClearance={0.3}
minViaHoleDiameter={0.2} minViaPadDiameter={0.4} minViaEdgeToPadEdgeClearance={0.15}>
<schematicsheet name="main" displayName="KEY FINDER • CR2032 / BLE / PIEZO" >
<schematicsection name="power" displayName=" "/>
<schematicsection name="radio" displayName=" "/>
<schematicsection name="alert" displayName=" "/>
<schematictext text="POWER / CR2032" schX={-9} schY={8} fontSize={0.3}/>
<schematictext text="BLE / PROGRAMMING" schX={0} schY={8} fontSize={0.3}/>
<schematictext text="ALERT / PAIR" schX={9} schY={8} fontSize={0.3}/>
</schematicsheet>
<ISP1807_LR_RS name="U1" {...loc(0,-10.7,0,0,"radio")} schWidth={5} schHeight={12}
noConnect={Array.from({length:78},(_,i)=>i+1).filter(p=>!used.includes(p)).map(p=>`pin${p}` as keyof typeof pinLabels)}
// VBUS is intentionally unpowered: no USB function. Imported automatic
// requiresPower metadata incorrectly marks VBUS mandatory for all designs.
>
<silkscreentext text="KEY FINDER" pcbX={0} pcbY={19.3} fontSize={0.85}/>
<silkscreentext text="ANTENNA - KEEP CLEAR" pcbX={0} pcbY={-4.8} fontSize={0.55}/>
<silkscreentext text="PAIR" pcbX={10} pcbY={17.3} fontSize={0.6}/>
{['VREF','GND','DIO','CLK','RST'].map((text,i)=><Fragment key={text}><silkscreentext text={text} pcbX={-9+i*3} pcbY={17.6} fontSize={0.5}/></Fragment>)}
</ISP1807_LR_RS>
<MY_2032_16 name="BT1" layer="bottom" {...loc(0,0,-22,12,"power")} />
<capacitor name="C1" capacitance="100nF" footprint="0603" manufacturerPartNumber="CL10B104KB8NNNC" {...loc(6.5,-7.8,-20,7,"power")}/>
<capacitor name="C2" capacitance="10uF" footprint="0805" manufacturerPartNumber="CL21A106KPFNNNE" {...loc(10.2,-7.6,-16,7,"power")}/>
<resistor name="R1" resistance="1k" footprint="0603" manufacturerPartNumber="RC0603FR-071KL" {...loc(1.8,-4.5,17,8,"alert")}/>
<resistor name="R2" resistance="1k" footprint="0603" manufacturerPartNumber="RC0603FR-071KL" {...loc(-1.5,-4.5,17,4,"alert")}/>
<resistor name="R3" resistance="3.3k" footprint="0603" manufacturerPartNumber="RC0603FR-073K3L" {...loc(6,-4.5,17,-2,"alert")}/>
<resistor name="R4" resistance="100k" footprint="0603" manufacturerPartNumber="RC0603FR-07100KL" {...loc(6,3,17,-8,"alert")}/>
<resistor name="R5" resistance="100k" footprint="0603" manufacturerPartNumber="RC0603FR-07100KL" {...loc(-6,-6.5,-14,-5,"radio")}/>
<XL_1608SURC_06 name="LED1" {...loc(10,-1,22,-2,"alert")}/>
<A_1TS026A_1600_0553_CT internallyConnectedPins={[["pin1","pin2"],["pin3","pin4"]]} name="SW1" {...loc(10,4,22,-8,"alert")}/>
{/* Off-board, two-wire piezo. Native pads are wire terminations, not an invented SMT package. */}
<chip name="BZ1" doNotPlace manufacturerPartNumber="AB1541B-LW100-R" pinLabels={{pin1:"PIEZO_A",pin2:"PIEZO_B"}}
{...loc(-7,2,24,6,"alert")} schWidth={3} schHeight={2}
footprint={<footprint>
<smtpad portHints={["pin1"]} shape="circle" radius={0.8} pcbX={-1.5}/>
<smtpad portHints={["pin2"]} shape="circle" radius={0.8} pcbX={1.5}/>
<courtyardoutline outline={[{x:-2.5,y:-1},{x:2.5,y:-1},{x:2.5,y:1},{x:-2.5,y:1}]}/>
<silkscreentext text="PIEZO A B" pcbY={1.8} fontSize={0.6}/>
</footprint>}/>
{['VTREF','GND','SWDIO','SWDCLK','RESET'].map((label,i)=><testpoint key={label} name={`TP${i+1}`} doNotPlace
footprintVariant="pad" padDiameter={1.2} {...loc(-9+i*3,5,-12+i*3,-15,"radio")}/>)}
{Object.entries(nets).map(([name,pins])=><Fragment key={name}>
<net name={name} isGroundNet={name==='GND'} isPowerNet={name==='VBAT'} nominalTraceWidth={0.15}/>
{pins.map(pin=>{const selector=`.${pin.split('.')[0]} > .${pin.split('.')[1]}`
const plane=name==='GND'?'inner1':name==='VBAT'?'inner2':undefined
return <Fragment key={pin}><trace from={selector} to={`net.${name}`} pcbPathRelativeTo={plane?selector:undefined}
pcbPath={plane?[{...padOffsets[pin],via:true,fromLayer:pin.startsWith('BT1.')?'bottom':'top',toLayer:pin.startsWith('BT1.')?'top':'bottom'}]:undefined}/></Fragment>
})}
</Fragment>)}
<trace name="RESET_ESCAPE" from=".U1 > .pin13" to=".R5 > .pin2" pcbPathRelativeTo=".U1 > .pin13"
pcbPath={[{x:-0.974979,y:0.850011,via:true,fromLayer:"top",toLayer:"bottom"},{x:-5.175,y:0.850011},{x:-5.175,y:4.2,via:true,fromLayer:"bottom",toLayer:"top"}]} thickness={0.15}/>
{/* Explicit signal paths keep the compact LGA fanout deterministic. */}
{manualRoutes.map(([name,from,to,origin,path])=><Fragment key={name}><trace name={name} from={from} to={to}
thickness={0.15} pcbPath={path.map(p=>({...p,x:p.x-origin[0],y:p.y-origin[1]}))}/></Fragment>)}
{/* Datasheet requires a short bridge between radio output and internal antenna. */}
<trace name="ANT_BRIDGE" from=".U1 > .pin20" to=".U1 > .pin22" pcbPath={[{x:1.300099,y:0.200025},{x:1.950085,y:0.200025}]} thickness={0.15}/>
{/* DS1807 p14: 18 mm-wide exclusion from module midline to board edge.
Only U1's isolated mechanical LGA pads may occupy this region. */}
<keepout shape="rect" width={18} height={6.29} pcbX={0} pcbY={-13.855} layers={["top","inner1","inner2","bottom"]} excludeRefs={[".U1"]}/>
<copperpour name="POWER" layer="inner2" connectsTo="net.VBAT" unbroken clearance={0.15} boardEdgeMargin={0.35}/>
<copperpour name="GROUND" layer="inner1" connectsTo="net.GND" unbroken clearance={0.15} boardEdgeMargin={0.35}/>
</board>}