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>
  }))
}