A Light Responsive Urban Canopy
Concept
As cities face increasingly intense sunlight and changing environmental conditions, public spaces need to become more adaptive and responsive. Conventional urban canopies provide fixed shade, regardless of the time of day or the amount of sunlight available. This limits their ability to respond to changing environmental needs and can reduce their effectiveness as comfortable, flexible public spaces.
Bloom explores an alternative: an adaptive urban canopy that responds to sunlight through an automated mechanical system, transforming the relationship between architecture, technology, and the environment.


Inspired by the opening and closing movement of a flower, Bloom is conceived as a light-responsive shading system. Its circular canopy is composed of three panels: one static panel and two movable ones that rotate and fold together with one continues movement, allowing the structure to transition between fully open and closed configurations.
During bright daylight, the canopy opens to provide shade for people occupying the public space below. When light levels decrease, the panels fold together. At night, the system remains closed and can be manually activated when needed. The project proposes a more dynamic approach to urban infrastructure, where architectural elements adapt to environmental conditions rather than remaining static.


Design and Technical Process
The project combines architectural design, physical computing, and mechanical prototyping. The initial concept was developed through urban visualizations and diagrams exploring the canopy’s different configurations, structural arrangement, and potential integration into public space.
The physical prototype uses an Arduino system connected to a Grove light sensor, a push button, and a stepper motor. The motor drives a system of interconnected gears that control the canopy’s rotation.
The programming process starts by calibrating initial and final positions for the mechanism. A manual button allows the user to calibrate the mechanism and override its automatic behavior. After the positions set with a double-click when at the aimed position, the light sensor continuously reads ambient light levels and compares them with predefined thresholds. When bright daylight is detected, the motor moves the canopy toward its open position. When the system detects shadow or low light, it returns to the closed position. This combination of sensor input, programmed logic, and mechanical movement creates a responsive architectural system.


Prototype

The mechanical system was developed through cardboard prototypes, testing gear ratios, panel movement, and structural stability before integrating the electronics.
Final Code
#include <Stepper.h>
// MOTOR PINS
// __________________________________________
const int LIGHT_PIN = A6;
const int BUTTON_PIN = 6;
const int STEPS_PER_REV = 2048;
const int MOTOR_SPEED_RPM = 10;
Stepper motor(STEPS_PER_REV, 11, 9, 10, 8);
// LIGHT SENSOR
// __________________________________________
const int LIGHT_HIGH = 600;
const int LIGHT_LOW = 400;
const unsigned long SENSOR_INTERVAL_MS = 50;
// MOTOR MOVEMENT
// __________________________________________
const int MOTOR_CHUNK_STEPS = 8;
const unsigned long DISPLAY_INTERVAL_MS = 250;
unsigned long lastDisplayUpdate = 0;
// BUTTON
// __________________________________________
const unsigned long DEBOUNCE_MS = 35;
const unsigned long DOUBLE_CLICK_MS = 400;
bool lastButtonReading = HIGH;
bool stableButtonState = HIGH;
unsigned long lastDebounceTime = 0;
unsigned long firstClickTime = 0;
bool clickPending = false;
// SYSTEM STATES
// __________________________________________
enum SystemState {
CALIBRATE_INITIAL,
CALIBRATE_FINAL,
AUTOMATIC_MODE
};
SystemState state = CALIBRATE_INITIAL;
bool rotating = false;
// CALIBRATED POSITIONS
// __________________________________________
long currentSteps = 0;
long initialPosition = 0;
long finalPosition = 0;
bool initialCalibrated = false;
bool finalCalibrated = false;
long automaticTarget = 0;
int lightState = -1;
unsigned long lastSensorRead = 0;
// MOTOR CONTROL
// __________________________________________
// SINGLE STEP
void moveOneStep(int direction) {
motor.step(direction);
currentSteps += direction;
}
// CALIBRATION ROTATION
void moveContinuously() {
for (int i = 0; i < MOTOR_CHUNK_STEPS; i++) {
readButton();
if (!rotating) return;
moveOneStep(1);
}
}
// CONTINUOUS AUTO MOVEMENT
// __________________________________________
void moveTowardTarget() {
// MOVE TO TARGET
while (currentSteps != automaticTarget) {
// SENSOR MONITORING
if (millis() – lastSensorRead >= SENSOR_INTERVAL_MS) {
lastSensorRead = millis();
automaticLightControl();
}
// TARGET DIRECTION
long difference = automaticTarget – currentSteps;
if (difference == 0) {
break;
}
int direction = (difference > 0) ? 1 : -1;
// CONTINUOUS STEPPING
motor.step(direction);
currentSteps += direction;
}
}
// CALIBRATION
// __________________________________________
void saveInitialPosition() {
initialPosition = currentSteps;
initialCalibrated = true;
rotating = false;
state = CALIBRATE_FINAL;
Serial.println(“INITIAL SAVED”);
Serial.print(“START STEPS: “);
Serial.println(initialPosition);
showStatus(“START SAVED”);
}
void saveFinalPosition() {
finalPosition = currentSteps;
finalCalibrated = true;
rotating = false;
long travel = finalPosition – initialPosition;
Serial.println(“FINAL SAVED”);
Serial.print(“END STEPS: “);
Serial.println(finalPosition);
Serial.print(“TRAVEL STEPS: “);
Serial.println(travel);
if (!initialCalibrated || travel == 0) {
Serial.println(“INVALID CALIBRATION”);
finalCalibrated = false;
state = CALIBRATE_FINAL;
showStatus(“RETRY END”);
return;
}
state = AUTOMATIC_MODE;
lightState = -1;
automaticTarget = finalPosition;
Serial.println(“CALIBRATION COMPLETE”);
Serial.println(“AUTO MODE ENABLED”);
showStatus(“AUTO READY”);
}
// BUTTON: SINGLE AND DOUBLE CLICK
// __________________________________________
void onSingleClick() {
if (state == AUTOMATIC_MODE) return;
if (!rotating) {
rotating = true;
if (state == CALIBRATE_INITIAL) {
showStatus(“FIND START”);
} else {
showStatus(“FIND END”);
}
}
}
void onDoubleClick() {
if (state == AUTOMATIC_MODE || !rotating) return;
if (state == CALIBRATE_INITIAL) {
saveInitialPosition();
} else if (state == CALIBRATE_FINAL) {
saveFinalPosition();
}
}
void readButton() {
bool reading = digitalRead(BUTTON_PIN);
if (reading != lastButtonReading) {
lastDebounceTime = millis();
}
if (millis() – lastDebounceTime > DEBOUNCE_MS) {
if (reading != stableButtonState) {
stableButtonState = reading;
if (stableButtonState == LOW) {
unsigned long now = millis();
if (clickPending &&
now – firstClickTime <= DOUBLE_CLICK_MS) {
clickPending = false;
onDoubleClick();
} else {
clickPending = true;
firstClickTime = now;
}
}
}
}
lastButtonReading = reading;
if (clickPending &&
millis() – firstClickTime > DOUBLE_CLICK_MS) {
clickPending = false;
onSingleClick();
}
}
// AUTOMATIC LIGHT CONTROL
// __________________________________________
void automaticLightControl() {
int light = analogRead(LIGHT_PIN);
int newLightState = lightState;
if (light >= LIGHT_HIGH) {
newLightState = 1;
} else if (light < LIGHT_LOW) {
newLightState = 0;
}
// UPDATE TARGET
if (newLightState != -1 &&
newLightState != lightState) {
lightState = newLightState;
if (lightState == 1) {
automaticTarget = finalPosition;
Serial.println(“LIGHT DETECTED”);
} else {
automaticTarget = initialPosition;
Serial.println(“SHADOW DETECTED”);
}
}
// SETUP
// __________________________________________
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP);
motor.setSpeed(MOTOR_SPEED_RPM);
oled.begin();
oled.setFlipMode(1);
oled.setFont(u8x8_font_chroma48medium8_r);
oled.clear();
Serial.println(“SYSTEM STARTED”);
Serial.println(“SINGLE CLICK: START”);
Serial.println(“DOUBLE CLICK: SAVE”);
showStatus(“CALIBRATE START”);
}
// MAIN LOOP
// __________________________________________
void loop() {
readButton();
if (rotating) {
moveContinuously();
return;
}
if (state != AUTOMATIC_MODE) {
return;
}
// SENSOR READING
if (millis() – lastSensorRead >= SENSOR_INTERVAL_MS) {
lastSensorRead = millis();
automaticLightControl();
}
// TARGET MOVEMENT
if (lightState != -1) {
moveTowardTarget();
}
}
Toward a Responsive City
Bloom imagines how adaptive shading systems could become part of a wider network of responsive urban infrastructure. Connected to environmental sensors and local control systems, multiple canopies could adjust to sunlight, temperature, wind, and pedestrian activity.
By combining simple mechanisms with programmable behavior, Bloom proposes an architectural element that is not only functional but also interactive, adaptable, and capable of improving the experience of public space.

Presentation and Demonstration Video
Project by Vyaas Rohaan and Tiago Barros | IAAC Master in Advanced Architecture – MAA01