Devesh36/bicycle-brake-light

A USB‑C rechargeable 1S Li‑ion bicycle brake-light PCB with BQ24074 charging/protection, 3.3V AVR control, ADXL345 motion sensing, six PWM-driven red LEDs, and ISP programming.

Version
0.1.3
License
unset
Stars
0

checks/board.test.ts

import { describe, expect, test } from 'bun:test'
const circuit = await Bun.file(new URL('../dist/index/circuit.json', import.meta.url)).json()
const elements = circuit as any[]
const all = (type: string) => elements.filter(e => e.type === type)
const component = (name: string) => all('source_component').find(e => e.name === name)!
const port = (name: string, pin: number) => all('source_port').find(p => p.source_component_id === component(name).source_component_id && p.pin_number === pin)!
const nets = (name: string, pin: number) => {
  const p = port(name, pin)
  expect(p).toBeDefined()
  const ids = all('source_trace').filter(t => t.connected_source_port_ids.includes(p.source_port_id)).flatMap(t => t.connected_source_net_ids)
  return [...new Set(all('source_net').filter(n => ids.includes(n.source_net_id)).map(n => n.name))].sort()
}
const onNet = (name: string, pin: number, net: string) => expect(nets(name,pin)).toEqual([net])

describe('Generated PCB electrical contract', () => {
  test('USB-C has two independent 5.1k sink terminations', () => {
    onNet('J1',6,'CC1'); onNet('J1',12,'CC2')
    for (const [r, cc] of [['R1','CC1'],['R2','CC2']]) {
      expect(component(r).resistance).toBe(5100)
      onNet(r,1,cc); onNet(r,2,'GND')
    }
    onNet('J1',15,'USB5V'); onNet('J1',16,'USB5V')
  })
  test('Charger uses power path, external NTC, current limiting and safety timer', () => {
    onNet('U1',13,'USB5V'); onNet('U1',2,'VBAT'); onNet('U1',3,'VBAT')
    onNet('U1',10,'VSYS'); onNet('U1',11,'VSYS'); onNet('U1',17,'GND')
    onNet('U1',1,'NTC'); onNet('J2',2,'NTC'); onNet('J2',1,'VBAT'); onNet('J2',3,'GND')
    onNet('U1',5,'VSYS'); onNet('U1',6,'GND'); onNet('U1',4,'GND')
    onNet('R3',1,'ISET'); onNet('R4',1,'ILIM'); onNet('R5',1,'TMR')
    expect(890/component('R3').resistance).toBeCloseTo(0.1003,3)
    expect(1525/component('R4').resistance).toBeCloseTo(0.235,3)
    expect(component('R5').resistance).toBe(68000)
  })
  test('Switch also disconnects the supply monitor to avoid back-powering the MCU', () => {
    onNet('SW1',2,'VSYS'); onNet('SW1',1,'SW_VIN')
    onNet('U2',1,'SW_VIN'); onNet('U2',5,'V3V3')
    onNet('R10',1,'SW_VIN'); onNet('U3',2,'SUPPLY_SENSE')
  })
  test('ADXL345 is physically wired for I2C address 0x53', () => {
    onNet('U4',7,'V3V3'); onNet('U4',12,'GND')
    onNet('U4',13,'SDA'); onNet('U3',5,'SDA')
    onNet('U4',14,'SCL'); onNet('U3',7,'SCL')
    for (const p of [1,6]) onNet('U4',p,'V3V3')
    for (const p of [2,4,5,11]) onNet('U4',p,'GND')
  })
  test('Each red LED has its own resistor and correct physical polarity', () => {
    for(let i=1;i<=6;i++) {
      onNet(`D${i}`,1,'LED_RETURN'); onNet(`D${i}`,2,`LED${i}_A`)
      const r = `R${11+i}`
      onNet(r,1,'V3V3'); onNet(r,2,`LED${i}_A`)
      expect(component(r).resistance).toBe(120)
    }
    onNet('Q1',1,'GATE'); onNet('Q1',2,'GND'); onNet('Q1',3,'LED_RETURN')
    onNet('R19',1,'GATE'); onNet('R19',2,'GND')
  })
  test('AVR ISP connector uses the standard six-pin assignment', () => {
    ['LED_PWM','V3V3','SCL','SDA','RESET','GND'].forEach((net,i)=>onNet('J3',i+1,net))
  })
})

describe('Generated manufacturing geometry', () => {
  test('No emitted errors or unrouted connections', () => {
    expect(elements.filter(e => /error$/.test(e.type))).toEqual([])
    expect(all('pcb_trace').length).toBeGreaterThan(80)
    expect(all('source_component').length).toBe(45)
  })
  test('Vias use at least 0.3mm drills and 0.6mm pads', () => {
    expect(all('pcb_via').length).toBeGreaterThan(0)
    for(const v of all('pcb_via')) {
      expect(v.hole_diameter).toBeGreaterThanOrEqual(0.3 - 1e-6)
      expect(v.outer_diameter).toBeGreaterThanOrEqual(0.6 - 1e-6)
    }
  })
  test('Both copper layers contain ground pours', () => {
    expect(new Set(all('pcb_copper_pour').map(p=>p.layer))).toEqual(new Set(['top','bottom']))
  })
})