0hmX/mq135-air-quality-sensor-module

This code defines a detailed PCB and schematic design for a Winsen MQ-135 air quality sensor module featuring the sensor itself, a comparator IC, voltage regulators, LEDs, resistors, a potentiometer, pin headers, and associated physical component footprints.

Version
1.0.1
License
unset
Stars
0

scripts/generate-models.mjs

import { createRequire } from "node:module"
import { convertJscadModelToGltf } from "jscad-to-gltf"

const require = createRequire(import.meta.url)
const modeling = require("@jscad/modeling")

const { cylinder, cuboid, sphere } = modeling.primitives
const { union, subtract, intersect } = modeling.booleans
const { rotateX, rotateZ, translate } = modeling.transforms

const zCylinder = (radius, height, centerZ, segments = 48) =>
  cylinder({ radius, height, center: [0, 0, centerZ], segments })

const outputGlb = async (filename, meshName, geometries) => {
  const result = await convertJscadModelToGltf(
    { geometries },
    { format: "glb", meshName, axisTransform: "none" },
  )
  await Bun.write(new URL(`../models/${filename}`, import.meta.url), result.data)
  return result.byteLength
}

// MQ-135: 19 mm body, six 1 mm leads on a 9.5 mm pitch circle.
const sensorPinAngles = [225, 180, 135, 45, 0, 315]
const leadPositions = sensorPinAngles.map((angleDegrees) => {
  const angle = (angleDegrees * Math.PI) / 180
  return [4.75 * Math.cos(angle), 4.75 * Math.sin(angle)]
})

// The breakout is an assembled module, so model the MQ leads after soldering
// and trimming rather than showing the bare sensor's full 7 mm lead length.
const sensorLeads = union(
  ...leadPositions.map(([x, y]) =>
    translate([x, y, 0], zCylinder(0.5, 2.8, -0.4, 24)),
  ),
)
const sensorBase = zCylinder(8.3, 3.2, 1.6, 72)
const sensorMetalSkirt = subtract(
  zCylinder(8.9, 4.2, 2.1, 72),
  zCylinder(8.35, 4.4, 2.2, 72),
)

const makeDome = (radius) =>
  union(
    zCylinder(radius, 7, 5.6, 72),
    intersect(
      sphere({ radius, center: [0, 0, 9.1], segments: 72 }),
      cuboid({ size: [20, 20, 7.9], center: [0, 0, 13.05] }),
    ),
  )

const innerSensorCore = makeDome(7.5)
const outerSensorCan = makeDome(7.9)
const topVentLocations = [
  [0, 0],
  ...[
    { radius: 1.5, count: 6 },
    { radius: 3, count: 10 },
    { radius: 4.6, count: 14 },
    { radius: 6.1, count: 18 },
  ].flatMap(({ radius, count }) =>
    Array.from({ length: count }, (_, ventIndex) => {
      const angle = (ventIndex * Math.PI * 2) / count
      return [radius * Math.cos(angle), radius * Math.sin(angle)]
    }),
  ),
]
const topVents = topVentLocations.map(([x, y]) =>
  translate([x, y, 0], zCylinder(0.2, 10, 15, 16)),
)
const sideVents = [4.2, 5.4, 6.6, 7.8, 9, 10.2, 11.4, 12.6, 13.8, 15].flatMap((z) =>
  Array.from({ length: 14 }, (_, ventIndex) => {
    const drill = rotateX(
      Math.PI / 2,
      cylinder({ radius: 0.2, height: 20, center: [0, 0, 0], segments: 16 }),
    )
    return translate(
      [0, 0, z],
      rotateZ((ventIndex * Math.PI * 2) / 14, drill),
    )
  }),
)
const perforatedSensorCan = subtract(
  outerSensorCan,
  ...topVents,
  ...sideVents,
)
const sensorFlange = zCylinder(9.5, 1.6, 2.6, 72)

const mq135Bytes = await outputGlb("mq135-sensor.glb", "MQ135", [
  {
    geom: union(perforatedSensorCan, sensorFlange, sensorMetalSkirt),
    color: [0.52, 0.54, 0.57],
  },
  { geom: sensorBase, color: [0.12, 0.08, 0.055] },
  { geom: innerSensorCore, color: [0.025, 0.025, 0.025] },
  { geom: sensorLeads, color: [0.72, 0.58, 0.22] },
])

// The FC-22 uses a bottom-mounted right-angle header. Its vertical tails pass
// through the PCB and its mating pins extend toward the negative Y board edge.
const headerPinX = [-3.81, -1.27, 1.27, 3.81]
const headerMetal = union(
  ...headerPinX.flatMap((x) => [
    cuboid({ size: [0.64, 0.64, 4], center: [x, 0, -1] }),
    cuboid({ size: [0.64, 7.2, 0.64], center: [x, -3.3, -3.23] }),
  ]),
)
const headerHousing = cuboid({
  size: [10.2, 2.5, 2.5],
  center: [0, 0.85, -2.85],
})
const headerBytes = await outputGlb(
  "right-angle-pin-header-1x4.glb",
  "RightAnglePinHeader1x4",
  [
    { geom: headerHousing, color: [0.035, 0.035, 0.035] },
    { geom: headerMetal, color: [0.66, 0.67, 0.7] },
  ],
)

// Compact 3362P top-adjust trimmer used on the electronics face.
const trimmerLeadPositions = [
  [-2.5, -1.25],
  [0, 1.25],
  [2.5, -1.25],
]
const trimmerLeads = union(
  ...trimmerLeadPositions.map(([x, y]) =>
    translate([x, y, 0], zCylinder(0.4, 4.2, 0.2, 20)),
  ),
)
const trimmerBody = cuboid({ size: [7, 6.8, 5], center: [0, 0, 2.5] })
const trimmerScrew = zCylinder(2.15, 0.45, 5.225, 48)
const trimmerSlot = cuboid({ size: [3, 0.45, 0.25], center: [0, 0, 5.48] })
const trimmerBytes = await outputGlb("3362p-trimmer.glb", "3362PTrimmer", [
  { geom: trimmerBody, color: [0.02, 0.16, 0.72] },
  { geom: trimmerScrew, color: [0.72, 0.74, 0.78] },
  { geom: trimmerSlot, color: [0.12, 0.12, 0.13] },
  { geom: trimmerLeads, color: [0.72, 0.58, 0.22] },
])

const soicLeadY = [-1.905, -0.635, 0.635, 1.905]
const soicLeads = union(
  ...[-1, 1].flatMap((side) =>
    soicLeadY.map((y) =>
      cuboid({ size: [1.2, 0.5, 0.25], center: [side * 2.45, y, 0.12] }),
    ),
  ),
)
const soicBody = cuboid({ size: [3.9, 4.9, 1.5], center: [0, 0, 0.75] })
const soicPinOne = zCylinder(0.3, 0.06, 1.53, 24)
const soicBytes = await outputGlb("soic8.glb", "SOIC8", [
  { geom: soicBody, color: [0.035, 0.035, 0.04] },
  { geom: soicLeads, color: [0.68, 0.69, 0.72] },
  {
    geom: translate([-1.25, -1.55, 0], soicPinOne),
    color: [0.82, 0.82, 0.82],
  },
])

console.log(
  JSON.stringify({ mq135Bytes, headerBytes, trimmerBytes, soicBytes }),
)