204а. Корпус прибора: от платы к корпусу, инструменты, проверка до печати
Плата из раздела 199 собрана, прошита, испытана — и лежит на столе голой. Прибором она станет в корпусе: он держит плату, выпускает наружу разъёмы и свет, защищает от пальцев, пыли и воды. Этот раздел — как корпус получают из той же модели платы, без ручного перерисовывания размеров: KiCad выгружает плату, скрипт строит по ней корпус и проверяет, что всё встало, и только потом корпус идёт на печать.
Весь путь прогнан на плате из 199 (F103 в LQFP-48, стабилизатор SOT-223,
кварц, две гребёнки, светодиод): kicad-cli 9.0.9 выгрузил плату в STEP,
скрипт на CadQuery 2.8.0 прочитал файл платы, нашёл места под винты,
построил дно и крышку, проверил пересечения и выгрузил STL. Высоты деталей —
таблица в скрипте: объёмных моделей в образе KiCad нет, и для всех деталей,
кроме LQFP-48, это допущения.
Как это называется в документации
Оригинал Перевод enclosure, housing корпус standoff, boss стойка под плату, бобышка под винт mounting hole крепёжное отверстие courtyard (F.CrtYd) контур, занятый деталью на плате STEP обменный формат твёрдых тел между CAD STL сетка треугольников для слайсера и печати slicer слайсер: режет модель на слои для принтера clearance зазор ingress protection (IP) степень защиты оболочки light pipe световод от светодиода к окну корпуса
204а.1 Готовый, напечатанный или свой
| Путь | Когда | Чем платят |
|---|---|---|
| готовый корпус из каталога | плата сделана под него: размеры и стойки по чертежу корпуса | плату рисуют под корпус, а не наоборот |
| печать FDM | десятки штук, макеты, свои формы | время печати, слоистость, пластик мягчеет от тепла |
| печать смолой (SLA) | мелкие детали, гладкая поверхность | хрупкость, постобработка |
| фрезеровка, литьё под давлением | серия, металл, прочность | оснастка и срок; литьё окупается на тысячах |
Порядок решений обратный тому, как обычно действуют. Сначала выбирают корпус или хотя бы его наружные размеры и места под винты — потом разводят плату. Готовый корпус из каталога почти всегда дешевле и крепче напечатанного; свой нужен, когда форма диктуется изделием.
204а.2 Что корпус берёт у платы
Корпусу нужны пять вещей, и все они уже лежат в файле платы:
| Что | Откуда | На плате из 199 |
|---|---|---|
| контур и толщина | слой Edge.Cuts, (thickness …) |
45 × 32 мм, 1,6 мм |
| где что стоит | контуры F.CrtYd каждой детали |
15 деталей |
| высота над платой | объёмные модели деталей | таблица допущений; LQFP-48 — 1,6 мм по даташиту F103 |
| что выходит наружу | разъёмы, светодиоды, кнопки | J1, J2 — гребёнки; D1 — светодиод |
| чем держится | крепёжные отверстия | их нет — место ищет скрипт (204а.4) |
Перерисовывать это руками в CAD — источник ошибок на каждой ревизии платы. Правильно — читать из файла: плата поменялась, скрипт перестроил корпус.
204а.3 Инструменты
| Инструмент | Что делает в этой цепочке | Лицензия |
|---|---|---|
KiCad, kicad-cli pcb export step |
выгружает плату (и детали, если есть их модели) в STEP | GPL |
| CadQuery (ядро OpenCascade) | корпус как программа на Python: параметры, проверки, выгрузка | Apache 2.0 |
| FreeCAD + KiCad StepUp | то же мышью: открыть STEP платы, нарисовать корпус вокруг | LGPL |
| OpenSCAD | корпус как текст на своём языке; удобно для простых коробок | GPL |
| Fusion, Onshape | облачные CAD; удобно рисовать, неудобно повторять | коммерческие |
| trimesh | проверка сеток STL: объём, замкнутость | MIT |
| слайсер (PrusaSlicer, Cura, OrcaSlicer) | STL → слои → G-код для принтера | открытые |
Здесь выбран CadQuery: корпус описан программой, как и всё в справочнике, поэтому его можно перестроить по новой плате и проверить тем же прогоном. FreeCAD хорош, когда корпус сложной формы проще нарисовать, чем описать; STEP платы он открывает так же.
Выгрузка платы — одна команда в образе kicad/kicad:9.0:
kicad-cli pcb export step --subst-models -f -o board.step board.kicad_pcb
CadQuery прочитал этот STEP: плата 45,00 × 32,00 × 1,51 мм. Не 1,6 — в STEP ушла только основа платы: общая толщина 1,6 мм минус медь и маска с двух сторон, (0,035 + 0,01) × 2 = 0,09 мм. Корпус считают по полной толщине из файла платы, а не по STEP.
204а.4 Крепёж — до разводки, а не после
У платы из 199 крепёжных отверстий нет. Скрипт ищет место под винт M2 (отверстие ⌀ 2,2 мм и ещё 0,5 мм запаса до детали) у каждого угла: ближайшую к углу точку, которая не ближе 3 мм к краю платы и не задевает контур ни одной детали.
| Угол | Отверстие | Запас сверх 0,5 мм до детали |
|---|---|---|
| левый верхний | x = 7,75, y = 10,75 | 0,30 мм |
| правый верхний | x = 42,00, y = 3,00 | 2,63 мм |
| левый нижний | x = 6,75, y = 22,25 | 0,38 мм |
| правый нижний | x = 42,00, y = 29,00 | 0,72 мм |
Правые углы свободны — винты встали ровно в 3 мм от краёв. Левые заняты: кварц тянется вдоль верхнего края почти на 14 мм, внизу стоят гребёнка J2 и стабилизатор. Винты уехали внутрь платы, и плата будет держаться несимметрично; стойки внутри корпуса встанут не там, где их ждёшь.
Вывод простой: места под крепёж ставят первыми, до расстановки деталей, и закрывают запретной зоной — так же, как контур платы. Чаще всего это четыре отверстия M2,5 или M3 в углах; их координаты и есть договор между платой и корпусом.
204а.5 Корпус программой
Параметры — в начале скрипта; всё остальное вычисляется из них и из файла платы:
WALL = 2.0 # стенка и дно, мм — для печати соплом 0,4 это 5 проходов
GAP = 0.5 # зазор плата — стенка с каждой стороны
STANDOFF = 4.0 # стойка под платой: хвосты выводов гребёнок снизу — до 3 мм
LID_CLEAR = 1.5 # над самой высокой деталью под крышкой
HOLE_D = 2.2 # отверстие в плате под винт M2
Дно — коробка с вырезанной полостью и стойками под саморезы в найденных местах:
split = z_board + thick # дно по верх платы, выше — крышка
base = (cq.Workplane("XY").box(out_w, out_h, split, centered=False).translate((ox, fy(by2) - GAP - WALL, 0))
.cut(cq.Workplane("XY").box(in_w, in_h, split, centered=False).translate((bx1 - GAP, fy(by2) - GAP, WALL))))
for cx, cy, _ in hs:
boss = (cq.Workplane("XY").circle(BOSS_D / 2).extrude(STANDOFF).translate((cx, fy(cy), WALL))
.cut(cq.Workplane("XY").circle(PILOT_D / 2).extrude(STANDOFF).translate((cx, fy(cy), WALL))))
base = base.union(boss)
Крышка — такая же коробка сверху; в ней прорези по контурам гребёнок
с запасом 0,3 мм и окно ⌀ 2 мм над светодиодом. Одна тонкость: в KiCad
ось Y направлена вниз, в CAD — вверх, поэтому все координаты с платы
проходят через fy(y); забудешь — прорези окажутся зеркально.
Что получилось:
| Размер | Как считается | Значение |
|---|---|---|
| снаружи, длина × ширина | 45 + 2 · 0,5 + 2 · 2 и 32 + 2 · 0,5 + 2 · 2 | 50,0 × 37,0 мм |
| снаружи, высота | 2 + 4 + 1,6 + 4,3 + 1,5 + 2 | 15,4 мм |
| прорезь J1 / J2 | контур гребёнки + 2 · 0,3 | 4,14 × 11,76 / 4,14 × 6,69 мм |
| объём пластика: дно / крышка | из модели | 5841 / 5467 мм³ |
Высота 4,3 мм под крышкой — это кварц HC49-SD из таблицы допущений; для своей платы её берут из чертежа самой высокой детали.
204а.6 Проверка до печати
Печать корпуса — часы. Ошибку, которая видна в модели за секунду, находят до неё:
- Пересечения. Корпус пересекают с платой и с объёмами деталей: оба объёма пересечения — 0,0000 мм³. Ненулевой — деталь упирается в стенку, стойку или крышку.
- Сверка с KiCad. Габарит платы из STEP — 45,00 × 32,00 мм, как контур в файле; толщина — 1,51 мм (204а.3).
- Запас у отверстий сверх обязательных 0,5 мм — от 0,30 до 2,63 мм: все положительные.
- Выход наружу. Прорези стоят над J1 и J2, окно — над D1; проверяют тем же пересечением: гребёнка не должна касаться крышки.
Скрипт выгружает case_base.stl и case_lid.stl для слайсера и case.step
для тех, кто будет дорабатывать корпус в другом CAD.
204а.7 Печать, материал и защита
Зазоры печати. FDM-принтер кладёт линию шире расчётной, отверстия выходят меньше, а посадки туже, чем в модели. Поэтому отверстия под саморезы делают по месту: пробная печать одной стойки дешевле корпуса. Стенку задают кратной ширине линии: 2,0 мм при сопле 0,4 — пять проходов.
Материал. PLA печатается легче всех, но мягчеет от тепла: корпус на солнце или в машине ведёт. Для тёплых мест и улицы берут PETG или ASA, для деталей под нагрузкой — их же с большим заполнением. Температуры размягчения — в паспорте конкретного пластика.
Степень защиты IP (ГОСТ 14254, он же IEC 60529). Первая цифра — от твёрдых тел и пыли, вторая — от воды: IP54 — пылезащищённый, от брызг со всех сторон; IP67 — пыленепроницаемый, выдерживает кратковременное погружение. Напечатанный корпус с прорезями под гребёнки — в лучшем случае IP20: от пальцев, не от воды. Уплотнение, герметичные разъёмы и испытание для заявленной цифры — задача не принтера, а конструкции (раздел 206).
Радио и корпус. Металл и пластик с наполнителем экранируют антенну. Модуль с антенной на плате ставят к пластиковой стенке и не закрывают металлом; подробнее — в 206.3.
204а.8 Грабли
- Крепёж после разводки. Углы уже заняты — винты уезжают внутрь платы, как у платы из 199. Отверстия ставят первыми.
- Толщина из STEP. KiCad выгружает основу платы 1,51 мм вместо 1,6. Высоту считают по толщине из файла платы.
- Ось Y. В KiCad она вниз, в CAD — вверх; прорези выходят зеркально.
- Хвосты выводов снизу. Выводные детали торчат под платой на 2–3 мм; стойка ниже — плата ложится на выводы.
- Высоты «на глаз». Без моделей деталей высоту берут из чертежа в даташите, а не из фотографии.
- Корпус без запаса по краю. Плата режется с допуском; зазор 0,5 мм с каждой стороны — чтобы она вошла.
- Проверка только глазами. Пересечение объёмов в модели находит упор детали в стойку за секунду; напечатанный корпус — за часы.
204а.9 Итоги
- Корпус строят из файла платы: контур, толщина, контуры деталей и высоты — всё читается программой, перерисовывать не нужно.
- Цепочка:
kicad-cli pcb export step→ CadQuery → проверка → STL в слайсер и STEP для других CAD. - На плате из 199 два угла заняты кварцем и гребёнкой: винты M2 уехали внутрь на 9,1 и 7,7 мм. Крепёж закладывают до разводки.
- Корпус 50,0 × 37,0 × 15,4 мм: стенка 2 мм, зазор 0,5, стойки 4 мм, 1,5 мм над самой высокой деталью; пересечений с платой и деталями — 0,0000 мм³.
- STEP платы из KiCad — 1,51 мм толщиной: без меди и маски.
- Материал — по температуре места, степень защиты — по конструкции и испытанию, а не по принтеру.
Полные исходники
case204.py — корпус по плате из KiCad — параметрически, в CadQuery.
# -*- coding: utf-8 -*-
"""204а: корпус по плате из KiCad — параметрически, в CadQuery.
Запуск в образе с CadQuery (cadquery-cad): python case204.py board.kicad_pcb
Читает из файла платы контур (Edge.Cuts), толщину и контуры деталей (F.CrtYd), подбирает
места под винты M2 в углах так, чтобы отверстие с запасом не задевало ни одной детали,
строит дно со стойками и крышку с прорезями под разъёмы и окном над светодиодом,
проверяет пересечения и зазоры и выгружает STL, STEP и проекции SVG."""
import math, re, sys
import cadquery as cq
# ── параметры корпуса (всё, что меняют под свой прибор) ─────────────────
WALL = 2.0 # стенка и дно, мм — для печати соплом 0,4 это 5 проходов
GAP = 0.5 # зазор плата — стенка с каждой стороны
STANDOFF = 4.0 # стойка под платой: хвосты выводов гребёнок снизу — до 3 мм
LID_CLEAR = 1.5 # над самой высокой деталью под крышкой
HOLE_D = 2.2 # отверстие в плате под винт M2
KEEP = 0.5 # запас от края отверстия до контура детали
BOSS_D = 5.0 # стойка под саморез M2
PILOT_D = 1.6 # отверстие в стойке под саморез M2 в пластик
CORNER_INSET = 3.0 # центр отверстия — не ближе 3 мм к краю платы
MIN_CLEAR = 0.3 # и не ближе 0,3 мм (сверх KEEP) к контуру детали
# высота деталей над платой, мм: LQFP-48 — «A max» из даташита F103, остальное — допущения
# (объёмных моделей в образе KiCad нет; сверять с даташитом своей детали)
HEIGHT = {"LQFP-48": 1.6, "SOT-223": 1.8, "Crystal_SMD_HC49-SD": 4.3, "C_0603": 0.9, "C_0805": 1.3,
"R_0603": 0.55, "LED_0805": 0.8, "PinHeader": 8.5}
THROUGH_LID = ("PinHeader",) # эти выходят наружу через прорези крышки
def num(s):
return float(s)
def parse(path):
s = open(path, encoding="utf-8").read()
thick = num(re.search(r"\(thickness ([\d.]+)\)", s).group(1))
xs, ys = [], []
for m in re.finditer(r'\(gr_line\s*\(start ([\d.\-]+) ([\d.\-]+)\)\s*\(end ([\d.\-]+) ([\d.\-]+)\).*?\(layer "Edge\.Cuts"\)', s, re.S):
a, b, c, d = map(num, m.groups())
xs += [a, c]
ys += [b, d]
board = (min(xs), min(ys), max(xs), max(ys))
parts = []
starts = [m.start() for m in re.finditer(r'\n\t\(footprint "', s)] + [len(s)]
for i in range(len(starts) - 1):
blk = s[starts[i]:starts[i + 1]]
name = re.search(r'\(footprint "([^"]+)"', blk).group(1)
at = re.search(r'\n\t\t\(at ([\d.\-]+) ([\d.\-]+)(?: ([\d.\-]+))?\)', blk)
x0, y0 = num(at.group(1)), num(at.group(2))
rot = math.radians(num(at.group(3))) if at.group(3) else 0.0
ref = re.search(r'\(property "Reference" "([^"]+)"', blk).group(1)
pts = []
for m in re.finditer(r'\(fp_(?:line|rect)\s*\(start ([\d.\-]+) ([\d.\-]+)\)\s*\(end ([\d.\-]+) ([\d.\-]+)\)(?:(?!\(fp_).)*?\(layer "F\.CrtYd"\)', blk, re.S):
a, b, c, d = map(num, m.groups())
pts += [(a, b), (c, d), (a, d), (c, b)]
# KiCad: поворот по часовой при оси Y вниз
tp = [(x0 + px * math.cos(rot) + py * math.sin(rot), y0 - px * math.sin(rot) + py * math.cos(rot)) for px, py in pts]
bx = (min(p[0] for p in tp), min(p[1] for p in tp), max(p[0] for p in tp), max(p[1] for p in tp))
h = next(v for k, v in HEIGHT.items() if name.startswith(k))
parts.append((ref, name, bx, h))
return thick, board, parts
def clear(cx, cy, parts):
"""Расстояние от края отверстия (с запасом) до ближайшего контура детали; > 0 — свободно."""
r = HOLE_D / 2 + KEEP
best = 1e9
for _, _, (x1, y1, x2, y2), _ in parts:
dx = max(x1 - cx, 0, cx - x2)
dy = max(y1 - cy, 0, cy - y2)
best = min(best, math.hypot(dx, dy) - r)
return best
def holes(board, parts):
"""У каждого угла — ближайшая к нему точка на сетке 0,25 мм, где отверстие не ближе
CORNER_INSET к краям платы и не ближе MIN_CLEAR к контуру любой детали."""
bx1, by1, bx2, by2 = board
out = []
for sx, sy in ((0, 0), (1, 0), (0, 1), (1, 1)):
best = None
for i in range(0, 81):
for j in range(0, 61):
dx, dy = CORNER_INSET + i * 0.25, CORNER_INSET + j * 0.25
cx = bx1 + dx if sx == 0 else bx2 - dx
cy = by1 + dy if sy == 0 else by2 - dy
c = clear(cx, cy, parts)
if c >= MIN_CLEAR:
d = math.hypot(dx, dy)
if best is None or d < best[0]:
best = (d, (round(cx, 2), round(cy, 2), round(c, 2)))
out.append(best[1] if best else None)
return out
def build(path):
thick, board, parts = parse(path)
bx1, by1, bx2, by2 = board
bw, bh = bx2 - bx1, by2 - by1
hs = holes(board, parts)
top_parts = [p for p in parts if not p[1].startswith(THROUGH_LID)]
tallest = max(p[3] for p in top_parts)
in_w, in_h = bw + 2 * GAP, bh + 2 * GAP
z_board = WALL + STANDOFF # низ платы
z_top_in = z_board + thick + tallest + LID_CLEAR # потолок изнутри
ox, oy = bx1 - GAP - WALL, by1 - GAP - WALL # угол корпуса в координатах платы
out_w, out_h = in_w + 2 * WALL, in_h + 2 * WALL
# координаты платы (Y вниз) → корпус (Y вверх): y' = by2 - y + by1
fy = lambda y: by2 - y + by1
split = z_board + thick # дно по верх платы, выше — крышка
base = (cq.Workplane("XY").box(out_w, out_h, split, centered=False).translate((ox, fy(by2) - GAP - WALL, 0))
.cut(cq.Workplane("XY").box(in_w, in_h, split, centered=False).translate((bx1 - GAP, fy(by2) - GAP, WALL))))
for cx, cy, _ in hs:
boss = (cq.Workplane("XY").circle(BOSS_D / 2).extrude(STANDOFF).translate((cx, fy(cy), WALL))
.cut(cq.Workplane("XY").circle(PILOT_D / 2).extrude(STANDOFF).translate((cx, fy(cy), WALL))))
base = base.union(boss)
lid_h = z_top_in + WALL - split
lid = (cq.Workplane("XY").box(out_w, out_h, lid_h, centered=False).translate((ox, fy(by2) - GAP - WALL, split))
.cut(cq.Workplane("XY").box(in_w, in_h, lid_h - WALL, centered=False).translate((bx1 - GAP, fy(by2) - GAP, split))))
cut_list = []
for ref, name, (x1, y1, x2, y2), h in parts:
if name.startswith(THROUGH_LID): # прорезь по контуру гребёнки + 0,3 мм
w, d = x2 - x1 + 0.6, y2 - y1 + 0.6
lid = lid.cut(cq.Workplane("XY").box(w, d, lid_h + 1, centered=False)
.translate((x1 - 0.3, fy(y2) - 0.3, split - 0.5)))
cut_list.append((ref, round(w, 2), round(d, 2)))
if name.startswith("LED"): # окно над светодиодом, ⌀ 2 мм
lid = lid.cut(cq.Workplane("XY").circle(1.0).extrude(WALL + 1).translate(((x1 + x2) / 2, fy((y1 + y2) / 2), z_top_in - 0.5)))
cut_list.append((ref, "окно 2", ""))
pcb = cq.Workplane("XY").box(bw, bh, thick, centered=False).translate((bx1, fy(by2), z_board))
for cx, cy, _ in hs:
pcb = pcb.cut(cq.Workplane("XY").circle(HOLE_D / 2).extrude(thick).translate((cx, fy(cy), z_board)))
comps = None
for ref, name, (x1, y1, x2, y2), h in parts:
body = cq.Workplane("XY").box(x2 - x1, y2 - y1, h, centered=False).translate((x1, fy(y2), z_board + thick))
comps = body if comps is None else comps.union(body)
return dict(thick=thick, board=board, parts=parts, holes=hs, tallest=tallest, out=(out_w, out_h, z_top_in + WALL),
base=base, lid=lid, pcb=pcb, comps=comps, cuts=cut_list, split=split, z_board=z_board)
def vol(wp):
return sum(s.Volume() for s in wp.vals()) if wp is not None else 0.0
if __name__ == "__main__":
r = build(sys.argv[1] if len(sys.argv) > 1 else "board.kicad_pcb")
bx1, by1, bx2, by2 = r["board"]
print("плата: %.1f × %.1f мм, толщина %.1f" % (bx2 - bx1, by2 - by1, r["thick"]))
for ref, name, bx, h in r["parts"]:
print("деталь %s %s: контур %.2f…%.2f × %.2f…%.2f, высота %.1f" % (ref, name, bx[0], bx[2], bx[1], bx[3], h))
for k, hcl in enumerate(r["holes"]):
print("отверстие %d: x=%.2f y=%.2f, запас сверх 0,5 мм до детали %.2f мм" % ((k + 1,) + hcl))
print("самая высокая деталь под крышкой: %.1f мм" % r["tallest"])
print("корпус снаружи: %.1f × %.1f × %.1f мм" % r["out"])
for c in r["cuts"]:
print("вырез %s: %s %s" % c)
solid = r["base"].union(r["lid"])
for what, other in (("плата", r["pcb"]), ("детали", r["comps"])):
inter = vol(solid.intersect(other))
print("пересечение корпуса и %s: %.4f мм³" % ("платы" if what == "плата" else "деталей", inter))
print("объём дна %.0f мм³, крышки %.0f мм³" % (vol(r["base"]), vol(r["lid"])))
import os
if os.path.exists("board.step"): # сверка с выгрузкой kicad-cli pcb export step
bb = cq.importers.importStep("board.step").val().BoundingBox()
print("плата из STEP: %.2f × %.2f × %.2f мм" % (bb.xlen, bb.ylen, bb.zlen))
cq.exporters.export(r["base"], "case_base.stl")
cq.exporters.export(r["lid"], "case_lid.stl")
cq.exporters.export(r["base"].union(r["lid"]), "case.step")
asm = r["base"].union(r["pcb"]).union(r["comps"])
cq.exporters.export(asm, "view_base.svg", opt={"projectionDir": (1, -1.2, 1.1), "showHidden": False,
"width": 520, "height": 360, "strokeWidth": 0.35})
cq.exporters.export(r["lid"], "view_lid.svg", opt={"projectionDir": (1, -1.2, 1.1), "showHidden": False,
"width": 520, "height": 360, "strokeWidth": 0.35})
print("выгружено: case_base.stl, case_lid.stl, case.step, view_base.svg, view_lid.svg")
Дальше: раздел 205 «Документация изделия: комплект, обозначения, ревизии» — прибор живёт годами; теперь нужно, чтобы его можно было повторить, чинить и менять, не раскапывая память разработчика.