Siddhant Kumar
Project 012 · Smart Home

Sleep Environment Optimizer.

A bedroom controller that manages the four environmental variables sleep research actually supports — temperature, light, sound and CO₂ — and correlates each night against how you slept.

Intermediate 10–15 hours 42 min read SensorsComfortAutomation
Jump to source Bill of materials
Sleep Environment Optimizer — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Intermediate
Build time
10–15 hours
Indicative cost
₹8,400 – ₹10,200
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Smart Home
Last updated
28 July 2026
Contents — 26 sections

Project Overview

A bedroom controller that manages the four environmental variables sleep research actually supports — temperature, light, sound and CO₂ — and correlates each night against how you slept.

Sleep advice is full of confident claims with thin evidence. This project deliberately targets only the four environmental factors with reasonably solid experimental support: ambient temperature, which affects sleep onset and slow-wave sleep through the body's core temperature drop; light exposure, particularly blue light suppressing melatonin; acoustic disturbance, where intermittent noise fragments sleep even when it does not wake you; and CO₂ accumulation in a closed bedroom, which is associated with reduced sleep quality and next-day performance.

The interesting engineering problem is not measuring these — that is straightforward — but acting on them without becoming a disturbance itself. A fan that switches on abruptly at 3 a.m. wakes people. A display that glows is itself light pollution. Every actuator in this design is therefore ramped over minutes, and every indicator is either off or deep red below a configurable illuminance.

The temperature strategy follows the physiology rather than a fixed setpoint. Core body temperature naturally falls by around 1 °C during the night, reaching its minimum roughly two hours before habitual wake time, and a cooler room facilitates that drop. The controller therefore runs a temperature ramp: comfortable at bedtime, coolest in the early hours, rising slightly before wake time — which is a much better match to what the body is doing than holding one number all night.

Finally, the system logs. Every night produces a record of the four variables at one-minute resolution, plus movement from an accelerometer under the mattress as a crude sleep-quality proxy. After a few weeks that data tells you something specific about your own room — for example, that your sleep fragments consistently when CO₂ passes 1800 ppm, or that the traffic peak at 05:30 is what wakes you rather than your alarm.

What this project does

  • Measures bedroom temperature, humidity, illuminance, sound level and CO₂ once a minute all night.
  • Ramps a fan or AC setpoint through the night following the natural core-temperature curve.
  • Fades a sunrise light from deep red to full brightness over 30 minutes before the alarm.
  • Generates pink noise through a small speaker, level-matched to measured room noise.
  • Alerts on CO₂ crossing a ventilation threshold — silently, by logging, not by beeping at 2 a.m.
  • Detects movement with an under-mattress accelerometer as a restlessness proxy.
  • Produces a nightly summary correlating environment against restlessness.

Real-World Applications

SettingHow it is used
Personal sleep optimisationThe core case — finding out which of the four variables actually matters in your specific room.
Shared bedroomsTwo people with different temperature preferences at least get data instead of an argument.
Infant and child roomsTemperature and CO₂ monitoring with silent logging rather than audible alerts.
Shift workersDaytime sleep needs aggressive light control, and blackout effectiveness is measurable rather than assumed.
Student accommodation and hostelsDocumenting noise and ventilation conditions gives a factual basis for a complaint.
Sleep research teachingA complete, honest instrumentation exercise including the limits of what a consumer sensor can conclude.

Deployment contexts where a build of this kind earns its keep.

Features & Capabilities

  • Physiological temperature ramp rather than a fixed setpoint, matched to the core-temperature curve.
  • All actuator changes ramped over minutes, so nothing in the system can itself wake you.
  • Light-aware indicators — every LED is off or deep red below 5 lx.
  • Pink noise generation with level matching to measured ambient sound.
  • Sunrise simulation using a 30-minute warm-to-cool brightness curve.
  • Under-mattress accelerometer for movement-based restlessness scoring.
  • Per-night logging at one-minute resolution to InfluxDB.
  • Correlation report pairing each night's environment against its restlessness score.

Difficulty, Time & Required Skills

AttributeValue
Difficulty levelIntermediate
Estimated completion time10–15 hours
Indicative build cost₹8,400 – ₹10,200
Primary disciplineSmart Home
Reference platformESP32 DevKit V1 (ESP-WROOM-32)

Skills you should have (or will pick up)

  • Arduino C++ with non-blocking scheduling
  • I²C and UART sensor reading
  • PWM and gradual ramping of outputs
  • Basic understanding of audio level in dB and A-weighting
  • MQTT and time-series logging

Bill of Materials

Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.

ComponentKey specificationQtyApprox. cost
ESP32 DevKit V1 (ESP-WROOM-32)
Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC1₹450
SHT31-D temperature + humidity sensor
The built-in heater burns off condensation — essential for outdoor or greenhouse use.
−40 to +125 °C ±0.2 °C, 0–100 %RH ±2 %, 1 Hz–10 Hz, on-chip heater1₹620
BH1750 digital ambient light sensor
Far more linear than an LDR — use it whenever you need real lux, not a relative value.
1–65535 lx, 16-bit, ±20 %, spectral response close to the human eye1₹140
MH-Z19B NDIR CO₂ sensor
Disable auto-baseline calibration (ABC) for sealed rooms or it drifts to 400 ppm.
0–5000 ppm ±(50 ppm + 5 %), NDIR, 60 s warm-up, UART + PWM output1₹2,600
INMP441 I²S MEMS microphone
Digital output means no analogue noise pickup — far better than an MAX9814 for keyword spotting.
61 dB SNR, −26 dBFS sensitivity, 60 Hz–15 kHz, 24-bit I²S output1₹220
ADXL345 3-axis accelerometer
Built-in activity/free-fall interrupts let the MCU deep-sleep until something moves.
±2/4/8/16 g, 13-bit, 0.004 g/LSB, tap and free-fall interrupts, 3200 Hz1₹220
WS2812B addressable RGB LED strip (60 LED/m)
Budget 60 mA × LED count; add a 1000 µF cap and a 330 Ω series resistor on data.
5 V, 60 mA per LED at full white, 800 kHz single-wire protocol, 8-bit per channel1₹900
5 V single-channel opto-isolated relay module
Cut the JD-VCC jumper and feed the coil separately for true isolation.
SPDT contacts rated 10 A @ 250 VAC / 10 A @ 30 VDC, opto-isolated input1₹90
LM2596 adjustable buck converter module
Set the output voltage with no load connected before wiring the board.
4.5–40 V in, 1.25–37 V out, 2 A (3 A peak), ~92 % efficiency1₹90
5 V 3 A regulated SMPS adapter
Measure the real output — many "3 A" adapters sag below 4.7 V at 2 A.
100–240 VAC in, 5 V ±5 % out, 3 A, short-circuit and over-voltage protection1₹350
Double-sided perfboard 7 × 9 cm + headers
Solder female headers so the MCU can be swapped without desoldering.
FR-4, 0.1″ pitch, plated through-holes, 24 × 18 grid1₹60
IP65 ABS junction enclosure 158 × 90 × 60 mm
Fit cable glands, not drilled holes, or the IP rating means nothing.
IP65, ABS, −20 to +80 °C, transparent lid, wall-mount lugs1₹260
Small full-range speaker + PAM8403 amplifier
For pink noise. A piezo buzzer is not suitable — it has no low-frequency content.
4 Ω 3 W, class-D amp1₹260
Warm-white + cool-white LED strip 0.5 m
Two channels give true colour-temperature control for sunrise simulation.
2700 K and 5000 K, 12 V1₹320
MOSFET dimmer pair (IRLZ44N)Logic level, one per LED channel2₹120

Estimated total: ₹6,700, excluding tools, shipping and consumables.

Tools and consumables

  • Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
  • Digital multimeter — continuity, DC volts and current ranges
  • Wire strippers, flush cutters and a small set of precision screwdrivers
  • Heat-shrink tubing and a heat gun (or a lighter, carefully)
  • A laptop with a USB port and the toolchain listed above

Hardware Specifications

PartSpecificationSupplyInterfaceReference
ESP32 DevKit V1 (ESP-WROOM-32)Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC3.3 V logic / 5 V USBUART, SPI, I²C, I²S, CAN, PWMDatasheet
SHT31-D temperature + humidity sensor−40 to +125 °C ±0.2 °C, 0–100 %RH ±2 %, 1 Hz–10 Hz, on-chip heater2.4–5.5 VI²C (0x44/0x45)Datasheet
BH1750 digital ambient light sensor1–65535 lx, 16-bit, ±20 %, spectral response close to the human eye2.4–3.6 VI²C (0x23/0x5C)Datasheet
MH-Z19B NDIR CO₂ sensor0–5000 ppm ±(50 ppm + 5 %), NDIR, 60 s warm-up, UART + PWM output4.5–5.5 VUART 9600 8N1, PWMDatasheet
INMP441 I²S MEMS microphone61 dB SNR, −26 dBFS sensitivity, 60 Hz–15 kHz, 24-bit I²S output1.8–3.3 VI²SDatasheet
ADXL345 3-axis accelerometer±2/4/8/16 g, 13-bit, 0.004 g/LSB, tap and free-fall interrupts, 3200 Hz2.0–3.6 VI²C / SPIDatasheet
WS2812B addressable RGB LED strip (60 LED/m)5 V, 60 mA per LED at full white, 800 kHz single-wire protocol, 8-bit per channel5 V1-wire timed protocolDatasheet
5 V single-channel opto-isolated relay moduleSPDT contacts rated 10 A @ 250 VAC / 10 A @ 30 VDC, opto-isolated input5 V coilDigital (active-low)Datasheet
LM2596 adjustable buck converter module4.5–40 V in, 1.25–37 V out, 2 A (3 A peak), ~92 % efficiency4.5–40 VScrew terminals + trimmerDatasheet
5 V 3 A regulated SMPS adapter100–240 VAC in, 5 V ±5 % out, 3 A, short-circuit and over-voltage protection5 VDC barrel / USBDatasheet
Double-sided perfboard 7 × 9 cm + headersFR-4, 0.1″ pitch, plated through-holes, 24 × 18 gridDatasheet
IP65 ABS junction enclosure 158 × 90 × 60 mmIP65, ABS, −20 to +80 °C, transparent lid, wall-mount lugsDatasheet

Consolidated electrical and interface specifications for every active part in the build.

Power Budget & Supply Sizing

Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
SHT31-D temperature + humidity sensor2.4–5.5 V1.5The built-in heater burns off condensation — essential for outdoor or greenhouse use.
BH1750 digital ambient light sensor2.4–3.6 V0.19Far more linear than an LDR — use it whenever you need real lux, not a relative value.
MH-Z19B NDIR CO₂ sensor4.5–5.5 V60Disable auto-baseline calibration (ABC) for sealed rooms or it drifts to 400 ppm.
INMP441 I²S MEMS microphone1.8–3.3 V1.4Digital output means no analogue noise pickup — far better than an MAX9814 for keyword spotting.
ADXL345 3-axis accelerometer2.0–3.6 V0.14Built-in activity/free-fall interrupts let the MCU deep-sleep until something moves.
WS2812B addressable RGB LED strip (60 LED/m)5 V60Budget 60 mA × LED count; add a 1000 µF cap and a 330 Ω series resistor on data.
5 V single-channel opto-isolated relay module5 V coil70Cut the JD-VCC jumper and feed the coil separately for true isolation.
LM2596 adjustable buck converter module4.5–40 V8Set the output voltage with no load connected before wiring the board.
5 V 3 A regulated SMPS adapter5 V3000Measure the real output — many "3 A" adapters sag below 4.7 V at 2 A.

Summed typical draw is 3361.23 mA. With a 1.5× design margin the supply should deliver at least 5100 mA continuously at the stated rail voltage.

Software Requirements & Development Environment

Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.

  • Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
  • Add https://espressif.github.io/arduino-esp32/package_esp32_index.json under File → Preferences → Additional Board Manager URLs, then install esp32 from the Boards Manager.
  • Set the correct port under Tools → Port. On Linux add yourself to the dialout group: sudo usermod -aG dialout $USER and log out and back in.
  • Open the Serial Monitor at 115200 baud — every sketch here logs its state there.
  • Keep File → Preferences → Show verbose output during: compilation switched on while you are debugging build errors.

Required libraries

LibraryWhy it is neededInstall
WiFi (ESP32 core) bundledStation/AP connection management for the ESP32.Bundled with the ESP32 Arduino core
PubSubClient 2.8Lightweight MQTT 3.1.1 client for constrained devices.Library Manager → "PubSubClient" by Nick O'Leary
ArduinoJson 7.xZero-allocation JSON serialisation and parsing.Library Manager → "ArduinoJson" by Benoit Blanchon
BH1750 1.3.0Digital lux readings with selectable resolution modes.Library Manager → "BH1750" by Christopher Laws
Adafruit SSD1306 + GFX 2.5.xFramebuffer and text/graphics primitives for the OLED.Library Manager → "Adafruit SSD1306"
Preferences (NVS) bundledWear-levelled key/value storage in ESP32 flash for settings.Bundled with the ESP32 core
NTPClient / configTime bundledWall-clock time from an NTP server for timestamping.Bundled (`configTime()` on ESP32)
FastLED 3.6.xTiming-exact WS2812B driver with colour-correction and palettes.Library Manager → "FastLED"

Block Diagram

The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.

Sleep Environment Optimizer — system block diagramFunctional block diagram of the Sleep Environment Optimizer system. SenseSHT31 + BH1750T / RH / luxMH-Z19BCO₂INMP441 + ADXL345sound / motionModelSleep phase clockbed → wakeRestlessness scoremotion eventsControlTemperature rampfan dutyLight + noiseramped onlyRecordMQTT → InfluxDB1 min resolutionNightly reportcorrelations1 min samplestarget curveall values
Sleep Environment Optimizer — system block diagram

Circuit Diagram & Wiring

Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.

Sleep Environment Optimizer — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBSHT31 temperature /humidityGPIO 21 / 22I²C at 0x44BH1750 illuminanceGPIO 21 / 22Shared I²C, 0x23ADXL345 under mattressGPIO 21 / 22Shared I²C, 0x53MH-Z19B CO₂GPIO 16 / 17UART2 9600INMP441 microphoneGPIO 14 / 15 / 32I²S, level onlyWarm white LED channelGPIO 25LEDC PWM 12-bitCool white LED channelGPIO 26LEDC PWM 12-bitPAM8403 audioGPIO 27 (DAC2)Pink noise outputRelay → fan / ACGPIO 33Ramped duty cycle
Sleep Environment Optimizer — wiring schematic
PeripheralPeripheral pinController pinSignal
SHT31 temperature / humiditySDA / SCLGPIO 21 / 22I²C at 0x44
BH1750 illuminanceSDA / SCLGPIO 21 / 22Shared I²C, 0x23
ADXL345 under mattressSDA / SCLGPIO 21 / 22Shared I²C, 0x53
MH-Z19B CO₂TX / RXGPIO 16 / 17UART2 9600
INMP441 microphoneBCLK / WS / SDGPIO 14 / 15 / 32I²S, level only
Warm white LED channelMOSFET gateGPIO 25LEDC PWM 12-bit
Cool white LED channelMOSFET gateGPIO 26LEDC PWM 12-bit
PAM8403 audioINGPIO 27 (DAC2)Pink noise output
Relay → fan / ACINGPIO 33Ramped duty cycle

Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.

Wiring explanation

  • The MH-Z19B is the largest heat source in the enclosure. Keep the SHT31 at least 80 mm away and on a separate small board, or your bedroom temperature reading will be two degrees high.
  • The accelerometer goes under the mattress, not on the bed frame. On the frame it picks up building vibration and footsteps in the corridor; under the mattress it picks up the sleeper.
  • Use 12-bit LEDC PWM for the LED channels. At 8-bit resolution the lowest usable step is visibly bright in a dark room, which defeats the whole point of a gentle sunrise.
  • Set the LEDC PWM frequency above 20 kHz. Below that, cheap LED drivers can produce an audible whine — in a bedroom, at 2 a.m., that is a disaster.
  • The microphone is used for level measurement only, and no audio is recorded or transmitted. Wire it so that is verifiably true, and say so on the enclosure if anyone else sleeps in the room.
  • Use a mains-rated relay only if you are switching a mains fan. For a 12 V fan, a logic-level MOSFET is quieter, has no clicking, and allows genuine speed ramping.
An ESP32 development board with the ESP-WROOM-32 module and USB connector
An ESP32 development board — enough I²C, UART and I²S peripherals to run all five sensors and three ramped outputs. Photograph sourced from Wikimedia Commons — ESP32 Espressif ESP-WROOM-32 Dev Board.jpg. Reused under the licence stated on that page; please check it before republishing.

System Architecture

Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.

Sleep Environment Optimizer — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · SHT31-D temperature + humidity sensor ·BH1750 digital ambient light sensor · MH-Z19B NDIR CO₂ sensorDriver layerwifi · pubsub · arduinojson · bh1750libApplication logicsampling loop · filtering · thresholds · state machineTransport layerWi-Fi + MQTT · TLS · retry and backoffPresentation layerdashboard · mobile notifications · historical charts
Sleep Environment Optimizer — architecture stack

Working Principle

The temperature strategy follows a well-established physiological fact: core body temperature falls by roughly 0.5–1.0 °C during sleep, reaching its nadir about two hours before habitual waking. Sleep onset is facilitated by that drop, and the drop happens through peripheral vasodilation — heat leaving the body through the hands and feet. A room that is too warm impedes it. Rather than holding one setpoint, the controller follows a curve: around 20–21 °C at bedtime, falling to 18–19 °C in the early hours, rising back before wake. The absolute numbers are individual; the shape is not.

Light matters most in two windows. Blue-rich light in the two hours before bed suppresses melatonin and delays sleep onset, which is why the room lighting fades warm and dim in the evening. And light in the last thirty minutes before waking advances the circadian phase and reduces sleep inertia — that grogginess on waking — which is what the sunrise simulation targets. Between those windows, darkness is the goal, which is why every indicator on the device goes dark below 5 lx.

Sound disturbs sleep through intermittency more than through absolute level. A steady 45 dB is far less disruptive than a quiet room punctuated by a 55 dB door. Pink noise works by raising the noise floor so intermittent events are less salient — the delta between background and disturbance shrinks. Pink rather than white is used because its power falls at 3 dB per octave, which sounds like rainfall rather than hiss and matches the ear's frequency weighting better. The level is matched to measured ambient noise rather than fixed, because a masking sound louder than what it masks is itself a disturbance.

CO₂ in a closed bedroom routinely reaches 2000–3000 ppm overnight, and several studies associate that range with poorer subjective sleep quality and reduced next-day performance. The controller measures it but deliberately does not act loudly on it — waking someone to tell them the air is stale is self-defeating. It logs, and it reports in the morning, and it can trigger a silent trickle vent if you have one.

The restlessness score deserves an honest caveat. An accelerometer under a mattress detects gross body movement, and movement correlates with sleep stage — you move more in light sleep and almost not at all in REM atonia. It is not polysomnography and it cannot stage sleep. What it gives is a repeatable, self-consistent number that can be correlated against environmental variables from the same device, which is exactly what you need to answer "does my room being cooler actually help me".

The maths behind it

Overnight temperature ramp

plainOvernight temperature ramp
Given bedtime B and wake time W (hours), fraction f = (t − B)/(W − B):

  T_target(f) = T_bed − ΔT × sin(π × f^0.8)

With T_bed = 21 °C, ΔT = 2.5 °C:
  f = 0.00 (bedtime)   → 21.0 °C
  f = 0.25             → 19.2 °C
  f = 0.50             → 18.5 °C
  f = 0.75 (nadir ≈)   → 18.7 °C
  f = 1.00 (wake)      → 21.0 °C

The f^0.8 exponent shifts the minimum slightly later
than the midpoint, matching the observed core
temperature nadir about two hours before waking.

Sound pressure level from I²S samples

plainSound pressure level from I²S samples
RMS of N samples, normalised to full scale:

  L = 20 · log10(rms / 32768) + K

K is the calibration offset: measure a known
source with a reference meter and solve for K.

Typical INMP441 at 94 dB SPL (1 kHz, 1 Pa):
  rms ≈ 1640 counts → 20·log10(1640/32768) = −26 dBFS
  K = 94 − (−26) = 120

So  SPL ≈ dBFS + 120  for this microphone.
Quiet bedroom ≈ 28 dB SPL, traffic peak ≈ 52 dB.

Pink noise from white

plainPink noise from white
Pink noise has power spectral density ∝ 1/f (−3 dB/octave).

Voss-McCartney approximation with 5 octave rows:
  each row updates at half the rate of the previous
  output = sum of all rows

  row 0 updates every sample
  row 1 every 2 samples
  row 2 every 4 samples ... row 4 every 16

This is far cheaper than filtering white noise and
sounds indistinguishable for masking purposes.

Program Flowchart

The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.

Sleep Environment Optimizer — firmware flowchartControl flow through the main program loop. Boot: load schedule andcalibrationSample all sensors every 60 sWithin the sleep window?yesidle logging onlyCompute target temperaturefrom the rampAdjust fan duty by ≤ 5 % perminuteWithin 30 min of waketime?run sunrise fadekeep darkLog sample, updaterestlessnessAt wake: publish the nightlysummary
Sleep Environment Optimizer — firmware flowchart

Assembly Instructions

Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.

Step-by-Step Implementation Guide

Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.

Complete Source Code

The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.

cppsleep-optimizer.ino
/* ═══════════════════════════════════════════════════════════════
   Sleep Environment Optimizer — ESP32

   Measures temperature, humidity, illuminance, CO2, sound level and
   body movement once a minute through the night. Every actuator is
   ramped slowly enough that the system itself can never be the thing
   that wakes you.
   ══════════════════════════════════════════════════════════════════ */

#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <BH1750.h>
#include <Adafruit_SHT31.h>
#include <Adafruit_ADXL345_U.h>
#include <driver/i2s.h>
#include <Preferences.h>
#include <time.h>
#include <math.h>

#define WIFI_SSID "YOUR_WIFI"
#define WIFI_PASS "YOUR_PASSWORD"
#define MQTT_HOST "192.168.1.50"
#define DEVICE_ID "sleep-master"

#define PIN_LED_WARM 25
#define PIN_LED_COOL 26
#define PIN_FAN      33
#define I2S_BCLK 14
#define I2S_LRCL 15
#define I2S_DOUT 32

#define BED_HOUR    22.5f      // 22:30
#define WAKE_HOUR    6.75f     // 06:45
#define T_BED       21.0f
#define T_DROP       2.5f
#define SUNRISE_MIN 30
#define DARK_LUX     5.0f
#define CO2_NOTE   1800

BH1750                       lux;
Adafruit_SHT31               sht = Adafruit_SHT31();
Adafruit_ADXL345_Unified     accel(12345);
HardwareSerial               co2Serial(2);
WiFiClient                   net;
PubSubClient                 mqtt(net);
Preferences                  prefs;

float tempC = 22, rh = 50, luxNow = 0, spl = 30;
int   co2 = 500;
float fanDuty = 0, warmDuty = 0, coolDuty = 0;
uint16_t movementEvents = 0, minutesLogged = 0;
float accelBaseline = 0;

/* ── clock helpers ──────────────────────────────────────────── */
float hourNow() {
  time_t t = time(nullptr); struct tm tm; localtime_r(&t, &tm);
  return tm.tm_hour + tm.tm_min / 60.0f;
}

bool inSleepWindow(float h) {
  return BED_HOUR > WAKE_HOUR ? (h >= BED_HOUR || h < WAKE_HOUR)
                              : (h >= BED_HOUR && h < WAKE_HOUR);
}

// 0.0 at bedtime, 1.0 at wake time, wrapping past midnight.
float sleepFraction(float h) {
  float span = BED_HOUR > WAKE_HOUR ? (24 - BED_HOUR + WAKE_HOUR)
                                    : (WAKE_HOUR - BED_HOUR);
  float el = h >= BED_HOUR ? h - BED_HOUR : (24 - BED_HOUR + h);
  float f = el / span;
  return f < 0 ? 0 : (f > 1 ? 1 : f);
}

/* ── sound level ────────────────────────────────────────────── */
float readSpl() {
  static int32_t raw[256];
  size_t got;
  i2s_read(I2S_NUM_0, raw, sizeof(raw), &got, 100 / portTICK_PERIOD_MS);
  int n = got / sizeof(int32_t);
  if (!n) return spl;

  double acc = 0;
  for (int i = 0; i < n; i++) { int16_t s = raw[i] >> 11; acc += (double)s * s; }
  float rms = sqrtf(acc / n);
  float dbfs = 20.0f * log10f(rms / 32768.0f + 1e-9f);
  return dbfs + 120.0f;                 // calibration offset, see equations
}

/* ── movement ───────────────────────────────────────────────── */
bool movementDetected() {
  sensors_event_t e;
  accel.getEvent(&e);
  float mag = sqrtf(e.acceleration.x * e.acceleration.x +
                    e.acceleration.y * e.acceleration.y +
                    e.acceleration.z * e.acceleration.z);
  if (accelBaseline == 0) { accelBaseline = mag; return false; }
  bool moved = fabsf(mag - accelBaseline) > 0.35f;      // m/s^2 threshold
  accelBaseline = 0.98f * accelBaseline + 0.02f * mag;  // slow drift tracking
  return moved;
}

/* ── CO2 ────────────────────────────────────────────────────── */
int readCo2() {
  uint8_t cmd[9] = { 0xFF, 0x01, 0x86, 0, 0, 0, 0, 0, 0 };
  uint8_t s = 0; for (int i = 1; i < 8; i++) s += cmd[i];
  cmd[8] = 0xFF - s + 1;
  while (co2Serial.available()) co2Serial.read();
  co2Serial.write(cmd, 9);

  uint8_t r[9]; int got = 0; uint32_t t0 = millis();
  while (got < 9 && millis() - t0 < 300)
    if (co2Serial.available()) r[got++] = co2Serial.read();
  if (got < 9 || r[0] != 0xFF || r[1] != 0x86) return co2;
  return r[2] * 256 + r[3];
}

/* ── ramped actuators ───────────────────────────────────────── */
void setDuty(uint8_t channel, float &current, float target, float maxStep) {
  if (target > current + maxStep) current += maxStep;
  else if (target < current - maxStep) current -= maxStep;
  else current = target;
  if (current < 0) current = 0;
  if (current > 1) current = 1;
  ledcWrite(channel, (uint32_t)(current * 4095));      // 12-bit
}

void climateService(float f) {
  float target = T_BED - T_DROP * sinf((float)M_PI * powf(f, 0.8f));
  float err = tempC - target;
  float want = err <= 0 ? 0 : (err > 2.0f ? 1.0f : err / 2.0f);
  setDuty(2, fanDuty, want, 0.05f);                     // max 5 % per minute
}

void lightService(float h, float f) {
  bool sunrise = false;
  float wakeIn = WAKE_HOUR - h;
  if (wakeIn < 0) wakeIn += 24;
  if (wakeIn * 60 <= SUNRISE_MIN && inSleepWindow(h)) sunrise = true;

  if (!sunrise) {
    setDuty(0, warmDuty, 0, 0.02f);
    setDuty(1, coolDuty, 0, 0.02f);
    return;
  }
  // 0 at start of the window, 1 at wake time.
  float p = 1.0f - (wakeIn * 60.0f / SUNRISE_MIN);
  // Warm leads, cool follows — a real sunrise starts red.
  setDuty(0, warmDuty, powf(p, 1.4f), 0.05f);
  setDuty(1, coolDuty, p < 0.4f ? 0 : powf((p - 0.4f) / 0.6f, 1.8f), 0.05f);
}

/* ── logging ────────────────────────────────────────────────── */
void publishSample() {
  JsonDocument d;
  d["temp"] = roundf(tempC * 10) / 10.0f;
  d["rh"]   = roundf(rh);
  d["lux"]  = roundf(luxNow * 10) / 10.0f;
  d["co2"]  = co2;
  d["spl"]  = roundf(spl * 10) / 10.0f;
  d["fan"]  = roundf(fanDuty * 100);
  d["warm"] = roundf(warmDuty * 100);
  d["cool"] = roundf(coolDuty * 100);
  d["moves"]= movementEvents;
  char b[256]; size_t n = serializeJson(d, b, sizeof(b));
  mqtt.publish("home/sleep/" DEVICE_ID "/sample", (uint8_t *)b, n, false);
}

void publishNightSummary() {
  JsonDocument d;
  d["minutes"]      = minutesLogged;
  d["movements"]    = movementEvents;
  d["restlessness"] = minutesLogged ? roundf(movementEvents * 1000.0f / minutesLogged) / 10.0f : 0;
  d["co2_peak"]     = co2;
  d["temp_end"]     = roundf(tempC * 10) / 10.0f;
  char b[224]; size_t n = serializeJson(d, b, sizeof(b));
  mqtt.publish("home/sleep/" DEVICE_ID "/night", (uint8_t *)b, n, true);
  Serial.printf("Night: %u min, %u movements, restlessness %.1f/hr\n",
                minutesLogged, movementEvents,
                minutesLogged ? movementEvents * 60.0f / minutesLogged : 0);
  movementEvents = 0;
  minutesLogged = 0;
}

/* ── setup / loop ───────────────────────────────────────────── */
void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);
  sht.begin(0x44);
  lux.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);
  accel.begin(0x53);
  accel.setRange(ADXL345_RANGE_2_G);
  co2Serial.begin(9600, SERIAL_8N1, 16, 17);

  // 12-bit PWM above audible range: LED whine at 2 a.m. is unacceptable.
  ledcSetup(0, 25000, 12); ledcAttachPin(PIN_LED_WARM, 0);
  ledcSetup(1, 25000, 12); ledcAttachPin(PIN_LED_COOL, 1);
  ledcSetup(2, 25000, 12); ledcAttachPin(PIN_FAN, 2);

  i2s_config_t cfg = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = 16000,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = 0, .dma_buf_count = 4, .dma_buf_len = 256, .use_apll = true
  };
  i2s_pin_config_t pins = { .bck_io_num = I2S_BCLK, .ws_io_num = I2S_LRCL,
                            .data_out_num = I2S_PIN_NO_CHANGE, .data_in_num = I2S_DOUT };
  i2s_driver_install(I2S_NUM_0, &cfg, 0, NULL);
  i2s_set_pin(I2S_NUM_0, &pins);

  WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS);
  for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) delay(250);
  configTime(19800, 0, "pool.ntp.org");
  mqtt.setServer(MQTT_HOST, 1883);

  Serial.println("Sleep optimizer running");
}

void loop() {
  if (!mqtt.connected() && WiFi.status() == WL_CONNECTED) mqtt.connect(DEVICE_ID);
  mqtt.loop();

  // Movement is polled fast; everything else once a minute.
  static uint32_t lastMove = 0;
  if (millis() - lastMove > 250) {
    lastMove = millis();
    if (movementDetected()) movementEvents++;
  }

  static uint32_t lastMinute = 0;
  static bool wasAsleepWindow = false;
  if (millis() - lastMinute < 60000) return;
  lastMinute = millis();

  tempC  = sht.readTemperature();
  rh     = sht.readHumidity();
  luxNow = lux.readLightLevel();
  co2    = readCo2();
  spl    = readSpl();

  float h = hourNow();
  bool inWindow = inSleepWindow(h);

  if (inWindow) {
    float f = sleepFraction(h);
    climateService(f);
    lightService(h, f);
    minutesLogged++;
    publishSample();
    if (co2 > CO2_NOTE) mqtt.publish("home/sleep/" DEVICE_ID "/note", "co2-high", false);
  } else {
    setDuty(2, fanDuty, 0, 0.05f);
    setDuty(0, warmDuty, 0, 0.05f);
    setDuty(1, coolDuty, 0, 0.05f);
  }

  if (wasAsleepWindow && !inWindow) publishNightSummary();
  wasAsleepWindow = inWindow;
}
setDuty maxStep 0.05 per minuteThe fan can only change by 5 % of full duty per minute, so going from off to full takes twenty minutes. That is deliberate — an abrupt fan change is exactly the kind of stimulus that fragments sleep.
ledcSetup(..., 25000, 12)25 kHz is above the audible range and 12-bit gives 4096 steps. Both matter: an audible PWM whine in a bedroom is unacceptable, and 8-bit resolution makes the dimmest usable step far too bright for a dark room.
powf(f, 0.8f) in the temperature rampThe exponent shifts the coolest point later than the midpoint of the night, matching the observed core-temperature nadir roughly two hours before habitual waking.
Warm leads, cool follows in lightServiceA real sunrise starts deep red and adds blue later. Bringing both channels up together produces a flat white fade that reads as a lamp switching on rather than a dawn.
accelBaseline slow trackingThe baseline follows the accelerometer's DC orientation with a long time constant, so the detector responds to movement rather than to how the sensor happens to be lying — which changes every time the mattress is disturbed.
co2-high published, never soundedWaking someone to tell them the air is stale defeats the purpose. The note is logged and surfaced in the morning report, not announced at 2 a.m.
publishNightSummary on window exitThe nightly aggregate is what makes the data actionable. A per-minute stream alone is a chart; a restlessness number per night is something you can correlate against a change you made.

Configuration & Calibration

Configuration steps

  • Set BED_HOUR and WAKE_HOUR to your actual habitual times. The temperature ramp and sunrise are both anchored to them.
  • Set T_BED and T_DROP. Start at 21 °C and 2.5 °C, then adjust from your own restlessness data over a fortnight rather than from a recommendation.
  • Calibrate the sound offset. The value of 120 in readSpl() is for an INMP441; measure against a phone SPL app or a reference meter and solve for your own constant.
  • Disable the MH-Z19B automatic baseline correction if the bedroom never reaches outdoor CO₂ — which it usually does not. See the air quality project for the command.
  • Set DARK_LUX and verify every indicator on the device is genuinely off below it. Walk into the room at 3 a.m. and look — if you can see the device, it is too bright.

Calibration procedure

An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.

  1. Calibrate the microphone

    Play a steady tone and measure with a reference SPL meter or a calibrated phone app at the same position. Adjust the offset constant until they agree. Absolute accuracy is not critical here — repeatability is, since you are comparing nights against each other.

  2. Establish the movement threshold

    Log raw accelerometer magnitude for one night at the 0.35 threshold and count events. A typical adult produces 20–60 movement events per night. If you are logging hundreds, the threshold is too low or the sensor is on the frame rather than under the mattress.

  3. Verify the sunrise curve in a dark room

    Trigger the sunrise manually and watch the full thirty minutes. It should be barely perceptible for the first five minutes and clearly bright at the end. If it jumps visibly at any point, increase the PWM resolution or reduce the ramp step.

  4. Check the temperature sensor is not self-heating

    Compare against a reference thermometer after two hours. If the SHT31 reads high, it is too close to the CO₂ sensor or the regulator — move it before applying an offset.

Network Architecture & Connectivity

Sleep Environment Optimizer — network topologyPath taken by telemetry from field node to end user. Edge nodesGatewayCloudClientsBedroom nodeESP32Wi-Fi 2.4 GHzRouterIoT VLANMQTT 1883InfluxDB + Grafanalocal onlyGrafananight chartsMorning reportphone
Sleep Environment Optimizer — network topology
Topic / endpointDirectionPayload
home/sleep/<id>/sampledevice → brokerJSON per minute: temp, rh, lux, co2, spl, fan, warm, cool, moves
home/sleep/<id>/nightdevice → broker (retained)JSON: minutes, movements, restlessness, co2_peak, temp_end
home/sleep/<id>/notedevice → brokerSilent advisory strings such as "co2-high"

Message contract between the device and the broker.

Dashboard setup

The chart that pays for the project is a single night with all five variables overlaid and movement events as markers. Clusters of movement lining up with a CO₂ threshold crossing, or with the 05:30 traffic peak, is the kind of finding you cannot get any other way.

The second chart is a scatter of nightly restlessness against mean overnight temperature across thirty nights. If there is a relationship in your room, it will be visible; if there is not, that is also worth knowing before you buy an air conditioner.

Security considerations

  • This device knows when you are asleep and when the bedroom is empty. Keep it entirely on your own broker and off any cloud service.
  • The microphone measures level only and never records or transmits audio. Verify that in the code before trusting it, and be able to show anyone else who sleeps in the room.
  • Use broker authentication and put the node on an IoT VLAN.

Testing Procedure & Expected Output

Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.

TestWhat you should see
Read all five sensors at oncePlausible values: 18–26 °C, 30–70 %RH, under 5 lx in a dark room, 500–2500 ppm CO₂, 25–40 dB SPL.
Trigger the fan target from off to fullDuty rises by no more than 5 % per minute, reaching full after about twenty minutes — never a step change.
Run the sunrise manuallyDeep red at the start, warm white at the midpoint, full bright at the end, with no perceptible steps.
Stand in the dark room with the device runningNothing visible. Any indicator LED above 5 lx is a design failure, not a feature.
Clap onceThe SPL reading rises and settles back within a few seconds.
Move on the bedThe movement counter increments. Walking past the bed should not increment it — if it does, the accelerometer is on the frame.
Watch one full nightTemperature follows the curve, CO₂ rises steadily, and a night summary is published at wake time.
Compare morning CO₂ with the door open versus closedA difference of well over 1000 ppm — the single most surprising number this project produces.

Bench-test checklist. If a row fails, stop and fix it before moving on.

Expected output

With everything wired and the firmware uploaded, the Serial Monitor at 115200 baud should look similar to the trace below. Values will differ; the shape of the output should not.

A Grafana time-series dashboard
A time-series dashboard. Overlaying movement events on the environmental traces is what turns raw logging into an actual finding. Photograph sourced from Wikimedia Commons — Grafana dashboard.png. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

The fan or LEDs make a faint high-pitched whine

Likely cause. PWM frequency inside the audible range.

Fix. Set the LEDC frequency to 25 kHz or above. In a bedroom this is not a cosmetic issue — a 4 kHz whine at 2 a.m. is worse than no controller at all.

The sunrise jumps visibly at the start

Likely cause. 8-bit PWM resolution, where the first non-zero step is already too bright for a dark-adapted eye.

Fix. Use 12-bit resolution and apply a gamma curve. The lowest visible step should be genuinely dim, which needs both the resolution and the curve.

Movement events number in the hundreds per night

Likely cause. The accelerometer is picking up the building, not the sleeper.

Fix. Move it under the mattress near the torso, not on the frame or the floor. Raise the threshold from 0.35 and confirm that footsteps in the corridor no longer register.

Temperature reads consistently 2 °C high

Likely cause. The MH-Z19B is heating the SHT31.

Fix. Separate them physically — 80 mm minimum, ideally with the temperature sensor on a small satellite board outside the main enclosure. A software offset is wrong because the error varies with the CO₂ sensor duty.

CO₂ readings drift downward over weeks

Likely cause. Automatic baseline correction in a room that never reaches outdoor levels.

Fix. Disable ABC and calibrate manually outdoors once a year. A bedroom is the textbook case where ABC does harm.

The sketch will not upload — "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools → Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it — it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 µF electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise — shorten the cable and keep motor wiring away from the USB lead.

An I²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an I²C scanner sketch first — it should print the device address. Most breakout boards include 4.7 kΩ pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

Wi-Fi connects but MQTT never does (state -2)

Likely cause. Wrong broker address or port, a firewall in the way, or the broker requiring credentials the sketch is not sending.

Fix. Test from a laptop on the same network first: mosquitto_sub -h <broker> -t "#" -v. If that works, the problem is on the device — check the IP literal, port 1883 (or 8883 for TLS), and that client.setServer() runs before connect(). PubSubClient state codes are documented in its header.

Readings arrive for a while and then stop

Likely cause. The Wi-Fi or MQTT session dropped and the sketch never reconnects, or the broker dropped the client on keep-alive timeout.

Fix. Never assume the link stays up. Check WiFi.status() and client.connected() at the top of every loop and reconnect with exponential backoff. Add a watchdog so a wedged network stack reboots the device instead of going silent.

Performance Optimisation

  • Sample once a minute. Every variable here changes over tens of minutes, and the data is more readable at one-minute resolution than at one-second.
  • Poll the accelerometer at 4 Hz rather than once a minute — movement events are brief and would be missed entirely at the slow rate.
  • Publish per-minute samples without the retain flag and the nightly summary with it. The stream is history; the summary is state.
  • Replace every delay() with a millis() comparison — blocking delays are the single most common cause of dropped readings.
  • Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
  • Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
  • Use uint8_t / uint16_t where the range allows; on an 8-bit AVR a 32-bit add costs four times as much.
  • Batch several samples into one MQTT publish. Radio time, not CPU time, dominates the energy budget.
  • Set the MQTT keep-alive to a value that matches your reporting interval so the broker does not churn reconnections.
  • For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 µA asleep, which is the difference between days and months of runtime.
  • Profile before optimising — print micros() deltas around each stage and fix the slowest one first.

Safety Precautions

  • Do not use this to diagnose a sleep disorder. Persistent snoring with pauses, daytime sleepiness or gasping needs a doctor and a proper sleep study — an accelerometer under a mattress cannot detect apnoea.
  • If anyone else sleeps in the room, tell them there is a microphone in it, even though it only measures level. Consent matters more than the technical detail.
  • Keep the cooling ramp conservative for infants and elderly people, who regulate temperature less effectively.
  • Mains voltage kills. Anything on the load side of the relay is at 230 V. Do not work on a powered circuit, and never leave exposed mains wiring on a bench where someone could touch it.
  • Keep at least 6 mm of creepage between the mains and low-voltage sides of any board you make, and never route mains tracks under the microcontroller.
  • Have a qualified electrician do the final installation into a consumer unit or wall fitting. In most jurisdictions this is a legal requirement, not a suggestion.
  • Fit an RCD/RCBO upstream and fuse the load appropriately for its rating.
  • Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
  • Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
  • Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.

Maintenance

  • Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
  • Keep the broker and dashboard containers patched, and rotate device credentials at least once a year.
  • Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
  • Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.

Future Improvements & Upgrades

A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.

  • Add a radar presence sensor to detect when you actually got into bed rather than assuming a fixed bedtime.
  • Add heart-rate and breathing detection with a ballistocardiography sensor or a 60 GHz radar — that gets you much closer to real sleep staging.
  • Add automatic window or trickle-vent control so high CO₂ is acted on rather than merely logged.
  • Add a morning report pushed to your phone with the night's chart and one specific observation rather than a wall of numbers.
  • Correlate against a wearable's sleep score for a fortnight to see how much the crude movement proxy actually tracks it.
  • Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
  • Add over-the-air firmware updates so you never have to physically reach a deployed node again.
  • Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
  • Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
  • Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
  • Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.

Frequently Asked Questions

Can this measure my sleep stages?

No, and any project that claims to from an accelerometer is overstating what it does. Movement correlates loosely with sleep stage — you move more in light sleep — but staging needs EEG, EOG and EMG. What this gives is a repeatable restlessness number you can correlate against your own environmental data, which is genuinely useful and a different claim.

What temperature is actually best?

The evidence supports a range of roughly 17–20 °C for most adults, but individual variation is large and bedding matters as much as air temperature. That is exactly why this project logs rather than prescribes — run it for a month at 21 °C and a month at 18.5 °C and compare your own restlessness numbers.

Is pink noise better than white noise?

For masking, marginally, and mostly because it is more pleasant. Pink noise falls at 3 dB per octave so it sounds like rain rather than hiss, and its energy distribution better matches the frequency range of typical disturbances. The bigger factor is level: masking noise louder than what it masks is itself a disturbance, which is why this design matches it to measured ambient.

Does the CO₂ number really matter?

The evidence is reasonable but not overwhelming. Several controlled studies find reduced subjective sleep quality and next-day performance above roughly 1500–2000 ppm. What is not in doubt is that a closed bedroom reaches those levels routinely — most people are genuinely surprised by their first overnight chart, and that surprise is often enough to change a habit.

Why not just buy a smart thermostat and a sunrise lamp?

You could, and they would work. What you would not get is the correlation: a commercial sunrise lamp cannot tell you whether it made any difference to how you slept. The value here is in the logging and the per-night summary, not in the actuation.

Will the fan itself wake me?

Not if it ramps. That is the reason for the 5 %-per-minute limit — sleep is disturbed by change in stimulus far more than by steady level, so a fan that reaches full speed over twenty minutes is far less disruptive than one that steps to half speed instantly.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. Okamoto-Mizuno & Mizuno, "Effects of thermal environment on sleep and circadian rhythm"Journal of Physiological Anthropology, 2012
  2. Strøm-Tejsen et al., "The effects of bedroom air quality on sleep and next-day performance"Indoor Air, 2016
  3. Basner et al., "Auditory and non-auditory effects of noise on health"The Lancet, 2014
  4. Zeitzer et al., "Sensitivity of the human circadian pacemaker to nocturnal light"The Journal of Physiology, 2000
  5. SHT31-DIS humidity and temperature sensor — datasheetSensirion
  6. Voss-McCartney algorithm for pink noise generationRobin Whittle
  7. ESP32 LEDC PWM peripheral — resolution and frequencyEspressif