
Concept
We spend around 90% of our lives indoors, under artificial light that is too dim by day and too bright by night, quietly eroding the body clock that governs our sleep, mood and focus. The cost is both human and environmental: circadian disruption is linked to poor sleep, stress and depression, while lamps burn at full power even as daylight pours in. The Circadian Room restores this lost rhythm, tuning the colour of its light to the circadian cycle, its brightness to the daylight already in the room and its blinds to the sky outside, so the space adapts to the body rather than the body to the space.
During the Night : With the windows open, ambient lighting automatically adjusts its intensity, colour tone, and temperature to align with the human circadian rhythm, promoting restful sleep.
During the Day : Window blinds automatically respond to sunlight intensity, regulating natural light indoors, while ambient lighting adapts its intensity, colour tone, and temperature to complement the circadian cycle.

Schematic
An Arduino Uno with a Grove Base Shield reads the indoor and outdoor light sensors on its analog pins (A0, A2). It drives the servo blinds, buzzer and NeoPixel strip from digital pins D6, D7 and D10, and the OLED display over I2C.


Bill of Materials

Prototype
During the Night : When the windows are open, the ambient lighting automatically adjusts in intensity, colour tone, and colour temperature in accordance with the human circadian rhythm, creating a soothing environment that promotes natural sleep patterns.
During the Day : The window blinds automatically adjust according to the intensity of sunlight, regulating the amount of natural light entering the space. Simultaneously, the ambient lighting adapts its intensity, colour tone, and colour temperature to align with the human circadian rhythm, maintaining a comfortable and naturally balanced indoor environment.

CODE
#include <Arduino.h>
#include <U8x8lib.h>
#include <Adafruit_NeoPixel.h>
#include <Servo.h>
#include <avr/pgmspace.h>
#ifdef AVR
#include <avr/power.h>
#endif
#define PIN 10
#define NUMPIXELS 19
#define LIGHT_SENSOR_INTERNAL_PIN A0
#define LIGHT_SENSOR_EXTERNAL_PIN A2
#define BUZZER_PIN 7
#define SERVO_PIN 6
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(/* reset=*/ U8X8_PIN_NONE);
Servo myservo;
const unsigned long dayDuration = 360000UL; // 1 simulated day = 6 minutes
int lastTenths = -1; // last 0.1 h step drawn on the OLED
int lastHour = 0; // starts at 0 so the first loop iteration doesn’t trigger a spurious hour-change
const int circadianRGB[24][3] = {
{8, 3, 1}, {7, 3, 1}, {6, 3, 2}, {6, 3, 2},
{8, 4, 2}, {15, 6, 2}, {35, 14, 5}, {75, 38, 18},
{130, 95, 65}, {190, 180, 155}, {220, 230, 240}, {230, 242, 255},
{235, 245, 255}, {230, 242, 255}, {220, 235, 250}, {205, 220, 240},
{180, 195, 220}, {150, 165, 195}, {125, 115, 150}, {90, 65, 90},
{55, 30, 35}, {30, 12, 8}, {15, 6, 2}, {10, 4, 1}
};
// ————————————————-
// Melody (c) 2022 HiBit https://www.hibit.dev
// ————————————————-
#define NOTE_GS3 208
#define NOTE_B3 247
#define NOTE_CS4 277
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_FS4 370
#define NOTE_GS4 415
#define NOTE_B4 494
const int dayMelody[] PROGMEM = {
NOTE_CS4, NOTE_E4, NOTE_CS4, NOTE_CS4, NOTE_E4,
NOTE_CS4, NOTE_CS4, NOTE_E4, NOTE_CS4, NOTE_DS4,
NOTE_CS4, NOTE_CS4, NOTE_E4, NOTE_CS4,
NOTE_B3,
NOTE_CS4, NOTE_E4, NOTE_CS4, NOTE_CS4, NOTE_E4,
NOTE_CS4, NOTE_DS4, NOTE_CS4, NOTE_E4,
NOTE_B3,
NOTE_E4, NOTE_E4, NOTE_E4, NOTE_E4, NOTE_E4, NOTE_E4, NOTE_E4, NOTE_E4, NOTE_E4,
NOTE_E4, NOTE_E4, NOTE_E4, NOTE_E4, NOTE_FS4, NOTE_GS4, NOTE_GS4,
NOTE_GS4, NOTE_E4, NOTE_FS4, NOTE_B4, NOTE_GS4, NOTE_GS4, NOTE_GS4, NOTE_GS4, NOTE_GS4,
NOTE_FS4, NOTE_FS4, NOTE_FS4, NOTE_GS4, NOTE_FS4, NOTE_FS4, NOTE_FS4, NOTE_FS4, NOTE_FS4, NOTE_GS4, NOTE_FS4,
NOTE_E4, NOTE_CS4, NOTE_CS4, NOTE_GS4, NOTE_GS4,
NOTE_GS4, NOTE_GS4, NOTE_GS4, NOTE_GS4, NOTE_GS4, NOTE_GS4, NOTE_GS4, NOTE_GS4, NOTE_GS4,
NOTE_GS4, NOTE_B4, NOTE_GS4, NOTE_GS4, NOTE_FS4, NOTE_FS4, NOTE_E4, NOTE_GS4,
NOTE_FS4, NOTE_E4, NOTE_E4, NOTE_E4, NOTE_B4, NOTE_GS4, NOTE_GS4, NOTE_GS4, NOTE_FS4,
NOTE_FS4, NOTE_FS4, NOTE_FS4, NOTE_FS4, NOTE_FS4, NOTE_FS4, NOTE_FS4, NOTE_FS4,
NOTE_E4, NOTE_CS4, NOTE_CS4, NOTE_CS4, NOTE_CS4, NOTE_CS4,
NOTE_GS3, NOTE_B3,
NOTE_CS4, NOTE_CS4, NOTE_FS4, NOTE_GS4, NOTE_E4, NOTE_FS4,
NOTE_B3,
NOTE_E4, NOTE_FS4, NOTE_GS4, NOTE_FS4, NOTE_E4, NOTE_CS4, NOTE_E4, NOTE_GS4,
NOTE_FS4, NOTE_E4, NOTE_FS4, NOTE_CS4, NOTE_B4, NOTE_GS4, NOTE_GS4, NOTE_FS4, NOTE_FS4,
NOTE_E4, NOTE_CS4, NOTE_E4, NOTE_FS4, NOTE_GS4, NOTE_FS4, NOTE_E4,
NOTE_FS4, NOTE_E4, NOTE_CS4, NOTE_CS4,
NOTE_B3,
NOTE_CS4, NOTE_CS4, NOTE_FS4, NOTE_GS4, NOTE_E4, NOTE_FS4, NOTE_FS4,
NOTE_B3,
NOTE_FS4, NOTE_GS4, NOTE_FS4, NOTE_E4, NOTE_CS4, NOTE_E4, NOTE_GS4, NOTE_FS4,
NOTE_E4, NOTE_FS4, NOTE_CS4, NOTE_B4, NOTE_GS4, NOTE_GS4, NOTE_FS4, NOTE_FS4, NOTE_E4,
NOTE_CS4, NOTE_B4, NOTE_GS4, NOTE_GS4, NOTE_FS4, NOTE_FS4, NOTE_E4,
NOTE_CS4, NOTE_CS4,
NOTE_GS3, NOTE_B3,
NOTE_E4, NOTE_FS4, NOTE_GS4, NOTE_FS4, NOTE_E4, NOTE_E4, NOTE_E4, NOTE_FS4, NOTE_FS4,
NOTE_E4, NOTE_FS4, NOTE_GS4, NOTE_FS4, NOTE_E4, NOTE_E4, NOTE_E4, NOTE_FS4,
NOTE_CS4, NOTE_E4, NOTE_FS4, NOTE_GS4, NOTE_CS4,
NOTE_E4, NOTE_E4, NOTE_FS4, NOTE_FS4, NOTE_E4, NOTE_FS4, NOTE_GS4,
NOTE_FS4, NOTE_E4, NOTE_E4, NOTE_FS4, NOTE_CS4, NOTE_CS4,
NOTE_FS4, NOTE_GS4, NOTE_B4, NOTE_GS4, NOTE_FS4, NOTE_E4, NOTE_E4, NOTE_FS4,
NOTE_E4, NOTE_FS4, NOTE_GS4, NOTE_FS4, NOTE_E4, NOTE_E4, NOTE_FS4,
NOTE_CS4, NOTE_CS4, NOTE_CS4, NOTE_CS4, NOTE_CS4, NOTE_E4, NOTE_FS4, NOTE_GS4, NOTE_CS4, NOTE_E4,
NOTE_FS4, NOTE_E4, NOTE_FS4, NOTE_E4, NOTE_FS4, NOTE_GS4,
NOTE_FS4, NOTE_E4, NOTE_E4, NOTE_FS4, NOTE_CS4, NOTE_CS4, NOTE_CS4, NOTE_E4, NOTE_E4,
NOTE_FS4, NOTE_FS4, NOTE_GS4, NOTE_GS4,
NOTE_E4, NOTE_FS4, NOTE_GS4, NOTE_FS4, NOTE_E4, NOTE_E4, NOTE_FS4, NOTE_CS4, NOTE_CS4,
NOTE_CS4, NOTE_E4, NOTE_E4, NOTE_FS4, NOTE_FS4, NOTE_GS4,
NOTE_GS4, NOTE_E4, NOTE_FS4, NOTE_GS4, NOTE_FS4,
NOTE_E4, NOTE_E4, NOTE_FS4, NOTE_CS4, NOTE_CS4, NOTE_CS4, NOTE_E4, NOTE_E4,
NOTE_FS4, NOTE_FS4, NOTE_GS4, NOTE_GS4,
NOTE_E4, NOTE_FS4, NOTE_GS4, NOTE_FS4, NOTE_E4, NOTE_E4, NOTE_FS4, NOTE_CS4, NOTE_CS4,
NOTE_CS4, NOTE_CS4, NOTE_CS4, NOTE_CS4, NOTE_E4, NOTE_FS4, NOTE_GS4, NOTE_CS4, NOTE_E4, NOTE_FS4,
NOTE_CS4,
};
const int dayDurations[] PROGMEM = {
2,2,4,2,2,4,2,2,4,2,2,2,2,2,2,2,2,4,2,2,4,2,2,2,2,8,8,4,4,8,8,8,8,8,8,8,2,8,8,4,2,8,8,4,8,8,8,4,4,8,8,8,8,8,4,8,4,8,8,8,8,4,2,8,8,8,8,4,8,8,8,8,8,8,2,8,4,8,8,8,8,8,2,4,8,8,2,4,8,8,8,8,8,8,2,8,8,4,4,4,8,8,2,4,4,2,4,2,2,4,8,8,8,2,2,8,4,8,4,4,4,4,8,8,8,8,2,8,8,4,8,8,4,2,8,4,8,4,4,4,4,2,2,2,2,4,8,8,8,2,2,2,4,8,4,4,4,4,8,8,8,8,2,8,8,4,8,8,4,2,8,8,4,8,8,4,2,2,4,2,8,8,4,8,8,4,8,8,2,8,8,4,8,8,4,8,8,2,8,8,4,4,4,8,8,2,8,8,4,8,8,8,4,8,2,4,4,8,8,4,4,8,2,8,8,4,4,4,8,8,4,8,8,8,8,8,8,4,4,4,4,8,2,8,8,4,8,8,8,4,8,4,4,4,4,4,4,4,2,8,8,4,8,8,8,4,8,4,4,4,4,4,4,4,2,8,8,4,8,8,8,4,8,4,4,4,4,4,4,4,2,8,8,4,8,8,8,4,8,4,8,8,8,8,8,8,4,4,4,4,2
};
const int dayMelodyLen = sizeof(dayDurations) / sizeof(int);
const int wholeNoteMs = 600;
int dayNoteIndex = 0;
int currentNoteDurationMs = 0;
unsigned long dayNoteStartTime = 0;
void updateDaySong() {
unsigned long now = millis();
if (currentNoteDurationMs == 0 || now – dayNoteStartTime >= (unsigned long)(currentNoteDurationMs * 1.3)) {
int noteFreq = pgm_read_word(&dayMelody[dayNoteIndex]);
int noteType = pgm_read_word(&dayDurations[dayNoteIndex]);
currentNoteDurationMs = wholeNoteMs / noteType;tone(BUZZER_PIN, noteFreq, currentNoteDurationMs); dayNoteStartTime = now; dayNoteIndex++; if (dayNoteIndex >= dayMelodyLen) { dayNoteIndex = 0; }
}
}
// =================================================
// Mood melodies by time of day (original motifs).
// The melody follows the hour: it plays, pauses, and repeats while the
// mood lasts. Sleep hours are silent. Set USE_MOOD_MELODIES to 0 to
// go back to the HiBit day song above.
// =================================================
#define USE_MOOD_MELODIES 1
#define MELODY_GAP_MS 2500UL // pause between repeats
#define M_REST 0
#define M_G3 196
#define M_A3 220
#define M_C4 262
#define M_D4 294
#define M_E4 330
#define M_F4 349
#define M_G4 392
#define M_A4 440
#define M_B4 494
#define M_C5 523
#define M_D5 587
#define M_E5 659
// note = frequency (Hz), length in units (1 unit = one eighth note)
struct MNote { uint16_t f; uint8_t u; };
// 1 Sunrise – rising C major pentatonic, slow
const MNote mDawn[] PROGMEM = {
{M_C4,2},{M_G4,2},{M_E4,2},{M_C5,4},{M_REST,2},
{M_E4,2},{M_G4,2},{M_A4,2},{M_G4,2},{M_E5,8}
};
// 2 Bright focus – steady, bouncing G major
const MNote mFocus[] PROGMEM = {
{M_G4,2},{M_B4,2},{M_D5,2},{M_B4,2},{M_G4,2},{M_A4,2},{M_B4,4},{M_REST,2},
{M_D5,2},{M_E5,2},{M_D5,2},{M_B4,2},{M_A4,2},{M_G4,6}
};
// 3 Warm relax – lilting F major
const MNote mRelax[] PROGMEM = {
{M_F4,3},{M_A4,1},{M_C5,2},{M_A4,2},
{M_G4,3},{M_A4,1},{M_G4,2},{M_F4,2},
{M_D4,3},{M_F4,1},{M_A4,2},{M_G4,2},
{M_F4,8}
};
// 4 Wind down – falling A minor pentatonic, slower
const MNote mDusk[] PROGMEM = {
{M_E5,4},{M_D5,2},{M_C5,2},{M_A4,4},{M_G4,2},{M_E4,2},
{M_D4,4},{M_E4,2},{M_C4,2},{M_A3,8}
};
// 5 Lullaby – soft, rocking C major
const MNote mLull[] PROGMEM = {
{M_C4,2},{M_E4,2},{M_D4,2},{M_G4,4},{M_REST,2},
{M_A4,2},{M_G4,2},{M_E4,2},{M_D4,6},{M_REST,2},
{M_E4,2},{M_D4,2},{M_C4,2},{M_G3,4},{M_C4,8}
};
// mood for each hour of the day: 0 silent, 1 sunrise, 2 focus, 3 relax, 4 wind down, 5 lullaby
const uint8_t hourMood[24] PROGMEM = {
0,0,0,0,0,0, // 00-05 sleep
1,1, // 06-07 wake and sunlight, morning
3, // 08 breakfast
2,2,2, // 09-11 focus
3,3, // 12-13 lunch, walk
2,2, // 14-15 deep work
3, // 16 tea break
2, // 17 creative
4,4,4,4, // 18-21 sunset, dinner, family, unwind
5,5 // 22-23 lullaby, bedtime
};
int8_t activeMood = -1;
const MNote* mNotes = NULL;
uint8_t mLen = 0, mIdx = 0;
uint16_t mUnitMs = 200;
unsigned long mNextAt = 0;
void selectMood(uint8_t m) {
mNotes = NULL; mLen = 0; mIdx = 0;
switch (m) {
case 1: mNotes = mDawn; mLen = sizeof(mDawn) / sizeof(MNote); mUnitMs = 200; break;
case 2: mNotes = mFocus; mLen = sizeof(mFocus) / sizeof(MNote); mUnitMs = 130; break;
case 3: mNotes = mRelax; mLen = sizeof(mRelax) / sizeof(MNote); mUnitMs = 170; break;
case 4: mNotes = mDusk; mLen = sizeof(mDusk) / sizeof(MNote); mUnitMs = 230; break;
case 5: mNotes = mLull; mLen = sizeof(mLull) / sizeof(MNote); mUnitMs = 260; break;
default: break; // 0 = silence
}
}
void updateMelody(int hourNow) {
uint8_t m = pgm_read_byte(&hourMood[hourNow]);
if (m != activeMood) { // mood changed: stop and start the new melody
activeMood = m;
noTone(BUZZER_PIN);
selectMood(m);
mNextAt = millis();
}
if (mNotes == NULL) return;
unsigned long now = millis();
if (now < mNextAt) return;
if (mIdx >= mLen) { // melody finished: rest, then repeat
mIdx = 0;
mNextAt = now + MELODY_GAP_MS;
return;
}
MNote n;
memcpy_P(&n, &mNotes[mIdx], sizeof(n));
unsigned long dur = (unsigned long)n.u * mUnitMs;
if (n.f > 0) tone(BUZZER_PIN, n.f, dur * 9 / 10);
mNextAt = now + dur;
mIdx++;
}
// Blinding-white flash at 6pm (visual only, 3 seconds)
bool blindingActive = false;
unsigned long blindStartTime = 0;
// Servo (window): 0 at midnight, 90 at 6 am, 180 at noon, 90 at 6 pm
int servoAngle = 0;
void setup() {
#if defined(AVR_ATtiny85) && (F_CPU == 16000000)
clock_prescale_set(clock_div_1);
#endif
pinMode(BUZZER_PIN, OUTPUT);
Serial.begin(9600);
pixels.begin();
myservo.attach(SERVO_PIN);
myservo.write(0);
u8x8.begin();
u8x8.setPowerSave(0);
u8x8.setFont(u8x8_font_chroma48medium8_r);
}
void loop() {
float hour = (millis() % dayDuration) / (float)dayDuration * 24.0;
int h0 = (int)hour;
int h1 = (h0 + 1) % 24;
float t = hour – h0;
if (h0 != lastHour) {
// Window follows the time of day: 00:00 closed (0), 06:00 half open (90),
// 12:00 fully open (180), 18:00 half open (90), midnight closed again (0)
if (h0 == 0) servoAngle = 0;
else if (h0 == 6) servoAngle = 90;
else if (h0 == 12) servoAngle = 180;
else if (h0 == 18) servoAngle = 90;
myservo.write(servoAngle);if (h0 == 18) { blindingActive = true; blindStartTime = millis(); } lastHour = h0;
}
if (blindingActive && millis() – blindStartTime >= 3000) {
blindingActive = false;
}
#if USE_MOOD_MELODIES
updateMelody(h0);
#else
updateDaySong();
#endif
float r = circadianRGB[h0][0] + t * (circadianRGB[h1][0] – circadianRGB[h0][0]);
float g = circadianRGB[h0][1] + t * (circadianRGB[h1][1] – circadianRGB[h0][1]);
float b = circadianRGB[h0][2] + t * (circadianRGB[h1][2] – circadianRGB[h0][2]);
int lightInternal = analogRead(LIGHT_SENSOR_INTERNAL_PIN);
int lightExternal = analogRead(LIGHT_SENSOR_EXTERNAL_PIN);
int lightValue = (lightInternal + lightExternal) / 2;
float scale = map(lightValue, 0, 1023, 255, 0) / 255.0;
int rOut, gOut, bOut;
if (blindingActive) {
rOut = gOut = bOut = 255;
} else {
rOut = constrain((int)(r * scale), 0, 255);
gOut = constrain((int)(g * scale), 0, 255);
bOut = constrain((int)(b * scale), 0, 255);
}
pixels.clear();
for (int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(rOut, gOut, bOut));
}
pixels.show();
// OLED refreshes in steps of 0.1 h (was 0.01 h): redraw only when the tenth changes
int hourTenths = (int)(hour * 10.0);
char hourStr[6];
dtostrf(hourTenths / 10.0, 4, 1, hourStr);
char line1[17];
char line2[17];
char line3[17];
char line4[17];
snprintf(line1, sizeof(line1), “Time: %s”, hourStr);
snprintf(line2, sizeof(line2), “Int: %4d”, lightInternal);
snprintf(line3, sizeof(line3), “Ext: %4d”, lightExternal);
snprintf(line4, sizeof(line4), “Avg: %4d”, lightValue);
if (hourTenths != lastTenths) {
lastTenths = hourTenths;
u8x8.clear();
u8x8.drawString(0, 0, line1);
u8x8.drawString(0, 1, line2);
u8x8.drawString(0, 2, line3);
u8x8.drawString(0, 3, line4);
}
Serial.print(“Hour: “); Serial.print(hour);
Serial.print(” Int:”); Serial.print(lightInternal);
Serial.print(” Ext:”); Serial.print(lightExternal);
Serial.print(” Avg:”); Serial.println(lightValue);
delay(50);
}
Video :
The video presents a 24-hour timelapse, illustrating the gradual changes in ambient lighting colour temperature and the automated movement of window blinds in response to the sun’s intensity throughout the day.