board
Network cabinet fan controller
Public Unreviewedby matt
Two independently controlled 12 V fans, Ethernet and Wi-Fi, and an RGB speed display in a custom printed puck. Routed development preview, not approved for manufacture.
main
matt
Add MTA-156 input revision and document enclosure and cable-fit limits
16485bb
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
"""Circuit intent. All generators consume netlist.json; no independent PCB connections."""
import json,pathlib
ROOT=pathlib.Path(__file__).resolve().parent
cs=[];nets={}
def add(ref,sym,value,fp,pins,block,mpn='',mfr='',lcsc='',note=''):
cs.append(dict(ref=ref,sym=sym,value=value,fp=fp,block=block,mpn=mpn,mfr=mfr,src=('LCSC '+lcsc if lcsc else ''),lcsc=lcsc,note=note))
for pin,net in pins.items():
if net:nets.setdefault(net,[]).append(f'{ref}.{pin}')
def R(ref,v,a,b,block,lcsc='',mpn=''):
add(ref,'Device:R',v,'Resistor_SMD:R_0603_1608Metric',{'1':a,'2':b},block,mpn,'',lcsc)
def C(ref,v,a,b,block,size='0603_1608Metric',mpn='',lcsc=''):
add(ref,'Device:C',v,'Capacitor_SMD:C_'+size,{'1':a,'2':b},block,mpn,'',lcsc)
add('J1','Connector_Generic:Conn_01x02','12V IN','fan_controller:KF128_2P_5.08',{'1':'VIN_RAW','2':'GND'},'input','KF128-5.08-2P-AA','Cixi Kefa','C474952')
add('F1','Device:Fuse','4A fast','Fuse:Fuse_1206_3216Metric',{'1':'VIN_RAW','2':'VIN_FUSED'},'input','0466004.NRHF','Littelfuse','C187596','Secondary protection; 2 A combined continuous design target; qualify start-up I²t and source fault current.')
add('Q1','fan_controller:AO4407A','AO4407A','Package_SO:SOIC-8_3.9x4.9mm_P1.27mm',{**{str(p):'VIN12' for p in [1,2,3]},'4':'RP_GATE',**{str(p):'VIN_FUSED' for p in [5,6,7,8]}},'input','AO4407A','Alpha & Omega Semiconductor','C16072','Drain toward source connector; body diode starts protected rail; source on protected side.')
R('R1','100k','RP_GATE','GND','input')
add('D1','Device:D_Zener','SMAJ15A','Diode_SMD:D_SMA',{'1':'VIN12','2':'GND'},'input','SMAJ15A/TR13','Brightking','C86819','15 V standoff; surge clamp is not fan overvoltage protection.')
add('C1','Device:C_Polarized','100u 25V','Capacitor_SMD:CP_Elec_6.3x5.8',{'1':'VIN12','2':'GND'},'input','EEEFT1E101AP','Panasonic','C178590','Bulk input; 6.3 mm diameter, 5.8 mm high, rated 300 mA ripple.')
C('C2','10u 25V','VIN12','GND','power','0805_2012Metric','CL21A106KAYNNNE','C15850')
C('C3','100n','VIN12','GND','power',mpn='CC0603KRX7R9BB104',lcsc='C14663')
add('U1','fan_controller:AP63203WU','AP63203WU-7','Package_TO_SOT_SMD:TSOT-23-6',{'1':'V3V3','2':'VIN12','3':'VIN12','4':'GND','5':'SW','6':'BST'},'power','AP63203WU-7','Diodes Incorporated','C780769')
C('C4','100n','BST','SW','power',mpn='CC0603KRX7R9BB104',lcsc='C14663')
add('L1','Device:L','3.9uH','fan_controller:CDRH6D38T125',{'1':'SW','2':'V3V3'},'power','CDRH6D38T125NP-3R9NC','Sumida','C2890514')
for ref in ['C5','C6']:C(ref,'22u 16V','V3V3','GND','power','1206_3216Metric','CL31A226KOHNNNE','C90146')
mcupins={str(p):'GND' for p in [1,2,11,14,*range(36,54)]}
mcupins.update({'3':'V3V3','5':'STRAP2','8':'EN','12':'PWM1_MCU','13':'PWM2_MCU','16':'STATUS','18':'TACH1_MCU','19':'TACH2_MCU','20':'ONEWIRE','21':'PWM_DISABLE','22':'STRAP8','23':'BOOT','30':'UART_RX','31':'UART_TX'})
add('U2','fan_controller:ESP32-C3-MINI-1U','ESP32-C3-MINI-1U-N4','fan_controller:ESP32-C3-MINI-1U',mcupins,'mcu','ESP32-C3-MINI-1U-N4','Espressif','C2911374','N4 is NRND; N4X not in current stocked search. External antenna required; existing silicon supported for this prototype.')
C('C7','10u 25V','V3V3','GND','mcu','0805_2012Metric','CL21A106KAYNNNE','C15850')
C('C8','100n','V3V3','GND','mcu',mpn='CC0603KRX7R9BB104',lcsc='C14663')
R('R2','10k','V3V3','EN','mcu');C('C9','1u','EN','GND','mcu')
for ref,net in [('R3','STRAP2'),('R4','STRAP8'),('R5','BOOT')]:R(ref,'10k','V3V3',net,'mcu')
add('U3','fan_controller:SN74LVC2G125','SN74LVC2G125DCTR','fan_controller:TI_DCT0008A',{'1':'PWM_DISABLE','2':'PWM1_MCU','3':'PWM2_DRV','4':'GND','5':'PWM2_MCU','6':'PWM1_DRV','7':'PWM_DISABLE','8':'V3V3'},'fans','SN74LVC2G125DCTR','Texas Instruments','C206035','DCT8 uses TI SM8; matched to SSOP-8 2.95x2.8 mm, 0.65 mm pitch.')
C('C10','100n','V3V3','GND','fans',mpn='CC0603KRX7R9BB104',lcsc='C14663')
R('R6','10k','V3V3','PWM_DISABLE','fans')
R('R7','47k','V3V3','PWM1_MCU','fans');R('R8','47k','V3V3','PWM2_MCU','fans')
R('R9','33','PWM1_DRV','PWM1','fans');R('R10','33','PWM2_DRV','PWM2','fans')
for n in [1,2]:
add('J'+str(n+1),'Connector_Generic:Conn_01x04','FAN'+str(n),'Connector:FanPinHeader_1x04_P2.54mm_Vertical',{'1':'GND','2':'VIN12','3':f'TACH{n}','4':f'PWM{n}'},'fans','47053-1000','Molex','C240840','Vertical mated height and cable bend require fit review.')
R('R'+str(9+n*2),'10k','V3V3',f'TACH{n}','fans')
R('R'+str(10+n*2),'1k',f'TACH{n}',f'TACH{n}_MCU','fans')
C('C'+str(10+n),'1n',f'TACH{n}_MCU','GND','fans')
add('J4','Connector_Generic:Conn_01x03','DS18B20 SENSOR','Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical',{'1':'GND','2':'ONEWIRE','3':'V3V3'},'sensor','B3B-XH-A(LF)(SN)','JST','C144394','Powered 3-wire DS18B20; cable shield if needed to GND at board only; remote 100 nF at sensor.')
R('R15','4.7k','V3V3','ONEWIRE','sensor')
add('J5','Connector_Generic:Conn_02x03_Odd_Even','3V3 UART PROGRAM','Connector_PinHeader_2.54mm:PinHeader_2x03_P2.54mm_Vertical',{'1':'GND','2':'V3V3','3':'UART_TX','4':'UART_RX','5':'EN','6':'BOOT'},'program','','','','Unpopulated service holes: use 3.3 V UART. Pin 2 is voltage sense only while 12 V is connected. Pull BOOT low then pulse EN low for download.')
R('R16','2.2k','STATUS','LED_A','indicator')
add('D2','Device:LED','STATUS red','LED_SMD:LED_0603_1608Metric',{'1':'GND','2':'LED_A'},'indicator','','','','Red LED; operating current about 0.6 mA.')
# Exposed copper test pads; no assembly line item.
for i,net in enumerate(['VIN_RAW','VIN12','V3V3','SW','PWM1','PWM2','TACH1','TACH2','ONEWIRE','EN','PWM_DISABLE','GND'],1):
add(f'TP{i}','Connector_Generic:Conn_01x01',net,'TestPoint:TestPoint_Pad_D1.0mm',{'1':net},'probe',note='Exposed copper only, no purchased component.')
# Flag each rail whose power arrives through passive protection/filter elements.
for i,net in enumerate(['VIN_RAW','VIN12','V3V3','GND'],1):add(f'#FLG0{i:02}','power:PWR_FLAG','PWR_FLAG','',{'1':net},'flags')
# Attach stocked orderable passives from saved catalogue responses.
source_by_value={'100k':'r-100k','10k':'r-10k','47k':'r-47k','1k':'r-1k','33':'r-33ohm','4.7k':'r-4.7k','2.2k':'r-2.2k','1u':'c-1u-basic','1n':'c-1n','STATUS red':'led-basic'}
for c in cs:
if c['value'] in source_by_value and not c['mpn']:
raw=(ROOT/('source-'+source_by_value[c['value']]+'.json')).read_text();part=json.loads(raw[raw.index('{'):])['components'][0]
c.update(mpn=part['mfr'],lcsc=part['lcsc'],src='LCSC '+part['lcsc'],mfr='UNI-ROYAL' if c['ref'].startswith('R') else 'Samsung' if c['ref'].startswith('C') else 'Hubei KENTO')
for c in cs:
if c['fp']:c['fp']='fan_controller:'+c['fp'].split(':')[1]
obj={'design':'fan-controller','revision':'A-development','components':cs,'nets':nets,'noConnect':{'U2':['4','6','7','9','10','15','17','24','25','26','27','28','29','32','33','34','35']},'board':{'width_mm':44.5,'height_mm':44.5,'corner_radius_mm':6.5,'thickness_mm':1.6,'layers':2},'loadAssumptions':{'VIN12_V':[11.4,12.6],'combined_continuous_fan_A':2.0,'each_fan_continuous_A':1.0,'regulator_output_A':0.6,'ambient_C':40,'qualification':'Self-set prototype envelope; actual fans, supply tolerance and start current still require confirmation.'}}
seen={}
for n,ps in nets.items():
for p in ps:
assert p not in seen,(p,n,seen.get(p));seen[p]=n
(ROOT/'netlist.json').write_text(json.dumps(obj,indent=2)+'\n')
print(f'{len(cs)} components, {len(nets)} named nets, {len(seen)} connected pins')