muse/book-reading-clip-lamp
A 60‑mm ESP32‑S3 lamp-head PCB combining USB‑C battery charging/power regulation, OV5640 autofocus camera interfacing, I²S microphone and differential speaker audio, six‑LED boost-driven reading light, buttons, status indicators, and battery monitoring.
- Version
- 0.2.5
- License
- unset
- Stars
- 0
schematic-layout.tsx
Schematic
import React, { type ReactElement, type ReactNode } from "react"
// Each functional block gets a real schematic sheet and section. The existing
// non-subcircuit groups retain their source order and zero PCB transforms, so
// all named nets remain board-wide without moving PCB components.
type ElementProps = Record<string, any>
const schematicOverrides: Record<string, ElementProps> = {
D1: { schRotation: -90 },
// Keep the built-in USB-C symbol; put shell contacts below signals on the
// same side to avoid shell/signal label collisions in this renderer.
J1: { schPinArrangement: { rightSide: [16,15,6,12,8,10,9,7,11,5,13,14,1,2,3,4] } },
R3: { schX: -24, schY: 4 },
R4: { schX: -24, schY: 1.5 },
LED1: { schY: 19 }, LED2: { schY: 19 }, LED3: { schY: 19 },
LED4: { schY: 22 }, LED5: { schY: 22 },
LED6: { schRotation: 180, schY: 22 },
U1: {
schHeight: 4.2,
schPinArrangement: {
leftSide: Array.from({ length: 20 }, (_, i) => i + 1),
rightSide: Array.from({ length: 20 }, (_, i) => i + 21),
bottomSide: [41],
},
},
// Keep each local rail's decoupling bank together in the schematic only.
C4: { schX: -28, schY: -34 },
C5: { schX: -26, schY: -34 },
C6: { schX: -24, schY: -34 },
C7: { schX: -15, schY: -34 },
C8: { schX: -13, schY: -34 },
C9: { schX: -11, schY: -34 },
C10: { schX: -2, schY: 17 },
C11: { schX: 0, schY: 17 },
C15: { schX: 25, schY: -15 },
C16: { schX: 27, schY: -15 },
}
export const schematicSheets = [
{ section: "USB", title: "USB-C input and USB data", notes: [
"J1 supplies VBUS and carries USB 2.0 data to the ESP32-S3.",
"R1/R2 advertise a sink; U7 protects D+/D-; R3/R4 are series resistors.",
"Q1 senses cable power as VBUS_N. SBU pins are intentionally unused.",
] },
{ section: "Charger", title: "Battery charging and clip harness", notes: [
"U2 charges the protected 1S cell and feeds the SYS power-path rail.",
"J3 carries battery, cell thermistor and differential speaker wiring.",
"Reset defaults to 100 mA USB input. The cell-mounted NTC is required.",
] },
{ section: "Regulator", title: "3.3 V supply and charge-only switch", notes: [
"U3 and L1 convert SYS into the regulated V3V3 logic/camera supply.",
"SW3 disables V3V3 for charge-only operation; charging stays active.",
"Input/output capacitors support load steps. SYS also feeds audio/light.",
] },
{ section: "MCU", title: "ESP32-S3 controller and boot configuration", notes: [
"U1 runs Wi-Fi, camera capture, I2S audio and the Muse application.",
"Pull resistors set reset/boot states; capacitors decouple V3V3 and EN.",
"N16R8 PSRAM pins 28-30 stay unconnected. RF uses an external antenna.",
] },
{ section: "Camera", title: "Direct FPC camera and signal-level conversion", notes: [
"J2: KLT-PAA40-OV5640-1B V1.0 ESP32, 24-pin DVP autofocus ribbon.",
"U10/U11 buffer 2.8 V images to 3.3 V; U12 shifts clock/reset/powerdown.",
"U13 translates SCCB. RESET low and PWDN high are the startup defaults.",
] },
{ section: "CameraPower", title: "Camera 2.8 V and 1.5 V power supplies", notes: [
"U8 supplies AVDD, DOVDD and autofocus pin 24 with regulated 2.8 V.",
"U9 supplies external 1.5 V core power; its enable follows CAM2V8.",
"Wait for rails, start XCLK, release PWDN then RESET, then initialize AF.",
] },
{ section: "Audio", title: "Microphone and spoken-response output", notes: [
"U4 sends microphone samples to the MCU; U5 drives the clip speaker.",
"BCLK/WS are shared I2S clocks, with separate MIC_DATA and SPK_DATA.",
"AMP_EN controls shutdown. SPK_P/SPK_N are differential: neither is GND.",
] },
{ section: "Light", title: "Reading-light boost driver and LED string", notes: [
"U6, L2 and D1 boost SYS to drive six series warm-white reading LEDs.",
"R17 sets about 15 mA; LED_PWM controls brightness from the MCU.",
"C19 stabilizes the control loop; C20 filters the boosted LED_ANODE rail.",
] },
{ section: "Controls", title: "Buttons, status and battery measurement", notes: [
"SW1 requests an AI interaction; SW2 controls light; LED7 shows status.",
"R20/R21 divide VBAT for BAT_ADC; C21 filters this voltage measurement.",
"TP1-TP6 expose power, boot/reset and cable-detect signals for bring-up.",
] },
] as const
function flatten(children: ReactNode): ReactElement<ElementProps>[] {
const result: ReactElement<ElementProps>[] = []
React.Children.forEach(children, (child) => {
if (!React.isValidElement<ElementProps>(child)) return
if (child.type === React.Fragment) result.push(...flatten(child.props.children))
else result.push(child)
})
return result
}
/** Wrap an existing board element; only schematic props and grouping change. */
export function withFunctionalSchematicSheets(board: ReactElement<ElementProps>) {
const children = flatten(board.props.children)
const sectionByRef = new Map<string, string>()
for (const child of children) {
if (child.props.name && child.props.schSectionName) {
sectionByRef.set(child.props.name, child.props.schSectionName)
}
}
const groups = new Map<string, ReactNode[]>()
const outside: ReactNode[] = []
for (const [i, child] of children.entries()) {
// Trace selectors created by netLinks are "REF.pin" -> "net.NAME".
// Keeping each trace beside its component gives it the same local scope.
const traceOwner = child.type === "trace"
? String(child.props.from ?? "").split(".")[0].replace(/^\./, "")
: undefined
const section = child.props.schSectionName ?? sectionByRef.get(traceOwner ?? "")
const updated = React.cloneElement(child, {
...schematicOverrides[child.props.name],
key: child.key ?? `schematic-child-${i}`,
})
if (!section) outside.push(updated)
else {
if (!groups.has(section)) groups.set(section, [])
groups.get(section)!.push(updated)
}
}
return React.cloneElement(board, {}, ...outside, ...Array.from(groups, ([section, members]) => {
const sheetIndex = schematicSheets.findIndex(sheet => sheet.section === section)
const sheet = schematicSheets[sheetIndex]
if (!sheet) throw new Error(`No schematic sheet is defined for ${section}`)
const parts = members.filter((member): member is ReactElement<ElementProps> =>
React.isValidElement<ElementProps>(member) && Boolean(member.props.schSectionName))
const minX = Math.min(...parts.map(part => Number(part.props.schX ?? 0)))
const minY = Math.min(...parts.map(part => Number(part.props.schY ?? 0)))
const maxY = Math.max(...parts.map(part => Number(part.props.schY ?? 0)))
return <React.Fragment key={`sheet-${section}`}><schematicsheet name={section}
displayName={`${sheetIndex + 1}. ${sheet.title}`} sheetIndex={sheetIndex} sheetSize="ANSI_B">
<group name={`Functional_${section}`} pcbX={0} pcbY={0} schX={0} schY={0}
pcbLayout={{ layoutMode: "none" }} schLayout={{ layoutMode: "none" }}>
<schematicsection name={section} displayName={sheet.title}/>
{members}
<schematictext text={`${String(sheetIndex + 1).padStart(2,"0")} / ${sheet.title}`}
schX={minX - 2} schY={maxY + 7.3} fontSize={0.65} anchor="top_left"/>
{sheet.notes.map((text, i) => <React.Fragment key={`note-${i}`}><schematictext text={text}
schX={minX - 2} schY={maxY + 6.1 - i * 0.8} fontSize={0.38} anchor="top_left"/></React.Fragment>)}
<schematictext text={`Book Reading Clip Lamp | Sheet ${sheetIndex + 1}/${schematicSheets.length} | Named nets connect between sheets`}
schX={minX - 2} schY={minY - 3} fontSize={0.32} anchor="top_left"/>
</group>
</schematicsheet></React.Fragment>
}))
}