laydas/grow_cabinet_controller

An ESP32-C6-based grow-cabinet controller board with four PWM/tach fan outputs, two filtered 0–10 V light-dimmer outputs, a 12 V master relay, protected dual 5 V supplies, USB power, and I²C/GPIO headers.

Version
0.0.1
License
unset
Stars
0

index.tsx

PCBPCB preview for index.tsx
SchematicSchematic preview for index.tsx
/**
 * Grow Cabinet Controller (lean) — ESP32-C6-DevKitC-1
 *
 * Only the parts that do the job.  All through-hole.  Single 12V DC input.
 *
 *  - 4x PC fan headers.  PWM driven straight from 3.3V GPIO, tach read with the ESP's internal pull-up.
 *  - 2x 0-10V dimmer outputs (RJ11 jacks) for the ViparSpectra P1000 dimming ports:
 *      2N7000 chops 12V, 10k pull-up + 68k to ground caps the output at ~10.5V, 10uF smooths it.
 *  - 1x master relay (G5LE-14 DC12) switching the outlet hot: NPN driver, base resistor, flyback diode.
 *  - 12V in on a screw terminal -> one Recom R-78B5.0-1.5 -> 5V to the DevKit and to a 5V terminal
 *    for the display.
 *  - 1x I2C header (3V3 GND SDA SCL) for the SHT31 breakout (breakout carries its own pull-ups).
 *
 * GPIO map:
 *   FAN1..4 PWM  = IO18 IO19 IO20 IO21   (direct drive, NOT inverted)
 *   FAN1..4 TACH = IO22 IO23 IO10 IO11   (internal pull-up)
 *   DIM1 DIM2    = IO2  IO3              (inverted: 100% duty = 0V = light off)
 *   RELAY        = IO1                   (high = relay on)
 *   I2C SDA/SCL  = IO6 / IO7
 */

import { Fragment } from "react"

const FP = {
  to92: "kicad:Package_TO_SOT_THT/TO-92_Inline",
  resV: "kicad:Resistor_THT/R_Axial_DIN0207_L6.3mm_D2.5mm_P2.54mm_Vertical",
  res: "kicad:Resistor_THT/R_Axial_DIN0207_L6.3mm_D2.5mm_P10.16mm_Horizontal",
  capElec: "kicad:Capacitor_THT/CP_Radial_D5.0mm_P2.50mm",
  do41: "kicad:Diode_THT/D_DO-41_SOD81_P10.16mm_Horizontal",
  relay: "kicad:Relay_THT/Relay_SPDT_Omron-G5LE-1",
  rj11: "kicad:Connector_RJ/RJ14_Connfly_DS1133-S4_Horizontal",
  fanHeader: "kicad:Connector_Molex/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical",
  recom: "kicad:Converter_DCDC/Converter_DCDC_RECOM_R-78E-0.5_THT",
  term508: "kicad:TerminalBlock_Phoenix/TerminalBlock_Phoenix_MKDS-1,5-2-5.08_1x02_P5.08mm_Horizontal",
  term762: "pinrow2_p7.62mm_id1.3mm_od2.6mm",
  hdr4: "pinrow4",
}

// KiCad footprints are anchored on pad 1.  at() takes a desired CENTRE and returns the anchor.
const CENTER_OFFSET: Record<string, [number, number]> = {
  to92: [1.27, 0], resV: [1.27, 0], res: [5.08, 0], capElec: [1.25, 0], do41: [5.08, 0],
  relay: [0, -8.65], rj11: [1.55, 1.8], fanHeader: [3.81, 0], recom: [2.45, 2.25],
  term508: [2.55, 0.2], term762: [0, 0], hdr4: [0, 0],
}
const at = (fp: keyof typeof FP, cx: number, cy: number, rot = 0) => {
  const [dx, dy] = CENTER_OFFSET[fp]
  const r = (rot * Math.PI) / 180
  const ox = dx * Math.cos(r) - dy * Math.sin(r)
  const oy = dx * Math.sin(r) + dy * Math.cos(r)
  return { footprint: FP[fp], pcbX: +(cx - ox).toFixed(3), pcbY: +(cy - oy).toFixed(3), pcbRotation: rot }
}

// KiCad diode footprints: pad 1 = cathode (band), pad 2 = anode
const K = "pin1"
const A = "pin2"

// ---------- DevKitC-1 socket footprint (antenna LEFT, USB-C RIGHT) ----------
const ROW_Y = 22.86 / 2
const PIN1_X = -19.05
const J1_PINS = ["3V3", "RST", "IO4", "IO5", "IO6", "IO7", "IO0", "IO1", "IO8", "IO10", "IO11", "IO2", "IO3", "5V", "GND_A", "NC_A"]
const J3_PINS = ["GND_B", "TX", "RX", "IO15", "IO23", "IO22", "IO21", "IO20", "IO19", "IO18", "IO9", "GND_C", "IO13", "IO12", "GND_D", "NC_B"]
const devkitPinLabels: Record<string, string> = {}
J1_PINS.forEach((n, i) => (devkitPinLabels[`pin${i + 1}`] = n))
J3_PINS.forEach((n, i) => (devkitPinLabels[`pin${i + 17}`] = n))

const DevKitFootprint = () => (
  <footprint>
    {J1_PINS.map((n, i) => (
      // @ts-expect-error key
      <platedhole key={`j1-${i}`} shape="circle" holeDiameter="1.0mm" outerDiameter="1.8mm"
        pcbX={PIN1_X + i * 2.54} pcbY={-ROW_Y} portHints={[`pin${i + 1}`]} />
    ))}
    {J3_PINS.map((n, i) => (
      // @ts-expect-error key
      <platedhole key={`j3-${i}`} shape="circle" holeDiameter="1.0mm" outerDiameter="1.8mm"
        pcbX={PIN1_X + i * 2.54} pcbY={ROW_Y} portHints={[`pin${i + 17}`]} />
    ))}
    <silkscreenrect pcbX={0} pcbY={0} width="51.8mm" height="25.4mm" strokeWidth="0.2mm" filled={false} />
    <silkscreentext text="ESP32-C6-DevKitC-1   antenna <- | -> USB" pcbX={0} pcbY={2} fontSize={1.2} />
    <silkscreentext text="J1 pin1 = 3V3" pcbX={PIN1_X + 4} pcbY={-ROW_Y + 2.2} fontSize={0.9} />
    <silkscreentext text="J3 pin1 = GND" pcbX={PIN1_X + 4} pcbY={ROW_Y - 2.2} fontSize={0.9} />
  </footprint>
)

// ---------- layout (component centres, mm from board centre) ----------
const DEVKIT_POS = { x: 14, y: 2 }        // outline x -11.9..39.9, y -10.7..14.7
const FAN_X = [-38, -24, -10, 4]
const DIM_X = [0, 17]

export default () => (
  <board width="100mm" height="72mm" defaultTraceWidth="0.25mm" minTraceWidth="0.2mm"
    minViaHoleDiameter="0.3mm" minViaPadDiameter="0.7mm">
    <net name="V12" isPowerNet nominalTraceWidth="0.6mm" />
    <net name="V5" isPowerNet nominalTraceWidth="0.6mm" />
    <net name="GND" isGroundNet nominalTraceWidth="0.5mm" />
    <net name="AC_COM" nominalTraceWidth="2mm" />
    <net name="AC_NO" nominalTraceWidth="2mm" />

    <hole name="H1" diameter="3.2mm" pcbX={-47} pcbY={33} />
    <hole name="H2" diameter="3.2mm" pcbX={47} pcbY={33} />
    <hole name="H3" diameter="3.2mm" pcbX={-47} pcbY={-33} />
    <hole name="H4" diameter="3.2mm" pcbX={47} pcbY={-33} />

    {/* ---------- DevKit ---------- */}
    <chip name="U1" footprint={<DevKitFootprint />} pinLabels={devkitPinLabels}
      pcbX={DEVKIT_POS.x} pcbY={DEVKIT_POS.y}
      manufacturerPartNumber="ESP32-C6-DevKitC-1-N8"
      schWidth={3} schHeight={9}
      schPinArrangement={{
        leftSide: { direction: "top-to-bottom", pins: J1_PINS },
        rightSide: { direction: "top-to-bottom", pins: J3_PINS },
      }}
      connections={{
        "5V": "net.V5",
        GND_A: "net.GND", GND_B: "net.GND", GND_C: "net.GND", GND_D: "net.GND",
        "3V3": "net.V3V3",
        IO6: "net.SDA", IO7: "net.SCL",
        IO1: "net.RELAY_CTL",
        IO2: "net.DIM1_PWM", IO3: "net.DIM2_PWM",
        IO18: "net.FAN1_PWM", IO19: "net.FAN2_PWM", IO20: "net.FAN3_PWM", IO21: "net.FAN4_PWM",
        IO22: "net.FAN1_TACH", IO23: "net.FAN2_TACH", IO10: "net.FAN3_TACH", IO11: "net.FAN4_TACH",
      }} />

    {/* ---------- power: 12V in -> Recom -> 5V ---------- */}
    <chip name="J1" {...at("term508", -44, 9, 180)} pinLabels={{ pin1: "V12", pin2: "GND" }}
      connections={{ V12: "net.V12", GND: "net.GND" }} />
    <silkscreentext text="12V DC IN" pcbX={-44} pcbY={16.5} fontSize={1} />
    <silkscreentext text="+" pcbX={-41.5} pcbY={1.7} fontSize={1.2} />
    <silkscreentext text="-" pcbX={-46.5} pcbY={1.7} fontSize={1.2} />
    <chip name="U2" {...at("recom", -42, -4, 0)} pinLabels={{ pin1: "VIN", pin2: "GND", pin3: "VOUT" }}
      manufacturerPartNumber="R-78B5.0-1.5"
      connections={{ VIN: "net.V12", GND: "net.GND", VOUT: "net.V5" }} />
    <chip name="J2" {...at("term508", 44, -22, 0)} pinLabels={{ pin1: "V5", pin2: "GND" }}
      connections={{ V5: "net.V5", GND: "net.GND" }} />
    <silkscreentext text="DISPLAY 5V OUT" pcbX={44} pcbY={-14.5} fontSize={0.9} />
    <silkscreentext text="+" pcbX={41.5} pcbY={-29.5} fontSize={1.2} />
    <silkscreentext text="-" pcbX={46.5} pcbY={-29.5} fontSize={1.2} />

    {/* ---------- fans: header only.  1 GND  2 +12V  3 TACH  4 PWM ---------- */}
    {FAN_X.map((x, i) => {
      const n = i + 1
      return (
        <Fragment key={`fan-${n}`}>
          <chip name={`J_FAN${n}`} {...at("fanHeader", x, 32, 0)}
            pinLabels={{ pin1: "GND", pin2: "V12", pin3: "TACH", pin4: "PWM" }}
            connections={{ GND: "net.GND", V12: "net.V12", TACH: `net.FAN${n}_TACH`, PWM: `net.FAN${n}_PWM` }} />
          <silkscreentext text={`FAN${n}`} pcbX={x} pcbY={27.5} fontSize={1} />
        </Fragment>
      )
    })}
    <silkscreentext text="FAN PINS: 1 GND  2 +12V  3 TACH  4 PWM" pcbX={-20} pcbY={23} fontSize={0.8} />

    {/* ---------- I2C header ---------- */}
    <pinheader name="J4" pinCount={4} pitch="2.54mm" gender="male" {...at("hdr4", 30, 33, 0)}
      pinLabels={["V3V3", "GND", "SDA", "SCL"]}
      connections={{ V3V3: "net.V3V3", GND: "net.GND", SDA: "net.SDA", SCL: "net.SCL" }} />
    <silkscreentext text="I2C: 3V3 GND SDA SCL" pcbX={30} pcbY={29.5} fontSize={0.8} />

    {/* ---------- dimmers: 2N7000 chops 12V; 10k up, 68k down, 10uF ---------- */}
    {DIM_X.map((x, i) => {
      const n = i + 1
      return (
        <Fragment key={`light-${n}`}>
          <chip name={`J_LIGHT${n}`} {...at("rj11", x, -28.8, 0)}
            pinLabels={{ pin1: "DIM_A", pin2: "GND_A", pin3: "GND_B", pin4: "DIM_B" }}
            connections={{ DIM_A: `net.DIM${n}_OUT`, DIM_B: `net.DIM${n}_OUT`, GND_A: "net.GND", GND_B: "net.GND" }} />
          <silkscreentext text={`LIGHT ${n}  0-10V`} pcbX={x} pcbY={-22.5} fontSize={0.9} />
          {/* 2N7000 TO-92: 1 = S, 2 = G, 3 = D */}
          <chip name={`Q_D${n}`} {...at("to92", x - 4.5, -18, 0)} pinLabels={{ pin1: "S", pin2: "G", pin3: "D" }}
            manufacturerPartNumber="2N7000"
            connections={{ S: "net.GND", G: `net.DIM${n}_PWM`, D: `net.DIM${n}_OUT` }} />
          <capacitor name={`C_D${n}`} capacitance="10uF" polarized {...at("capElec", x + 4, -18, 0)}
            connections={{ pin1: `net.DIM${n}_OUT`, pin2: "net.GND" }} />
          <resistor name={`R_DU${n}`} resistance="10k" {...at("resV", x - 3.25, -13, 0)}
            connections={{ pin1: "net.V12", pin2: `net.DIM${n}_OUT` }} />
          <resistor name={`R_DL${n}`} resistance="68k" {...at("resV", x + 3.25, -13, 0)}
            connections={{ pin1: `net.DIM${n}_OUT`, pin2: "net.GND" }} />
        </Fragment>
      )
    })}

    {/* ---------- master relay.  G5LE pads: 1,2 coil ; 3 COM ; 4 NC ; 5 NO ---------- */}
    <chip name="K1" {...at("relay", -36, -22, 0)}
      pinLabels={{ pin1: "COIL1", pin2: "COIL2", pin3: "COM", pin4: "NC", pin5: "NO" }}
      manufacturerPartNumber="G5LE-14 DC12"
      connections={{ COIL1: "net.V12", COIL2: "net.RELAY_COIL", COM: "net.AC_COM", NO: "net.AC_NO" }} />
    <chip name="J3" {...at("term762", -22, -28, 90)} pinLabels={{ pin1: "COM", pin2: "NO" }}
      connections={{ COM: "net.AC_COM", NO: "net.AC_NO" }} />
    <silkscreentext text="LINE" pcbX={-17.5} pcbY={-31.8} fontSize={0.8} />
    <silkscreentext text="OUTLET" pcbX={-17.5} pcbY={-24.2} fontSize={0.8} />
    <silkscreentext text="! 120V MAINS !" pcbX={-30} pcbY={-34.8} fontSize={1} />
    {/* routing fence: keeps low-voltage traces out of the mains corner */}
    <keepout shape="rect" pcbX={-13.25} pcbY={-24} width="1.5mm" height="24mm" />
    <keepout shape="rect" pcbX={-23.5} pcbY={-11.5} width="21mm" height="3mm" excludeRefs={[".K1"]} />
    {/* PN2222A TO-92: 1 = E, 2 = B, 3 = C */}
    <chip name="Q2" {...at("to92", -29, -3, 0)} pinLabels={{ pin1: "E", pin2: "B", pin3: "C" }}
      manufacturerPartNumber="PN2222A"
      connections={{ E: "net.GND", B: ".R3 > .pin2", C: "net.RELAY_COIL" }} />
    <resistor name="R3" resistance="1k" {...at("res", -24, 1.5, 0)}
      connections={{ pin1: "net.RELAY_CTL" }} />
    <diode name="D3" {...at("do41", -24, -8, 0)} manufacturerPartNumber="1N4007"
      connections={{ [A]: "net.RELAY_COIL", [K]: "net.V12" }} />

    <silkscreentext text="GROW CABINET CONTROLLER  lean v1" pcbX={14} pcbY={-14} fontSize={1.2} />
  </board>
)