muse/book-reading-clip-lamp
A Wi‑Fi/USB‑C ESP32‑S3 reading lamp with OV5640 camera, microphone, speaker amplifier, rechargeable Li‑Po power management, six-LED boost light, USB protection/charging detection, and a clip/gooseneck enclosure with external FlexPIFA antenna.
- Version
- 0.4.3
- License
- unset
- Stars
- 0
schematic-layout.tsx
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>
// Measured circuit-content centers (symbols, labels and routed wires), excluding
// the page annotations. Place each block below a reserved heading band on a
// fixed ANSI B page. check:schematic also checks the complete drawing bounds.
const sectionCenters: Record<string, { x: number; y: number }> = {
USB: { x: -23.175, y: 7.71 },
USBPower: { x: 0.6, y: 0.986 },
MCU: { x: 8.1, y: 9.5 },
Charger: { x: -19.644, y: -10.415 },
Regulator: { x: -19.5, y: -29.54 },
Camera: { x: -2.275, y: -5.86 },
CameraPower: { x: 0.1, y: -1.41 },
Audio: { x: 28.68, y: -9.47 },
Light: { x: 33.11, y: 14.378 },
Controls: { x: 5.955, y: -27.576 },
}
const schematicOverrides: Record<string, ElementProps> = {
U7: { schX: -25 },
U3: { schWidth: 1.5 },
U15: { schWidth: 2.525 },
U16: { schWidth: 1.38 },
R42: { schX: 14, schY: -1 },
R41: { schX: 2.2, schY: -3.5 },
C39: { schX: -12.8 },
C40: { schX: 4.5, schY: -10 },
C41: { schX: 6.3, schY: -10 },
C42: { schX: 8.1, schY: -10 },
C44: { schX: 9.9, schY: -10 },
D1: { schRotation: -90, schX: 33 },
// 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: { schWidth: 1.6, 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 },
Q1: { schX: -20, schY: 3, schWidth: 1.36 },
// Fold the ordered LED string back toward the driver feedback pin.
LED1: { schX: 30, schY: 26, schRotation: 0 },
LED2: { schX: 35, schY: 26, schRotation: 0 },
LED3: { schX: 40, schY: 26, schRotation: 0 },
LED4: { schX: 40, schY: 22, schRotation: 180 },
LED5: { schX: 35, schY: 22, schRotation: 180 },
LED6: { schX: 30, schY: 22, schRotation: 180 },
// Keep chip-to-ground resistor branches beside the pins they configure.
R5: { schX: -17.5, schY: -4 },
R6: { schX: -17.5, schY: -8 },
R12: { schX: 0, schY: 2.6 },
C2: { schX: -14, schY: -9 },
U1: {
schHeight: 4.2,
schPinArrangement: {
leftSide: Array.from({ length: 20 }, (_, i) => i + 1),
rightSide: Array.from({ length: 20 }, (_, i) => i + 21),
bottomSide: [41],
},
},
// Compact each rail/support path in the schematic. These overrides never
// change PCB coordinates, footprints, pin numbering or electrical nets.
R25: { schX: -26, schRotation: -90 },
C36: { schY: -23.5 },
L1: { schX: -18 },
R9: { schX: -1, schY: 9 },
U11: { schX: 7 },
R32: { schX: 3, schY: 0 },
L2: { schX: 33 },
// Keep each local rail's decoupling bank together in the schematic only.
C4: { schX: -27.8, schY: -34 },
C5: { schX: -26, schY: -34 },
C6: { schX: -24.2, schY: -34 },
C7: { schX: -14.8, schY: -34 },
C8: { schX: -13, schY: -34 },
C9: { schX: -11.2, schY: -34 },
C10: { schX: -1.8, schY: 17 },
C11: { schX: 0, schY: 17 },
C13: { schX: -10.8, schY: -3 },
C22: { schX: -7.2, schY: -7 },
C26: { schX: -3.6, schY: -7 },
C27: { schX: -4.2, schY: -13 },
C31: { schX: -0.6, schY: -13 },
C15: { schX: 25, schY: -15 },
C16: { schX: 26.8, 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.",
"U15 supplies CC sink termination; U14 detects chargers before USB data.",
"Q1 senses cable power as VBUS_N. SBU pins are intentionally unused.",
] },
{ section: "USBPower", title: "Automatic USB charging-source detection", notes: [
"U14 identifies BC1.2 chargers; U15 detects USB-C 1.5 A / 3 A offers.",
"U17 selects 500 mA for either detector or an enumerated USB host.",
"U18 runs from USB with the lamp OFF. No USB-PD; NTC remains required.",
] },
{ section: "Charger", title: "Battery charging and clip harness", notes: [
"U2 charges the protected 1S cell and feeds the SYS power-path rail.",
"J3: 1/2 battery; 3/4 NTC; 5/6 speaker; 7/8 Chat/power.",
"Detected chargers select 500 mA input; unknown sources stay at 100 mA.",
] },
{ section: "Regulator", title: "3.3 V supply and remote power switch", notes: [
"U3 and L1 convert SYS into the regulated V3V3 logic/camera supply.",
"Base SW3: J3.8 to J3.2 = OFF/charge; open = ON via R25 pull-up.",
"R36/C37 filter remote EN. Charging stays active; open harness = ON.",
] },
{ 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.",
"PSRAM pins 28-30 stay NC. ANT1 (Ezurio 001-0014) plugs into U1 U.FL.",
] },
{ section: "Camera", title: "Direct FPC camera and signal-level conversion", notes: [
"J2: Seeed 114993115 OV5640 autofocus ribbon; pin 23 is AF ground.",
"U10/U11 translate 1.8 V data/sync to 3.3 V; U12 drives 1.8 V controls.",
"U13 translates SCCB. RESET low and PWDN high are the startup defaults.",
] },
{ section: "CameraPower", title: "Camera I/O, analog and internal core power", notes: [
"U9 supplies 1.8 V DOVDD; R34/C32 delay U8's 2.8 V AVDD/AF supply.",
"Pin 10 uses the camera's internal 1.5 V regulator, decoupled by C24/C25.",
"Verify rail ramps; wait, start XCLK, release PWDN/RESET, 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: [
"Base SW1: momentarily join J3.7 to J3.2 for Chat. SW2 stays in head.",
"R35/C36 filter Chat; R18 pulls up. R20/R21/C21 sense battery voltage.",
"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 center = sectionCenters[section]
return <React.Fragment key={`sheet-${section}`}><schematicsheet name={section}
displayName={`${sheetIndex + 1}. ${sheet.title}`} sheetIndex={sheetIndex} sheetSize="ANSI_B"
sheetWidth={431.8} sheetHeight={279.4}>
<group name={`Functional_${section}`} pcbX={0} pcbY={0} schX={-center.x} schY={-center.y - 1}
pcbLayout={{ layoutMode: "none" }} schLayout={{ layoutMode: "none" }}>
{/* The explicit sheet title and notes below label this section. Auto
captions use component-only bounds and can land on routed rails. */}
<schematicsection name={section}/>
{members}
</group>
<schematictext text={`${String(sheetIndex + 1).padStart(2,"0")} / ${sheet.title}`}
schX={-21.5} schY={14} fontSize={0.55} anchor="top_left"/>
{sheet.notes.map((text, i) => <React.Fragment key={`note-${i}`}><schematictext text={text}
schX={-21.5} schY={12.9 - i * 0.6} fontSize={0.32} anchor="top_left"/></React.Fragment>)}
<schematictext text={`Book Reading Clip Lamp | Sheet ${sheetIndex + 1}/${schematicSheets.length} | Named nets connect between sheets`}
schX={-21.5} schY={-13.8} fontSize={0.3} anchor="top_left"/>
</schematicsheet></React.Fragment>
}))
}