Add parametric enclosure concept and checks
This commit is contained in:
@@ -8,6 +8,11 @@ Working memory, not an implementation timeline. Source is authoritative; begin w
|
|||||||
- README was refined from the user's already-thinned version, not restored from Git. New `docs/getting_started.md`, `docs/user_administration.md`, and `docs/firmware_update.md` provide operating procedures; `docs/web_administration.md` is now a user guide and its original contract is preserved under `docs/devel/`. Flash layout and PCB research moved into the developer archive; contextual test/hardware/provenance READMEs remain beside artifacts.
|
- README was refined from the user's already-thinned version, not restored from Git. New `docs/getting_started.md`, `docs/user_administration.md`, and `docs/firmware_update.md` provide operating procedures; `docs/web_administration.md` is now a user guide and its original contract is preserved under `docs/devel/`. Flash layout and PCB research moved into the developer archive; contextual test/hardware/provenance READMEs remain beside artifacts.
|
||||||
- Documentation relocation adds no new device evidence and does not change historical acceptance limits. Firmware, generated assets, CAD and historical hash JSON are untouched. Hardware scripts have only relocated-note reference/protection adjustments; Python syntax was checked without executing generators. Documentation links/anchors and `git diff --check` were checked; no firmware build, upload, erase, or hardware test was performed.
|
- Documentation relocation adds no new device evidence and does not change historical acceptance limits. Firmware, generated assets, CAD and historical hash JSON are untouched. Hardware scripts have only relocated-note reference/protection adjustments; Python syntax was checked without executing generators. Documentation links/anchors and `git diff --check` were checked; no firmware build, upload, erase, or hardware test was performed.
|
||||||
|
|
||||||
|
## Parametric enclosure — initial concept, physical validation pending
|
||||||
|
|
||||||
|
- `hardware/case/` corrected per user: 5 mm base insert bosses directly support carrier underside Z7.4/top Z9; two M3 RS-232 and four M2 OLED brass male/female bodies ABOVE carrier span 10 mm to module underside Z19. Studs cross actual 1.6 mm carrier holes into respective inserts; four separate carrier holes take direct M3 screws. ESP32 has no supplied mounting holes and remains header-supported. No under-carrier brass or printable spacers remain. Shell now **106 × 86.8 × 37.4 mm**, roof underside Z35 (10 mm lower); ports/display/DB9 panel/hangers/buttons follow the corrected datums. Red RF overlay/enclosure reserve restriction removed; physical antenna retained, no Wi-Fi guarantee and no PCB/schematic/copper/reserve/layout/export changes.
|
||||||
|
- Four default CGAL exports, native registration (including hole diameters/no ESP32 NPTH), evaluated stack/stud/nozzle-web fixtures, closed-edge mesh checks and two parameter CGAL smoke tests passed; both updated previews visually inspected. Not printed, assembled or physically tested. Actual underside solder/prong clearance within 5 mm, brass shoulder/insert lands, unknown stud lengths (4 mm reference gives 2.4 mm engagement), insert usable threads, header mating height, USB Z/plug envelopes, OLED height, switches and DB9 panel-jackscrew thread/removability/reach remain explicit gates. Panel cable-lock axes are 25 mm, module holes 24.13 mm; no M3 inference from STEP bores. See `hardware/case/README.md` and `checks.md` before printing.
|
||||||
|
|
||||||
## PCB accepted-fit labels and worksheet — complete
|
## PCB accepted-fit labels and worksheet — complete
|
||||||
|
|
||||||
- Removed visible `USB ACCESS - VERIFY` and title comment `USB / antenna geometry pending; verify module pinout / assembly`; replaced the duplicated pending-clearance caption with `Nominal RS-232 metal verified; RF qualification not established`. Specific RF and connector cautions remain; this is user acceptance, not certification.
|
- Removed visible `USB ACCESS - VERIFY` and title comment `USB / antenna geometry pending; verify module pinout / assembly`; replaced the duplicated pending-clearance caption with `Nominal RS-232 metal verified; RF qualification not established`. Specific RF and connector cautions remain; this is user acceptance, not certification.
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# Parametric prototype enclosure
|
||||||
|
|
||||||
|
**Initial mechanical concept — not print-tested, assembled, load-tested, RF-qualified or a validated fit.** The included `*-default.stl` files are exports of the provisional values in [enclosure.scad](enclosure.scad), not production drawings. No PCB, schematic, routing, footprint or prior hardware export was modified.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Parts and operation
|
||||||
|
|
||||||
|
Open `enclosure.scad` in OpenSCAD (tested here with **2021.01**). Set `part` to:
|
||||||
|
|
||||||
|
| Mode | Result |
|
||||||
|
|---|---|
|
||||||
|
| `assembly` / `exploded` | Assembled / exploded reference preview; **not printable combined parts** |
|
||||||
|
| `base` | Floor, carrier insert bosses, daughterboard insert bosses, side assembly bosses |
|
||||||
|
| `lid` | Roof, skirt, screw head wells, button guides, DB9 panel attachment hangers, raised labels |
|
||||||
|
| `panel` | Separate externally installed DB9 retaining panel, oriented flat for printing |
|
||||||
|
| `plunger` / `plungers` | One / three identical captive button actuators |
|
||||||
|
|
||||||
|
| `labels` | Raised text alone for a second material or separately attached lettering |
|
||||||
|
| `layout` | Spaced parts for inspection; orient/support each part separately in the slicer |
|
||||||
|
|
||||||
|
`show_references` displays simplified hardware only in **F5 preview** (`$preview` and `%` background geometry); it is excluded from STL/F6 geometry. The reference boxes are not complete component models. Orange/brown stock DB9 post envelopes deliberately reveal the conflict with a panel fitted without replacing/reseating the posts. Gold dummies represent **two M3 and four M2 male/female brass standoffs above the carrier**, with lower studs and blind upper bores; their OD, stud length and bore depth are provisional. The red RF overlay and enclosure RF-reserve restriction have been removed at the user's request. Actual antenna geometry remains; no PCB reserve/copper/layout was changed and no Wi-Fi performance guarantee is implied.
|
||||||
|
|
||||||
|
All dimensions are **mm**. Parameter/source coordinates are carrier-local, X right and Y down toward USB; add `(100,100)` for KiCad coordinates. The rendered model maps `Y_world = 80 - Y_carrier`, so the DB9 is above the USB end in top view. Text is separately transformed to remain readable from the USB end.
|
||||||
|
|
||||||
|
### Default envelope and exact chosen stack
|
||||||
|
|
||||||
|
The carrier remains **82 × 80**, with its **36 × 19 top-left notch** represented in the reference. The case is rectangular, covering the notch without adding internal metal there. Its width is **106** (`X=-12…94`), depth **86.8** (`Y=-3.4…83.4`), roof height **37.4**. Raised labels reach **38.0** and released plunger tips **38.9**. This is exactly **10 mm lower** than the mistaken under-carrier-brass stack. Only 1 mm internal clearance and a 2.4 mm wall are added at each cable end; the case assembly hardware is placed laterally, not beyond the top/bottom PCB edges.
|
||||||
|
|
||||||
|
| Datum above exterior bottom | Default Z |
|
||||||
|
|---|---:|
|
||||||
|
| Floor upper surface | 2.4 |
|
||||||
|
| All carrier/daughterboard insert-boss tops / carrier underside | 7.4 |
|
||||||
|
| Carrier top, saved PCB thickness 1.6 / brass body bottoms | 9.0 |
|
||||||
|
| B3F-1000 released actuator top | 13.3 |
|
||||||
|
| Base top / lid skirt bottom | 15.0 / 15.25 |
|
||||||
|
| Button guide bottom / roof underside | 15.6 / 35.0 |
|
||||||
|
| Brass body tops / module underside, **carrier-top-to-underside gap 10** | 19.0 |
|
||||||
|
| RS-232 module top, STEP thickness 1.57 | 20.57 |
|
||||||
|
| ESP32 / OLED module tops, provisional thickness 1.6 | 20.6 |
|
||||||
|
| DB9 shell/lock centre axis | 26.84762024 |
|
||||||
|
| DB9 nominal flange upper edge | 33.12262024 |
|
||||||
|
| Roof underside / exterior | 35.0 / 37.4 |
|
||||||
|
|
||||||
|
The user's intended/reported module elevation is **10 mm**. The descriptions “8.5 mm female + 2.5 mm male” do **not** independently prove a 10 or 11 mm mating height. This design explicitly interprets the requested 10 as the gap between carrier top and module underside, and exposes `module_gap`; verify that datum against the actual mated stack. `module_gap` is the brass body height, **not an additional height below the carrier**. `carrier_bottom = floor_t + carrier_boss_h = 7.4`; the 5 mm bosses give nominal floor-to-carrier-underside clearance for solder/prongs, subject to actual component measurements. There is no under-carrier brass. Unknown module thicknesses, display height and USB height are not promoted to measurements.
|
||||||
|
|
||||||
|
### PCB and daughterboard fixture
|
||||||
|
|
||||||
|
- Carrier M3 axes: **(3.5,22.5), (3.5,75), (78,4), (78,76)**; verified against actual saved Ø3.2 NPTH holes. Base bosses Ø8 end at Z7.4 and directly support the carrier underside. Fasten the carrier with **four M3 screws through these holes into the base inserts**, not with brass underneath. These four positions do not coincide with daughterboard mounting holes.
|
||||||
|
- Case M3 axes: **(-6,12), (-6,68), (88,12), (88,68)**. Ø9 bosses and lateral ribs stay outside the board; top-access Ø6.4 × 2.8 deep head wells, Ø3.4 screw paths, base inserts. Flat butt seam has a 0.25 vertical gap; screws locate it. No snap-fit or sealing claim.
|
||||||
|
- RS-232 module M3 axes: **(44.905,17.907), (69.035,17.907)**, **24.13** spacing, real saved carrier Ø3 holes.
|
||||||
|
- OLED M2 axes: **(45.72,37.75), (68.22,37.75), (45.72,60.25), (68.22,60.25)**, **22.5** grid, real saved carrier Ø2 holes. Do not confuse this with a screw clearance specification.
|
||||||
|
- With `daughter_supports=true`, dedicated insert bosses terminate at **carrier underside Z7.4**. Screw the **two M3 RS-232 and four M2 OLED brass male/female standoffs** down through their respective existing carrier holes into the inserts. Their 10 mm bodies rest on the actual carrier top Z9.0 and support the module undersides at Z19.0. Short screws through each daughterboard engage the upper female threads. M3 boss OD7, brass reference OD6; M2 boss OD6, brass reference OD4. **ESP32 has no supplied mounting holes**: despite the request referring to three components, it remains supported by its two headers (simplified black references), not fictional standoffs or newly drilled PCB holes. No printed column crosses a PCB plane; only the metal studs pass through existing holes.
|
||||||
|
- Check underside solder-joint/prong, trace, insert, washer and pad clearances before tightening; the nominal underside clearance is now **5 mm**, not 15 mm. The user's mostly-copper-free observation is not a measured clearance proof for conductive brass shoulders. The exact underside envelopes and mounting-land allowances are unavailable. Plastic shoulders must not crush components; exposed insert tops must not contact copper. If necessary use insulating washers and account for their thickness, reduce shoulder diameters after checking wall strength, or set `daughter_supports=false` and provide another verified fixture. Do not drill or modify the carrier to force a fit.
|
||||||
|
|
||||||
|
**Insert pockets are examples, not validated fits:** M3 Ø4.2 × 4 deep; M2 Ø3.2 × 3 deep, 0.3 entry flare. These numbers do not specify insert OD/length, thread engagement or thermal process. Choose the actual insert family and set diameter/depth/boss OD using its supplier recommendations and a material-specific test coupon first. Install inserts before electronics. Default carrier pockets leave 3.4 mm solid material to the outside bottom (1 mm boss web plus 2.4 floor).
|
||||||
|
|
||||||
|
Default insert count with all options enabled: **12 M3** (4 carrier, 4 case, 2 RS-232, 2 panel hangers) and **4 M2** (OLED). The last two M3 pockets face outward horizontally in the lid; install with the lid removed. These M3s are **not** the connector cable-lock threads.
|
||||||
|
|
||||||
|
Screw-length selection is by measured engagement, not a fixed BOM promise:
|
||||||
|
|
||||||
|
- Carrier screws: **1.6 mm** carrier thickness + washers + engagement into the base insert. Keep the head and shoulder clear of nearby components/copper.
|
||||||
|
- Case screw under-head plane Z34.6 to insert mouth Z15 is **19.6**, plus chosen engagement (e.g. 3 gives 22.6); do not blindly use an overlong 25 mm screw in a 4 mm pocket.
|
||||||
|
- Lower brass studs first traverse **1.6 mm carrier**, then engage the base inserts. `brass_m3_stud_l` / `brass_m2_stud_l` default to **4 mm reference lengths**, not measured hardware: tips at Z5.0 give **2.4 mm engagement**, leaving 1.6 mm to the M3 pocket bottom / 0.6 mm to the M2 pocket bottom. Actual insert usable threads may differ from pocket depth: measure before selecting. Upper daughterboard screws traverse **1.57 RS-232 / 1.6 OLED**, plus washers and chosen engagement into the **top female**, not the base. Preview female depths are 4/3 mm, also unknown. Verify studs/screws pass the nominal 3/2 mm holes without force; these are not generous M3/M2 clearance holes.
|
||||||
|
- Panel-to-lid screws traverse **2 mm** panel plus selected M3 engagement. Round/pan head clearance is open from outside; use small heads/washers which do not foul cable hardware.
|
||||||
|
|
||||||
|
## Cable interfaces
|
||||||
|
|
||||||
|
### USB: retain the accepted inboard mouths
|
||||||
|
|
||||||
|
Measured shell centres **X15.23 and 26.63**, widths **8.87**, mouth plane **Y77.705**, remain unchanged — mouths are **2.295 inside** the carrier bottom. The user's PCB fit/USB acceptance is retained, but it does not validate this new case.
|
||||||
|
|
||||||
|
A single joined bay spans **X9.595…32.265**, **Z19.6…26.6** (22.67 × 7), through the bottom-end wall. It admits both plug overmoulds instead of adding fragile individual webs. There are no internal bosses or tunnels between the wall and mouths. Mouth-to-exterior-wall distance is **5.695**: the plug must reach through that recess. The opening is deliberately generous relative to measured *widths*; shell height/depth and both plug overmoulds still need measurement. `usb_below_module_top`, `usb_above_module_top`, `usb_extra_width` are cutout controls; `usb_reference_depth/h` affect only the approximate preview. Enlarge for the actual plugs rather than moving U1 or growing Y without justification.
|
||||||
|
|
||||||
|
Signage says **USB**, not a guessed left/right native-USB vs USB-UART mapping. Both connections remain physically available independent of networking.
|
||||||
|
|
||||||
|
### DB9: external flange retention, not a guessed thread
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The STEP source registers the flange front at **Y1.930**, stock lock mouths **Y−2.870**, shell nose **Y−3.870**, with module front at Y0. The flange is **30.8 × 12.55**. Shell maximum root/lip envelope is **16.878038 × 9.1**; the panel hole is an intentionally rectangular **18.078038 × 10.3** envelope with 0.6 margin per side, not a reconstructed D-profile.
|
||||||
|
|
||||||
|
The **separate 46 × 13.68 × 2 panel** sits at **Y−0.070…1.930**, above the module PCB at Z21.07…34.75. The module PCB extends under/forward of it; the panel must not be extended down through that PCB. A **48 mm wide** main-wall service recess is open at X33…81, Z18.4…35, making the connector and panel screws externally reachable. Shell nose remains 0.47 beyond the ordinary outer wall plane. Check your cable hood against this recess; no universal connector/cable compatibility is claimed.
|
||||||
|
|
||||||
|
There are **three independent fastener systems**:
|
||||||
|
|
||||||
|
1. **Connector flange/cable lock axes X44.47 / 69.47, spacing 25**, parallel to Y, Z26.84762024. Panel holes default to Ø3.6 clearance, adjustable with `external_fastener_clearance`. The source models Ø3 cylindrical bores/Ø3.4 mouth chamfers, **no threads**. This does not establish M3, 4-40, pitch, removable posts, or usable engagement.
|
||||||
|
2. **Panel-to-lid M3 screws X38 / 76**, spacing 38, same height, into forward-facing insert hangers. These carry panel loads to the case and can be serviced externally.
|
||||||
|
3. **Module-to-carrier M3 screws**, Z axes, **24.13 spacing**, as above. Connector anchor/prong holes are not used as mounting screws.
|
||||||
|
|
||||||
|
**Required unresolved hardware selection:** confirm the actual connector posts can be removed/reseated, determine their real thread and required engagement, and fit suitable panel-mount replacement jackscrews (or a verified equivalent) that sandwich this panel against the flange **while preserving the cable's mating screw receptacles and nose reach**. Stock 4.8 mm posts occupy the panel volume; simply passing ordinary screws into their front bores does not clamp the panel to the flange and consumes the cable locks. **Do not do that. Do not assume an M3 screw fits.** `panel_t`, `panel_flange_gap` and clearance are adjustable for verified hardware; keeping the replacement posts too far forward can prevent full cable mating even if threads engage.
|
||||||
|
|
||||||
|
The detachable panel is intentional: an integral panel with closed shell hole could not be lowered vertically over the installed horizontal connector. Lower the lid first, then introduce the panel along the mating axis from outside and fasten it. `db_panel=false` removes lid hangers and assembled panel for a temporary access-only configuration — **not mechanically equivalent to external flange retention**. If the stock connector cannot accept suitable panel hardware, this retention concept must be revised, not declared validated.
|
||||||
|
|
||||||
|
## Display, buttons and antenna
|
||||||
|
|
||||||
|
- OLED window follows the supplied **display boundary X44.97…69.47, Y40.5…57**, plus 0.6 margin: **25.7 × 17.7**. This is **not a known active-pixel rectangle**. It leaves the M2 fixings outside the aperture. Glass thickness/height and optical viewing angle remain unknown; the provisional 2 mm preview glass top is Z22.6, leaving a fairly deep 12.4 mm viewing recess below the roof. Check readability and consider a later stepped bezel after measuring; no lid load is applied to the glass.
|
||||||
|
- Buttons are Omron **B3F-1000**, body **6 × 6**, actuator **Ø3.5**, released height **4.3 ±0.2**, travel **0.25 +0.2/−0.1**. Actual saved body centres are **(44.97,70.25), (56.97,70.25), (68.97,70.25)**. Labels are BACK / SELECT / NEXT, consistent with existing board/function evidence.
|
||||||
|
- Each **25.6 long Ø5 plunger** contacts the released switch and protrudes **1.5 above the roof**. An Ø8 × 1.2 retaining flange starts 0.5 above its contact tip and stays **below** the Ø9 outer / Ø5.5 bore guide. Guide bottom is 2.3 above the nominal switch top, giving 0.6 upward retention clearance. Insert stems **from under the lid**; they cannot be installed from above. The closed case/switch holds them from below, the flange stops extraction upward. Hold with removable tape during assembly; they can fall out of a detached lid.
|
||||||
|
- These are free sliding, switch-returned actuators, not printed springs. There is **no independent downward overload stop**: use normal tactile-switch force, never drive/hammer the plunger. Verify no preload, reliable return in different orientations, bore friction, tolerances, stroke and switch force before use. Shorten the tip/adjust flange/guide parameters if actual stack requires it. A ruggedized overload stop requires measured switch/body tolerances and is not claimed here.
|
||||||
|
- The case **does not enforce the carrier RF reserve** and has no red RF overlay, per user instruction. The measured projecting antenna **X12.045…29.675/Y14.745…21.635**, at Z19…20.6, remains physically represented and unobstructed by case hardware, with 14.4 mm to the roof underside. The PCB, its reserve and copper are unchanged. The plastic cover is not an RF-performance guarantee; no new RF qualification is claimed.
|
||||||
|
|
||||||
|
## Assembly and printing
|
||||||
|
|
||||||
|
1. Measure insert family, actual assembled stack and connector hardware before committing to a full print. Inspect the rendered assembly against physical parts. Print cutout/insert/guide coupons or slicer-cut samples first.
|
||||||
|
2. Print the base floor down. Indicative starting point only: 0.4 mm nozzle, 0.2 mm layers, 4 perimeters, 4–5 top/bottom layers, 25–40% infill with local solid regions around inserts. PETG is a reasonable prototype option; PLA is for fit checks away from heat. Determine material/thermal suitability yourself. No sealing, flame or temperature rating is established.
|
||||||
|
3. Lid can be printed roof-up with supports beneath the roof, guides, panel hangers and cable openings, avoiding supports in bores where practical. Alternatively print roof-down with `emboss=false`, attach separately printed text, or use a proper multi-material setup. **Integrated raised text prevents a flat roof-down first layer**; do not ignore this 0.6 mm offset. No support-free claim for the supplied embossed lid.
|
||||||
|
4. Print panel flat in its supplied orientation. **No plastic spacers are printed**; the brass dummies are preview-only purchased hardware. Print plungers with brim and support under their low retaining flange, or choose a slicer orientation that yields smooth, strong shafts. Deburr guides without opening them so far that stems tilt/jam. Test all three independently.
|
||||||
|
5. Heat-set inserts with a controlled tool to the selected supplier process. Panel hanger inserts install horizontally from the front. Remove strings/debris; inspect for splits, exposed insert metal and screw bottoming.
|
||||||
|
6. Seat the carrier directly on all base support bosses and secure its four M3 mounting holes. Install the two M3 and four M2 **10 mm brass standoffs above the carrier**, with lower studs through the corresponding existing carrier holes into inserts. Seat the RS-232/OLED on them and fasten with short screws into the top females; seat the ESP32 on its headers. Check that brass body height matches the header's seated height, neither bending boards nor lifting connectors; check DB9 prongs and all underside solder joints against the 5 mm floor clearance.
|
||||||
|
7. Insert plungers from under the lid. Lower lid onto the base with DB9 panel **off**. Verify actuation/return before fastening the four case screws. Check screw heads fit the wells and screws engage without bottoming.
|
||||||
|
8. After validating/removing the stock connector posts as required, slide the panel in from outside. Fit verified replacement connector jackscrews to the flange and the two independent M3 panel-to-lid screws. Do not use assembly screws to pull a misaligned flange/PCB into position. Test full USB/DB9 mating and cable locks; perform a restrained cable-load test before trusting retention.
|
||||||
|
9. To remove the lid, disconnect cables and remove panel/connector retention hardware first. The full electronics assembly should remain independently secured by carrier/daughterboard fasteners.
|
||||||
|
|
||||||
|
Raised labels are positive **0.6 mm emboss**, not engraving. Single-colour is the default; roof-up printing allows a filament change just above the roof for contrasting text (check actual sliced layers). For a separate material export `labels`, and export the lid with `emboss=false`. In a multi-object slicer, preserve XY origin and translate the flat text object up **22.13 mm** relative to the default lid export (its initial 0.02 overlaps the roof). Do not auto-centre individual letters; STL carries neither colour nor assembly transforms. Changes to stack require recalculating that offset: `top_z - eps - split_z - seam_clearance`.
|
||||||
|
|
||||||
|
## Regeneration and checks
|
||||||
|
|
||||||
|
Run from `hardware/case`; no network or added dependencies are needed. Shell commands below terminate; allow roughly a minute per export on slower machines.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
QT_QPA_PLATFORM=offscreen openscad -o base-default.stl -D 'part="base"' enclosure.scad
|
||||||
|
QT_QPA_PLATFORM=offscreen openscad -o lid-default.stl -D 'part="lid"' enclosure.scad
|
||||||
|
QT_QPA_PLATFORM=offscreen openscad -o panel-default.stl -D 'part="panel"' enclosure.scad
|
||||||
|
QT_QPA_PLATFORM=offscreen openscad -o plungers-default.stl -D 'part="plungers"' enclosure.scad
|
||||||
|
|
||||||
|
python3 validate.py
|
||||||
|
/usr/bin/python3 validate.py --source
|
||||||
|
QT_QPA_PLATFORM=offscreen openscad -o assembly-preview.png --imgsize=1200,1000 --viewall --autocenter --projection=o --camera=0,0,0,55,0,25,200 -D 'part="exploded"' enclosure.scad
|
||||||
|
QT_QPA_PLATFORM=offscreen openscad -o db9-preview.png --imgsize=1200,900 --viewall --autocenter --projection=o --camera=0,0,0,80,0,180,200 -D 'part="assembly"' enclosure.scad
|
||||||
|
```
|
||||||
|
|
||||||
|
`validate.py` uses Python's standard library and installed OpenSCAD for evaluated datum, stud-engagement and nozzle-web fixtures, plus ASCII STL checks; `--source` additionally uses the **already installed system KiCad `pcbnew` binding**, read-only. It verifies current outline/holes/placements, measured U1 USB mouth graphics and STEP-derived lock/shell data against case constants. It is a drift tripwire, not a full source parser for every SCAD expression. It checks each STL has two oppositely directed uses of every triangle edge, nonzero triangle area, the expected connected component count (1 base, 1 lid, 1 panel, 3 plungers), positive total signed volume and minimum Z≈0. It does **not** prove vertex-manifoldness, absence of self-intersections, full component collision clearance or physical fit.
|
||||||
|
|
||||||
|
Executed for this corrected design: all four default CGAL exports completed with `Simple: yes`; native-source registration, evaluated geometry fixtures and the mesh checks passed; exploded and connector-side PNGs rendered and were visually inspected. `spacers-default.stl` and the `spacers` mode/layout objects were removed. The base is 10 mm shorter; lid/panel/plunger print-local dimensions stay unchanged because their assembly datums all descend together. All four exports were regenerated. KiCad printed its existing enum-property assertions but completed the read-only checks. No slicer, physical print, screw-fit, cable-load, hardware/firmware, thermal or RF tests were performed. Additional geometry regression results, if any, are recorded in [checks.md](checks.md).
|
||||||
|
|
||||||
|
## Evidence and remaining measurements
|
||||||
|
|
||||||
|
Authoritative sources used, all local:
|
||||||
|
|
||||||
|
- Saved [carrier PCB](../PCB/serial-carrier.kicad_pcb): exact stepped outline, 1.6 thickness, all mounting holes, module/button placements and integrated measured U1 graphics.
|
||||||
|
- [User acceptance](../../docs/devel/hardware/PCB/user-acceptance.md): aligned centreline **56.97**, approved M3/M2 hardware and 10 mm brass standoffs, user-accepted USB fit, unresolved mating-height arithmetic.
|
||||||
|
- [Footprint measurements](../../docs/devel/hardware/PCB/footprint-notes.md), [Omron notes](../../docs/devel/hardware/PCB/button-notes.md).
|
||||||
|
- [DB9 mechanical notes](../../docs/devel/hardware/PCB/db9-mechanical-notes.md), [extracted measurements](../PCB/reference/db9-cad/measurements.json), [source STEP](../PCB/reference/db9-cad/5988%20RS-232%20Level%20Shifter.step), [CAD provenance/license](../PCB/reference/db9-cad/README.md). These are extracted **selected face bounds**, not complete connector reconstruction. Original female-module STEP reference is applied to the male module only under the user's reported external-geometry equivalence. No third-party STEP geometry is redistributed into the printed parts.
|
||||||
|
|
||||||
|
**Before calling this a fit:** measure actual insert OD/length/process and all screw heads/studs/engagement; module underside-to-carrier-top gap and board thicknesses; USB shell Z/depth and both real overmoulds; glass top/active area/view angle; component/solder underside heights and support lands; switch height/force/return; actual DB9 thread, removability, panel-jackscrew seating, flange Z, cable hood/lock reach and panel strength. Check assembled height against screw-length choices. Tune parameters and regenerate **all** affected STLs; previews and prior exports do not update automatically. Keep the 10 mm report and header arithmetic ambiguity explicit until measured.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
|||||||
|
# Corrected enclosure checks — 2026-09-26
|
||||||
|
|
||||||
|
This supersedes the initial under-carrier-brass stack. It is a CAD validation record, **not physical acceptance**. Tools already installed: OpenSCAD 2021.01, system Python 3 and KiCad `pcbnew`. No packages downloaded/installed. PCB hardware files were read only.
|
||||||
|
|
||||||
|
## Default design
|
||||||
|
|
||||||
|
Commands are in [README.md](README.md). All four printable part exports completed within bounded 180-second command timeouts, with OpenSCAD CGAL reporting `Simple: yes`:
|
||||||
|
|
||||||
|
| Export | Connected triangle components | Triangle count |
|
||||||
|
|---|---:|---:|
|
||||||
|
| `base-default.stl` | 1 | 8364 |
|
||||||
|
| `lid-default.stl` | 1 | 13080 |
|
||||||
|
| `panel-default.stl` | 1 | 816 |
|
||||||
|
| `plungers-default.stl` | 3 | 1716 |
|
||||||
|
|
||||||
|
The shell is **106 × 86.8 × 37.4 mm** (roof), 10 mm lower than the initial mistaken stack. Carrier underside/top are **Z7.4/9**, module underside **Z19**, roof underside **Z35**. The base top descends to Z15. The lid, panel, plungers, USB bay, DB9 hangers and display all descend with their parent datums; their relative fit remains unchanged. Print-local lid/panel/plunger nominal dimensions therefore remain unchanged, although all exports were regenerated. Gold hardware references replace printed spacers; no under-carrier brass, printable spacer mode, layout spacers or active spacer STL remains. The obsolete initial spacer STL had no unstaged user modifications before removal.
|
||||||
|
|
||||||
|
`/usr/bin/python3 validate.py --source` passed:
|
||||||
|
|
||||||
|
- Actual SCAD-evaluated default stack, including roof, split, button/guide, DB9 axis/panel bottom, USB bay and plunger length.
|
||||||
|
- Independent `carrier_boss_h=7` fixture moves assembly Z by 2 without changing plunger length; `module_gap=12, lid_above_carrier=28` raises modules/roof/ports and lengthens plungers without moving the carrier or button contact.
|
||||||
|
- Reference 4 mm studs cross the 1.6 mm carrier and give 2.4 mm nominal engagement. Negative fixtures reject an M3 stud that cannot reach past the carrier, an M2 stud that bottoms in the pocket, and a 0.8 mm shell wall.
|
||||||
|
- Analytical 0.4 mm nozzle checks: shell/floor/roof at least three nozzle widths; insert entry and guide webs at least two. The limiting default panel-hanger entry web is 0.8 mm, M3 daughter entry web 1.1 mm, M2 daughter entry web 1.1 mm and guide radial web 1.75 mm. These selected web checks are not slicer coverage or proof that all text/features print reliably.
|
||||||
|
- Native PCB outline vertex set, 1.6 mm thickness, U1/U2/DS1 registration and orientation.
|
||||||
|
- Actual carrier/daughterboard NPTH centres **and diameters** (carrier 3.2, RS-232 3, OLED 2), no U1 NPTH mounting holes, and button body centres. The ESP32 remains header-supported; no mounting holes were invented.
|
||||||
|
- Saved measured U1 USB mouth graphic centres/widths and mouth Y; extracted STEP shell bounds and front-lock axes (25 mm, not 24.13 mm module holes).
|
||||||
|
- Every exported triangle has nonzero area, every edge has exactly two oppositely wound uses, positive total signed volume, expected connected component counts, bottom Z≈0 and default maximum bounds (including the shortened base) to detect stale exports. These are **edge-manifold checks**, not an exhaustive mesh-intersection/vertex-manifold proof.
|
||||||
|
|
||||||
|
KiCad emitted its known enum-property assertions but completed. It did not save or regenerate the board.
|
||||||
|
|
||||||
|
PNG rendering succeeded headlessly with `QT_QPA_PLATFORM=offscreen`, within a bounded 120-second command timeout. Both `assembly-preview.png` and `db9-preview.png` were visually inspected: lower base, gold supports above carrier, simplified ESP32 header supports, separated lid/base, button tips, raised labels, USB bay and externally approachable DB9 panel. No red RF overlay remains. The actual projecting antenna rectangle is retained; the enclosure no longer reserves the PCB RF region. PCB copper/reserve/layout/schematic are unchanged and no Wi-Fi performance guarantee is implied. Hardware envelopes are incomplete; stock connector-post references still intentionally overlap the panel to expose the required replacement/reseating. Visual review does not certify clearance.
|
||||||
|
|
||||||
|
## Parameter CGAL smoke tests
|
||||||
|
|
||||||
|
Temporary STL files were rendered and checked for one closed consistently wound edge-manifold component:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
QT_QPA_PLATFORM=offscreen openscad -o /tmp/case-correction-variant-lid.stl -D 'part="lid"' -D 'module_gap=12' -D 'lid_above_carrier=28' -D 'carrier_boss_h=7' -D 'emboss=false' -D 'db_panel=false' enclosure.scad
|
||||||
|
QT_QPA_PLATFORM=offscreen openscad -o /tmp/case-correction-variant-base.stl -D 'part="base"' -D 'daughter_supports=false' -D 'side_extra=14' -D 'm3_insert_d=4.4' -D 'panel_mount_boss_d=7' enclosure.scad
|
||||||
|
```
|
||||||
|
|
||||||
|
Both reported `Simple: yes`. The first is a 41.4 mm roof-height configuration, lid export 3848 triangles. The second is a 110 mm-wide base, 4972 triangles. These test parameter propagation; **neither alternative is a selected or physically validated stack**. Arbitrary parameter combinations are not guaranteed valid; review every regenerated assembly.
|
||||||
|
|
||||||
|
## Explicit limits
|
||||||
|
|
||||||
|
No physical print, slicer analysis, insert installation, board assembly, connector/thread fit, plunger return/overload test, cable-load test, RF/thermal check or firmware test was performed. No full solid-interference solver was used. In particular, 5 mm carrier-to-floor clearance must be checked against actual solder/prongs, and conductive brass shoulders/insert faces against real lands/traces. Stud lengths, upper female depths, insert usable threads, USB Z/overmoulds, display height and DB9 hardware are still provisional as detailed in the README. The DB9 retention concept remains conditional on verified compatible panel jackscrews; stock posts are not claimed to fit the panel.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,364 @@
|
|||||||
|
// Provisional enclosure, mm. See README.md before printing or ordering hardware.
|
||||||
|
// Saved carrier coordinates: X right, Y toward USB, Z above base floor.
|
||||||
|
// Display in conventional top view via world(): X right, cable/DB9 end up.
|
||||||
|
part = "assembly"; // assembly, exploded, base, lid, panel, plunger, plungers, labels, layout, check
|
||||||
|
show_references = true; // preview only: never included in STL
|
||||||
|
emboss = true;
|
||||||
|
$fn = 48;
|
||||||
|
eps = 0.02;
|
||||||
|
|
||||||
|
/* [Print and enclosure] */
|
||||||
|
wall = 2.4;
|
||||||
|
floor_t = 2.4;
|
||||||
|
lid_t = 2.4;
|
||||||
|
side_extra = 12; // width added at EACH side of the 82 mm carrier
|
||||||
|
end_clearance = 1; // inner wall to carrier at cable ends
|
||||||
|
corner_r = 3;
|
||||||
|
seam_clearance = 0.25;
|
||||||
|
label_raise = 0.6;
|
||||||
|
|
||||||
|
/* [Carrier and module stack: heights NOT independently measured] */
|
||||||
|
carrier_t = 1.6; // saved PCB setting
|
||||||
|
carrier_boss_h = 5; // directly supports carrier underside; solder/prong clearance above floor
|
||||||
|
module_gap = 10; // brass body height: actual carrier TOP to module UNDERSIDE
|
||||||
|
esp_t = 1.6; // provisional
|
||||||
|
rs232_t = 1.57; // STEP
|
||||||
|
oled_t = 1.6; // provisional
|
||||||
|
lid_above_carrier = 26; // lid underside relative to carrier top
|
||||||
|
split_above_carrier = 6;
|
||||||
|
|
||||||
|
/* [Heat inserts: EXAMPLE pocket sizes, select for actual inserts] */
|
||||||
|
m3_insert_d = 4.2;
|
||||||
|
m3_insert_depth = 4;
|
||||||
|
m2_insert_d = 3.2;
|
||||||
|
m2_insert_depth = 3;
|
||||||
|
insert_lead = 0.3;
|
||||||
|
carrier_boss_d = 8;
|
||||||
|
case_boss_d = 9;
|
||||||
|
m3_clearance = 3.4;
|
||||||
|
m2_clearance = 2.3;
|
||||||
|
m3_head_d = 6.4;
|
||||||
|
m3_head_depth = 2.8;
|
||||||
|
// Insert bosses stop at carrier underside; brass male/female bodies sit ABOVE it.
|
||||||
|
daughter_supports = true;
|
||||||
|
daughter_m3_boss_d = 7;
|
||||||
|
daughter_m2_boss_d = 6;
|
||||||
|
brass_m3_d = 6; // reference OD only; measure purchased hardware
|
||||||
|
brass_m2_d = 4;
|
||||||
|
brass_m3_stud_l = 4; // UNKNOWN actual lengths: reference values, includes carrier thickness
|
||||||
|
brass_m2_stud_l = 4;
|
||||||
|
brass_m3_female_depth = 4; // reference only, not a hardware specification
|
||||||
|
brass_m2_female_depth = 3;
|
||||||
|
nozzle_d = 0.4; // analytical minimum-web checks, not a slicer simulation
|
||||||
|
|
||||||
|
/* [USB: only X widths and mouth Y measured] */
|
||||||
|
usb_extra_width = 2.4; // each shell's total added width; overlap joins into one bay
|
||||||
|
usb_below_module_top = 1.0;
|
||||||
|
usb_above_module_top = 6.0;
|
||||||
|
usb_reference_depth = 7; // preview estimate ONLY
|
||||||
|
usb_reference_h = 3.5; // preview estimate ONLY
|
||||||
|
|
||||||
|
/* [DB9 panel: replacement jackscrews must be verified] */
|
||||||
|
db_panel = true; // false leaves service opening; no case-to-flange retention
|
||||||
|
panel_t = 2;
|
||||||
|
panel_flange_gap = 0; // nominal flange contact; tune after physical measurement
|
||||||
|
external_fastener_clearance = 3.6; // NOT a thread designation
|
||||||
|
panel_mount_x = [38,76]; // separate M3 panel-to-lid screws, NOT connector locks
|
||||||
|
panel_mount_boss_d = 6.4;
|
||||||
|
shell_margin = 0.6; // per edge on STEP maximum shell envelope
|
||||||
|
panel_bottom_above_module = 0.5;
|
||||||
|
|
||||||
|
/* [Display and controls] */
|
||||||
|
display_margin = 0.6; // around entire measured display, NOT active pixels
|
||||||
|
button_h = 4.3; // Omron B3F-1000, +/-0.2
|
||||||
|
plunger_flange_offset = 0.5; // above switch contact face; no external travel stop
|
||||||
|
plunger_d = 5;
|
||||||
|
plunger_clearance = 0.5; // diametral, tune printer
|
||||||
|
plunger_flange_d = 8;
|
||||||
|
plunger_flange_t = 1.2;
|
||||||
|
plunger_above_lid = 1.5;
|
||||||
|
guide_od = 9;
|
||||||
|
guide_bottom_above_button = 2.3;
|
||||||
|
|
||||||
|
// Source geometry, carrier-local. Do not replace hole centres with body centres.
|
||||||
|
board_outline = [[0,19],[36,19],[36,0],[82,0],[82,80],[0,80]];
|
||||||
|
carrier_holes = [[3.5,22.5],[3.5,75],[78,4],[78,76]];
|
||||||
|
rs_holes = [[44.905,17.907],[69.035,17.907]]; // 24.130 spacing, Z axes
|
||||||
|
oled_holes = [[45.72,37.75],[68.22,37.75],[45.72,60.25],[68.22,60.25]];
|
||||||
|
button_centres = [[44.97,70.25],[56.97,70.25],[68.97,70.25]];
|
||||||
|
usb_centres = [15.23,26.63];
|
||||||
|
usb_mouth_y = 77.705;
|
||||||
|
module_cx = 56.97;
|
||||||
|
lock_x = [44.47,69.47]; // 25.000 spacing, Y axes; NOT rs_holes
|
||||||
|
|
||||||
|
carrier_bottom = floor_t + carrier_boss_h;
|
||||||
|
carrier_top = carrier_bottom + carrier_t;
|
||||||
|
module_bottom = carrier_top + module_gap;
|
||||||
|
rs_top = module_bottom + rs232_t;
|
||||||
|
esp_top = module_bottom + esp_t;
|
||||||
|
oled_top = module_bottom + oled_t;
|
||||||
|
lid_z = carrier_top + lid_above_carrier;
|
||||||
|
top_z = lid_z + lid_t;
|
||||||
|
split_z = carrier_top + split_above_carrier;
|
||||||
|
xmin = -side_extra;
|
||||||
|
xmax = 82 + side_extra;
|
||||||
|
ymin = -end_clearance-wall;
|
||||||
|
ymax = 80+end_clearance+wall;
|
||||||
|
case_holes = [[-side_extra/2,12],[-side_extra/2,68],
|
||||||
|
[82+side_extra/2,12],[82+side_extra/2,68]];
|
||||||
|
db_axis_z = rs_top + 6.27762024;
|
||||||
|
flange_y = 1.930;
|
||||||
|
panel_rear_y = flange_y-panel_flange_gap;
|
||||||
|
panel_front_y = panel_rear_y-panel_t;
|
||||||
|
panel_low_z = rs_top+panel_bottom_above_module;
|
||||||
|
button_top = carrier_top+button_h;
|
||||||
|
guide_low_z = button_top+guide_bottom_above_button;
|
||||||
|
guide_high_z = lid_z;
|
||||||
|
flange_bottom_z = button_top+plunger_flange_offset;
|
||||||
|
plunger_length = top_z+plunger_above_lid-button_top;
|
||||||
|
|
||||||
|
assert(side_extra >= case_boss_d+2, "Widen side rails for case bosses");
|
||||||
|
assert(carrier_boss_h > max(m3_insert_depth,m2_insert_depth), "Leave material under inserts");
|
||||||
|
assert(min(wall,floor_t,lid_t) >= 3*nozzle_d, "Shell needs at least three nozzle widths");
|
||||||
|
assert((min(carrier_boss_d,case_boss_d,daughter_m3_boss_d,panel_mount_boss_d)
|
||||||
|
-m3_insert_d-2*insert_lead)/2 >= 2*nozzle_d, "M3 entry web too thin");
|
||||||
|
assert((daughter_m2_boss_d-m2_insert_d-2*insert_lead)/2 >= 2*nozzle_d, "M2 entry web too thin");
|
||||||
|
assert((guide_od-plunger_d-plunger_clearance)/2 >= 2*nozzle_d, "Guide web too thin");
|
||||||
|
assert(brass_m3_stud_l > carrier_t && brass_m3_stud_l-carrier_t < m3_insert_depth,
|
||||||
|
"M3 reference stud must cross carrier and engage without bottoming");
|
||||||
|
assert(brass_m2_stud_l > carrier_t && brass_m2_stud_l-carrier_t < m2_insert_depth,
|
||||||
|
"M2 reference stud must cross carrier and engage without bottoming");
|
||||||
|
assert(module_gap > max(brass_m3_female_depth,brass_m2_female_depth)+1);
|
||||||
|
assert(m3_insert_d+2 < min(carrier_boss_d,case_boss_d,daughter_m3_boss_d));
|
||||||
|
assert(m2_insert_d+2 < daughter_m2_boss_d);
|
||||||
|
assert(lid_z > rs_top+12.55262024+0.8, "Lid intersects DB9 flange");
|
||||||
|
assert(panel_low_z < db_axis_z-external_fastener_clearance/2-0.8);
|
||||||
|
assert(panel_front_y > -2.870, "Panel would obscure stock cable-lock mouth plane");
|
||||||
|
assert(guide_high_z > guide_low_z+4);
|
||||||
|
assert(flange_bottom_z+plunger_flange_t < guide_low_z, "Plunger flange trapped below guide");
|
||||||
|
assert(split_z > carrier_top+2 && split_z < module_bottom-1);
|
||||||
|
assert(usb_above_module_top+esp_top < lid_z, "USB bay cuts roof");
|
||||||
|
|
||||||
|
echo("PROVISIONAL overall W,D,H (without labels/plungers)",xmax-xmin,ymax-ymin,top_z);
|
||||||
|
echo("carrier bottom/top, module underside, RS232 top",carrier_bottom,carrier_top,module_bottom,rs_top);
|
||||||
|
echo("DB9 lock axis Z, split, lid underside",db_axis_z,split_z,lid_z);
|
||||||
|
echo("CHECK stack",[carrier_bottom,carrier_top,module_bottom,rs_top,lid_z,top_z,
|
||||||
|
split_z,button_top,guide_low_z,db_axis_z,panel_low_z,
|
||||||
|
esp_top-usb_below_module_top,esp_top+usb_above_module_top,plunger_length]);
|
||||||
|
echo("CHECK engagement",[brass_m3_stud_l-carrier_t,brass_m2_stud_l-carrier_t]);
|
||||||
|
|
||||||
|
// Reflect carrier Y for a conventional top view; text compensates separately.
|
||||||
|
module world() { translate([0,80,0]) rotate([0,0,0]) mirror([0,1,0]) children(); }
|
||||||
|
module box(x,y,z,w,d,h) { translate([x,y,z]) cube([w,d,h]); }
|
||||||
|
module zcyl(p,z,d,h) { translate([p[0],p[1],z]) cylinder(d=d,h=h); }
|
||||||
|
module ybore(x,y,z,d,len) { translate([x,y,z]) rotate([-90,0,0]) cylinder(d=d,h=len); }
|
||||||
|
module outer_2d(inset=0) {
|
||||||
|
translate([xmin+inset+corner_r,ymin+inset+corner_r])
|
||||||
|
offset(r=corner_r) square([xmax-xmin-2*inset-2*corner_r,
|
||||||
|
ymax-ymin-2*inset-2*corner_r]);
|
||||||
|
}
|
||||||
|
module outline_volume(z,h,inset=0) { translate([0,0,z]) linear_extrude(h) outer_2d(inset); }
|
||||||
|
module walls(z,h) {
|
||||||
|
difference() {
|
||||||
|
outline_volume(z,h);
|
||||||
|
outline_volume(z-eps,h+2*eps,wall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module insert_pocket(p,top,d,depth) {
|
||||||
|
zcyl(p,top-depth,d,depth+eps);
|
||||||
|
translate([p[0],p[1],top-insert_lead])
|
||||||
|
cylinder(d1=d,d2=d+2*insert_lead,h=insert_lead+eps);
|
||||||
|
}
|
||||||
|
module daughter_bosses() {
|
||||||
|
for(p=rs_holes) zcyl(p,floor_t-eps,daughter_m3_boss_d,carrier_bottom-floor_t+eps);
|
||||||
|
for(p=oled_holes) zcyl(p,floor_t-eps,daughter_m2_boss_d,carrier_bottom-floor_t+eps);
|
||||||
|
}
|
||||||
|
module cable_cuts() {
|
||||||
|
// Whole DB9 service recess clears PCB overhang, flange and wrench approach.
|
||||||
|
// Panel lives inward of this wall and ABOVE the module PCB, never through it.
|
||||||
|
box(33,ymin-1,module_bottom-0.6,48,panel_front_y-ymin+1,lid_z-module_bottom+0.6);
|
||||||
|
// Wide joined bay admits overmoulds to the inboard USB mouths. No thin web.
|
||||||
|
box(usb_centres[0]-(8.87+usb_extra_width)/2,usb_mouth_y-1,
|
||||||
|
esp_top-usb_below_module_top,
|
||||||
|
usb_centres[1]-usb_centres[0]+8.87+usb_extra_width,
|
||||||
|
ymax-usb_mouth_y+2,usb_below_module_top+usb_above_module_top);
|
||||||
|
}
|
||||||
|
module base() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
outline_volume(0,floor_t);
|
||||||
|
walls(floor_t-eps,split_z-floor_t+eps);
|
||||||
|
for(p=carrier_holes) zcyl(p,floor_t-eps,carrier_boss_d,carrier_boss_h+eps);
|
||||||
|
for(p=case_holes) {
|
||||||
|
zcyl(p,floor_t-eps,case_boss_d,split_z-floor_t+eps);
|
||||||
|
// Lateral ribs connect bosses to walls; not in cable approach paths.
|
||||||
|
box(p[0]<0?xmin:p[0],p[1]-2,floor_t-eps,
|
||||||
|
side_extra/2,4,split_z-floor_t+eps);
|
||||||
|
}
|
||||||
|
if(daughter_supports) daughter_bosses();
|
||||||
|
}
|
||||||
|
for(p=carrier_holes) insert_pocket(p,floor_t+carrier_boss_h,m3_insert_d,m3_insert_depth);
|
||||||
|
for(p=case_holes) insert_pocket(p,split_z,m3_insert_d,m3_insert_depth);
|
||||||
|
if(daughter_supports) {
|
||||||
|
for(p=rs_holes) insert_pocket(p,carrier_bottom,m3_insert_d,m3_insert_depth);
|
||||||
|
for(p=oled_holes) insert_pocket(p,carrier_bottom,m2_insert_d,m2_insert_depth);
|
||||||
|
}
|
||||||
|
cable_cuts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module panel() {
|
||||||
|
difference() {
|
||||||
|
// Separate externally fitted part: a closed shell hole cannot lower over DB9.
|
||||||
|
box(34,panel_front_y,panel_low_z,46,panel_t,lid_z-panel_low_z-seam_clearance);
|
||||||
|
box(module_cx-16.878038/2-shell_margin,panel_front_y-eps,
|
||||||
|
db_axis_z-9.1/2-shell_margin,16.878038+2*shell_margin,
|
||||||
|
panel_t+2*eps,9.1+2*shell_margin);
|
||||||
|
for(x=lock_x) ybore(x,panel_front_y-eps,db_axis_z,
|
||||||
|
external_fastener_clearance,panel_t+2*eps);
|
||||||
|
for(x=panel_mount_x) ybore(x,panel_front_y-eps,db_axis_z,m3_clearance,panel_t+2*eps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module panel_hangers() {
|
||||||
|
for(x=panel_mount_x) difference() {
|
||||||
|
box(x-panel_mount_boss_d/2,panel_rear_y,db_axis_z-panel_mount_boss_d/2,
|
||||||
|
panel_mount_boss_d,m3_insert_depth+2,lid_z-db_axis_z+panel_mount_boss_d/2+eps);
|
||||||
|
ybore(x,panel_rear_y-eps,db_axis_z,m3_insert_d,m3_insert_depth+eps);
|
||||||
|
translate([x,panel_rear_y-eps,db_axis_z]) rotate([-90,0,0])
|
||||||
|
cylinder(d1=m3_insert_d+2*insert_lead,d2=m3_insert_d,h=insert_lead+eps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module guide(p) {
|
||||||
|
difference() {
|
||||||
|
// Flange stays BELOW this continuous guide: insert stem from underside.
|
||||||
|
zcyl(p,guide_low_z,guide_od,guide_high_z-guide_low_z+eps);
|
||||||
|
zcyl(p,guide_low_z-eps,plunger_d+plunger_clearance,
|
||||||
|
guide_high_z-guide_low_z+2*eps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module lid_body() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
outline_volume(lid_z,lid_t);
|
||||||
|
walls(split_z+seam_clearance,lid_z-split_z-seam_clearance+eps);
|
||||||
|
for(p=case_holes) {
|
||||||
|
zcyl(p,split_z+seam_clearance,case_boss_d,lid_z-split_z-seam_clearance+eps);
|
||||||
|
box(p[0]<0?xmin:p[0],p[1]-2,split_z+seam_clearance,
|
||||||
|
side_extra/2,4,lid_z-split_z-seam_clearance+eps);
|
||||||
|
}
|
||||||
|
for(p=button_centres) guide(p);
|
||||||
|
if(db_panel) panel_hangers();
|
||||||
|
}
|
||||||
|
// Measured glass/display boundary plus margin, not a guessed active area.
|
||||||
|
box(44.97-display_margin,40.5-display_margin,lid_z-eps,
|
||||||
|
24.5+2*display_margin,16.5+2*display_margin,lid_t+2*eps);
|
||||||
|
for(p=button_centres) zcyl(p,lid_z-eps,plunger_d+plunger_clearance,lid_t+2*eps);
|
||||||
|
for(p=case_holes) {
|
||||||
|
zcyl(p,split_z-eps,m3_clearance,top_z-split_z+2*eps);
|
||||||
|
// Recess spans thick boss, leaving lid thickness below screw head.
|
||||||
|
zcyl(p,top_z-m3_head_depth,m3_head_d,m3_head_depth+eps);
|
||||||
|
}
|
||||||
|
cable_cuts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module label_at(x,y,s,size=2.5) {
|
||||||
|
// Undo world's Y reflection for text only: readable from USB end, DB9 above.
|
||||||
|
translate([x,y,top_z-eps]) mirror([0,1,0]) linear_extrude(label_raise+eps)
|
||||||
|
text(s,size=size,halign="center",valign="center",font="Liberation Sans:style=Bold");
|
||||||
|
}
|
||||||
|
module labels() {
|
||||||
|
label_at(module_cx,7,"RS-232",3);
|
||||||
|
label_at(20.93,74,"USB",3); // native vs UART left/right NOT established
|
||||||
|
label_at(module_cx,35,"DISPLAY",2.4);
|
||||||
|
for(i=[0:2]) label_at(button_centres[i][0],77,["BACK","SELECT","NEXT"][i],2.1);
|
||||||
|
}
|
||||||
|
module lid() { lid_body(); if(emboss) labels(); }
|
||||||
|
module plunger() {
|
||||||
|
// Local bottom at zero, prints vertically; brim recommended for slender stem.
|
||||||
|
cylinder(d=plunger_d,h=plunger_length);
|
||||||
|
translate([0,0,flange_bottom_z-button_top]) cylinder(d=plunger_flange_d,h=plunger_flange_t);
|
||||||
|
}
|
||||||
|
module plungers() { for(i=[0:2]) translate([i*12,0,0]) plunger(); }
|
||||||
|
// Preview-only unthreaded hardware dummies, never printable parts.
|
||||||
|
module brass_standoff(p,od,thread_d,stud_l,female_depth) {
|
||||||
|
difference() {
|
||||||
|
zcyl(p,carrier_top,od,module_gap);
|
||||||
|
zcyl(p,module_bottom-female_depth,thread_d,female_depth+eps);
|
||||||
|
}
|
||||||
|
zcyl(p,carrier_top-stud_l,thread_d,stud_l+eps);
|
||||||
|
}
|
||||||
|
module pcb_shape() {
|
||||||
|
difference() {
|
||||||
|
linear_extrude(carrier_t) polygon(board_outline);
|
||||||
|
for(p=carrier_holes) zcyl(p,-eps,3.2,carrier_t+2*eps);
|
||||||
|
for(p=rs_holes) zcyl(p,-eps,3,carrier_t+2*eps);
|
||||||
|
for(p=oled_holes) zcyl(p,-eps,2,carrier_t+2*eps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module references() {
|
||||||
|
// All simplified envelopes, not complete component CAD or collision certificates.
|
||||||
|
color("seagreen") translate([0,0,carrier_bottom]) pcb_shape();
|
||||||
|
// Four independent carrier screws, not daughterboard standoff locations.
|
||||||
|
color("silver") for(p=carrier_holes) {
|
||||||
|
zcyl(p,carrier_bottom-2.4,3,carrier_t+2.4);
|
||||||
|
zcyl(p,carrier_top,5.5,2); // head/engagement reference only
|
||||||
|
}
|
||||||
|
// ESP32 is header-supported: no supplied mounting holes, no invented fasteners.
|
||||||
|
color("black") for(x=[8,33.4]) box(x-1.25,21.75,carrier_top,2.5,55.84,module_gap);
|
||||||
|
color("darkgreen") {
|
||||||
|
box(6.635,21.635,module_bottom,28.09,56.07,esp_t);
|
||||||
|
difference() {
|
||||||
|
box(41.095,0,module_bottom,31.75,29.337,rs232_t);
|
||||||
|
for(p=rs_holes) zcyl(p,module_bottom-eps,3,rs232_t+2*eps);
|
||||||
|
}
|
||||||
|
difference() {
|
||||||
|
box(43.97,36,module_bottom,26,26,oled_t);
|
||||||
|
for(p=oled_holes) zcyl(p,module_bottom-eps,2,oled_t+2*eps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color("orange") box(12.045,14.745,module_bottom,17.63,6.89,esp_t);
|
||||||
|
|
||||||
|
color("black") box(44.97,40.5,oled_top,24.5,16.5,2); // glass height unknown
|
||||||
|
color("silver") {
|
||||||
|
for(x=usb_centres) box(x-8.87/2,usb_mouth_y-usb_reference_depth,esp_top,
|
||||||
|
8.87,usb_reference_depth,usb_reference_h);
|
||||||
|
box(41.57,1.930,rs_top+0.00262024,30.8,0.4,12.55);
|
||||||
|
box(module_cx-16.878038/2,-3.870,db_axis_z-9.1/2,16.878038,5.8,9.1);
|
||||||
|
}
|
||||||
|
// Stock hex envelopes conflict with panel intentionally: must be replaced/reseated.
|
||||||
|
color([0.8,0.5,0.1,0.65]) for(x=lock_x)
|
||||||
|
ybore(x,-2.870,db_axis_z,5.311622,4.8);
|
||||||
|
color("gray") for(p=button_centres) {
|
||||||
|
box(p[0]-3,p[1]-3,carrier_top,6,6,3.4);
|
||||||
|
zcyl(p,carrier_top+3.4,3.5,button_h-3.4);
|
||||||
|
}
|
||||||
|
if(daughter_supports) color("gold") {
|
||||||
|
for(p=rs_holes) brass_standoff(p,brass_m3_d,3,brass_m3_stud_l,brass_m3_female_depth);
|
||||||
|
for(p=oled_holes) brass_standoff(p,brass_m2_d,2,brass_m2_stud_l,brass_m2_female_depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module assembled(explode=0) {
|
||||||
|
color("slategray") base();
|
||||||
|
color([0.8,0.85,0.9,0.7]) translate([0,0,explode]) lid();
|
||||||
|
if(db_panel) color("slategray") translate([0,-explode/2,0]) panel();
|
||||||
|
color("orange") for(p=button_centres) translate([p[0],p[1],button_top+explode]) plunger();
|
||||||
|
if($preview && show_references) %references();
|
||||||
|
}
|
||||||
|
// Each printable export is on Z=0, without hardware reference geometry.
|
||||||
|
if(part=="base") world() base();
|
||||||
|
else if(part=="lid") translate([0,0,-split_z-seam_clearance]) world() lid();
|
||||||
|
else if(part=="panel") rotate([90,0,0]) translate([-34,-panel_front_y,-panel_low_z]) panel();
|
||||||
|
else if(part=="plunger") plunger();
|
||||||
|
else if(part=="plungers") plungers();
|
||||||
|
|
||||||
|
else if(part=="labels") translate([0,0,-top_z+eps]) world() labels();
|
||||||
|
else if(part=="layout") {
|
||||||
|
world() base();
|
||||||
|
translate([xmax-xmin+12,0,-split_z-seam_clearance]) world() lid();
|
||||||
|
translate([0,-22,0]) plungers();
|
||||||
|
|
||||||
|
translate([60,-20,0]) rotate([90,0,0]) translate([-34,-panel_front_y,-panel_low_z]) panel();
|
||||||
|
}
|
||||||
|
else if(part=="check") {} // datums/assertions only, used by validate.py
|
||||||
|
else if(part=="assembly") world() assembled();
|
||||||
|
else if(part=="exploded") world() assembled(28);
|
||||||
|
else assert(false,"Unknown part");
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Read-only source registration and ASCII STL topology checks; no packages added.
|
||||||
|
|
||||||
|
Use /usr/bin/python3 for optional installed KiCad bindings. This is not a full
|
||||||
|
solid collision, slicer, strength, RF, or physical-fit test.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import ast
|
||||||
|
from collections import defaultdict
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
PCB = ROOT.parent / "PCB"
|
||||||
|
SCAD = (ROOT / "enclosure.scad").read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def literal(name):
|
||||||
|
match = re.search(r"^" + re.escape(name) + r"\s*=\s*([^;]+);", SCAD, re.M)
|
||||||
|
assert match, name
|
||||||
|
return ast.literal_eval(match.group(1))
|
||||||
|
|
||||||
|
|
||||||
|
def near(a, b):
|
||||||
|
assert len(a) == len(b) and all(abs(x-y) < 1e-5 for x, y in zip(a, b)), (a, b)
|
||||||
|
|
||||||
|
|
||||||
|
def source_check():
|
||||||
|
import pcbnew as p # installed KiCad, not required for mesh-only checking
|
||||||
|
board = p.LoadBoard(str(PCB / "serial-carrier.kicad_pcb"))
|
||||||
|
fps = {f.GetReference(): f for f in board.GetFootprints()}
|
||||||
|
|
||||||
|
def local(v):
|
||||||
|
return (p.ToMM(v.x)-100, p.ToMM(v.y)-100)
|
||||||
|
|
||||||
|
def holes(ref):
|
||||||
|
return sorted(local(q.GetPosition()) for q in fps[ref].Pads()
|
||||||
|
if q.GetAttribute() == p.PAD_ATTRIB_NPTH)
|
||||||
|
|
||||||
|
near([p.ToMM(board.GetDesignSettings().GetBoardThickness())], [literal("carrier_t")])
|
||||||
|
for ref, xy in {"U1": [8, 23], "U2": [43, 26.670], "DS1": [43.97, 36]}.items():
|
||||||
|
near(local(fps[ref].GetPosition()), xy)
|
||||||
|
assert fps[ref].GetOrientationDegrees() == 0
|
||||||
|
assert not holes("U1"), "ESP32 has no supplied mounting holes; retain header support"
|
||||||
|
for ref, diameter in [("H1", 3.2), ("H2", 3.2), ("H3", 3.2), ("H4", 3.2), ("U2", 3), ("DS1", 2)]:
|
||||||
|
for pad in fps[ref].Pads():
|
||||||
|
if pad.GetAttribute() == p.PAD_ATTRIB_NPTH:
|
||||||
|
near([p.ToMM(pad.GetDrillSize().x), p.ToMM(pad.GetDrillSize().y)], [diameter]*2)
|
||||||
|
for ref, xy in zip(["H1", "H2", "H3", "H4"], literal("carrier_holes")):
|
||||||
|
near(holes(ref)[0], xy)
|
||||||
|
for ref, key in [("U2", "rs_holes"), ("DS1", "oled_holes")]:
|
||||||
|
for actual, expected in zip(holes(ref), sorted(literal(key))):
|
||||||
|
near(actual, expected)
|
||||||
|
assert len(holes(ref)) == len(literal(key))
|
||||||
|
for ref, xy in zip(["SW1", "SW2", "SW3"], literal("button_centres")):
|
||||||
|
origin = local(fps[ref].GetPosition())
|
||||||
|
near([origin[0]+3.25, origin[1]+2.25], xy)
|
||||||
|
assert fps[ref].GetOrientationDegrees() == 0
|
||||||
|
edges = {tuple(round(v, 6) for v in local(d.GetStart()))
|
||||||
|
for d in board.GetDrawings() if d.GetLayer() == p.Edge_Cuts}
|
||||||
|
assert edges == {tuple(v) for v in literal("board_outline")}
|
||||||
|
# Measured embedded U1 F.Fab lines: mouth widths/position are source authority.
|
||||||
|
mouth_lines = []
|
||||||
|
for item in fps["U1"].GraphicalItems():
|
||||||
|
if isinstance(item, p.PCB_SHAPE) and item.GetLayer() == p.F_Fab:
|
||||||
|
a, b = local(item.GetStart()), local(item.GetEnd())
|
||||||
|
if abs(a[1]-77.705) < 1e-5 and abs(b[1]-77.705) < 1e-5:
|
||||||
|
mouth_lines.append((a, b))
|
||||||
|
measured = sorted((a[0]+b[0])/2 for a, b in mouth_lines
|
||||||
|
if abs(abs(a[0]-b[0])-8.87) < 1e-5)
|
||||||
|
near(measured, literal("usb_centres"))
|
||||||
|
near([literal("usb_mouth_y")], [77.705])
|
||||||
|
cad = json.loads((PCB / "reference/db9-cad/measurements.json").read_text())
|
||||||
|
near([axis["location"][0]+43-1.905 for axis in cad["front_lock_axes"]], literal("lock_x"))
|
||||||
|
near(cad["shell_outer_including_root_and_lip"]["dimensions"], [16.878038, 5.8, 9.1])
|
||||||
|
for axis in cad["front_lock_axes"]:
|
||||||
|
near([axis["location"][2]-1.57], [6.27762024])
|
||||||
|
print("PASS native PCB outline, thickness, placements, NPTH centres, buttons, USB mouths, STEP lock/shell data")
|
||||||
|
|
||||||
|
|
||||||
|
def mesh_check(path, expected_components):
|
||||||
|
text = path.read_text() # OpenSCAD 2021 default ASCII STL, fail on other format
|
||||||
|
vertices = [tuple(map(float, xyz)) for xyz in re.findall(
|
||||||
|
r"\bvertex\s+([-+\deE.]+)\s+([-+\deE.]+)\s+([-+\deE.]+)", text)]
|
||||||
|
assert vertices and len(vertices) % 3 == 0, path
|
||||||
|
triangles = [tuple(vertices[i:i+3]) for i in range(0, len(vertices), 3)]
|
||||||
|
edges = defaultdict(list)
|
||||||
|
neighbours = defaultdict(set)
|
||||||
|
volume = 0
|
||||||
|
for idx, (a, b, c) in enumerate(triangles):
|
||||||
|
assert len({a, b, c}) == 3, (path, "degenerate triangle", idx)
|
||||||
|
u, v = [b[i]-a[i] for i in range(3)], [c[i]-a[i] for i in range(3)]
|
||||||
|
cross = (u[1]*v[2]-u[2]*v[1], u[2]*v[0]-u[0]*v[2], u[0]*v[1]-u[1]*v[0])
|
||||||
|
assert sum(q*q for q in cross) > 1e-18, (path, "zero area", idx)
|
||||||
|
volume += sum(a[i]*cross[i] for i in range(3))/6
|
||||||
|
for start, end in [(a, b), (b, c), (c, a)]:
|
||||||
|
edges[tuple(sorted((start, end)))].append((idx, start, end))
|
||||||
|
for edge, uses in edges.items():
|
||||||
|
assert len(uses) == 2, (path, "nonmanifold/open edge", edge, len(uses))
|
||||||
|
assert uses[0][1:] == uses[1][1:][::-1], (path, "inconsistent winding", edge)
|
||||||
|
i, j = uses[0][0], uses[1][0]
|
||||||
|
neighbours[i].add(j)
|
||||||
|
neighbours[j].add(i)
|
||||||
|
remaining, components = set(range(len(triangles))), 0
|
||||||
|
while remaining:
|
||||||
|
stack = [remaining.pop()]
|
||||||
|
components += 1
|
||||||
|
while stack:
|
||||||
|
for n in neighbours[stack.pop()]:
|
||||||
|
if n in remaining:
|
||||||
|
remaining.remove(n)
|
||||||
|
stack.append(n)
|
||||||
|
assert components == expected_components, (path, "components", components)
|
||||||
|
assert volume > 0, (path, "inverted/nonpositive volume", volume)
|
||||||
|
low = [min(v[i] for v in vertices) for i in range(3)]
|
||||||
|
high = [max(v[i] for v in vertices) for i in range(3)]
|
||||||
|
assert abs(low[2]) < 1e-4, (path, "not on print bed", low)
|
||||||
|
return {"file": path.name, "triangles": len(triangles), "components": components,
|
||||||
|
"closed_consistently_wound_edges": True, "volume_mm3": round(volume, 3),
|
||||||
|
"bounds_mm": [low, high], "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
|
||||||
|
|
||||||
|
|
||||||
|
def geometry_check():
|
||||||
|
"""Evaluate actual SCAD expressions, including negative stud/web fixtures."""
|
||||||
|
assert 'module spacers(' not in SCAD and 'part=="spacers"' not in SCAD
|
||||||
|
assert not (ROOT/"spacers-default.stl").exists(), "Obsolete printable spacers must not ship"
|
||||||
|
assert 'brass_standoff_h' not in SCAD, "No brass height below carrier"
|
||||||
|
assert 'box(10,19,carrier_top' not in SCAD, "RF overlay must not return"
|
||||||
|
assert 'box(12.045,14.745,module_bottom,17.63,6.89,esp_t)' in SCAD
|
||||||
|
with tempfile.TemporaryDirectory(prefix="case-check-") as temp:
|
||||||
|
def run(defines=(), valid=True):
|
||||||
|
command = ["openscad", "-o", str(Path(temp)/"check.csg"), "-D", 'part="check"']
|
||||||
|
for define in defines:
|
||||||
|
command.extend(["-D", define])
|
||||||
|
result = subprocess.run(command+[str(ROOT/"enclosure.scad")],
|
||||||
|
env={**os.environ, "QT_QPA_PLATFORM": "offscreen"},
|
||||||
|
capture_output=True, text=True, timeout=30)
|
||||||
|
output = result.stdout+result.stderr
|
||||||
|
if not valid:
|
||||||
|
assert "ERROR: Assertion" in output, output
|
||||||
|
return
|
||||||
|
assert result.returncode == 0 and "ERROR:" not in output, output
|
||||||
|
def datum(label):
|
||||||
|
match = re.search(r'ECHO: "CHECK '+label+r'", (\[[^\n]+\])', output)
|
||||||
|
assert match, output
|
||||||
|
return ast.literal_eval(match.group(1))
|
||||||
|
return datum("stack"), datum("engagement")
|
||||||
|
stack, engagement = run()
|
||||||
|
# Independent expected defaults, not values copied from exported meshes.
|
||||||
|
near(stack, [7.4, 9, 19, 20.57, 35, 37.4, 15, 13.3, 15.6,
|
||||||
|
26.8476, 21.07, 19.6, 26.6, 25.6])
|
||||||
|
near(engagement, [2.4, 2.4])
|
||||||
|
raised, _ = run(["carrier_boss_h=7"])
|
||||||
|
near(raised, [value+(0 if i == 13 else 2) for i, value in enumerate(stack)])
|
||||||
|
taller, _ = run(["module_gap=12", "lid_above_carrier=28"])
|
||||||
|
near(taller, [value+(2 if i in [2,3,4,5,9,10,11,12,13] else 0)
|
||||||
|
for i, value in enumerate(stack)])
|
||||||
|
run(["brass_m3_stud_l=1.6"], valid=False)
|
||||||
|
run(["brass_m2_stud_l=5"], valid=False)
|
||||||
|
run(["wall=0.8"], valid=False)
|
||||||
|
print("PASS actual SCAD default/raised-carrier/taller-module datums, stud engagement and nozzle-web rejection fixtures")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--source", action="store_true", help="also check saved KiCad/STEP registration")
|
||||||
|
args = parser.parse_args()
|
||||||
|
geometry_check()
|
||||||
|
if args.source:
|
||||||
|
source_check()
|
||||||
|
reports = [mesh_check(ROOT / name, n) for name, n in [
|
||||||
|
("base-default.stl", 1), ("lid-default.stl", 1), ("panel-default.stl", 1),
|
||||||
|
("plungers-default.stl", 3)]]
|
||||||
|
for report, high in zip(reports, [[94,83.4,15], [94,83.4,22.75],
|
||||||
|
[46,0,2], [28,4,25.6]]):
|
||||||
|
near(report["bounds_mm"][1], high) # catch stale exports of the old stack
|
||||||
|
print(json.dumps(reports, indent=2))
|
||||||
|
print("PASS edge-manifold topology, winding, component counts, positive total volumes and print-bed Z")
|
||||||
|
print("Does not prove absence of self-intersections, physical fit, strength or printability.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user