gokul/macro-six-usbc

USB-powered ATmega32U4 controller board with protected/switched 5 V power, six tactile keys, rotary encoder, reset/boot/ISP interfaces, power LED, and six daisy-chained SK6812 RGB LEDs driven through MOSFETs.

Version
0.1.6
License
MIT
Stars
0

scripts/check-availability.ts

import { mkdirSync } from "node:fs"

type SupplierPartNumber = string
interface CatalogPart {
  lcsc: number
  stock?: number
  price?: number | string
}
interface CatalogResponse { components: CatalogPart[] }
interface SupplierAttributes {
  componentCode?: string
  componentModelEn?: string
  assemblyMode?: string
  fixtureFlag?: boolean
  needAuditFlag?: boolean
  orderInstructionEnglish?: string
  moistureSensitivityLevelEn?: string
  lossNumber?: number
  preMinPurchaseNum?: number
}

const folder = "outputs/release"
mkdirSync(`${folder}/supplier-evidence`, { recursive: true })
const bom = await Bun.file(`${folder}/BOM.csv`).text()
const supplierPartNumbers = [...bom.matchAll(/"(C\d+)"$/gm)].map(match => match[1] as SupplierPartNumber)
async function checkPart(supplierPartNumber: SupplierPartNumber) {
  const checkedAt = new Date().toISOString()
  const sourceUrl = `https://jlcpcb.com/partdetail/${supplierPartNumber}`
  let attributes: SupplierAttributes = {}
  let pcbaType = "unconfirmed"
  let stock: number | null = null
  let price: number | string | null = null
  const lookupErrors: string[] = []
  try {
    const response = await fetch(sourceUrl, { signal: AbortSignal.timeout(30000) })
    if (!response.ok) throw new Error(`Public part page HTTP ${response.status}`)
    const html = await response.text()
    const readablePage = html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ")
    pcbaType = readablePage.match(/PCBA Type\s+(Economic and Standard|Standard Only)/)?.[1] ?? "unconfirmed"
    await Bun.write(`${folder}/supplier-evidence/${supplierPartNumber}.html`, html)
    const streams: string[] = []
    for (const match of html.matchAll(/self\.__next_f\.push\((\[.*?\])\)<\/script>/g)) {
      const segment = JSON.parse(match[1]) as [number, string | null]
      if (typeof segment[1] === "string") streams.push(segment[1])
    }
    const stream = streams.join("\n")
    for (const match of stream.matchAll(/"componentInfo":(\{)/g)) {
      const start = match.index + '"componentInfo":'.length
      let depth = 0, quoted = false, escaped = false
      for (let end = start; end < stream.length; end++) {
        const character = stream[end]
        if (quoted) {
          if (escaped) escaped = false
          else if (character === "\\") escaped = true
          else if (character === '"') quoted = false
        } else if (character === '"') quoted = true
        else if (character === "{") depth++
        else if (character === "}" && --depth === 0) {
          const supplier = JSON.parse(stream.slice(start, end + 1)) as SupplierAttributes
          if (supplier.componentCode === supplierPartNumber) attributes = { ...attributes, ...supplier }
          break
        }
      }
    }
  } catch (error) { lookupErrors.push(String(error)) }
  try {
    // Read-only public catalog lookup; no account, order, or board upload.
    const response = await fetch(`https://jlcsearch.tscircuit.com/api/search?q=${supplierPartNumber}&limit=5`, { signal: AbortSignal.timeout(15000) })
    if (!response.ok) throw new Error(`Catalog HTTP ${response.status}`)
    const catalog = await response.json() as CatalogResponse
    await Bun.write(`${folder}/supplier-evidence/${supplierPartNumber}-catalog.json`, JSON.stringify(catalog, null, 2))
    const exact = catalog.components.find(part => `C${part.lcsc}` === supplierPartNumber)
    stock = exact?.stock ?? null
    price = exact?.price ?? null
  } catch (error) { lookupErrors.push(String(error)) }
  const bomRow = bom.split("\n").find(line => line.endsWith(`"${supplierPartNumber}"`))!
  const designators = bomRow.match(/^"[^"]*","([^"]*)"/)![1].split(",")
  return { supplierPartNumber, checkedAt, sourceUrl, stock, price, stockStatus: stock === null ? "unconfirmed" : stock === 0 ? "out_of_stock" : "listed_in_stock", quantityPerBoard: designators.length, designators, pcbaType, model: attributes.componentModelEn ?? null, assemblyMode: attributes.assemblyMode ?? null, fixtureRequired: attributes.fixtureFlag ?? null, supplierAuditRequired: attributes.needAuditFlag ?? null, supplierInstructions: attributes.orderInstructionEnglish ?? "", moistureSensitivity: attributes.moistureSensitivityLevelEn ?? null, procurementMinimum: attributes.preMinPurchaseNum ?? null, assemblyAttrition: attributes.lossNumber ?? null, lookupErrors }
}
const rows = []
for (const supplierPartNumber of supplierPartNumbers) {
  rows.push(await checkPart(supplierPartNumber))
  await Bun.sleep(500)
}
await Bun.write(`${folder}/supplier-availability-snapshot.json`, JSON.stringify(rows, null, 2))
for (const row of rows) console.log(row.supplierPartNumber, row.stockStatus, row.stock, row.assemblyMode, row.supplierInstructions)
console.log("Catalog stock is indicative, not reserved. Order quantity and supplier approval remain required.")