hrithik18k/smartknob
A two-board SmartKnob assembly combining USB-C programming, regulated power, CH340C USB-UART, HX711 force sensing, MT6701 magnetic angle sensing, VEML7700 ambient-light sensing, MOSFET/BJT drivers, an SK6812 RGB LED, connectors, and supporting passives.
- Version
- 1.0.5
- License
- Apache-2.0
- Stars
- 0
smartknob-board.tsx
import { importedParts } from './imported-parts'
/** Preserve the released KiCad land patterns. Supplier models supply MPN/BOM/3D data;
* geometry and physical-pad aliases are deliberately overridden for exact placement.
*/
function ReferenceFootprint({ component: c }: { component: any }) {
return <footprint insertionDirection={c.value.includes("USB") ? "from_y_neg" : "from_above"}>{c.pads.flatMap((p: any, i: number) => {
const common = { pcbX: p.x, pcbY: p.y, portHints: [p.pin] }
if (p.type === 'thru_hole') {
if (p.drill[0] === 'oval') return <platedhole {...common} shape="pill" outerWidth={p.width} outerHeight={p.height} holeWidth={Number(p.drill[1])} holeHeight={Number(p.drill[2])} pcbRotation={p.rotation} />
return <platedhole {...common} shape="circle" outerDiameter={p.width} holeDiameter={Number(p.drill[0])} />
}
if (p.shape === 'circle') return <smtpad {...common} layer={p.layer} shape="circle" radius={p.width / 2} coveredWithSolderMask={p.mask} />
if (p.shape === 'oval') {
return <smtpad {...common} layer={p.layer} shape="rotated_pill" width={p.width} height={p.height} radius={Math.min(p.width,p.height)/2} ccwRotation={p.rotation} coveredWithSolderMask={p.mask} />
}
if (p.shape === 'custom') return <smtpad {...common} layer={p.layer} shape="polygon" points={p.polygon} coveredWithSolderMask={p.mask} />
return <smtpad {...common} layer={p.layer} shape="rotated_rect" width={p.width} height={p.height} ccwRotation={p.rotation} cornerRadius={p.radius ?? 0} coveredWithSolderMask={p.mask} />
})}</footprint>
}
function Part({ component: c }: { component: any }) {
const Component = c.importKey ? importedParts[c.importKey] : c.name === 'J2' && c.value.includes('USB') ? 'connector' : 'chip'
const props: any = {
name: c.name, ...(!c.importKey ? {manufacturerPartNumber: c.value} : {}),
pcbX: c.x, pcbY: c.y, ...(c.layer === 'bottom' ? {layer: 'bottom'} : {}),
schX: c.schX, schY: c.schY, ...(c.sheet ? { schSheetName: c.sheet } : { schSectionName: c.section }),
schPinArrangement: {leftSide: {pins: Object.keys(c.pinLabels), direction: "top-to-bottom"}},
pinLabels: c.pinLabels, pinAttributes: c.pinAttributes,
shouldBeOnEdgeOfBoard: c.value.includes("USB"),
allowOffBoard: c.value.includes("USB"),
noConnect: c.noConnect, doNotPlace: c.doNotPlace,
symbol: undefined, symbolName: undefined,
footprint: <ReferenceFootprint component={c} />,
pcbStyle: { silkscreenTextVisibility: 'hidden' },
...(c.lcsc ? {supplierPartNumbers: {jlcpcb: [c.lcsc]}} : {}),
...(c.name === 'NT1' ? {internallyConnectedPins: [['pin1', 'pin2']]} : {}),
}
return <Component {...props} />
}
/** Both boards have independent net scopes and should be built as separate entrypoints. */
export function SmartKnobBoard({ data }: { data: any }) {
const sheetNames = data.title.includes('Base')
? ['USB-C connector', 'USB and programming', 'Power', 'MCU and interfaces',
'Motor driver', 'Force sensing', 'Magnetic encoder', 'RGB ring', 'Test points', 'Mounting']
: []
const sheetFor = (c: any) => c.name === 'J2' ? 'USB-C connector' : c.section
const components = data.components.map((c: any) => {
if (!sheetNames.length) return c
const members = data.components.filter((part: any) => sheetFor(part) === sheetFor(c))
const minX = Math.min(...members.map((part: any) => part.schX))
const maxX = Math.max(...members.map((part: any) => part.schX))
const minY = Math.min(...members.map((part: any) => part.schY))
const maxY = Math.max(...members.map((part: any) => part.schY))
return { ...c, sheet: sheetFor(c), schX: (c.schX - (minX + maxX) / 2) * (c.section === 'RGB ring' ? 0.25 : 0.4),
schY: (c.schY - (minY + maxY) / 2) * (c.section === 'RGB ring' ? 0.25 : 0.45) }
})
return <board width={Math.max(...data.outline.map((p: any) => p.x))-Math.min(...data.outline.map((p: any) => p.x))} height={Math.max(...data.outline.map((p: any) => p.y))-Math.min(...data.outline.map((p: any) => p.y))} title={data.title} outline={data.outline} thickness={data.thickness} layers={2} isViaInPadAllowed
solderMaskColor="white" silkscreenColor="black" routingDisabled routeRemaining={false}
// The checker boxes angled LED footprints axis-aligned and flags clear
// flexure slots. Exact pad/body-to-cutout geometry is audited separately.
placementDrcChecksDisabled={data.title.includes('Base')}
pcbStyle={{silkscreenTextVisibility:'hidden'}}>
{sheetNames.length
? sheetNames.map((name, sheetIndex) => <schematicsheet name={name} displayName={name} sheetIndex={sheetIndex} sheetWidth={100} sheetHeight={100} />)
: data.sections.map((name: string) => <schematicsection name={name} />)}
{data.cutouts.map((points: any, i: number) => <cutout shape="polygon" points={points} />)}
{data.holes.map((h: any, i: number) => <hole pcbX={h.x} pcbY={h.y} diameter={h.diameter} />)}
{components.map((c: any) => <Part component={c} />)}
{/* One source trace per net, in stable data.nets order. validate.py asserts
that each explicit copper segment resolves to the expected source net. */}
{data.nets.map((n: any) => <trace path={[...n.pins, `net.${n.name}`]} />)}
{/* Core accepts the circuit-json source link; current JSX declarations omit it. */}
{data.traces.map((t: any) => <pcbtrace {...({source_trace_id: `source_trace_${t.sourceIndex}`, route: t.route} as any)} />)}
{data.vias.map((v: any, i: number) => <via pcbX={v.x} pcbY={v.y} holeDiameter={v.drill} outerDiameter={v.diameter} fromLayer="top" toLayer="bottom" connectsTo={`net.${v.net}`} />)}
{data.pours.map((p: any, i: number) => <copperpour name={`POUR_${i}`} layer={p.layer} connectsTo={`net.${p.net}`} outline={p.outline} clearance={0} padMargin={0} traceMargin={0} boardEdgeMargin={0.22} cutoutMargin={0} />)}
</board>
}