Drogheda/03-Architecture-Hardware/EinarBoard-Manual/tools/generate_production_bom.py

356 lines
26 KiB
Python

#!/usr/bin/env python3
"""
generate_production_bom.py — Helgen EinarBoard 1.0 (hbox-watchdog-v3-PROD) Rev 1.1
Builds helgen_einarboard_1.0_production_bom.xlsx:
Tab 1 Summary — project properties, fab rules, live cross-sheet cost roll-up
Tab 2 Internal PCBA BOM — on-board parts (JLC SMD / hand THT / DNP), live line-cost formulas
Tab 3 External Integration BOM — enclosure, modules, pack, harnesses, hardware
Source of truth: Assembly-BOM-Split.md + LCSC-search-list.md (2026-07-06, verified vs JLC live
stock) + 2026-07-06 Pre-Fab Audit closure (Rev 1.1: RIG1/RDFU1 = 22k, Q5/Q6 = BEC map, Q7 S/D
corrected). Re-confirm LCSC stock at upload — stock moves.
Usage: python3 generate_production_bom.py [outdir] (default: parent of tools/)
"""
import sys
from datetime import date
from pathlib import Path
from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
# ---------------------------------------------------------------- Helgen palette
INDIGO = "2C3C5B" # primary (brand)
SLATE = "44597A" # secondary panel
CYAN = "51F3DA" # accent (brand)
MIST = "EAEFF5" # row banding
WHITE = "FFFFFF"
DARK = "1B2436"
F_TITLE = Font(name="Calibri", size=16, bold=True, color=INDIGO)
F_SUB = Font(name="Calibri", size=10, italic=True, color=SLATE)
F_HDR = Font(name="Calibri", size=10, bold=True, color=WHITE)
F_SEC = Font(name="Calibri", size=11, bold=True, color=WHITE)
F_BODY = Font(name="Calibri", size=10, color=DARK)
F_BOLD = Font(name="Calibri", size=10, bold=True, color=INDIGO)
F_TOTAL = Font(name="Calibri", size=11, bold=True, color=INDIGO)
FILL_HDR = PatternFill("solid", fgColor=INDIGO)
FILL_SEC = PatternFill("solid", fgColor=SLATE)
FILL_ACC = PatternFill("solid", fgColor=CYAN)
FILL_BAND= PatternFill("solid", fgColor=MIST)
FILL_TOT = PatternFill("solid", fgColor=CYAN)
THIN = Side(style="thin", color="B9C4D6")
B_ALL = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
A_L = Alignment(horizontal="left", vertical="center", wrap_text=True)
A_C = Alignment(horizontal="center", vertical="center", wrap_text=True)
A_R = Alignment(horizontal="right", vertical="center")
MONEY3 = '"$"#,##0.000'
MONEY2 = '"$"#,##0.00'
def style_header_row(ws, row, ncols):
for c in range(1, ncols + 1):
cell = ws.cell(row=row, column=c)
cell.font, cell.fill, cell.border, cell.alignment = F_HDR, FILL_HDR, B_ALL, A_C
def section_bar(ws, row, ncols, text):
ws.cell(row=row, column=1, value=text)
ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=ncols)
c = ws.cell(row=row, column=1)
c.font, c.fill, c.alignment = F_SEC, FILL_SEC, A_L
# ================================================================ Tab 2 — Internal PCBA BOM
# (qty, refdes, description, package, manufacturer, mpn, lcsc, assy, unit_usd, notes)
PCBA = [
("— Active / power semiconductors —",),
(1, "U6", "Battery-backup manager: 1S Li-ion charger + power-path + 2.5 A boost (Rev 1.1 values: RPROG 825R -> 2.42 A ~ C/5, RS 8 mR -> 3.1 A, F-straps 4.00 V Li-ion)", "QFN-24 4x5", "Analog Devices", "LTC4040EUFD#TRPBF", "C687163", "SMD (JLC)", 6.80, "Ext part, stock 2341 (2026-07-06). EP = GND stitch."),
(2, "U2, U7", "Buck 1.5 MHz 3 A — U2: 24 V -> 5V_FAN; U7: SW_5V -> 3V3_MODEM (stack cycle reboots modem)", "SOIC-8-EP", "MPS", "MP1584EN-LF-Z", "C15051", "SMD (JLC)", 0.55, "Only in-stock LCSC variant — do not let auto-match pick a 0-stock clone."),
(1, "U3", "Relative humidity / temperature sensor, I2C 0x44", "DFN-4 1.5x1.5", "Sensirion", "SHT40-AD1B-R3", "C2848306", "SMD (JLC)", 2.60, "R2 alt: C2909890."),
(1, "U4", "Temperature sensor +/-0.5 degC, I2C 0x48 (ADD0=GND)", "SOT-563", "Texas Instruments", "TMP102AIDRLR", "C99269", "SMD (JLC)", 1.10, "UMW clone alt: C5692997."),
(1, "Q7", "N-FET 30 V <=13.6 mR — LTC4040 VIN->VSYS disconnect. Rev 1.1 (H1): S=VIN_SW, D=MAIN5V (gate=IGATE)", "SOIC-8", "AOS", "AO4406AL", "C35349", "SMD (JLC)", 0.28, "Orientation is the audit's board-killer #1 — verify at CPL review."),
(1, "Q10", "P-FET -55 V, 24 V reverse-polarity front end (with DzG 15 V gate zener)", "TO-252", "Infineon", "IRFR5305TRPBF", "C2624", "SMD (JLC)", 0.65, ""),
(1, "Q1", "P-FET -30 V, RTK-stack high-side load switch (SW_IN -> SW_5V)", "SOT-23", "AOS", "AO3401A", "C15127", "SMD (JLC)", 0.05, "JLC Basic."),
(2, "Q2, Q4", "N-FET — fan low-side PWM / status-LED driver", "SOT-23", "AOS", "AO3400A", "C20917", "SMD (JLC)", 0.04, "JLC Basic."),
(2, "Q5, Q6", "NPN drivers — load-switch / K1 coil. Rev 1.1 (H2): Q_NPN_BEC pin map (1=B,2=E,3=C)", "SOT-23", "onsemi", "MMBT3904", "C20526", "SMD (JLC)", 0.02, "JLC Basic. BCE symbol on this part was audit finding H2."),
("— Relay, protection, inductors —",),
(1, "K1", "Signal relay SPDT 5 V coil — Modo-B fail-safe bypass, NC = F9P TX2 -> 4G DIN (corrections survive dead MCU)", "THT DIP", "Omron", "G5V-1 DC5", "verify at LCSC", "THT (hand)", 1.90, "Coil pins 2/9; COM 5&6 doubled; NC=1. Verified vs Omron K048-E1-07."),
(1, "L6", "Power inductor 2.2 uH, Isat 14 A (LTC4040 SW<->BAT)", "8.8x8.2 molded", "Sunlord", "MWSA0804S-2R2MT", "C17700165", "SMD (JLC)", 0.35, "SUB for Bourns SRU8028-2R2Y (JLC out of stock) — pad overlap verified rule: confirm before commit."),
(2, "L2, L7", "Power inductor 10 uH, Isat 6.5 A (fan buck / modem buck)", "8.8x8.2 molded", "Sunlord", "MWSA0804S-100MT", "C17700171", "SMD (JLC)", 0.32, "SUB for SRU8028-100Y. L7 margin vs 2 A modem peak = audit L5 (OK, thin)."),
(1, "RS1", "Current-sense resistor 8 mR 1% 1 W (LTC4040 input limit 3.1 A)", "1206", "FOJAN", "FRM121WFR008TM", "C7420012", "SMD (JLC)", 0.15, "Premium alt: Vishay WSLP C2076276."),
(1, "F1", "PPTC resettable fuse 3 A 24 V (SYS5V -> SW_IN)", "1812", "LUTE", "1812L300/24GR", "C20627123", "SMD (JLC)", 0.25, "If node <=6 V: genuine Littelfuse C127824."),
(1, "D10", "TVS 33 V 600 W, 24 V input clamp", "SMB", "Littelfuse", "SMBJ33A", "C224019", "SMD (JLC)", 0.12, "Cheap alt: C353366."),
(1, "DzG", "Zener 15 V (Q10 gate protection)", "SOT-23", "onsemi", "BZX84C15LT1G", "C80857", "SMD (JLC)", 0.03, ""),
(1, "D5", "Schottky BAT54 — PA5 ignition clamp to 3V3", "SOD-123", "—", "BAT54", "auto-match", "SMD (JLC)", 0.02, "Left to JLC auto-match on purpose (ambiguous variants)."),
(1, "D6", "1N4148W — K1 coil flyback", "SOD-123", "—", "1N4148W", "C2099", "SMD (JLC)", 0.01, ""),
(3, "D2, Dbk, Dbk2", "Schottky 40 V 3 A — fan flyback / buck freewheel x2", "SMA", "—", "SS34", "C8678", "SMD (JLC)", 0.06, "JLC Basic."),
(6, "D1, D4G, DB1-DB4", "LED green 0805 — status, modem, 4-LED battery gauge", "0805", "—", "—", "C28105", "SMD (JLC)", 0.01, "Single SKU by design; recolor D4G/gauge in JLC BOM editor if desired. D4G dim @1.8 V drive (audit L4 — accepted)."),
("— Precision resistors (0603, 1%) —",),
(1, "R50", "825R — LTC4040 PROG: I_CHG = 2000/RPROG = 2.42 A ~ C/5 (12 Ah pack)", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.010, "Critical value — do not substitute."),
(1, "R51", "287k — PFI divider top (VEH5V trip 4.60 V falling)", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.010, "With R52 100k."),
(1, "R53", "499k — BSTFB/RSTFB divider top (backup 4.79 V; RST -7.5%)", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.010, "With R54 100k; shared divider is datasheet-endorsed."),
(1, "R21", "19.1k — U2 FB bottom (5V_FAN = 5.0 V)", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.010, ""),
(1, "R61", "31.6k — U7 FB bottom (3V3_MODEM = 3.33 V)", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.010, ""),
(3, "R52, R54, R60", "100k 1% — PFI / FB dividers", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.010, ""),
(2, "R42, R55", "10k 1% — NTC dividers (PB1 hotspot / LTC4040 NTC bias)", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.010, "R55 low-drift; B3950 NTC trip shift noted at bring-up."),
("— Standard resistors (0603, 5%) —",),
(2, "RIG1, RDFU1", "22k — ignition divider top (raw 12-24 V) / DFU pull. Rev 1.1 (H3): corrected from stale 100R", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, "100R here burns D5 + Rig at 24 V ignition — audit board-killer #3."),
(10, "R2, R5, R6, R7, R8, R41, Rigb, Rled, Rrp, Rtp", "10k — pulls, sense divider halves, Rigb ignition divider bottom", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, ""),
(8, "R20, R56, R57, R58, Ren1, Rfreq, Rfreq2, Rgs", "100k — FB tops, pulls, FREQ set, EN UVLO top", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, ""),
(5, "R1, R3, R40, Rts, Rmled", "1k — base drives, LED limits, tach series", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, ""),
(4, "RB1-RB4", "330R — battery-gauge LED limits (PB12-PB15)", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, ""),
(2, "R30, R31", "4.7k — I2C1 pull-ups (SCL/SDA)", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, ""),
(2, "Rcomp, Rcomp2", "40.2k — MP1584 COMP loop", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, "Bench-tune placeholder per delta doc."),
(2, "Rcc1, Rcc2", "5.1k — USB-C CC1/CC2 device-mode pulldowns", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, ""),
(1, "Rg", "100R — Q2 gate series", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, ""),
(1, "Ren2", "20k — MP1584 EN UVLO bottom (EN abs-max 6 V — never tie EN to 24 V)", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, ""),
("— Capacitors —",),
(7, "C5, C7, C9, C11, C24, Cu3, Cu4", "100 nF X7R — decoupling, PA5/PB1 filters", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, ""),
(2, "Cbst, Cbst2", "10 nF — MP1584 BST bootstrap (U2/U7)", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, ""),
(2, "Ccomp, Ccomp2", "2.2 nF — MP1584 COMP", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.004, ""),
(1, "C31", "2.2 uF — U6 VIN aux", "0603", "—", "—", "Basic auto", "SMD (JLC)", 0.01, ""),
(2, "C30, C34", "10 uF X5R — U6 VIN / VSYS", "0805", "—", "—", "Basic auto", "SMD (JLC)", 0.01, ""),
(3, "C35, C40, C41", "22 uF >=16 V — U6 BAT / U7 in / U7 out", "0805", "—", "—", "Basic auto", "SMD (JLC)", 0.02, ""),
(1, "C23", "22 uF 16 V — U2 buck out", "0805", "—", "—", "Basic auto", "SMD (JLC)", 0.02, ""),
(1, "C22", "22 uF 50 V — U2 buck in (24 V node)", "1210", "—", "—", "Basic auto", "SMD (JLC)", 0.06, "Voltage rating critical."),
(2, "C32, C33", "47 uF >=25 V low-ESR ceramic — VSYS hold-up (with C34: 104 uF >= datasheet 100 uF min)", "1210", "—", "—", "Basic auto", "SMD (JLC)", 0.08, ""),
(1, "C3", "47 uF radial electrolytic — SW_5V bulk", "radial 5x7", "—", "—", "auto/hand", "THT (hand)", 0.03, ""),
(1, "C10", "47 uF 50 V radial — 24 V bulk", "radial 6.3x7.7", "—", "—", "auto/hand", "THT (hand)", 0.04, ""),
(1, "C43", "100 uF 6.3 V radial — 3V3_MODEM bulk (LTE TX bursts)", "radial 6.3x7.7", "—", "—", "auto/hand", "THT (hand)", 0.04, "Verify body fit under shield."),
("— USB service port + switches —",),
(1, "J9", "USB-C receptacle USB 2.0 (modem service console: VBUS->J7.6 enables modem USB; no rail backfeed)", "SMD 14P", "GCT", "USB4085-GF-A", "search USB4085", "THT/SMD (hand rec.)", 0.45, "Hand-solder recommended per order guide."),
(1, "SW4G", "Tactile switch SMD — modem RESET (J7.5 -> GND)", "6x3.5 gull-wing", "C&K", "PTS636SK25SMTRLFS", "C2689642", "SMD (JLC)", 0.15, "Confirm pads vs footprint."),
(1, "SW1", "Tactile 6 mm THT — MCU RESET (NRST -> GND)", "THT 6 mm", "—", "—", "hand", "THT (hand)", 0.05, ""),
("— Connectors & module sockets (THT, hand-solder per order guide) —",),
(1, "P1", "Terminal block 5-pos 5.08 mm — GX16 machine power in (24V_IN / GND / IGN_IN)", "THT", "Phoenix", "MKDS 1,5/5-5,08", "hand", "THT (hand)", 1.20, ""),
(5, "P2, P8, P10, P11, P12", "JST-XH 2-pin top-entry — 5V-IN, panel LED, ext reset, cutoff switch, hotspot NTC", "THT B2B-XH", "JST", "B2B-XH-A", "hand", "THT (hand)", 0.08, ""),
(1, "P4", "JST-XH 3-pin — battery 1S4P: BAT+ / GND / NTC", "THT B3B-XH", "JST", "B3B-XH-A", "hand", "THT (hand)", 0.09, ""),
(2, "P5, P6", "JST-XH 4-pin — I2C probe port / fan (5V_FAN, RET, GND, TACH)", "THT B4B-XH", "JST", "B4B-XH-A", "hand", "THT (hand)", 0.10, ""),
(2, "J1, J2", "Female header 1x20 2.54 mm — Blackpill socket", "THT", "—", "—", "hand", "THT (hand)", 0.35, ""),
(2, "J7, J8", "Female socket 1x10 2.00 mm — XBee 4G socket (rows 22.35 mm)", "THT", "—", "—", "hand", "THT (hand)", 0.60, ""),
(1, "MOD1", "Arduino-UNO female header kit 10+8+8+6 — simpleRTK2B Pro shield socket", "THT 2.54", "—", "—", "hand", "THT (hand)", 0.90, ""),
("— DNP / options (populate cost = 0) —",),
(0, "P3, P13, H3", "DNP — harness alternatives made redundant by carrier sockets + K1 NC bypass (P3 UART1 tap, P13 Modo-B uplink, H3 USART1 header)", "THT", "—", "—", "—", "DNP", 0.00, "Real DNP attribute set in board rev 1.1; drop rows before JLC upload."),
(0, "JP1", "0R rail-select option — OPEN by default (dedicated fan buck); close ONLY if SD-15B-05 upsized", "0603", "—", "—", "C21189", "DNP", 0.00, "JP2/JP3/JP4 = copper solder jumpers, F-straps 4.00 V Li-ion — no BOM part."),
]
PCBA_HEADERS = ["Item", "Qty", "RefDes", "Description", "Package", "Manufacturer",
"MPN", "JLCPCB / LCSC", "Assembly", "Est. Unit Cost (USD)",
"Line Cost (USD)", "Notes"]
def build_pcba(ws):
ws.sheet_view.showGridLines = False
ws.cell(row=1, column=1, value="EinarBoard 1.0 — Internal PCBA BOM (on-board, per board)").font = F_TITLE
ws.cell(row=2, column=1, value="Rev 1.1 Post-Audit Production Master · 132 footprints · LCSC verified vs JLC live stock 2026-07-06 — re-confirm at upload").font = F_SUB
hdr_row = 4
for c, h in enumerate(PCBA_HEADERS, 1):
ws.cell(row=hdr_row, column=c, value=h)
style_header_row(ws, hdr_row, len(PCBA_HEADERS))
r, item, band = hdr_row + 1, 1, False
first_data, last_data = None, None
for row in PCBA:
if len(row) == 1: # section bar
section_bar(ws, r, len(PCBA_HEADERS), row[0]); r += 1; band = False
continue
qty, refs, desc, pkg, mfr, mpn, lcsc, assy, unit, notes = row
vals = [item, qty, refs, desc, pkg, mfr, mpn, lcsc, assy, unit,
f"=B{r}*J{r}", notes]
for c, v in enumerate(vals, 1):
cell = ws.cell(row=r, column=c, value=v)
cell.font, cell.border = F_BODY, B_ALL
cell.alignment = A_C if c in (1, 2, 5, 8, 9) else A_L
if band:
cell.fill = FILL_BAND
ws.cell(row=r, column=10).number_format = MONEY3
ws.cell(row=r, column=11).number_format = MONEY3
ws.cell(row=r, column=10).alignment = A_R
ws.cell(row=r, column=11).alignment = A_R
if first_data is None:
first_data = r
last_data = r
item += 1; r += 1; band = not band
# TOTAL row
ws.cell(row=r, column=4, value="TOTAL — PCBA parts per board").font = F_TOTAL
tc = ws.cell(row=r, column=11, value=f"=SUM(K{first_data}:K{last_data})")
tc.font, tc.number_format, tc.alignment = F_TOTAL, MONEY2, A_R
for c in range(1, len(PCBA_HEADERS) + 1):
ws.cell(row=r, column=c).fill = FILL_TOT
ws.cell(row=r, column=c).border = B_ALL
widths = [6, 6, 24, 58, 14, 16, 22, 16, 13, 12, 12, 44]
for c, w in enumerate(widths, 1):
ws.column_dimensions[get_column_letter(c)].width = w
ws.freeze_panes = f"A{hdr_row + 1}"
ws.auto_filter.ref = f"A{hdr_row}:L{last_data}"
return r # total row index
# ================================================================ Tab 3 — External Integration BOM
EXT = [
("— Enclosure & mechanical —",),
(1, "HBOX-135100 enclosure set", "ASA printed base + top + brackets, resized for the 135x100 mm carrier (IP67 strategy: conduction, no vents)", "Helgen in-house (SKU HBOX-135100)", 18.00, "Mechanical fit VERIFIED 2026-07-07 (audit M4 closed)."),
(4, "M3 brass standoff M3x10", "Board mount — MH1-MH4, 3.2 mm holes", "generic brass", 0.15, "4-5 mm edge margins per audit."),
(8, "M3x6 screw, zinc", "Standoff top + enclosure boss", "generic", 0.03, ""),
(1, "Thermal interface pad set", "Conduction path: pack/modem hotspot -> enclosure wall", "generic silicone 1.5 mm", 1.20, "Supports canonical invariant #4."),
("— Plug-in modules (installed by Helgen, NEVER on the JLC BOM) —",),
(1, "simpleRTK2B Pro (ZED-F9P)", "GNSS RTK receiver, seats into MOD1 as UNO shield", "ArduSimple AS-RTK2B-PROF9P-L1L2-NH-00", 210.00, "Switch/switchless revisions — see manual field warnings."),
(1, "4G NTRIP Master (XBee LTE)", "Corrections modem, seats into J7/J8 (3V3_MODEM)", "ArduSimple AS-XBEE-4G-NA-UFL-00", 155.00, "2 A peak at 3.3 V (audit L5)."),
(1, "STM32F411 Blackpill (WeAct)", "Watchdog MCU, seats into J1/J2", "WeAct STM32F411CEU6", 6.50, "Flash via USB-C DFU (H1 SWD dropped)."),
("— Antennas & RF —",),
(1, "u-blox multiband GNSS antenna", "L1/L2 survey antenna, SMA", "ArduSimple AS-ANT2B-ANN-L1L2-50SMA", 65.00, ""),
(2, "LTE antenna", "4G blade antenna, u.FL/SMA per modem kit", "AR1582", 8.00, ""),
("— Battery pack (1S4P ~12 Ah) —",),
(4, "18650 Li-ion cell ~3 Ah", "1S4P pack, 3.6 V nominal — charge 4.00 V strap (cycle life + hot-cell margin, RCA #1)", "quality tier-1 cell (e.g. INR18650)", 4.50, "Pack ~12 Ah confirmed 2026-06-30."),
(1, "1S4P pack hardware", "Holder / nickel strip / lead set + heatshrink", "generic", 3.00, ""),
(1, "Pack NTC 10k B3950", "Bonded to cell, wired to P4.3 (charger-owned NTC)", "generic epoxy bead", 0.30, "B3380 ideal; B3950 accepted (trip shifts a few degC — bring-up note)."),
("— Cabinet-side power & panel —",),
(1, "Mean Well SD-15B-05", "24->5 V 3 A supply feeding VEH5V (P2)", "Mean Well SD-15B-05", 14.50, "RS1 3.1 A input limit protects this unit."),
(1, "GX16-5 connector pair", "Panel + cable plug: 1=24V_IN, 2=GND, 3=IGN_IN (RAW 12-24 V)", "GX16-5", 2.20, "Pin 3 carries RAW ignition — Rev 1.1 divider (22k/10k) expects it."),
(1, "Panel-mount dual LED", "External status indicator, wired to P8 (LED_A/LED_K)", "generic 8 mm dual-color", 0.80, ""),
(1, "Cutoff switch SPST panel", "P11 in series MAIN5V->SYS5V — kills vehicle AND battery", "generic 21x15 rocker/toggle", 1.50, "Ship P11 with jumper plug installed — open P11 = dead board."),
(1, "External reset button", "Panel momentary NO, wired to P10 (NRST/GND)", "generic 12 mm", 0.60, ""),
(1, "Fan 5 V 2-wire", "Ducted per thermal v3.0 concept; PWM low-side", "generic 40x40 5 V", 3.50, "2-wire: no tach — fan fault inferred from temperature."),
(1, "External NTC probe 10k B3950", "Cabinet hot-spot probe, wired to P12", "generic sealed probe", 0.50, ""),
("— Pre-wired harnesses (JST) —",),
(5, "JST-XH 2-pin pre-wired harness", "P2 5V-IN / P8 panel LED / P10 reset / P11 cutoff / P12 NTC — 22-24 AWG, 300 mm", "JST XHP-2 + pre-crimped", 0.35, "Power pairs (P2/P11) in 22 AWG."),
(1, "JST-XH 3-pin pre-wired harness", "P4 battery: BAT+ / GND / NTC — 20 AWG power pair", "JST XHP-3 + pre-crimped", 0.40, ""),
(2, "JST-XH 4-pin pre-wired harness", "P5 I2C probe / P6 fan", "JST XHP-4 + pre-crimped", 0.45, ""),
(1, "JST-PH pre-wired set (spares)", "PH 2.0 mm pigtails for space-constrained sensor drops", "JST PHR-2/3", 0.40, "Optional — XH is the board standard."),
("— Connectivity —",),
(1, "Telcel M2M micro-SIM", "LTE data for NTRIP corrections", "Telcel M2M plan", 0.00, "Cost = monthly plan, not BOM."),
]
EXT_HEADERS = ["Item", "Qty / unit", "Part", "Description", "Source / MPN",
"Est. Unit Cost (USD)", "Line Cost (USD)", "Notes"]
def build_ext(ws):
ws.sheet_view.showGridLines = False
ws.cell(row=1, column=1, value="EinarBoard 1.0 — External Integration BOM (per HBOX unit)").font = F_TITLE
ws.cell(row=2, column=1, value="Secondary assembly: enclosure, plug-in modules, pack, panel hardware, harnesses — NOT in the JLC order").font = F_SUB
hdr_row = 4
for c, h in enumerate(EXT_HEADERS, 1):
ws.cell(row=hdr_row, column=c, value=h)
style_header_row(ws, hdr_row, len(EXT_HEADERS))
r, item, band = hdr_row + 1, 1, False
first_data, last_data = None, None
for row in EXT:
if len(row) == 1:
section_bar(ws, r, len(EXT_HEADERS), row[0]); r += 1; band = False
continue
qty, part, desc, src, unit, notes = row
vals = [item, qty, part, desc, src, unit, f"=B{r}*F{r}", notes]
for c, v in enumerate(vals, 1):
cell = ws.cell(row=r, column=c, value=v)
cell.font, cell.border = F_BODY, B_ALL
cell.alignment = A_C if c in (1, 2) else A_L
if band:
cell.fill = FILL_BAND
ws.cell(row=r, column=6).number_format = MONEY2
ws.cell(row=r, column=7).number_format = MONEY2
ws.cell(row=r, column=6).alignment = A_R
ws.cell(row=r, column=7).alignment = A_R
if first_data is None:
first_data = r
last_data = r
item += 1; r += 1; band = not band
ws.cell(row=r, column=4, value="TOTAL — external integration per unit").font = F_TOTAL
tc = ws.cell(row=r, column=7, value=f"=SUM(G{first_data}:G{last_data})")
tc.font, tc.number_format, tc.alignment = F_TOTAL, MONEY2, A_R
for c in range(1, len(EXT_HEADERS) + 1):
ws.cell(row=r, column=c).fill = FILL_TOT
ws.cell(row=r, column=c).border = B_ALL
widths = [6, 9, 30, 56, 30, 13, 12, 46]
for c, w in enumerate(widths, 1):
ws.column_dimensions[get_column_letter(c)].width = w
ws.freeze_panes = f"A{hdr_row + 1}"
ws.auto_filter.ref = f"A{hdr_row}:H{last_data}"
return r
# ================================================================ Tab 1 — Summary dashboard
def build_summary(ws, pcba_total_row, ext_total_row):
ws.sheet_view.showGridLines = False
ws.cell(row=1, column=2, value="HELGEN EINARBOARD 1.0").font = Font(name="Calibri", size=22, bold=True, color=INDIGO)
ws.cell(row=2, column=2, value="HBOX RTK Watchdog Carrier — Production BOM Workbook · Rev 1.1 Post-Audit Production Master").font = F_SUB
ws.cell(row=3, column=2, value=f"Generated {date.today().isoformat()} · Project Drogheda (GPS RTK) · Helgen Technologies").font = F_SUB
for c in range(1, 9):
ws.cell(row=4, column=c).fill = FILL_ACC
props = [
("Assembly", "Helgen EinarBoard 1.0 (EINARBoard HBOX 1.0)"),
("KiCad project", "kicad/hbox-watchdog-v3-PROD.kicad_pcb"),
("Revision", "1.1 — Post-Audit Production Master (H1-H4 fixed, M3/M4 closed 2026-07-07)"),
("Board", "135.1 x 100.1 mm · 132 footprints · 130 nets"),
("Verification", "ERC 0 errors · DRC 0 errors / 0 unconnected · fab package regenerated after last save"),
("Order status", "Bare-board RELEASED · PCBA gated by M1 remainder (passives auto-match) + M2 (LTC4040 bench validation)"),
("Modules (plug-in)", "Blackpill -> J1/J2 · simpleRTK2B Pro -> MOD1 · 4G NTRIP Master -> J7/J8 — never on the JLC BOM"),
]
fab = [
("Layers", "4 — F.Cu / In1.Cu (solid GND) / In2.Cu / B.Cu"),
("Material / TG", "FR-4, standard TG150 (JLC 4-layer default)"),
("Thickness / copper", "1.6 mm · 1 oz outer"),
("Surface finish", "ENIG (QFN-24 + DFN + SOT-563 fine pitch)"),
("Soldermask / silk", "Blue mask, white silk (Helgen indigo family; black = approved alternate)"),
("Min track/space", "0.15 mm present (JLC 4-layer capability 0.09 mm)"),
("Netclasses", "PWR_HI 1.5 mm · PWR_MID 1.0 mm (SW_IN in PWR_MID per Rev 1.1/H4)"),
("Impedance control", "No (USB 2.0 FS service port only)"),
("Vias", "Tented (default)"),
("PCBA type", "Standard (QFN/fine-pitch) · SMD by JLC, THT hand-soldered"),
]
r = 6
section_bar(ws, r, 8, "PROJECT PROPERTIES"); r += 1
for k, v in props:
ws.cell(row=r, column=2, value=k).font = F_BOLD
ws.cell(row=r, column=3, value=v).font = F_BODY
ws.merge_cells(start_row=r, start_column=3, end_row=r, end_column=8)
ws.cell(row=r, column=3).alignment = A_L
r += 1
r += 1
section_bar(ws, r, 8, "TARGET FABRICATION RULES (JLCPCB)"); r += 1
for k, v in fab:
ws.cell(row=r, column=2, value=k).font = F_BOLD
ws.cell(row=r, column=3, value=v).font = F_BODY
ws.merge_cells(start_row=r, start_column=3, end_row=r, end_column=8)
ws.cell(row=r, column=3).alignment = A_L
r += 1
r += 1
section_bar(ws, r, 8, "LIVE COST ROLL-UP (cross-sheet formulas)"); r += 1
qty_row = r
ws.cell(row=r, column=2, value="Build quantity (boards / units)").font = F_BOLD
qc = ws.cell(row=r, column=4, value=5)
qc.font, qc.fill, qc.border, qc.alignment = F_TOTAL, FILL_ACC, B_ALL, A_C
ws.cell(row=r, column=5, value="<- edit: JLC min 5; fleet target 30 by Aug").font = F_SUB
r += 1
rows = [
("Internal PCBA parts / board", f"='Internal PCBA BOM'!K{pcba_total_row}"),
("External integration / unit", f"='External Integration BOM'!G{ext_total_row}"),
("Combined cost / unit", f"=D{r}+D{r+1}"),
("Order total (x build qty)", f"=D{r+2}*D{qty_row}"),
]
for i, (k, f) in enumerate(rows):
rr = r + i
ws.cell(row=rr, column=2, value=k).font = F_BOLD
cc = ws.cell(row=rr, column=4, value=f)
cc.font, cc.number_format, cc.alignment, cc.border = F_TOTAL, MONEY2, A_R, B_ALL
r += len(rows)
ws.cell(row=r, column=2, value="Excludes: bare-PCB fab + PCBA service fees (live JLC quote), SIM plan, labor.").font = F_SUB
for col, w in zip("ABCDEFGH", [3, 30, 26, 16, 44, 12, 12, 12]):
ws.column_dimensions[col].width = w
def main():
outdir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parent.parent
wb = Workbook()
ws_sum = wb.active
ws_sum.title = "Summary"
ws_pcba = wb.create_sheet("Internal PCBA BOM")
ws_ext = wb.create_sheet("External Integration BOM")
pcba_total = build_pcba(ws_pcba)
ext_total = build_ext(ws_ext)
build_summary(ws_sum, pcba_total, ext_total)
wb.properties.title = "Helgen EinarBoard 1.0 — Production BOM"
wb.properties.creator = "Helgen Technologies — Project Drogheda"
out = outdir / "helgen_einarboard_1.0_production_bom.xlsx"
wb.save(out)
print(f"OK {out} (PCBA total row K{pcba_total} · EXT total row G{ext_total})")
if __name__ == "__main__":
main()