ShiboSoftwareDev/solar-battery-charger-module

This hardware setup integrates a solar panel, a lithium-ion battery, and a multi-rail buck converter with a battery charging IC, protection circuitry, and monitoring components, enabling solar power harvesting, battery charging, and power distribution in an embedded system.

Version
1.0.4
License
unset
Stars
0

scripts/audit-circuit.mts

import { readFileSync } from "node:fs"
import { PcbConnectivityMap } from "circuit-json-to-connectivity-map"

const circuit = JSON.parse(readFileSync("dist/index/circuit.json", "utf8"))
const byType = (type: string) => circuit.filter((element: any) => element.type === type)

const sourceNets = new Map(byType("source_net").map((net: any) => [net.source_net_id, net.name]))
const sourcePortToNet = new Map<string, string>()
for (const trace of byType("source_trace")) {
  for (const portId of trace.connected_source_port_ids ?? []) {
    for (const netId of trace.connected_source_net_ids ?? []) sourcePortToNet.set(portId, netId)
  }
}

const pcbPortsByNet = new Map<string, string[]>()
for (const port of byType("pcb_port")) {
  const netId = sourcePortToNet.get(port.source_port_id)
  if (!netId) continue
  const ports = pcbPortsByNet.get(netId) ?? []
  ports.push(port.pcb_port_id)
  pcbPortsByNet.set(netId, ports)
}

const physicalConnectivity = new PcbConnectivityMap(circuit).connMap
const disconnectedNets = [...pcbPortsByNet.entries()]
  .filter(([, ports]) => ports.length > 1 && !physicalConnectivity.areAllIdsConnected(ports))
  .map(([netId, ports]) => ({ net: sourceNets.get(netId), ports: ports.length }))

const errorRecords = circuit.filter((element: any) => /error$/i.test(element.type))
const copperFeaturesByPort = new Map<string, any[]>()
for (const feature of circuit.filter((element: any) =>
  element.type === "pcb_smtpad" || element.type === "pcb_plated_hole"
)) {
  if (!feature.pcb_port_id) continue
  const features = copperFeaturesByPort.get(feature.pcb_port_id) ?? []
  features.push(feature)
  copperFeaturesByPort.set(feature.pcb_port_id, features)
}

const pointInPolygon = (x: number, y: number, points: Array<{ x: number, y: number }>) => {
  let inside = false
  for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
    const a = points[i]
    const b = points[j]

    // Treat points on the polygon boundary as pad-internal. The small tolerance
    // absorbs floating-point differences between imported pad and route geometry.
    const cross = (x - a.x) * (b.y - a.y) - (y - a.y) * (b.x - a.x)
    const onSegment = Math.abs(cross) <= 1e-5 &&
      x >= Math.min(a.x, b.x) - 1e-5 && x <= Math.max(a.x, b.x) + 1e-5 &&
      y >= Math.min(a.y, b.y) - 1e-5 && y <= Math.max(a.y, b.y) + 1e-5
    if (onSegment) return true

    const intersects = ((a.y > y) !== (b.y > y)) &&
      x < ((b.x - a.x) * (y - a.y)) / (b.y - a.y) + a.x
    if (intersects) inside = !inside
  }
  return inside
}

const pointInsideCopperFeature = (point: any, feature: any) => {
  if (feature.layer && point.layer && feature.layer !== point.layer) return false
  if (feature.layers && point.layer && !feature.layers.includes(point.layer)) return false

  if (feature.shape === "polygon") {
    return pointInPolygon(point.x, point.y, feature.points ?? [])
  }
  if (feature.shape === "circle") {
    const diameter = feature.outer_diameter ??
      (feature.radius != null ? feature.radius * 2 : feature.width)
    const radius = diameter / 2
    return Math.hypot(point.x - feature.x, point.y - feature.y) <= radius + 1e-5
  }
  if (feature.shape === "rect") {
    return Math.abs(point.x - feature.x) <= feature.width / 2 + 1e-5 &&
      Math.abs(point.y - feature.y) <= feature.height / 2 + 1e-5
  }
  return false
}

const pointInsideEndpointPad = (point: any, trace: any) =>
  (trace.connectsTo ?? []).some((pcbPortId: string) =>
    (copperFeaturesByPort.get(pcbPortId) ?? []).some((feature) =>
      pointInsideCopperFeature(point, feature)
    )
  )

const routesByNet = new Map<string, any[]>()
for (const trace of byType("pcb_trace")) {
  const traces = routesByNet.get(trace.connection_name) ?? []
  traces.push(trace)
  routesByNet.set(trace.connection_name, traces)
}

const routedNets = [...routesByNet.entries()].map(([netId, traces]) => {
  const endpointTapers: any[] = []
  const invalidSubminimumWires: any[] = []
  const wires = traces.flatMap((trace) => {
    const route = trace.route ?? []
    const wireIndexes = route
      .map((point: any, index: number) => point.route_type === "wire" ? index : -1)
      .filter((index: number) => index >= 0)
    const firstWireIndex = wireIndexes[0]
    const lastWireIndex = wireIndexes.at(-1)
    const firstWire = route[firstWireIndex]
    const lastWire = route[lastWireIndex]

    return wireIndexes.flatMap((index: number) => {
      const wire = route[index]
      if (wire.is_inside_copper_pour) return []
      if (wire.width >= 0.1) return [wire]

      const distanceFromStart = firstWire
        ? Math.hypot(wire.x - firstWire.x, wire.y - firstWire.y)
        : Number.POSITIVE_INFINITY
      const distanceFromEnd = lastWire
        ? Math.hypot(wire.x - lastWire.x, wire.y - lastWire.y)
        : Number.POSITIVE_INFINITY
      const isPadEntryTaper = pointInsideEndpointPad(wire, trace)
      const record = {
        pcbTraceId: trace.pcb_trace_id,
        widthMm: wire.width,
        distanceFromNearestEndpointMm: Math.min(distanceFromStart, distanceFromEnd),
        insideEndpointPad: pointInsideEndpointPad(wire, trace),
      }
      if (isPadEntryTaper) endpointTapers.push(record)
      else invalidSubminimumWires.push(record)
      return []
    })
  })
  const vias = traces.flatMap((trace) => trace.route.filter((point: any) => point.route_type === "via"))
  return {
    net: sourceNets.get(netId) ?? netId,
    branches: traces.length,
    minExposedWidthMm: Math.min(...wires.map((wire: any) => wire.width)),
    maxExposedWidthMm: Math.max(...wires.map((wire: any) => wire.width)),
    vias: vias.length,
    endpointTaperRecords: endpointTapers.length,
    invalidSubminimumWires,
  }
})

const invalidSubminimumWires = routedNets.flatMap((net) =>
  net.invalidSubminimumWires.map((wire: any) => ({ net: net.net, ...wire })),
)

console.log(JSON.stringify({
  sourceNetCount: sourceNets.size,
  routedNetCount: routesByNet.size,
  pcbTraceCount: byType("pcb_trace").length,
  pcbViaCount: byType("pcb_via").length,
  platedHoleCount: byType("pcb_plated_hole").length,
  errorRecordCount: errorRecords.length,
  errorRecords,
  disconnectedNets,
  invalidSubminimumWires,
  endpointTaperRecordCount: routedNets.reduce((total, net) => total + net.endpointTaperRecords, 0),
  keyPowerNets: routedNets.filter((net) => [
    "SOLAR_IN",
    "VIN_PROTECTED",
    "PH",
    "CHG_POWER_PRE_SENSE",
    "BATTERY_POWER",
    "BAT_NEG",
    "BUCK_INPUT",
    "VOUT_3V3",
    "GND",
  ].includes(net.net as string)),
}, null, 2))

if (
  errorRecords.length > 0 ||
  disconnectedNets.length > 0 ||
  invalidSubminimumWires.length > 0 ||
  routesByNet.size !== sourceNets.size
) process.exit(1)