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