Overview
This project is in progress. The sensors and most of the control hardware are in hand; the humidifier is next.
Lion’s mane is picky about where it fruits. It wants cool air, very high humidity, and fresh air with low CO₂. Miss any of those and you get small, dry, or oddly shaped fruiting bodies. Instead of misting by hand and hoping, I'm building a chamber that measures its own environment and corrects it continuously.
Hardware
| Part | Role |
|---|---|
| SHT31 | Temperature and humidity sensor (I²C) |
| MH-Z19E | NDIR CO₂ sensor |
| 128×32 I²C OLED | Live readout of all three conditions |
| Enclosed IoT relay | Switches the mains-powered humidifier safely |
| IRF3205 MOSFET | Drives the 5 V fan for fresh-air exchange |
| 5 V relay | Switches the grow light |
| Blue COB LED strip | Grow light; blue light helps trigger fruiting |
| Humidifier | Still to buy |
Control approach
Each condition has its own loop:
- Humidity is the tightest target. A humidifier behind a relay can only be on or off, so the PID output is turned into a duty cycle over a fixed window (time-proportioning). If the controller asks for 30%, the humidifier runs 3 seconds out of every 10.
- CO₂ is controlled by fresh-air exchange. When CO₂ rises above 650 ppm, the fan brings in outside air; the PID sets how hard it runs.
- Temperature is monitored and displayed.
- Light runs on a daily schedule.
The loops interact: running the fan to clear CO₂ also pulls out humid air. Tuning the humidity loop so it recovers quickly after each air exchange, without overshooting into condensation, is the main controls problem.
Code
A sketch of the time-proportioning PID planned for the humidifier.
// PID on relative humidity, converted to on/off time for a relay.
const float RH_SETPOINT = 90.0f; // percent
const uint32_t WINDOW_MS = 10000; // relay decision window
float kp = 8.0f, ki = 0.05f, kd = 0.0f; // untuned starting guesses
float integral = 0, lastErr = 0;
uint32_t windowStart = 0, lastUpdate = 0;
float pidDuty(float rh) {
uint32_t now = millis();
float dt = (now - lastUpdate) / 1000.0f;
lastUpdate = now;
float err = RH_SETPOINT - rh;
integral = constrain(integral + err * dt, -100.0f / ki, 100.0f / ki);
float d = dt > 0 ? (err - lastErr) / dt : 0;
lastErr = err;
return constrain(kp * err + ki * integral + kd * d, 0.0f, 100.0f);
}
void updateHumidifier(float rh) {
float duty = pidDuty(rh); // 0-100 %
if (millis() - windowStart >= WINDOW_MS) windowStart = millis();
bool on = (millis() - windowStart) < duty / 100.0f * WINDOW_MS;
digitalWrite(HUMIDIFIER_RELAY_PIN, on ? HIGH : LOW);
}
Next steps
- Buy and install the humidifier.
- Tune the humidity and CO₂ loops against each other.
- Log sensor data so I can plot how well the chamber holds its targets.
- Grow the first flush and document the results here.