diff --git a/README.md b/README.md index aad871c..bbe09f1 100644 --- a/README.md +++ b/README.md @@ -90,3 +90,4 @@ README.md # 本文档 | 2026-10-17 | 新增 CYD+DHT11 桌面斜立 3D 外壳(OpenSCAD / STL) | | 2026-10-17 | 增加浏览器 Three.js STL 预览页 `cad/viewer.html` | | 2026-10-17 | 预览增强:合盖/开盖、分解、装配动画、剖切/线框等 | +| 2026-10-17 | 外壳改定:DHT11 后盖卡位微突出;前后盖止口合盖 | diff --git a/cad/README.md b/cad/README.md index 1e9b35a..bfaddf5 100644 --- a/cad/README.md +++ b/cad/README.md @@ -1,86 +1,55 @@ # CYD 温湿度仪表盘 3D 外壳 -桌面斜立分体外壳,适配 **ESP32-2432S028R(CYD)** + **DHT11(CN1 / GPIO22)**。 +桌面斜立分体外壳,适配 **ESP32-2432S028R(CYD)** + **DHT11**。 + +## 设计要点(当前) + +- **DHT11 在后盖外侧**:卡位槽 + 左右卡舌 + 前端止挡,模块从背面按入 +- **传感器微突出**约 **1.8 mm**(通风口/探头露出后盖平面) +- **前后盖合盖**:前框公止口落入后盖母槽;可用 `4×M3×10` 辅助固定 +- USB 侧孔、BOOT/RST 开孔、桌面支脚 ## 浏览器预览 -不要直接双击 HTML(`file://` 无法加载 STL)。在项目根目录执行: - ```bash python -m http.server 8765 --directory cad ``` -然后打开:**http://localhost:8765/viewer.html** - -### 预览操作 +打开:**http://localhost:8765/viewer.html** | 操作 | 说明 | |------|------| -| 合盖 / 开盖 | 前框铰链开合动画 | -| 开合度滑条 | 0% 合拢 → 100% 打开 | -| 分解滑条 | 拉开零件看内部 | -| 播放装配 | 分解 → 合拢 → 合盖 全程 | -| 模拟板 / DHT | 显示 PCB 与传感器示意 | -| 线框 / 透视 / 剖切 | 看结构与开孔 | -| 等轴/正/侧/顶 | 快速视角 | -| 截图 | 导出 PNG | -| 快捷键 | `O` 开盖 · `C` 合盖 · `空格` 播放 · `W` 线框 · `X` 透视 | - -其它查看方式:切片软件打开 `export/*.stl`,或 [viewstl.com](https://www.viewstl.com/) / [3dviewer.net](https://3dviewer.net/)。 +| 合盖 / 开盖 | 前框相对后盖开合 | +| 开合度 / 分解 | 拖动查看止口与 DHT 仓 | +| 看后盖 | 快速转到背面看 DHT 突出 | +| 模拟屏板 / DHT11模块 | 示意件开关 | +| 快捷键 | `C` 合盖 · `O` 开盖 · `空格` 播放装配 | ## 零件 | 文件 | 说明 | |------|------| -| `viewer.html` | 浏览器 3D 预览 | -| `export/front.stl` | 前框(屏窗 + 压边) | -| `export/back.stl` | 后盖 + 斜立支脚 + DHT11 通风仓 | -| `cyd_climate_case.scad` | 参数化源文件(OpenSCAD,可改尺寸) | -| `generate_stl.py` | 无 OpenSCAD 时用 Python 导出 STL | +| `export/front.stl` | 前框(屏窗 + 公止口) | +| `export/back.stl` | 后盖(母止口 + DHT 卡位仓 + 支脚) | +| `generate_stl.py` | Python 生成 STL | +| `cyd_climate_case.scad` | OpenSCAD 参数源 | +| `viewer.html` | 交互预览 | -## 设计要点 - -- PCB 基准:`86 × 50 × 1.6` mm,四角安装孔 -- 前框可视窗约 `59.5 × 45` mm -- 后盖背面净空约 7 mm;Micro-USB / USB-C 合并开孔 -- BOOT / RST 背面开孔;底部散热缝 -- **DHT11 独立通风仓**(后盖长边外侧),减少板温干扰;线槽通向 CN1 -- 固定:`4 × M3×10` 自攻进后盖塑料柱(孔径约 2.5 mm) - -## 打印建议 - -| 参数 | 建议 | -|------|------| -| 材料 | PLA 或 PETG | -| 层高 | 0.2 mm | -| 喷嘴 | 0.4 mm | -| 墙厚 | 2–3 圈 | -| 填充 | 15–20% | -| 支撑 | **不需要**(按 STL 默认朝向) | -| 前框 | 屏窗朝上打印 | -| 后盖 | 底板朝下,支脚伸出床外方向按切片预览摆放 | - -## 重新导出 STL +## 重新导出 ```bash pip install trimesh manifold3d numpy python cad/generate_stl.py ``` -或用 OpenSCAD: +## 打印 -```bash -openscad -o cad/export/front.stl -D part=\"front\" cad/cyd_climate_case.scad -openscad -o cad/export/back.stl -D part=\"back\" cad/cyd_climate_case.scad -``` - -改尺寸:编辑 `cyd_climate_case.scad` / `generate_stl.py` 顶部参数后重新导出。 +PLA/PETG,0.2 mm,墙 2–3 圈,填充 15–20%,一般无需支撑。 +后盖建议 **外底朝下**(DHT 仓朝上或按切片预览,必要时对 DHT 卡舌加少量支撑)。 ## 装配 -1. DHT11 放入后盖通风仓,杜邦线经线槽接到 **CN1**(`3V3 / IO22 / GND`,按你现有接线) -2. CYD 屏朝前框放入,USB 对齐侧孔 -3. 合上后盖,四角 M3×10 拧紧(勿过紧防裂) -4. 桌面斜立放置;点屏可调背光 - -若孔位与你的板子有 ±1 mm 偏差,优先改 `generate_stl.py` 里 `USB_Y` / `MH_INSET` 后重导。 +1. 将 DHT11 模块从 **后盖背面** 按入卡位,探头朝外微突出;线经仓内小孔接到 CN1(GPIO22) +2. CYD 放入后盖腔体,USB 对齐 +3. 前框止口对准后盖槽 **合盖**,四角螺丝固定 +4. 桌面斜立使用 diff --git a/cad/cyd_climate_case.scad b/cad/cyd_climate_case.scad index e8a64a8..8c795e6 100644 --- a/cad/cyd_climate_case.scad +++ b/cad/cyd_climate_case.scad @@ -1,300 +1,131 @@ /* - * CYD ESP32-2432S028R + DHT11 桌面斜立外壳 - * Climate Dashboard Case — parametric OpenSCAD + * CYD + DHT11 外壳(与 generate_stl.py 一致) * - * 零件: - * part = "front" 前框(屏窗) - * part = "back" 后盖 + 斜立支架 + DHT 通风仓 - * part = "assembly" 装配预览(半透明板子) + * - DHT11:后盖外侧卡位仓,传感器微突出 + * - 前后盖:公/母止口合盖 * - * 导出 STL(本机安装 OpenSCAD 后): - * openscad -o export/front.stl -D part=\"front\" cyd_climate_case.scad - * openscad -o export/back.stl -D part=\"back\" cyd_climate_case.scad + * part = "front" | "back" | "assembly" */ -/* [Which part] */ -part = "assembly"; // ["front", "back", "assembly"] +part = "assembly"; // ["front","back","assembly"] -/* [PCB / CYD] */ -pcb_l = 86.0; -pcb_w = 50.0; -pcb_t = 1.6; -pcb_corner_r = 4.0; -mh_inset = 4.0; // 安装孔距角边中心 -mh_dia = 3.2; // PCB 孔 +pcb_l = 86; pcb_w = 50; +wall = 2.4; gap = 0.35; +bezel_t = 2.2; cavity = 7.5; +lip_h = 2.4; lip_t = 1.15; +disp_h = 4; skirt_h = disp_h + 1; +view_l = 59.5; view_w = 45; view_ix = 7; view_iy = 2.5; disp_x = 8; +mh_inset = 4; screw = 3.3; boss_od = 7; boss_id = 2.5; +usb_w = 15; usb_h = 9; usb_y = 30.5; +stand_foot = 16; +dht_l = 16; dht_w = 12.5; dht_h = 8; dht_proud = 1.8; dht_clip = 1.1; -/* [Display] */ -disp_l = 69.0; -disp_w = 50.0; -disp_h = 4.0; -disp_x = 8.0; // 屏相对 PCB 左边偏移 -view_l = 59.5; -view_w = 45.0; -view_inset_x = 7.0; // 相对屏模组 -view_inset_y = 2.5; +inner_l = pcb_l + 2*gap; +inner_w = pcb_w + 2*gap; +outer_l = inner_l + 2*wall; +outer_w = inner_w + 2*wall; +shell_h = cavity + wall; +$fn = 40; -/* [Case shell] */ -wall = 2.4; -gap = 0.4; // 板与壳间隙 -front_lip = 1.2; // 屏边压边宽度 -bezel_front_t = 2.0; // 前框面板厚度 -cavity_back = 7.0; // 背面元件净空 -stand_angle = 60; // 桌面倾角(度,相对水平) -stand_foot = 18; // 支脚接触深度 +mh = [[mh_inset,mh_inset],[pcb_l-mh_inset,mh_inset], + [pcb_l-mh_inset,pcb_w-mh_inset],[mh_inset,pcb_w-mh_inset]]; -/* [USB dual cutout] */ -usb_dual = true; -usb_w = 14.0; // 双口合并开孔宽 -usb_h = 8.5; -usb_y = 30.5; // 相对 PCB 原点(短边为 X=0 侧) - -/* [Buttons] */ -btn_enable = true; -btn_dia = 4.5; - -/* [DHT11 bay] */ -dht_enable = true; -dht_bay_l = 22; -dht_bay_w = 18; -dht_bay_h = 14; -dht_vent_slot = 1.6; -dht_vent_gap = 2.8; - -/* [Screws] */ -screw_m3 = 3.2; -boss_od = 7.2; -boss_id = 2.6; // 自攻进塑料 -$fn = 48; - -// ---- derived ---- -inner_l = pcb_l + 2 * gap; -inner_w = pcb_w + 2 * gap; -outer_l = inner_l + 2 * wall; -outer_w = inner_w + 2 * wall; -case_h = bezel_front_t + disp_h + pcb_t + cavity_back + wall; - -mh_pts = [ - [mh_inset, mh_inset], - [pcb_l - mh_inset, mh_inset], - [pcb_l - mh_inset, pcb_w - mh_inset], - [mh_inset, pcb_w - mh_inset] -]; - -module rounded_rect(l, w, h, r) { - hull() { - for (x = [r, l - r], y = [r, w - r]) - translate([x, y, 0]) cylinder(h = h, r = r); - } +module rr(l,w,h,r) { + hull() for (x=[r,l-r], y=[r,w-r]) translate([x,y,0]) cylinder(h=h,r=r); } -module cyd_mock() { - color("gold") - difference() { - rounded_rect(pcb_l, pcb_w, pcb_t, pcb_corner_r); - for (p = mh_pts) - translate([p[0], p[1], -0.1]) - cylinder(d = mh_dia, h = pcb_t + 0.2); - } - color("black") - translate([disp_x, 0, pcb_t]) - cube([disp_l, disp_w, disp_h]); - color("navy") - translate([disp_x + view_inset_x, view_inset_y, pcb_t + disp_h]) - cube([view_l, view_w, 0.2]); -} - -// 前框:从屏面方向看,Z=0 为最前表面 -module front_bezel() { - view_x = wall + gap + disp_x + view_inset_x; - view_y = wall + gap + view_inset_y; - +module front() { + z1 = bezel_t + skirt_h; difference() { union() { - // 前表面 - rounded_rect(outer_l, outer_w, bezel_front_t, 3); - // 侧壁向下包住屏+PCB 前半 - translate([0, 0, bezel_front_t]) + rr(outer_l, outer_w, z1, 3); + // male lip + translate([wall-lip_t, wall-lip_t, z1-0.05]) difference() { - rounded_rect(outer_l, outer_w, disp_h + pcb_t * 0.5, 3); - translate([wall, wall, -0.1]) - rounded_rect(inner_l, inner_w, disp_h + pcb_t + 1, 2); - } - // 止口(与后盖搭接) - translate([wall * 0.45, wall * 0.45, bezel_front_t + disp_h + pcb_t * 0.5 - 0.01]) - difference() { - rounded_rect(outer_l - wall * 0.9, outer_w - wall * 0.9, 2.2, 2.4); - translate([wall * 0.55, wall * 0.55, -0.1]) - rounded_rect(outer_l - wall * 2.0, outer_w - wall * 2.0, 3, 1.6); + rr(outer_l-2*(wall-lip_t), outer_w-2*(wall-lip_t), lip_h, 2.2); + translate([lip_t+0.15, lip_t+0.15, -0.1]) + rr(outer_l-2*wall-0.3, outer_w-2*wall-0.3, lip_h+0.5, 1.4); } } - - // 可视窗 - translate([view_x - 0.3, view_y - 0.3, -0.1]) - cube([view_l + 0.6, view_w + 0.6, bezel_front_t + 1]); - - // 屏模组沉台(背面) - translate([wall + gap + disp_x - 0.3, wall + gap - 0.2, bezel_front_t - 0.05]) - cube([disp_l + 0.6, disp_w + 0.4, disp_h + 0.5]); - - // 螺丝通孔 - for (p = mh_pts) - translate([wall + gap + p[0], wall + gap + p[1], -0.1]) - cylinder(d = screw_m3 + 0.2, h = case_h); - } - - // 压屏边框内侧(窄边) - difference() { - translate([wall + gap + disp_x, wall + gap, bezel_front_t]) - cube([disp_l, disp_w, 0.8]); - translate([view_x - 0.2, view_y - 0.2, bezel_front_t - 0.1]) - cube([view_l + 0.4, view_w + 0.4, 2]); - } -} - -// 后盖主体(不含支架倾转);本地坐标:PCB 底面朝上放置时的壳 -module back_shell_body() { - shell_h = cavity_back + wall; - pcb_z = wall + cavity_back; // PCB 底面高度(元件朝下进腔) - - difference() { - union() { - rounded_rect(outer_l, outer_w, shell_h, 3); - - // 止口凹槽配合前框(外壳顶部一圈台阶) - translate([0, 0, shell_h - 0.01]) - difference() { - rounded_rect(outer_l, outer_w, 2.0, 3); - translate([wall * 0.5, wall * 0.5, -0.05]) - rounded_rect(outer_l - wall, outer_w - wall, 2.5, 2.4); - } - - // 螺丝柱 - for (p = mh_pts) { - translate([wall + gap + p[0], wall + gap + p[1], wall]) - difference() { - cylinder(d = boss_od, h = cavity_back - 0.2); - translate([0, 0, 1]) - cylinder(d = boss_id, h = cavity_back); - } - } - - // DHT 通风仓(PCB 长边外侧,靠近 CN1:x≈51) - if (dht_enable) { - dht_x = wall + gap + 48; - dht_y = outer_w - wall * 0.2; - translate([dht_x, dht_y, wall]) - dht_bay(); - } - } - - // 内腔 - translate([wall, wall, wall]) - rounded_rect(inner_l, inner_w, shell_h, 1.8); - - // USB 开孔(短边 x=0 侧) - usb_cut_y = wall + gap + usb_y - usb_w / 2; - translate([-0.1, usb_cut_y, wall + 1.5]) - cube([wall + 1.2, usb_w, usb_h]); - - // SD 卡槽(可选宽缝) - translate([wall + gap + 28, outer_w - wall - 0.1, wall + 2]) - cube([16, wall + 1, 3.2]); - - // BOOT / RST 软按开孔(背面) - if (btn_enable) { - translate([wall + gap + 10, wall + gap + 12, -0.1]) - cylinder(d = btn_dia, h = wall + 0.3); - translate([wall + gap + 10, wall + gap + 22, -0.1]) - cylinder(d = btn_dia, h = wall + 0.3); - } - - // 散热缝 - for (i = [0:4]) - translate([wall + 12 + i * 12, wall + 8, -0.1]) - hull() { - cylinder(d = 2.2, h = wall + 0.3); - translate([0, 10, 0]) cylinder(d = 2.2, h = wall + 0.3); - } - - // DHT 线槽通向 CN1 - if (dht_enable) { - translate([wall + gap + 48, outer_w - wall - 2, wall + 2]) - cube([8, wall + 4, 4]); - } + translate([wall,wall,bezel_t]) rr(inner_l, inner_w, z1, 1.8); + // view + vx = wall+gap+disp_x+view_ix; vy = wall+gap+view_iy; + translate([vx-0.25, vy-0.25, -0.1]) cube([view_l+0.5, view_w+0.5, bezel_t+1]); + // screen pocket + translate([wall+gap+disp_x-0.25, wall+gap-0.15, bezel_t-0.05]) + cube([69.5, 50.3, disp_h+0.8]); + for (p=mh) + translate([wall+gap+p[0], wall+gap+p[1], -0.1]) + cylinder(d=screw+0.2, h=z1+lip_h+1); } } module dht_bay() { - // 局部:仓体贴在 outer 长边外侧 + cx = wall+gap+62; cy = wall+gap+pcb_w/2; + bay_l = dht_l+3.2; bay_w = dht_w+3.2; + bay_h = dht_h + wall*0.6 + dht_proud; difference() { - translate([0, 0, 0]) - rounded_rect(dht_bay_l, dht_bay_w + wall, dht_bay_h, 2); - translate([wall * 0.6, wall * 0.6, wall * 0.6]) - cube([dht_bay_l - wall * 1.2, dht_bay_w, dht_bay_h]); - - // 顶面百叶通风 - for (i = [0:4]) - translate([3 + i * dht_vent_gap, dht_bay_w * 0.3, dht_bay_h - wall - 0.1]) - cube([dht_vent_slot, dht_bay_w * 0.55, wall + 0.3]); - - // 侧面通风 - for (i = [0:2]) - translate([4 + i * 5.5, dht_bay_w + wall - 0.2, 3 + i * 0.2]) - cube([dht_vent_slot, wall + 0.4, 6]); + translate([cx-bay_l/2, cy-bay_w/2, -bay_h+wall*0.35]) + cube([bay_l, bay_w, bay_h+0.2]); + // cavity + translate([cx-dht_l/2-0.25, cy-dht_w/2-0.25, -bay_h+wall*0.35+1]) + cube([dht_l+0.5, dht_w+0.5, bay_h]); + // grill + translate([cx-dht_l/2+1.2, cy-dht_w/2+1.2, -bay_h-0.2]) + cube([dht_l-2.4, dht_w-2.4, wall+dht_proud+0.8]); + // wire hole into case + translate([cx-3.5, cy-4, wall-0.5]) cube([7, 8, 3]); } + // 卡位 left/right + z0 = -bay_h+wall*0.35+1.2; + translate([cx-dht_l/2-1.2, cy-3.5, z0]) cube([1.2+dht_clip, 7, 3.2]); + translate([cx+dht_l/2-dht_clip, cy-3.5, z0]) cube([1.2+dht_clip, 7, 3.2]); + // stop ledge + translate([cx-dht_l/2+1.5, cy-dht_w/2+0.8, -bay_h+wall*0.35+0.8]) + cube([dht_l-3, dht_w-1.6, 1]); } -// 带斜立支架的后盖(打印时支脚朝下) -module back_with_stand() { - shell_h = cavity_back + wall; - - // 后盖:PCB 元件朝下;支架使屏幕斜立 - // 组装时:屏幕朝向 -Z 转 stand_angle - union() { - back_shell_body(); - - // 三角形支脚(沿 PCB 长边下方) - stand_len = outer_l * 0.72; - stand_x = (outer_l - stand_len) / 2; - // 支脚从后盖底面向后延伸 - translate([stand_x, -stand_foot + 0.5, 0]) +module back() { + difference() { + union() { + rr(outer_l, outer_w, shell_h, 3); + // collar + female slot difference() { - cube([stand_len, stand_foot, shell_h]); - // 挖空减料 - translate([2, 2, wall]) - cube([stand_len - 4, stand_foot - 2, shell_h]); - // 斜切贴合桌面视觉(打印可平放) - translate([-0.1, 0, shell_h]) - rotate([-stand_angle + 90, 0, 0]) - cube([stand_len + 0.2, 40, 40]); + translate([0,0,shell_h-0.05]) rr(outer_l, outer_w, lip_h, 3); + translate([wall+0.2, wall+0.2, shell_h-0.2]) + rr(outer_l-2*wall-0.4, outer_w-2*wall-0.4, lip_h+0.5, 1.6); } - - // 后支撑肋 - translate([outer_l * 0.5 - 1.2, -stand_foot + 1, wall]) - cube([2.4, stand_foot - 1, shell_h - wall - 1]); + for (p=mh) + translate([wall+gap+p[0], wall+gap+p[1], wall]) + difference() { + cylinder(d=boss_od, h=cavity-0.4); + translate([0,0,1]) cylinder(d=boss_id, h=cavity); + } + // stand + translate([(outer_l-outer_l*0.7)/2, -stand_foot+0.4, 0]) + difference() { + cube([outer_l*0.7, stand_foot, shell_h*0.85]); + translate([2,2,wall]) cube([outer_l*0.7-4, stand_foot-2, shell_h]); + } + dht_bay(); + } + translate([wall,wall,wall]) rr(inner_l, inner_w, shell_h+lip_h, 1.8); + // female ring cut + translate([wall-lip_t-0.12, wall-lip_t-0.12, shell_h+0.15]) + difference() { + cube([outer_l-2*(wall-lip_t-0.12), outer_w-2*(wall-lip_t-0.12), lip_h-0.3]); + translate([lip_t+0.15, lip_t+0.15, -0.1]) + cube([outer_l-2*wall-0.1, outer_w-2*wall-0.1, lip_h]); + } + // USB + translate([-0.1, wall+gap+usb_y-usb_w/2, wall+1.2]) cube([wall+1.2, usb_w, usb_h]); } } -module assembly() { - // 后盖在下 - color("SteelBlue", 0.85) back_with_stand(); - - // PCB:元件朝下坐在腔顶 - translate([wall + gap, wall + gap, wall + cavity_back]) - rotate([180, 0, 0]) - translate([0, -pcb_w, -pcb_t]) - %cyd_mock(); - - // 前框盖上 - translate([0, 0, wall + cavity_back + pcb_t + disp_h + bezel_front_t]) - rotate([180, 0, 0]) - translate([0, -outer_w, 0]) - color("SlateGray", 0.7) front_bezel(); +if (part=="front") front(); +else if (part=="back") back(); +else { + color("teal", 0.85) back(); + translate([0,0,shell_h-2.2]) color("slategray", 0.75) + translate([0, outer_w, bezel_t+skirt_h+lip_h]) rotate([180,0,0]) front(); } - -if (part == "front") - front_bezel(); -else if (part == "back") - back_with_stand(); -else - assembly(); diff --git a/cad/export/back.stl b/cad/export/back.stl index 77f0eb2..3bb6f18 100644 Binary files a/cad/export/back.stl and b/cad/export/back.stl differ diff --git a/cad/export/front.stl b/cad/export/front.stl index 1daf666..bfdb986 100644 Binary files a/cad/export/front.stl and b/cad/export/front.stl differ diff --git a/cad/generate_stl.py b/cad/generate_stl.py index 122f28c..c76a900 100644 --- a/cad/generate_stl.py +++ b/cad/generate_stl.py @@ -1,33 +1,43 @@ #!/usr/bin/env python3 -"""Generate printable STL for CYD climate case using trimesh + manifold boolean.""" +""" +CYD climate case STL generator. + +Design intent: + - DHT11 on the BACK cover, held by snap/clip posts (卡位), tip slightly proud of back face + - Front + back close with a interlocking lip (合盖) +""" from __future__ import annotations from pathlib import Path -import numpy as np import trimesh -from trimesh.creation import box, cylinder +from trimesh.creation import box, cylinder # noqa: F401 — cylinder used for holes/bosses -# ---- mm, aligned with cyd_climate_case.scad ---- +# ---- mm ---- PCB_L, PCB_W = 86.0, 50.0 MH_INSET = 4.0 -DISP_L, DISP_W, DISP_H, DISP_X = 69.0, 50.0, 4.0, 8.0 +DISP_X, DISP_L, DISP_W, DISP_H = 8.0, 69.0, 50.0, 4.0 VIEW_L, VIEW_W, VIEW_IX, VIEW_IY = 59.5, 45.0, 7.0, 2.5 -WALL, GAP = 2.4, 0.4 -BEZEL_T, CAVITY = 2.0, 7.0 -PCB_T_HALF = 0.8 -USB_W, USB_H, USB_Y = 14.0, 8.5, 30.5 -STAND_FOOT = 18.0 -DHT_L, DHT_W, DHT_H = 22.0, 18.0, 14.0 +WALL, GAP = 2.4, 0.35 +BEZEL_T, CAVITY = 2.2, 7.5 +LIP_H, LIP_T = 2.4, 1.15 # front male / back female for 合盖 +USB_W, USB_H, USB_Y = 15.0, 9.0, 30.5 +STAND_FOOT = 16.0 SCREW = 3.3 -BOSS_OD, BOSS_ID = 7.2, 2.5 +BOSS_OD, BOSS_ID = 7.0, 2.5 + +# DHT11 module (common PCB module ~15.5 x 12 x 7.5 + pins) +DHT_MOD_L, DHT_MOD_W, DHT_MOD_H = 16.0, 12.5, 8.0 +DHT_PROUD = 1.8 # how much grill/tip sticks out past back outer face +DHT_CLIP = 1.1 # undercut lip thickness for 卡位 INNER_L = PCB_L + 2 * GAP INNER_W = PCB_W + 2 * GAP OUTER_L = INNER_L + 2 * WALL OUTER_W = INNER_W + 2 * WALL SHELL_H = CAVITY + WALL +SKIRT_H = DISP_H + 1.0 MH = [ (MH_INSET, MH_INSET), @@ -37,141 +47,240 @@ MH = [ ] -def place(mesh: trimesh.Trimesh, center: tuple[float, float, float]) -> trimesh.Trimesh: - m = mesh.copy() - m.apply_translation(np.array(center) - m.bounding_box.centroid) - return m - - def aabb(x0, y0, z0, x1, y1, z1) -> trimesh.Trimesh: - dx, dy, dz = x1 - x0, y1 - y0, z1 - z0 + xa, xb = sorted((float(x0), float(x1))) + ya, yb = sorted((float(y0), float(y1))) + za, zb = sorted((float(z0), float(z1))) + dx, dy, dz = xb - xa, yb - ya, zb - za + if dx < 1e-6 or dy < 1e-6 or dz < 1e-6: + raise ValueError(f"degenerate box {[xa,ya,za,xb,yb,zb]}") m = box(extents=[dx, dy, dz]) - m.apply_translation([x0 + dx / 2, y0 + dy / 2, z0 + dz / 2]) + m.apply_translation([xa + dx / 2, ya + dy / 2, za + dz / 2]) return m def u(*meshes: trimesh.Trimesh) -> trimesh.Trimesh: - result = meshes[0] + r = meshes[0] for m in meshes[1:]: - result = result.union(m, engine="manifold") - return result + r = r.union(m, engine="manifold") + return r def d(a: trimesh.Trimesh, *subs: trimesh.Trimesh) -> trimesh.Trimesh: - result = a + r = a for s in subs: - result = result.difference(s, engine="manifold") - return result + r = r.difference(s, engine="manifold") + return r def make_front() -> trimesh.Trimesh: - skirt_h = DISP_H + PCB_T_HALF - z1 = BEZEL_T + skirt_h + """Front bezel: screen window + skirt + male lip for closing onto back.""" + z1 = BEZEL_T + SKIRT_H + LIP_H - body = aabb(0, 0, 0, OUTER_L, OUTER_W, z1) - cavity = aabb(WALL, WALL, BEZEL_T, OUTER_L - WALL, OUTER_W - WALL, z1 + 1) + body = aabb(0, 0, 0, OUTER_L, OUTER_W, BEZEL_T + SKIRT_H) + # hollow skirt + cavity = aabb(WALL, WALL, BEZEL_T, OUTER_L - WALL, OUTER_W - WALL, BEZEL_T + SKIRT_H + 1) vx = WALL + GAP + DISP_X + VIEW_IX vy = WALL + GAP + VIEW_IY - view = aabb(vx - 0.3, vy - 0.3, -0.5, vx + VIEW_L + 0.3, vy + VIEW_W + 0.3, BEZEL_T + 0.5) + view = aabb(vx - 0.25, vy - 0.25, -0.5, vx + VIEW_L + 0.25, vy + VIEW_W + 0.25, BEZEL_T + 0.6) - # screen pocket - sx = WALL + GAP + DISP_X - 0.3 - pocket = aabb(sx, WALL + GAP - 0.2, BEZEL_T - 0.05, sx + DISP_L + 0.6, WALL + GAP + DISP_W + 0.2, z1 + 0.5) + sx = WALL + GAP + DISP_X - 0.25 + pocket = aabb( + sx, WALL + GAP - 0.15, BEZEL_T - 0.05, + sx + DISP_L + 0.5, WALL + GAP + DISP_W + 0.15, BEZEL_T + DISP_H + 0.8, + ) out = d(body, cavity, view, pocket) + # male lip (inserts into back female groove) + lip_outer = aabb( + WALL - LIP_T, WALL - LIP_T, BEZEL_T + SKIRT_H - 0.05, + OUTER_L - (WALL - LIP_T), OUTER_W - (WALL - LIP_T), BEZEL_T + SKIRT_H + LIP_H, + ) + lip_inner = aabb( + WALL + 0.15, WALL + 0.15, BEZEL_T + SKIRT_H - 0.1, + OUTER_L - WALL - 0.15, OUTER_W - WALL - 0.15, BEZEL_T + SKIRT_H + LIP_H + 0.5, + ) + lip = d(lip_outer, lip_inner) + out = u(out, lip) + + # screw clearance through bezel for px, py in MH: x = WALL + GAP + px y = WALL + GAP + py - hole = cylinder(radius=SCREW / 2, height=z1 + 2, sections=32) - hole.apply_translation([x, y, z1 / 2]) + hole = cylinder(radius=SCREW / 2 + 0.1, height=z1 + 4, sections=28) + hole.apply_translation([x, y, (BEZEL_T + SKIRT_H) / 2]) out = d(out, hole) out.merge_vertices() return out -def make_back() -> trimesh.Trimesh: - body = aabb(0, 0, 0, OUTER_L, OUTER_W, SHELL_H) - cavity = aabb(WALL, WALL, WALL, OUTER_L - WALL, OUTER_W - WALL, SHELL_H + 1) +def dht_pocket_on_back() -> trimesh.Trimesh: + """ + Pocket on outer back face (z=0 side is exterior bottom of shell). + Module inserts from outside; clips hold it; grill protrudes DHT_PROUD. + Coordinate: back shell sits z=0 exterior, z=SHELL_H toward PCB. + Pocket carved into floor from outside, clips on the outer face. + """ + # center DHT on back wall area away from USB (right-ish of board back) + cx = WALL + GAP + 62 + cy = WALL + GAP + PCB_W / 2 - # USB on -X - usb_y0 = WALL + GAP + USB_Y - USB_W / 2 - usb = aabb(-1, usb_y0, WALL + 1.5, WALL + 1, usb_y0 + USB_W, WALL + 1.5 + USB_H) + # outer boss that contains the module (sits mostly outside z<=0) + bay_l = DHT_MOD_L + 3.2 + bay_w = DHT_MOD_W + 3.2 + bay_h = DHT_MOD_H + WALL * 0.6 + DHT_PROUD - # SD slot on +Y - sd = aabb(WALL + GAP + 28, OUTER_W - WALL - 1, WALL + 2, WALL + GAP + 44, OUTER_W + 1, WALL + 5.2) + # bay extends below z=0 (outside) and a bit into floor + bay = aabb( + cx - bay_l / 2, cy - bay_w / 2, -bay_h + WALL * 0.35, + cx + bay_l / 2, cy + bay_w / 2, WALL + 0.2, + ) - # button holes on floor - btn1 = cylinder(radius=2.3, height=WALL + 2, sections=24) - btn1.apply_translation([WALL + GAP + 10, WALL + GAP + 12, WALL / 2]) - btn2 = cylinder(radius=2.3, height=WALL + 2, sections=24) - btn2.apply_translation([WALL + GAP + 10, WALL + GAP + 22, WALL / 2]) + # cavity for module (from outside) + cav = aabb( + cx - DHT_MOD_L / 2 - 0.25, cy - DHT_MOD_W / 2 - 0.25, -bay_h + WALL * 0.35 + 1.0, + cx + DHT_MOD_L / 2 + 0.25, cy + DHT_MOD_W / 2 + 0.25, WALL - 0.3, + ) - # vent slots on floor + # window through back wall into interior for wires (small) + wire = aabb( + cx - 3.5, cy - 4.0, WALL - 0.5, + cx + 3.5, cy + 4.0, WALL + 2.5, + ) + + # front face of bay: grill opening, leaving rim so tip is proud + grill = aabb( + cx - DHT_MOD_L / 2 + 1.2, cy - DHT_MOD_W / 2 + 1.2, -bay_h - 0.2, + cx + DHT_MOD_L / 2 - 1.2, cy + DHT_MOD_W / 2 - 1.2, -bay_h + WALL + DHT_PROUD + 0.5, + ) + + # side vent slots vents = [] - for i in range(5): - v = aabb(WALL + 12 + i * 12, WALL + 8, -0.5, WALL + 14 + i * 12, WALL + 18, WALL + 0.5) - vents.append(v) + for i in range(3): + vents.append(aabb( + cx - DHT_MOD_L / 2 - 2, cy - 4 + i * 3.2, -bay_h + 3, + cx - DHT_MOD_L / 2 + 0.2, cy - 4 + i * 3.2 + 1.4, -bay_h + 8, + )) + vents.append(aabb( + cx + DHT_MOD_L / 2 - 0.2, cy - 4 + i * 3.2, -bay_h + 3, + cx + DHT_MOD_L / 2 + 2, cy - 4 + i * 3.2 + 1.4, -bay_h + 8, + )) - out = d(body, cavity, usb, sd, btn1, btn2, *vents) + # 卡位: L/R snap ledges + end stops (press-fit module from outside) + z_base = -bay_h + WALL * 0.35 + 1.2 + left_clip = aabb( + cx - DHT_MOD_L / 2 - 1.2, cy - 3.5, z_base, + cx - DHT_MOD_L / 2 + DHT_CLIP, cy + 3.5, z_base + 3.2, + ) + right_clip = aabb( + cx + DHT_MOD_L / 2 - DHT_CLIP, cy - 3.5, z_base, + cx + DHT_MOD_L / 2 + 1.2, cy + 3.5, z_base + 3.2, + ) + # end stop ledge near grill (prevents module falling out front) + stop = aabb( + cx - DHT_MOD_L / 2 + 1.5, cy - DHT_MOD_W / 2 + 0.8, -bay_h + WALL * 0.35 + 0.8, + cx + DHT_MOD_L / 2 - 1.5, cy + DHT_MOD_W / 2 - 0.8, -bay_h + WALL * 0.35 + 0.8 + 1.0, + ) + # rear retention bar (卡入后沿) + retain = aabb( + cx - 5.0, cy - DHT_MOD_W / 2 - 0.2, WALL - 2.2, + cx + 5.0, cy + DHT_MOD_W / 2 + 0.2, WALL - 0.4, + ) + + bay = d(bay, cav, grill, wire, *vents) + bay = u(bay, left_clip, right_clip, stop, retain) + return bay + + +def make_back() -> trimesh.Trimesh: + """Back shell: cavity, female lip, screw bosses, stand, DHT snap bay on outer back.""" + body = aabb(0, 0, 0, OUTER_L, OUTER_W, SHELL_H + LIP_H) + cavity = aabb(WALL, WALL, WALL, OUTER_L - WALL, OUTER_W - WALL, SHELL_H + LIP_H + 1) + + # female groove for front lip (near top rim) + groove = aabb( + WALL - LIP_T - 0.15, WALL - LIP_T - 0.15, SHELL_H, + OUTER_L - (WALL - LIP_T - 0.15), OUTER_W - (WALL - LIP_T - 0.15), SHELL_H + LIP_H + 0.2, + ) + # keep inner wall so groove is a channel, not open to cavity fully + # actually: cut a U-channel from the top rim + groove_cut = aabb( + WALL - LIP_T - 0.1, WALL - LIP_T - 0.1, SHELL_H - 0.05, + OUTER_L - (WALL - LIP_T - 0.1), OUTER_W - (WALL - LIP_T - 0.1), SHELL_H + LIP_H + 0.3, + ) + keep_inner = aabb( + WALL + 0.05, WALL + 0.05, SHELL_H - 0.2, + OUTER_L - WALL - 0.05, OUTER_W - WALL - 0.05, SHELL_H + LIP_H + 0.5, + ) + # rim solid then groove: start from body, remove cavity, then remove groove channel between lip walls + out = d(body, cavity) + + # Add rim collar then cut female slot + collar = aabb(0, 0, SHELL_H - 0.05, OUTER_L, OUTER_W, SHELL_H + LIP_H) + collar_in = aabb(WALL + 0.2, WALL + 0.2, SHELL_H - 0.2, OUTER_L - WALL - 0.2, OUTER_W - WALL - 0.2, SHELL_H + LIP_H + 0.5) + collar = d(collar, collar_in) + out = u(out, collar) + + # female slot (accepts front male lip) + slot = aabb( + WALL - LIP_T - 0.12, WALL - LIP_T - 0.12, SHELL_H + 0.15, + OUTER_L - (WALL - LIP_T - 0.12), OUTER_W - (WALL - LIP_T - 0.12), SHELL_H + LIP_H - 0.15, + ) + slot_keep = aabb( + WALL + 0.05, WALL + 0.05, SHELL_H, + OUTER_L - WALL - 0.05, OUTER_W - WALL - 0.05, SHELL_H + LIP_H + 0.2, + ) + # slot is the ring between slot outer and slot_keep — cut only the ring + slot_ring = d(slot, slot_keep) + out = d(out, slot_ring) + + # USB + usb_y0 = WALL + GAP + USB_Y - USB_W / 2 + usb = aabb(-1, usb_y0, WALL + 1.2, WALL + 1.2, usb_y0 + USB_W, WALL + 1.2 + USB_H) + out = d(out, usb) + + # SD optional slit + sd = aabb(WALL + GAP + 28, OUTER_W - WALL - 1, WALL + 2, WALL + GAP + 44, OUTER_W + 1, WALL + 5) + out = d(out, sd) + + # BOOT/RST access + for y in (12, 22): + btn = cylinder(radius=2.2, height=WALL + 2, sections=20) + btn.apply_translation([WALL + GAP + 10, WALL + GAP + y, WALL / 2]) + out = d(out, btn) + + # floor vents (not under DHT) + vents = [] + for i in range(4): + vents.append(aabb(WALL + 10 + i * 12, WALL + 8, -0.5, WALL + 12 + i * 12, WALL + 16, WALL + 0.5)) + out = d(out, *vents) # screw bosses bosses = [] for px, py in MH: x = WALL + GAP + px y = WALL + GAP + py - boss = cylinder(radius=BOSS_OD / 2, height=CAVITY - 0.3, sections=32) - boss.apply_translation([x, y, WALL + (CAVITY - 0.3) / 2]) - hole = cylinder(radius=BOSS_ID / 2, height=CAVITY + 1, sections=24) + boss = cylinder(radius=BOSS_OD / 2, height=CAVITY - 0.4, sections=28) + boss.apply_translation([x, y, WALL + (CAVITY - 0.4) / 2]) + hole = cylinder(radius=BOSS_ID / 2, height=CAVITY + 1, sections=20) hole.apply_translation([x, y, WALL + CAVITY / 2]) bosses.append(d(boss, hole)) out = u(out, *bosses) - # stand foot - stand_len = OUTER_L * 0.72 + # stand foot (desk) + stand_len = OUTER_L * 0.7 sx = (OUTER_L - stand_len) / 2 - foot = aabb(sx, -STAND_FOOT + 0.5, 0, sx + stand_len, WALL, SHELL_H) - # hollow foot - foot_in = aabb(sx + 2, -STAND_FOOT + 2.5, WALL, sx + stand_len - 2, WALL - 0.2, SHELL_H + 1) + foot = aabb(sx, -STAND_FOOT + 0.4, 0, sx + stand_len, WALL, SHELL_H * 0.85) + foot_in = aabb(sx + 2, -STAND_FOOT + 2.2, WALL, sx + stand_len - 2, WALL - 0.15, SHELL_H) foot = d(foot, foot_in) - rib = aabb(OUTER_L * 0.5 - 1.2, -STAND_FOOT + 1, WALL, OUTER_L * 0.5 + 1.2, WALL, SHELL_H - 1) + rib = aabb(OUTER_L / 2 - 1.2, -STAND_FOOT + 1, WALL, OUTER_L / 2 + 1.2, WALL, SHELL_H * 0.8) out = u(out, foot, rib) - # DHT bay - dx = WALL + GAP + 48 - dy = OUTER_W - 0.2 - bay = aabb(dx, dy, WALL, dx + DHT_L, dy + DHT_W + WALL, WALL + DHT_H) - bay_in = aabb(dx + 1.2, dy + 1.2, WALL + 1.0, dx + DHT_L - 1.2, dy + DHT_W, WALL + DHT_H + 1) - # top vents - top_vents = [] - for i in range(5): - tv = aabb( - dx + 2.5 + i * 3.6, - dy + 3, - WALL + DHT_H - WALL - 0.2, - dx + 2.5 + i * 3.6 + 1.5, - dy + DHT_W - 2, - WALL + DHT_H + 0.5, - ) - top_vents.append(tv) - # side vents - for i in range(3): - sv = aabb( - dx + 4 + i * 5.5, - dy + DHT_W + WALL - 0.5, - WALL + 3, - dx + 4 + i * 5.5 + 1.5, - dy + DHT_W + WALL + 1, - WALL + 9, - ) - top_vents.append(sv) - bay = d(bay, bay_in, *top_vents) - - # cable channel from bay to CN1 - channel = aabb(dx + 2, OUTER_W - WALL - 2, WALL + 2, dx + 10, OUTER_W + DHT_W, WALL + 6) - out = d(u(out, bay), channel) + # DHT11 snap bay on outer back (protruding) + out = u(out, dht_pocket_on_back()) out.merge_vertices() return out @@ -181,19 +290,18 @@ def main() -> None: out_dir = Path(__file__).resolve().parent / "export" out_dir.mkdir(parents=True, exist_ok=True) - print("Building front…") + print("Building front (合盖公止口)…") front = make_front() - print("Building back…") + print("Building back (DHT后盖卡位 + 母止口)…") back = make_back() front.export(out_dir / "front.stl") back.export(out_dir / "back.stl") - - print(f"front: {front.faces.shape[0]} faces, watertight={front.is_watertight}") - print(f"back: {back.faces.shape[0]} faces, watertight={back.is_watertight}") - print(f"Outer: {OUTER_L:.1f} x {OUTER_W:.1f} x front/back shells") + print(f"front watertight={front.is_watertight} faces={len(front.faces)}") + print(f"back watertight={back.is_watertight} faces={len(back.faces)}") print(f"Wrote {out_dir / 'front.stl'}") print(f"Wrote {out_dir / 'back.stl'}") + print(f"Outer ~ {OUTER_L:.1f} x {OUTER_W:.1f} mm; DHT proud ~{DHT_PROUD} mm") if __name__ == "__main__": diff --git a/cad/viewer.html b/cad/viewer.html index 020ea3a..9437581 100644 --- a/cad/viewer.html +++ b/cad/viewer.html @@ -3,31 +3,26 @@
-左键旋转 · 滚轮缩放 · 右键平移 · 触屏双指缩放
+DHT11 在后盖外侧卡位,微微突出 · 前后盖止口合盖
-合盖=0%:前框止口落入后盖槽。DHT 从后盖背面卡入并突出约 1.8mm。