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)