main
ae007 Project page: AFS_Adom rev A (Rice Eclipse AFS clean-room AI redesign): highlight reel and narrated process video with… cbe460e 7d ago
"""AFS_Adom engineering datasheet (project-authored), rev A. Built from the design record: DESIGN_NOTES.md,
design/calcs.json, sim/*.json, aiflow/run/fields/fields.json, the final board. reportlab platypus."""
import json, os
from reportlab.lib.pagesizes import letter
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreak, Preformatted, KeepTogether
S = '/tmp/claude-1001/-home-adom-project/8819fb5f-c2c6-4524-9528-99428cd6b88e/scratchpad'; P = S + '/AFS_Adom/'; G = S + '/gallery/'
OUT = P + 'design/AFS_Adom-datasheet.pdf'
ss = getSampleStyleSheet()
H1 = ParagraphStyle('h1', parent=ss['Heading1'], fontSize=17, spaceAfter=6, textColor=colors.HexColor('#0b3d2e'))
H2 = ParagraphStyle('h2', parent=ss['Heading2'], fontSize=12.5, spaceBefore=8, spaceAfter=4, textColor=colors.HexColor('#0b3d2e'))
B = ParagraphStyle('b', parent=ss['BodyText'], fontSize=9, leading=12)
SM = ParagraphStyle('sm', parent=B, fontSize=7.8, leading=10, textColor=colors.HexColor('#444444'))
TC = ParagraphStyle('tc', parent=B, fontSize=7.8, leading=9.6)
def T(rows, widths, head=True):
    data = [[Paragraph(str(c), TC) for c in r] for r in rows]
    t = Table(data, colWidths=widths, repeatRows=1 if head else 0)
    st = [('GRID', (0, 0), (-1, -1), 0.3, colors.HexColor('#9aa5a0')), ('VALIGN', (0, 0), (-1, -1), 'TOP'),
          ('LEFTPADDING', (0, 0), (-1, -1), 3), ('RIGHTPADDING', (0, 0), (-1, -1), 3), ('TOPPADDING', (0, 0), (-1, -1), 2), ('BOTTOMPADDING', (0, 0), (-1, -1), 2)]
    if head: st += [('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#dfe9e4'))]
    t.setStyle(TableStyle(st)); return t
def lim(l):
    if isinstance(l, dict): return ' '.join(('>= %g' if k == 'min' else '<= %g') % round(v, 3) for k, v in l.items())
    return '<= %g' % round(l, 3)
def img(path, w):
    from PIL import Image as PI
    im = PI.open(path); return Image(path, width=w, height=w * im.height / im.width)
pp = json.load(open(P + 'sim/powerpath.json')); sp = json.load(open(P + 'sim/servo_pulse.json')); bk = json.load(open(P + 'sim/buck_ripple.json'))
fj = json.load(open(P + 'aiflow/run/fields/fields.json'))
peaks = {n['net']: round(n['peakJ']) for n in fj['nets']}; chips = {c['reference']: c for c in fj['chips']}
st = []
def footer(c, doc):
    c.saveState(); c.setFont('Helvetica', 7); c.setFillColor(colors.HexColor('#666666'))
    c.drawString(15 * mm, 10 * mm, 'AFS_Adom rev A  |  engineering datasheet  |  design data, not a qualified product  |  wiki.adom.inc/ae007/afs-adom')
    c.drawRightString(200 * mm, 10 * mm, 'page %d' % doc.page); c.restoreState()
# ---- page 1
st += [Paragraph('AFS_Adom flight computer, rev A', H1),
       Paragraph('Active fin stabilization (AFS) controller for a sounding rocket: RP2350B with a 6-axis IMU, a high-g accelerometer, a magnetometer and a barometer on one SPI bus, eight 5 V-logic servo outputs on the raw 2S pack, microSD logging and USB-C, on a 4-layer 92 x 68 mm Adom molecule. Board designed, laid out, analysed and exported by an AI (Claude, adom-aiflow run AFS_Adom-claude-fable-5-1-20261015-2055) through the Adom KiCad Bridge, KiCad 10.0.6, 2026-10-15/16.', B),
       Spacer(1, 4), img(G + 'board-iso.png', 138 * mm), Spacer(1, 4),
       Paragraph('Status of every number in this document', H2),
       T([['Kind', 'Meaning here'],
          ['Design limit', 'What the part choice and copper were sized for (datasheet ratings, derated). Not a test result.'],
          ['Simulation', 'Own ngspice and Adom Fields models on the as-laid-out copper; screening estimates, not measurements.'],
          ['Measured', 'None yet. No board has been fabricated or tested. Every "measured" cell below says so.']], [32 * mm, 148 * mm]),
       Paragraph('Key figures', H2),
       T([['Item', 'Value', 'Kind'],
          ['Input', '2S LiPo 6.0-8.4 V on XT30 (J1), or USB-C 5 V; seamless switchover (ideal-diode OR)', 'design'],
          ['Servo outputs', '8 x 1x3 0.1" (S / VBAT / GND); signal 5 V logic (74AHCT541); power = raw pack', 'design'],
          ['Servo current', '4 A sustained, 8 A all-servo stall peak (20 ms) through the 5 mOhm INA219 shunt', 'design'],
          ['Rails', '+5 V 2 A buck (AP63205), +3.3 V 2 A buck (AP63203), 1.1 V core (RP2350 on-chip)', 'design'],
          ['MCU', 'RP2350B QFN-80, 12 MHz crystal, 16 MB W25Q128 QSPI flash', 'design'],
          ['Sensors', 'ICM-45686 IMU, KX134-1211 +-64 g accel, MMC5983MA magnetometer, MS5611 baro (SPI1); TMP117, INA219A (I2C0)', 'design'],
          ['Board', '92 x 68 mm, 1.6 mm FR4, 4 layers, ENIG, PCBWay standard rules; Adom molecule (MP1-MP4, MC1-MC14)', 'design'],
          ['DRC / ERC', '0 errors, 0 unconnected / 0 errors (3 explained warnings); netlist = board on 493/493 pins', 'checked'],
          ['Hottest part', 'U5 (5 V buck) +%.1f C still air, +%.1f C moving air at 0.35 W' % (chips['U5']['riseC'], chips['U5']['riseMovingC']), 'simulation'],
          ['Peak current density', '%d A/mm2 (VSYS), screen 60 A/mm2' % max(peaks.values()), 'simulation']], [32 * mm, 120 * mm, 28 * mm]),
       PageBreak()]
# ---- page 2: architecture
st += [Paragraph('1. Architecture and power', H1),
       Preformatted('''XT30 -- R1 5 mOhm --+-- VBAT (servo rail) -- J2..J9 servo headers (S / VBAT / GND)
                   |      |  D1 SMAJ12A TVS, C3/C4 2 x 220 uF bulk
       INA219A <---+      +-- U3 LTC4359 + Q1 DMN3404L (ideal diode) --+
USB-C VBUS ------------------ U4 LTC4359 + Q2 DMN3404L (ideal diode) --+-- VSYS
   VSYS -+- U5 AP63205 -> +5 V  -> U13 74AHCT541 (servo signals), J20/J21 5 V headers
         +- U6 AP63203 -> +3V3  -> RP2350B, flash, microSD, sensors, J18/J19 3V3 headers
            RP2350B on-chip buck (L3 3.3 uH) -> DVDD 1.1 V
RP2350B: SPI1 -> U8 ICM-45686, U9 KX134-1211, U10 MMC5983MA, U11 MS5611 (+ J16 spare CS)
         SPI0 -> J11 microSD   I2C0 -> U12 TMP117, U2 INA219, J14   I2C1 -> J15
         UART0 J13, SWD J12, PWM x 8 -> U13 -> J2..J9''', ParagraphStyle('pre', fontName='Courier', fontSize=7.2, leading=8.6)),
       Paragraph('Rails (design limits)', H2),
       T([['Rail', 'Source', 'Capacity', 'Expected load', 'Notes'],
          ['VBAT_IN', 'XT30, 2S 6.0-8.4 V', 'XT30 15 A', 'all of the board', 'no fuse on board (harness); TVS SMAJ12A'],
          ['VBAT', 'after R1 5 mOhm 2512 2 W', '8 A peak', '8 servos: 4 A sustained, 8 A stall', 'shunt 0.32 W at 8 A'],
          ['VSYS', 'ideal-diode OR (LTC4359 x2)', '1.5 A', 'both buck inputs', 'follows the higher source'],
          ['+5V', 'AP63205WU, 4.7 uH, 1.1 MHz', '2 A', 'about 1.1 A max (level shifter, 5 V headers 1 A budget)', 'about 4.7 V on USB only (dropout)'],
          ['+3V3', 'AP63203WU, 4.7 uH, 1.1 MHz', '2 A', 'about 1.0 A max (MCU, flash, SD, sensors, headers 0.5 A)', 'never leaves regulation on source change'],
          ['DVDD 1.1 V', 'RP2350 on-chip buck', '200 mA', 'core', 'L3 3.3 uH per RP2350 datasheet 6.3.8']], [20 * mm, 38 * mm, 18 * mm, 56 * mm, 48 * mm]),
       Paragraph('Source switchover', H2),
       Paragraph('USB only: VSYS = VBUS minus about 30 mV, the 3.3 V buck regulates, the board boots and enumerates; servos are unpowered without the pack. Battery only: VSYS = VBAT. Both: VSYS follows the higher (the pack). Plugging or unplugging either source moves VSYS between about 4.8 V and VBAT; the 3.3 V buck input never leaves its 3.8-32 V window, so the RP2350 does not reset. LTC4359 IN is rated -40 V, which also protects the logic path from a reversed pack.', B),
       Paragraph('Simulation results (own ngspice models, sim/*.json)', H2),
       T([['Check', 'Result', 'Limit', 'Pass']] + [[c['name'], c['value'], lim(c['limit']), 'yes' if c['pass'] else 'no'] for c in pp['checks'] + sp['checks']], [70 * mm, 30 * mm, 50 * mm, 15 * mm]),
       Spacer(1, 3),
       T([['Buck', 'VIN (V)', 'Ripple (mVpp)', 'Vout (V)', '0.5 A step dev (mV)']] + [[r['rail'], r['vin'], r['ripple_mVpp'], r['vout_mean_V'], r['step_deviation_mV']] for r in bk['results']], [25 * mm, 25 * mm, 35 * mm, 30 * mm, 40 * mm]),
       Paragraph('The buck model is a behavioural peak-current-mode model with a fitted compensation (the AP632xx internal compensation is not published); it compares ripple and step trends, it is not the vendor model.', SM),
       PageBreak()]
# ---- page 3: connectors
hdr = [['Ref', 'Function', 'Type', 'Pin 1', 'Pin 2', 'Pin 3', 'Pin 4'],
       ['J1', 'Battery in', 'Amass XT30PW-M', '+ (VBAT_IN)', '- (GND)', '', ''],
       ['J2-J9', 'SERVO1-8', '1x3 2.54 mm', 'S (5 V logic)', 'VBAT', 'GND', ''],
       ['J10', 'USB-C (USB 2.0 FS)', 'GCT USB4105', 'VBUS 5 V, D+/D- (27 Ohm series), CC 5.1 k', '', '', ''],
       ['J11', 'microSD (SPI mode)', 'Hirose DM3AT push-push', 'CS/MOSI/MISO/SCK, card detect -> GPIO6', '', '', ''],
       ['J12', 'SWD', 'JST PH 4', 'SWCLK', 'SWDIO', '3V3', 'GND'],
       ['J13', 'UART0', 'JST PH 4', 'TX (GPIO0)', 'RX (GPIO1)', '3V3', 'GND'],
       ['J14', 'I2C0', 'JST PH 4', 'SCL', 'SDA', '3V3', 'GND'],
       ['J15', 'I2C1', 'JST PH 4', 'SCL', 'SDA', '3V3', 'GND'],
       ['J16', 'spare SPI (sensor bus)', 'JST PH 4', 'MISO', 'CS (GPIO17)', 'SCK', 'MOSI'],
       ['J17', 'spare GPIO', 'JST PH 4', 'GPIO28', 'GPIO29', 'GPIO30', 'GPIO31'],
       ['J18/J19', '3V3 out', 'JST PH 2', '3V3', 'GND', '', ''],
       ['J20/J21', '5V out', 'JST PH 2', '5V', 'GND', '', ''],
       ['SW1 / SW2', 'BOOTSEL / RESET', '6 mm tactile', 'hold BOOTSEL, tap RESET = USB boot', '', '', '']]
mc = [['Contact', 'Position from MP1 (mm, y up)', 'Net'], ['MP1-MP4', '(0,0) (88,0) (0,64) (88,64)', 'GND (machine pins)'],
      ['MC1-MC4', 'left column x=0, y=38/42/46/50', 'VBAT_IN, GND, VBAT, GND'], ['MC5-MC7', 'right column x=88, y=50/46/42', '+5V, GND, +3V3'],
      ['MC13 / MC14', '(88,56) / (88,4)', 'RUN / SERVO1 signal'], ['MC8', '(32,8)', 'GND'], ['MC9-MC12', '(32,4) (34,4) (36,4) (32,6)', 'UART0 TX, UART0 RX, SWCLK, SWDIO']]
gp = [['Function', 'GPIO'], ['UART0 TX/RX', '0 / 1'], ['SPI0 SCK/MOSI/MISO (microSD)', '2 / 3 / 4'], ['SD CS, SD card detect', '5, 6'], ['I2C0 SDA/SCL', '8 / 9'],
      ['SPI1 SCK/MOSI/MISO (sensors)', '10 / 11 / 12'], ['CS: IMU, ACC, MAG, BARO, spare', '13, 14, 15, 16, 17'], ['INT: IMU1, IMU2, ACC1, ACC2, MAG, TMP ALERT', '18-23'],
      ['User LED', '25'], ['I2C1 SDA/SCL', '26 / 27'], ['Spare header', '28-31'], ['SERVO1-8 (PWM 8A..11B)', '32-39'], ['Unused (ADC capable)', '7, 24, 40-47']]
st += [Paragraph('2. Connectors, contacts and pin map', H1), T(hdr, [17 * mm, 30 * mm, 30 * mm, 38 * mm, 24 * mm, 20 * mm, 21 * mm]),
       Paragraph('Adom molecule contacts', H2), T(mc, [28 * mm, 70 * mm, 82 * mm]),
       Paragraph('RP2350B GPIO map (datasheet 9.4, table 678)', H2), T(gp, [110 * mm, 70 * mm]),
       Paragraph('Sensor addresses and modes: INA219 0x40 (A0 = A1 = GND), TMP117 0x48 (ADD0 = GND); the four SPI sensors each have their own CS and interrupt; MS5611 has no interrupt pin and is polled. The KX134 is the high-g channel for boost; the ICM-45686 is the primary rate/attitude sensor.', B),
       PageBreak()]
# ---- page 4: mechanical, fab, analysis, qualification
th = [['Part', 'Watts (design)', 'Heatsink vias', 'Rise still air', 'Rise moving air']] + [[r, '%.2f' % chips[r]['watts'], chips[r]['vias'], '+%.1f C' % chips[r]['riseC'], '+%.1f C' % chips[r]['riseMovingC']] for r in ('U5', 'U6', 'R1', 'Q1', 'U1')]
st += [Paragraph('3. Mechanical and fabrication', H1),
       T([['Item', 'Value'], ['Outline', '92 x 68 mm, 2 mm corner radii centred on the machine pins'], ['Mounting', '4 x M3 plated holes at (7, 5), (85, 5), (7, 63), (85, 63) mm from the top-left corner'],
          ['Machine pins', 'MP1-MP4 on an 88 x 64 mm span (Adom molecule)'], ['Tallest parts', 'XT30 (about 8.3 mm), 220 uF electrolytics, 1x3 headers'],
          ['Stack-up', '4 layers 1.6 mm FR4, 1 oz: F.Cu signal + pours / In1 GND / In2 +3V3 (VBAT, VSYS islands) / B.Cu signal + pours'],
          ['Rules', 'PCBWay standard 4-layer: 0.1016 mm track, 0.127 mm clearance, 0.454/0.2 mm via; used 0.15-0.2 mm signals, 0.46/0.2 mm signal vias'],
          ['Finish / silk', 'ENIG; silkscreen 0.8 mm minimum text (PCBWay), both faces'], ['Assembly', 'SMT both outside; 0603 minimum passive; QFN-80 0.4 mm pitch with a pre-routed fan-out']], [35 * mm, 145 * mm]),
       Paragraph('4. Current and temperature (simulation, Adom Fields on the final copper)', H1),
       Paragraph('Loads modelled as explicit pin currents at the sustained figures: servo rail and return 4 A, battery input 4 A, VSYS 1.5 A, +5 V 1.1 A, +3V3 1.0 A, DVDD 0.2 A. Peak current density per net (A/mm2, screen 60): ' + ', '.join('%s %d' % kv for kv in sorted(peaks.items(), key=lambda kv: -kv[1])) + '.', B),
       T(th, [25 * mm, 30 * mm, 30 * mm, 35 * mm, 35 * mm]),
       Paragraph('Budget: +35 C rise in still air (amended from the designer\'s 30 C working figure; AP63205 TJ max 125 C at a 60 C bay ambient). The 8 A all-servo stall is a 20 ms event checked in the servo-pulse simulation, not in the steady-state screens.', SM),
       Spacer(1, 4), img(G + 'fields-heat.png', 170 * mm),
       Paragraph('5. Qualification status and known limits', H1),
       T([['Item', 'Status'], ['Fabrication / assembly', 'Not built. Gerbers, drill, pick-and-place and BOM exported 2026-10-16.'], ['Measured electrical / thermal', 'None. All figures above are design limits or simulation.'],
          ['Firmware', 'None written; pin map above is the contract.'], ['Sourcing', 'Every purchased line on Mouser except J1 XT30PW-M (LCSC C431092, accepted by the owner).'],
          ['3D models', 'RP2350B, KX134 and XT30PW-M bodies are placeholder boxes for clearance only.'], ['Silkscreen', '65 small-part references could not fit at 0.8 mm; listed in DESIGN_NOTES 3.6.'],
          ['USB', 'Full-speed only, not impedance-controlled (standard stack-up), < 30 mm run.']], [45 * mm, 135 * mm])]
doc = SimpleDocTemplate(OUT, pagesize=letter, leftMargin=15 * mm, rightMargin=15 * mm, topMargin=14 * mm, bottomMargin=16 * mm, title='AFS_Adom rev A datasheet', author='ae007 / Claude (adom-aiflow)')
doc.build(st, onFirstPage=footer, onLaterPages=footer); print('wrote', OUT, os.path.getsize(OUT))