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 }),
)