# Wilting Flower indicator — 3D-printed enclosure and mechanism

A decorative pot whose fake sunflower stands up when the air is good and
wilts when it is bad. **Rev H (2026-09-20 onward): lead-screw drive.** A
28BYJ-48 stepper hangs under the dock lid; its shaft comes up through the lid
into a printed coupler that carries a 100 mm Tr8×8 lead screw. A carriage with
a printed trapezoidal nut rides the screw between two grooved posts and lifts
an M3 rod that slides inside the flower's own hollow stem. The higher the rod,
the straighter the stem.

The vase is a plain cover that locks onto the dock's lid with a 30° bayonet
twist. The lid carries the mechanism and clicks into the dock base without
screws. All electronics live in the dock: Raspberry Pi Zero, Sensirion SEN6x
sensor in a sealed pocket on the right, ULN2003 stepper driver, and the status
LED. Mains only since 2026-09-28 (no battery; a read-only SD card is planned
instead). The dock (120 × 120 × 33.5, rounded square, flat back) sits on a
shelf or slides down onto two wall screws.

Rev H went back to a motorised rod, the approach of the June–August 2026
prototypes (`leadscrew-original/`), and replaced the servo crank and Scotch
yoke of Revs D–G. The original design brief (`reference/HANDOFF.md`) is kept
for the reasoning behind the pot; its kinematics describe the retired yoke.

![cutaway, upright](renders/revH_cut_upright.png)

Live to-do list, bench state and open questions: `TODO.md`. Exploded view,
parts table and build order: `assembly.html`. What to buy and at what price:
`sourcing-spec.html`.

## How far it moves

Heights are measured above the **old** home stop, because that is the frame
the bench calibration and the controller use. Since 2026-09-28 the stop post
is 25 mm taller (`stop_h` 32.5), so the mechanical bottom *is* the wilted
position. The controller knows this as `stop_position_mm = 25`.

| Position | Above the old stop | Above the stop | Flower |
|---|---|---|---|
| Wilted | 25 mm | 0 | past horizontal, about 7 o'clock |
| Horizontal | 30 mm | 5 | 9 o'clock |
| Leaning | 45.5 mm | 20.5 | 11 o'clock |
| Erect | 72.5 mm | 47.5 | 12 o'clock, straight |

Wilted, horizontal and leaning were accepted on the bench 2026-09-28. A rise
test on 2026-09-29 found the physical top at 75 mm above the old stop (not
the 80 of the stop-to-stop estimate), with 65 to 72.5 all clean, and Martin
raised erect from 62.5 to 70, then on 2026-10-01 to 72.5 (73 grated at the
top on a full cycle, so the carriage is about 1 mm under the roof sleeve
there, as the CAD says). The software never goes higher. The head now
carries extra weight for a steeper wilt. Lids printed before 2026-09-28 have the
old 7.5 mm stop ring: glue `stl/stop_spacer.stl` over it.

## Files

| File | What |
|---|---|
| `wilting-flower.scad` | The whole design, parametric. Every number lives at the top; the Rev H block starts at "Rev H drive". |
| `stl/shell.stl` | The visible vase. Already inverted for printing (roof on the bed). |
| `stl/dish.stl` | Planter dish for the roof, plain since 2026-10-04 (the lettering and NFC pocket moved to the rim band). Prints open side up. |
| `stl/rim_band.stl` | **Classic rim band (2026-10-04)**: a ring that drops over the top of the pot and rests on the dish's rim, so it turns by hand to face SCHOOLAIR.ORG to the audience. Carries the lettering and the NFC pocket (inside, behind the letters). Prints top down, no supports. Binary STL. **Straight bore since 2026-10-06** (117.3 mm, 0.5 mm over the dish bead): the first print hugged the pot's taper and could not go on, because the pot is widest at the top. It now stands off the pot by about 3 mm at its bottom edge, and its face is 5.5 mm proud. `rim_style = "flare"` restores the old lettered dish. |
| `stl/dock_lid.stl` | Dock lid: the vase floor with the bayonet ring, the two grooved guide posts, the 32.5 mm home stop post and the motor holes, pilots for the limit-switch bracket and a 5 mm hole for its leads. Prints flat, mechanism up. |
| `stl/dock_lid_flat.stl` | Plain lid for the box model (sensor, Pi and LED, no flower). Same click-in plate, nothing on it. |
| `stl/dock_base.stl` | Dock base, plain front: sensor pocket, grilles, keyholes, cable holes, ledge and barbs, screw posts with pilot holes for the Pi (58 × 23), the ULN2003 driver (29 × 26, front-left) and the SEN6x adapter (20 × 12.6, front-right), and a snap-in holder for the 5 mm LED behind the front grille. Prints floor down. |
| `stl/dock_base_screen.stl` | Same base with the 1.47" screen window and holder (`front = "screen"`). Screen version not currently planned. |
| `stl/screen_blank.stl` | Blank panel with an LED hole that stands in for the screen in the screen base. |
| `stl/coupler.stl` | Joins the motor's D-shaft (pocket from below) to the lead screw (pocket from above, two M3 grub screws). |
| `stl/carriage.stl` | Nut sleeve with the Tr8×8 thread printed in, a tab each side for the post grooves, rod boss with an M3 set screw, two M2 pilots under the tabs for the limit-switch flag. |
| `stl/ls_bracket.stl` + `stl/ls_flag.stl` | **Limit switches (2026-10-04)**: bracket for two lever micro-switches, screwed to the back ends of the guide posts; flag screwed under the carriage that presses them. Bottom clicks 0.5 mm before the stop post, top at erect; slotted holes set each trigger ±2 mm by hand. Wire C + NC. The leads press into a U channel down the outer face of the bracket's left rail and drop through the lid's 5 mm hole straight below it. |
| `stl/stop_spacer.stl` | Only for lids printed before 2026-09-28: raises the old 7.5 mm stop ring to 32.5. |
| `stl/stem_gland.stl` + `stl/stem_gland_nut.stl` | **Current stem anchor (2026-10-02): adjustable.** A compression gland: foot glued into the roof sleeve (bore 8.2; drill older shells to 8 mm), split threaded collet above the roof, nut with an inner cone. Loosen, slide the tube to any height, tighten. Inside the dish under the moss (dish hole 14). |
| `stl/stem_spigot.stl` | The fixed alternative (2026-10-01): a barbed plug for a 6.3 mm sleeve bore, the tube pushed over it. Export the shell and dish with `stem_anchor = "barb"` for this one. |
| `stl/module.stl`, `arm.stl`, `arm_direct.stl`, `yoke.stl`, `coupon.stl`, `spline_coupon.stl` | Retired servo-crank parts (Revs D–G). Kept for reference; do not print for Rev H. |
| `exploded.scad` | The exploded view with numbered balloons used in `assembly.html`. |
| `anim/` | Headless Blender pipeline for the how-to animation (still shows the servo design). |
| `concept/` | Concept records per revision; `concept-revH-breakout.html` is the Rev H design record. |
| `leadscrew-original/` | Sources of the June–August 2026 lead-screw prototypes that Rev H is based on. |
| `reference/` | Original brief, drawings, kinematics check, interactive tools (servo era). |
| `qr/make_qr.py` | QR sticker generator, kept for venues where phones are allowed; not used on the school unit. |

Regenerate any STL after changing a parameter:

```
openscad -D 'part="carriage"' -o stl/carriage.stl wilting-flower.scad
```

Printed parts: `shell dish rim_band dock_base dock_lid dock_lid_flat coupler carriage ls_bracket ls_flag stem_gland stem_gland_nut stop_spacer screen_blank`.
Views: `assembly cutaway noshell dock_floor`, with `-D pos=0` (wilted, on the
stop) to `pos=1` (erect, 47.5 mm higher).

Renders in `renders/` (current: `exploded.png`, `revH_*.png`, `dock_floor.png`):

```
openscad -D 'part="cutaway"' -D pos=1 --camera=-8,0,50,70,0,200,600 --imgsize=900,1300 --projection=o --colorscheme=Tomorrow -o renders/revH_cut_upright.png wilting-flower.scad
openscad --camera=10,0,150,72,0,25,2100 --imgsize=1100,2400 --projection=o --colorscheme=Tomorrow -o renders/exploded.png exploded.scad
```

## Filament per unit (estimate)

About 290–300 g, roughly €8 at €20–30/kg: white ≈ 105 g (base 70, lid plate 35), wood ≈ 140 g (shell 101, rim band 27, lid ring and posts 14), dish 30 g, small parts ≈ 12 g, plus a few grams of purge for the lid's colour change. Estimated from the STL volumes at 2 walls and 15 % infill; Bambu Studio gives exact figures.

## Bill of materials (non-printed)

| Qty | Item | Notes |
|---|---|---|
| 1 | 28BYJ-48 stepper, **5 V** version, with ULN2003 driver board | 64:1 gearbox, 4096 half-steps per output turn. Holds position unpowered. Not the 12 V version. |
| 1 | Tr8×8 lead screw, 100 mm, four-start, 8 mm per turn | Screw only; the nut is printed into the carriage. Chamfer the cut end. |
| 2 | M3 × 6 self-tapping screws | Motor ears to the lid, from below: through each ear into the lid's 2.7 mm holes. |
| 2 | M3 grub screws, 4 mm | Lead screw in the coupler's top pocket. |
| 1 | M3 × 6 self-tapping screw | Rod set screw in the carriage boss (2.3 mm pilot). |
| 1 | M3 threaded rod, 146 mm | Drive rod. Round the top tip: a sharp end digs into the stem tube and stalls upward moves. |
| 1 | Artificial-flower stem, 6 mm OD / 4 mm ID, 150 mm | The hollow floppy stem the flowers come with. Head weighted to 12 g (5 g flower + 7 g washers on its stub). |
| 6 | Jumper wires, female–female, 10–20 cm | Pi header to the ULN2003 board: IN1–IN4 on GPIO17/27/22/23 (pins 11/13/15/16), + on pin 2 (5 V), − on pin 9. The motor plugs into the board's own socket. |
| 1 | Jumper link, pin 37 to pin 39 | **Flower strap**: ties GPIO26 to ground. On first boot the endnode firmware reads GPIO26 with its pull-up on; low means a flower is fitted (`FLOWER_STRAP_GPIO`, endnode branch feature/flower-install). The box model leaves it off. |
| 1 | 1000 µF 10 V electrolytic capacitor | Across 5 V and ground at the driver board, for the motor's and the Wi-Fi's current peaks. |
| 1 | 5 V 2.5 A adapter | Mains only. |
| 1 | Raspberry Pi Zero WH, 8 GB microSD, Adafruit SEN6x STEMMA QT power adapter (25 × 16, holes 20 × 12.6), Qwiic cable, CAB-17261 jumper, Adafruit 5754 lead | The AQU kit electronics. The adapter sits front-right on its own posts; its spare QT port is free for add-ons (pressure sensor, small I2C display). No MultiPort in the flower. |
| 1 | Sensirion SEN63C (interior units) or SEN65 (exterior units) | In the sealed pocket on the right. |
| 1 | Smart LED, 5 mm through-hole (WS2812D-F5 or PL9823), + 330 Ω resistor | Status light: air quality colours plus connecting / waiting / error states, all from one data pin (Martin chose the smart LED over a plain one, 2026-10-04). Snaps into the holder behind the middle slot of the front grille (floor slot removed). Four legs: 5 V, GND, data in, data out (unused). The 330 Ω goes in series on the data lead, under heat-shrink; no current-limiting resistor needed. |
| 1 | NFC tag, NTAG213, about 20 × 8 mm | Written with the school's portal link (NFC Tools, then locked); glued in the pocket on the inside of the rim band, behind the lettering. The lettering is the tap target. |
| 1 | EPDM foam, 3 mm and 4 mm | Gasket frame with divider behind the sensor grille; block behind the sensor. |
| 4 | M2.5 × 6 self-tapping screws | Pi Zero to its standoffs (pilots in the posts, no drilling). |
| 4 | M2.5 × 6 self-tapping screws | SEN6x adapter to its posts. |
| 4 | M3 × 6 self-tapping screws | ULN2003 driver board to its posts (board holes are M3). |
| 2 | Lever micro-switches, SS-5 size (20 × 9.7 × 6.5, holes 2.0 on 9.2 mm, hinge lever) | Limit switches, top and bottom. Wire common + normally closed: open circuit = end reached or broken wire. |
| 4 | M2 × 12 self-tapping screws | Limit-switch bracket to the post ends (1.6 mm pilots; drill 1/16" on lids printed before 2026-10-04). |
| 4 | M2 × 12 screws + M2 nuts | Switches to the bracket; nuts sit in the slotted pockets. |
| 2 | M2 × 8 self-tapping screws | Flag under the carriage tabs (1.6 mm pilots; drill 1/16" on older carriages). |
| 4 | Female jumper leads, 20 cm | Limit-switch leads to the Pi header (pins to be chosen with the endnode firmware). Solder the cut ends to the C and NC tabs, heat-shrink. |
| 2 | Stick-on cable clips, small | Under the lid: hold the switch leads from the lid hole to the Pi header, clear of the motor. |
| 4 | Stick-on rubber feet, 3 mm | Under the dock's corners. |
| 1 | Waveshare 1.47" Touch LCD, screen base only (not currently planned) | Board 22.05 × 41.69, display 17.63 × 32.83 per the wiki. Measure thickness and display offset (`scr_board_t`, `scr_off_z`) when it arrives. |
| 1 | Fake sunflower head + CA glue | The glue is for the gland's foot (or the spigot's plug), not the tube. |

No screws hold the vase or the lid. The servo, the 5 mm guide rail, the M2
servo screws, the 18650 cell and the charger module belong to earlier
revisions and are not needed.

## Print settings

All parts PLA unless noted, supports off, 15 % infill.

| Part | Orientation | Layers | Notes |
|---|---|---|---|
| dock_lid | as exported (flat, mechanism up) | 0.28 mm | White plate; add a filament change at the first layer above 3 mm so the pot ring (and the posts, hidden) print in the pot colour. Posts and stop post print upright. **4 walls, 25 % infill** (posts nearly solid); gussets at the post roots, 8 mm outside and 3.5 inside, 14 tall. |
| dock_base | as exported (floor down) | 0.28 mm | White (PLA Matte Ivory White blends with walls). Keyholes and grilles print in the vertical walls; the ledge is chamfered. |
| carriage | as exported (sleeve down) | 0.12 or 0.16 mm | 4 walls. Thread clearance 0.25 (`nut_slop`): it should turn onto the screw with light finger force and run the full length. |
| coupler | as exported (upright) | 0.16 mm | 4 walls. Shaft pocket is shaft + 0.15 since 2026-10-06, confirmed a perfect fit on 2026-10-07 (+0.3 was loose on the reprint, +0.1 needed a very hard press). `stl/shaft_coupon.stl` tries +0.10 / +0.15 / +0.20 on the real shaft: set `mt_shaft_fit` to the best one. |
| shell | as exported (inverted) | 0.2 mm | PLA Wood (terracotta look). No supports: the flare leans inward. Bayonet slots near the top of the print. |
| dish | as exported (open side up) | 0.2 mm | Dark. Sits on the roof unglued so it turns with the band; the stem passes through both. |
| rim_band | as exported (top down) | 0.2 mm | Same filament as the shell. Drops on from above over the dish and turns on the dish bead (0.5 mm clearance). Put it on before the flower head if the head is wider than 111 mm (the lip's opening). |
| ls_bracket, ls_flag | as exported (flat) | 0.2 mm | Any PLA. Bracket nut pockets face up. |
| stop_spacer | pocket **up** (flip it) | 0.2 mm | Only for pre-2026-09-28 lids. Pocket up needs no bridge. |
| stem_gland, stem_gland_nut | as exported (upright) | 0.12 mm | **PETG**, 3 walls, 100 % infill. Printed M9×1 thread (0.3 slop): the nut should turn on by hand. Older shells: drill the roof sleeve bore to 8 mm; older dishes: centre hole to 14 mm, or reprint the dish. |
| stem_spigot | as exported (upright) | 0.12 mm | PETG, 3 walls, 100 % infill. The shank wall is 0.7 mm; never drill the bore. Only with a 6.3 mm sleeve bore and a 7.6 dish hole. |

## Assembly

See `assembly.html` for the exploded view and the build order. In short:
electronics into the base; motor under the lid; coupler on the shaft; screw in
the coupler; carriage onto the screw and down onto the stop; rod into the
carriage boss; lid into the base; shell over the posts with a 30° twist; stem
and flower on top. Kits should ship with the carriage resting on the stop.

## Controller

The Pi software lives in the SchoolAir/Flower-End-node repo, not here. What the
mechanics mean for it:

- **8 mm per turn, 4096 half-steps per turn: 512 half-steps per mm.** Moves up
  in full steps at 2.5 ms (clean with the weighted head; 2.0 skipped), about
  30 s wilted to erect; down in half steps at 1.5 ms, about 37 s.
- **Homing is a deliberate stall.** Drive down past the stop; the gearbox
  skips harmlessly while the rod boss sits on the stop post. That position is
  `stop_position_mm`. Never trust the count after an unexpected stall: re-home.
- **Air quality to height** comes from tables in `calibration.json` for CO₂ and
  PM2.5; the worse metric wins, with 2 mm hysteresis. A missing or implausible
  reading holds the flower still: wilted always means bad air, never no data.
- On a cold power-up the flower runs a full cycle as a self-test: home, rise
  to erect, then go to the reading.

## Still to verify on the bench

- Printed nut fit on the Tr8×8 at 0.25 clearance over the full length.
- Lubricant per sliding pair (the water-based cable gel dries and gums). See the lubrication notes in `TODO.md`.
- Rounded rod tip, and whether a sleeve on the top 30 mm of the rod helps.
- Stem gland on the bench: does the printed M9 thread run, does the nut's cone close the fingers enough to hold the tube through full cycles with the heavier head, and can the nut be turned under the moss? Open `gl_cone_d` (less squeeze) or close it (more) if not.
- The Pi's supply: several unexplained reboots during motor work.
- Sensor temperature offset with the Pi and the motor running (target under 3 °C), now that the pocket walls reach the lid (they were 2 mm short until 2026-10-02; older bases: foam strip along the walls' top).
- Screen version: not currently planned (2026-09-29). If it comes back, first resolve its pin clash with the stepper (TODO) and measure the board.
- LED behind the front grille instead of the floor slot (Oded's call); then the floor slot closes and the box model shares the base.
- Wall model: two screws 60 mm apart, heads ⌀8 max, slide the dock down onto them.
- Bayonet: drop the shell in with the slots over the lugs, press, turn 30° clockwise. Lid: clipped corner at the back-left, press until the barbs click; lift at the back notch to open.

## From the brief to the pot

The pot's size still follows from the brief's constraints, not its numbers:

- **Pot is 88 mm base / 114 mm rim / 114 mm tall**, not 66 / 100 / 90. The
  mechanism needs the height; the 90 mm pot of the brief could not hold the
  stroke.
- **Shell prints inverted.** Base-down would make the closed top an
  unsupported 114 mm bridge.
- **The shell is a pure cover.** Everything that moves stands on the lid, so
  the shell can be moulded later as one simple part and lifted off for
  service.
