Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ jobs:
'*.cpp' '*.h' '*.md' '*.sdoc' 'CMakeLists.txt' '*/CMakeLists.txt' \
'firmware/controller/**/Kconfig*' 'firmware/controller/*.csv' 'firmware/controller/sdkconfig.*' \
'tools/*.sh' 'tools/*.py' '.github/workflows/*.yml' 'strictdoc.toml' \
'*.scad' 'hardware/enclosure/*.ini' 'hardware/enclosure/*.sh' \
| grep -v '^LICENSE' \
| grep -vE '^fixture/(board|firmware)/')
# fixture/board and fixture/firmware are generated verbatim by
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@ firmware/supervisor/latex/
# Python bytecode from the tools/ scripts.
__pycache__/
*.pyc

# Rendered STLs and sliced G-code: generated by hardware/enclosure/slice.sh,
# reproducible from the .scad and the profile, and large.
hardware/enclosure/build/
32 changes: 32 additions & 0 deletions docs/03_hardware_req.sdoc
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,38 @@ RELATIONS:
VALUE: SYS-HW-24
ROLE: Refines

[REQUIREMENT]
UID: HWR-24
TITLE: The board has mounting holes
STATEMENT: >>>
The PCB shall carry **four M3 mounting holes**, 4 mm in from each corner of the 100 × 70 outline, plated or with keep-out to the nearest copper.

There are none today. Nothing fixes the board to anything, so the enclosure's standoffs ([`hardware/enclosure`](../hardware/enclosure/README.md)) are a proposal the board has to adopt rather than a fit to something that exists. A controller held in place by its own wiring is a controller whose terminal blocks take the weight of the loom.

The positions are the enclosure's, not the other way round, because the enclosure is printed from a parameterised model and the board is not.
<<<
PRIORITY: M
VERIFICATION: I
RELATIONS:
- TYPE: Parent
VALUE: SYS-HW-05
ROLE: Refines

[REQUIREMENT]
UID: HWR-25
TITLE: Every footprint is inside the outline
STATEMENT: >>>
Every footprint shall sit inside the board outline, and the placement shall be reviewed against it before routing continues.

**14 of 125 do not**, measured 2026-10-11 against the `gr_rect` at 50,40 to 150,110: `U4` (the second MAX31856) at y = 117.9 and `J7` (the second thermocouple terminal) at y = 73.2 are the two that matter, with a cluster of decoupling capacitors and test points beside them. A part outside the outline is not placed at all: it will not be fabricated where it appears, and the netlist that routes to it is routing into free space.
<<<
PRIORITY: M
VERIFICATION: I
RELATIONS:
- TYPE: Parent
VALUE: SYS-HW-10
ROLE: Refines

[[/SECTION]]

[[SECTION]]
Expand Down
99 changes: 99 additions & 0 deletions hardware/enclosure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<!--
SPDX-FileCopyrightText: 2026 Bitcrush Testing
SPDX-License-Identifier: GPL-3.0-or-later
-->

# Enclosure

Panel-mount housing for the Safe Kiln Controller, in three printed parts.

```sh
./slice.sh # render and slice all three for PLA
./slice.sh knob # just one
./slice.sh --stl-only # STLs only, slice them yourself
```

| Part | Size | 0.2 mm PLA | Prints |
|---|---|---|---|
| `shell` | 54 × 81 × 115 mm | 8 h 49 m | open face up, no support |
| `bezel` | 70 × 97 × 7 mm | 1 h 40 m | face down, no support |
| `knob` | 25 × 25 × 16 mm | 27 m | bore up, no support |

Panel cut-out **54.4 × 81.3 mm**; outside size 70 × 97 × 115 mm.

## ⚠ PLA is probably the wrong material here

PLA softens at **55 to 60 °C**. This box bolts to the panel of a kiln, and
`SWR-SAF-11` exists because that panel can get hot enough to matter: the
firmware raises a fault on enclosure over-temperature at a configurable limit
whose default is well inside PLA's softening range.

So print PLA for **fit, feel and review**, which is what the knob's bore and
the panel cut-out need, and it is cheap and accurate. Before a real
installation, measure the panel at temperature, and if it runs warm use PETG
or ASA. Both slice from the same profile with their own filament settings;
nothing in the geometry assumes PLA.

A softened enclosure is not a safety failure on its own: nothing in the
protection chain depends on the plastic. It is a mounting failure, which means
a board hanging off a hot panel by its wiring.

## Print it in this order

1. **The knob**, alone, and try it on the encoder. `knob_bore` is 6.15 mm,
which suits a splined EC11E shaft on a well-calibrated machine; if it is
tight or loose, change that one number. Four minutes, and it saves
discovering the same error at the end of an 8 hour print.
2. **The bezel**, which is the part you look at. Check the display window
against the module you actually bought before printing the shell: the three
`CHECK` parameters in the `.scad` are the common 0.96″ variant and modules
differ.
3. **The shell**, last, because it is the long one.

## What the design is built around

Every dimension comes out of `hardware/safekiln.kicad_pcb`, not from a guess,
and the board's own placement decides the shape:

- the outline is **100 × 70 mm**;
- **SW3, the rotary encoder, is on the board** at its left edge, so that edge
is the operator's face and the board stands on edge behind the panel;
- `J1` (5 V in) and `J2` (USB-C) are on that same edge, under the knob;
- six 5 mm terminal blocks leave the **right** edge (coil, SSR, lid, CT and
the two thermocouples), so that edge faces into the back of the box, which
is where the kiln's wiring belongs;
- `J5` is an OLED module **on a cable**, so the display is the one thing free
to go where it should.

The display sits in its own lane beside the board's column rather than above
the knob, because the front face is only as wide as the board's thickness plus
that lane, and the first version of this model put a 27 mm module on a 23 mm
face. The render caught it; see the note at `in_w` in the `.scad`.

## Before this can be screwed together

**The PCB has no mounting holes.** Nothing fixes the board to anything, so the
four M3 standoffs in the shell are a *proposal* the board has to adopt: holes
4 mm in from each corner of the 100 × 70 outline. Tracked as `HWR-24`.

The supervisor's SWD tag-connect (`J4`) needs a probe to reach it through the
shell's side opening. That opening is positioned from `J4`'s coordinates, but
a TC2030 needs the probe held square to the pads, so check the clearance with
the probe in hand before trusting it (`HWR-19`).

## Files

| | |
|---|---|
| `safekiln-enclosure.scad` | all three parts, parameterised, `-D part="…"` |
| `pla-0.2mm.ini` | PrusaSlicer configuration, flat so `--load` applies it |
| `slice.sh` | render, slice, and verify the profile was actually used |

`slice.sh` checks more than it looks like it should, and each check is there
because the failure it catches is silent: a render that is not a closed solid,
a G-code file too small to be a part, a slicer that exits **zero** while
refusing a configuration and writing nothing, and a profile that was ignored.
The last one happened: the first `pla-0.2mm.ini` used `[print:]`-style
sections, which makes it a config *bundle*, and `--load` ignored it without a
word and sliced at 0.3 mm with a cold bed. The script now reads the settings
back out of the finished G-code.
147 changes: 147 additions & 0 deletions hardware/enclosure/pla-0.2mm.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
; SPDX-FileCopyrightText: 2026 Bitcrush Testing
; SPDX-License-Identifier: GPL-3.0-or-later
;
; PrusaSlicer configuration for the Safe Kiln Controller enclosure in PLA, on
; a 0.4 mm nozzle at 0.2 mm layers. Load it in the GUI or let
; tools/slice.sh use it.
;
; ===========================================================================
; WHY THESE VALUES AND NOT THE STOCK PROFILE
; ===========================================================================
; The stock 0.2 mm SPEED profile prints this enclosure perfectly well. The
; departures below are deliberate and each has a reason tied to this part
; rather than to a general preference for quality:
;
; PERIMETERS = 3. The walls are 2.4 mm, which is exactly three extrusions at
; 0.45 mm. Two would leave a gap filled with infill, and the part that gets
; handled is the wall a terminal screw is torqued against.
;
; TOP/BOTTOM = 5/4. The bezel's visible face is a bottom surface and the
; display window's chamfer is a top surface, both of which show every
; missing layer.
;
; NO SUPPORT. The geometry is drawn so that nothing needs it: the shell
; prints open-face-up and its standoffs grow off the floor, the bezel prints
; face-down, the knob prints bore-up. If a future change needs support, the
; change is wrong rather than the profile.
;
; BRIM 3 mm ON THE SHELL ONLY, set by the script per part. The shell is
; 115 mm tall on a 54 x 81 footprint, which is exactly the aspect ratio that
; lets a knock mid-print take it off the bed.
;
; FAN 100 % after the first layers. PLA in a part with small bosses and thin
; ribs needs the cooling more than it needs the layer adhesion, and this
; enclosure is not structural.
;
; What this profile is NOT for: anything that gets hot. PLA softens around
; 55 to 60 degC, and an enclosure bolted to a kiln's panel can exceed that.
; See the warning in README.md before printing this in PLA for real use.
;
; ===========================================================================
; WHY THIS FILE IS FLAT AND NOT A BUNDLE
; ===========================================================================
; No [print:...] / [filament:...] / [printer:...] sections. Those make it a
; config BUNDLE, which the GUI imports and which `--load` silently ignores:
; the first version of this file had them, PrusaSlicer accepted it without a
; word, and sliced at 0.3 mm layers, 200 degC and a cold bed. The G-code was
; 891 kB and looked perfectly healthy.
;
; `--load` wants a flat configuration, which is what the GUI's "Export Config"
; writes. slice.sh now reads the settings back out of the finished G-code and
; fails if they are not these, because a profile that is quietly not applied is
; indistinguishable from one that is until the print is on the bed.

layer_height = 0.2
first_layer_height = 0.25
perimeters = 3
top_solid_layers = 5
bottom_solid_layers = 4
fill_density = 20%
fill_pattern = gyroid
top_fill_pattern = monotonic
bottom_fill_pattern = monotonic
solid_infill_every_layers = 0
infill_every_layers = 1
ensure_vertical_shell_thickness = 1
thin_walls = 0
gap_fill_enabled = 1
support_material = 0
support_material_auto = 0
brim_width = 0
brim_type = outer_only
skirts = 1
skirt_distance = 2
skirt_height = 1
seam_position = aligned
external_perimeters_first = 0
perimeter_speed = 45
external_perimeter_speed = 25
infill_speed = 80
solid_infill_speed = 80
top_solid_infill_speed = 40
travel_speed = 180
first_layer_speed = 20
bridge_speed = 25
bridge_flow_ratio = 1
extrusion_width = 0.45
first_layer_extrusion_width = 0.42
external_perimeter_extrusion_width = 0.45
perimeter_extrusion_width = 0.45
infill_extrusion_width = 0.45
solid_infill_extrusion_width = 0.45
top_infill_extrusion_width = 0.4
elefant_foot_compensation = 0.2
; Holes print undersize. 0.1 mm of compensation gets an M3 clearance hole to
; actually pass an M3 and the knob's bore onto the shaft, and is the one
; setting most likely to need adjusting for a particular machine.
hole_size_compensation = 0.1
xy_size_compensation = 0

filament_type = PLA
filament_diameter = 1.75
temperature = 210
first_layer_temperature = 215
bed_temperature = 60
first_layer_bed_temperature = 60
fan_always_on = 1
cooling = 1
min_fan_speed = 100
max_fan_speed = 100
bridge_fan_speed = 100
disable_fan_first_layers = 2
slowdown_below_layer_time = 8
min_print_speed = 15
filament_retract_length = nil
filament_colour = #FF8000
filament_settings_id = "SafeKiln PLA"

printer_technology = FFF
printer_model =
nozzle_diameter = 0.4
; A generic Prusa i3 envelope: MK3S is 250 x 210 x 210 and MK4 250 x 210 x 220,
; so this is the smaller of the two and every part fits either.
bed_shape = 0x0,250x0,250x210,0x210
max_print_height = 210
z_offset = 0
retract_length = 0.8
retract_speed = 35
deretract_speed = 25
retract_before_travel = 1.5
retract_layer_change = 1
wipe = 1
use_relative_e_distances = 1
; Required with relative E, and PrusaSlicer refuses to slice without it:
; relative extrusion accumulates floating-point error unless the position is
; reset each layer. Worth knowing how it refuses -- it printed the reason and
; exited ZERO, writing no file, which is why slice.sh checks that the G-code
; exists and contains a plausible number of moves rather than trusting the
; exit status.
layer_gcode = G92 E0
gcode_flavor = marlin2
; Deliberately minimal: the real start sequence belongs to the machine's own
; profile, and a start G-code copied into a project file is a start G-code
; that goes stale. The script warns when it is slicing with this one.
start_gcode = M115 U3.13.2 ; tell the firmware the slicer version\nG28 ; home\nG29 ; mesh bed level (ignored by firmware that does not have it)\nM104 S[first_layer_temperature] ; set nozzle\nM140 S[first_layer_bed_temperature] ; set bed\nM109 S[first_layer_temperature] ; wait nozzle\nM190 S[first_layer_bed_temperature] ; wait bed\nG92 E0 ; zero the extruder
end_gcode = M104 S0 ; nozzle off\nM140 S0 ; bed off\nG1 X0 Y200 F3000 ; present the part\nM84 ; motors off
between_objects_gcode =
printer_settings_id = "SafeKiln Prusa 0.4"
Loading
Loading