BMic
Published © GPL3+

Chicken Coop Automation

Automating a chicken coop with sensors and actors, using an Arduino MKR1000 and the Blynk internet dashboard.

BeginnerWork in progress25,813
Chicken Coop Automation

Things used in this project

Hardware components

Arduino MKR1000
Arduino MKR1000
×1
CHIHAI DC Motor - 12V 200rpm Encoder with Mounting Bracket
×1
Geekcreit L298N Dual H Bridge Stepper Motor Driver Board For Arduino
×1
Photo resistor
Photo resistor
×1
DHT22 Temperature Sensor
DHT22 Temperature Sensor
×1

Software apps and online services

Blynk
Blynk

Story

Read more

Schematics

kippendeur_Ndx0y55bgl.fzz

2018-04-29_14_40_35-kippendeur_OpOH7Fj2Gg.png

Code

Chicken Coop Code

Arduino
/* Code developed by BMic on 19 March 2018
 * Used to connect with Blynk IOT service and to control a chicken coop: 
 * - Door control based on encoder value
 * - Environmental monitoring: Temperature, Humidity and light
*/

#include "DHT.h"
#include <SPI.h>
#include <WiFi101.h>
#include <BlynkSimpleMKR1000.h>


//========================================================== //
//============== IO & Variables definition ================= //
//========================================================== //

#define encoderPinA 0
#define encoderPinB 1
#define O_MotorDriverOut1 7
#define O_MotorDriverOut2 8

#define I_LightSensor A0

#define DHTPIN 9
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

float hum;  //Stores humidity value
float temp; //Stores temperature value

int LightValue = 0;
int deltaLight = 0;
int deltaDark = 0;

volatile int encoderPos = 0;
int encoderClosePos = 0;

//--- Used for sequential programming steps during initialize procedure ---
int step = 0;

//--- The different states of the system ---
enum states {INITIALIZE, DOOR_ERROR, DOOR_IS_OPEN, CLOSING_DOOR, DOOR_IS_CLOSED, OPENING_DOOR};
String OldState;

states state;

//========================================================== //
//=========== Blynk connection with Read/Write ============== //
//========================================================== //
// You should get Auth Token in the Blynk App.
// Go to the Project Settings (nut icon).
char auth[] = "xxxxxxxxxxxxxxxxx";

// Your WiFi credentials.
// Set password to "" for open networks.
char ssid[] = "WiFi-SSID";
char pass[] = "password";

//========================================================== //
//============== Initial setup at Startup ================= //
//========================================================== //

BlynkTimer timer;

void setup() 
{
  
//--- initialize digital pin LED_BUILTIN as an output ---
  pinMode(O_MotorDriverOut1, OUTPUT);
  pinMode(O_MotorDriverOut2, OUTPUT);
  pinMode(encoderPinA, INPUT_PULLUP);
  pinMode(encoderPinB, INPUT_PULLUP);
  
 state = INITIALIZE;

  attachInterrupt(0, doEncoder, CHANGE);

  dht.begin();
  timer.setInterval(5000L, dhtTimerEvent);

  Blynk.begin(auth, ssid, pass);
  timer.setInterval(1000L, BlynkTimerEvent);
}

int ThresHoldGettingDark = 25;
  BLYNK_WRITE(V21)
{
  ThresHoldGettingDark = param.asInt();
}

int ThresHoldGettingLight = 650;
  BLYNK_WRITE(V22)
{
  ThresHoldGettingLight = param.asInt();
}

void BlynkTimerEvent()
{
  Blynk.virtualWrite(V2, encoderPos);
}

void dhtTimerEvent()
{
  hum = dht.readHumidity();
  temp= dht.readTemperature();
  Blynk.virtualWrite(V3, temp); 
  Blynk.virtualWrite(V4, hum);

  LightValue = analogRead(I_LightSensor);
  deltaLight = ThresHoldGettingLight - LightValue;
  deltaDark = ThresHoldGettingDark - LightValue;
  Blynk.virtualWrite(V1, LightValue);
  Blynk.virtualWrite(V11, deltaLight);
  Blynk.virtualWrite(V12, deltaDark);
}

int buttonOpenValue;
  BLYNK_WRITE(V7)
{
  buttonOpenValue = param.asInt();
}

int buttonCloseValue;
  BLYNK_WRITE(V8)
{
  buttonCloseValue = param.asInt();
}

int switchOpenValue;
  BLYNK_WRITE(V9)
{
  switchOpenValue = param.asInt();
}

int switchCloseValue;
  BLYNK_WRITE(V10)
{
  switchCloseValue = param.asInt();
}

int operatingModeValue = 0;
  BLYNK_WRITE(V20)
{
  operatingModeValue = param.asInt();
}

//========================================================== //
//============== Finite State Machine Loop ================= //
//========================================================== //
void loop() 
{ 

  Blynk.run(); 
  timer.run(); 
 
  switch (state)
  {
    case INITIALIZE:
      Initializing();
      break;

    case DOOR_IS_OPEN: 
      StopDoorMotor();
      CheckDownButton(); 
      CheckGettingDark();
      ChangeState("Door is OPEN");
    break;

    case CLOSING_DOOR:
      ClosingDoor(); 
      CheckCloseSwitch();
      CheckEncoderCloseValue();
      ChangeState("Closing door");
    break;   

    case DOOR_IS_CLOSED:
      StopDoorMotor();
      CheckUpButton(); 
      CheckGettingLight();
      ChangeState("Door is CLOSED");
    break; 

    case OPENING_DOOR:
      OpeningDoor();
      CheckOpenSwitch();
      CheckEncoderOpenValue();
      ChangeState("Opening door");
    break; 

    case DOOR_ERROR:
      ChangeState("Door Error");
    break;
  }


}

//========================================================== //
//============== Finite State - Functions ================= //
//========================================================== //

// --------------- Initializing Function ---------------
void Initializing() //Calibrate Encoder values for Open & Closed position

{
  switch(step)
  {   
    case 0:
      Blynk.virtualWrite(V6, "Give Open Command");
      if(buttonOpenValue == HIGH)
      {
        OpeningDoor();
        step = 1;
      }
      else if(switchOpenValue == HIGH)
      {
        StopDoorMotor();
        encoderPos = 0;
        step = 2;  
      }
      break;

    case 1:
      Blynk.virtualWrite(V6, "Opening - Stop when Open");
      if(switchOpenValue == HIGH)
      {
        StopDoorMotor();
        encoderPos = 0;
        step = 2;         
      }
      break;

    case 2:
      Blynk.virtualWrite(V6, "Give Close Command");
      if(buttonCloseValue == HIGH)
      {
        ClosingDoor();
        step = 3;
      }
      break;

    case 3:
      Blynk.virtualWrite(V6, "Closing - Stop when Closed");
      if(switchCloseValue == HIGH)
        {
          StopDoorMotor();
          encoderClosePos = encoderPos;
          state = DOOR_IS_CLOSED;         
        }
        break;
    }
    
}

// --------------- MOTOR Functions ---------------
void StopDoorMotor()
{
  digitalWrite(O_MotorDriverOut1, LOW);
  digitalWrite(O_MotorDriverOut2, LOW);
}

void ClosingDoor()
{
 digitalWrite(O_MotorDriverOut1, HIGH);
 digitalWrite(O_MotorDriverOut2, LOW);
}

void OpeningDoor()
{
 digitalWrite(O_MotorDriverOut1, LOW);
 digitalWrite(O_MotorDriverOut2, HIGH);
}

// --------------- Input Buttons ---------------
void CheckDownButton()
{
  if(buttonCloseValue == HIGH) // Virtual value from Blynk
  {
    state = CLOSING_DOOR;
  }  
}

void CheckUpButton()
{
  if(buttonOpenValue == HIGH) // Virtual value from Blynk
  {
    state = OPENING_DOOR;
  }
}

// --------------- Input Switches ---------------
void CheckCloseSwitch()
{
  if(switchCloseValue == HIGH) // Virtual value from Blynk
    {
      state = DOOR_IS_CLOSED;
    }
}

void CheckOpenSwitch()
{
  if(switchOpenValue == HIGH) // Virtual value from Blynk
  {
    state = DOOR_IS_OPEN;
  }
}

void CheckEncoderCloseValue()
{
  if(encoderPos >= encoderClosePos)
  {
    StopDoorMotor();
    state = DOOR_IS_CLOSED;    
  }
}

void CheckEncoderOpenValue()
{
  if(encoderPos <= 0)
  {
    StopDoorMotor();
    state = DOOR_IS_OPEN;    
  }
}

// --------------- Light Sensor ---------------
void CheckGettingDark()
{
  if(operatingModeValue && LightValue < ThresHoldGettingDark)
  {
    state = CLOSING_DOOR;
  }
}

void CheckGettingLight()
{
  if(operatingModeValue && LightValue > ThresHoldGettingLight)
  {
    state = OPENING_DOOR;
  }
}

// --------------- Status String ---------------
void ChangeState(String Status)
{
  if(OldState != Status)
  {
    Blynk.virtualWrite(V6, Status);
    OldState = Status;
  }
}

//========================================================== //
//=========== Interrupts - Detect fast changes ============== //
//========================================================== //
void doEncoder()
{
  if(digitalRead(encoderPinA) == digitalRead(encoderPinB))
  {
    encoderPos++;
  }
  else 
  {
    encoderPos--;
  }
}

Chicken Coop Code - V1

Arduino
- Including Blynk reconnect logic
- Reduced data exchange interval
/* Code developed by BMic on 21 May 2018
 * Used to connect with Blynk IOT service and to control a chicken coop: 
 * - Door control based on encoder value
 * - Environmental monitoring: Temperature, Humidity and light
 * 
 * Change V1:
 * - Decrease amount of values sent to Blynk only upon Change or every minute for historical values
 * - Split data exchanges in Pull and Push mode
 * - Include input button for Open/Close commands and Reed position switches 
 * - Reconnection logic to Blynk in case it's down
 * 
*/
#define BLYNK_PRINT Serial
//#define BLYNK_DEBUG        // Optional, this enables more detailed prints

#include "DHT.h"
#include <SPI.h>
#include <WiFi101.h>
#include <BlynkSimpleMKR1000.h>

//========================================================== //
//============== IO & Variables definition ================= //
//========================================================== //

//--- Digital inputs ---//
#define encoderPinA 0            // Encoder input Channel A
#define encoderPinB 1            // Encoder input Channel A
#define I_CloseDoorSwitch 2      // Reed switch for Closed position
#define I_OpenDoorSwitch 3       // Reed switch for Open position
#define I_CloseDoorButton 4      // Button to send Close command
#define I_OpenDoorButton 5       // Button to send Open command
#define O_MotorDriverOut1 7      // Motor Output to Close the Door
#define O_MotorDriverOut2 8      // Motor Output to Open the Door

//--- Analog inputs ---//
#define I_LightSensor A0         // Analog input to measure Photo resistor 0 - 1023

//--- DHT Sensor input ---//
#define DHTPIN 9
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

float hum;              //Stores humidity value
float temp;             //Stores temperature value

//--- Light sensor ---//
int LightValue = 0;                   // Actual analog input measurement
int deltaLight = 0;                   // Difference between Light threshold and actual value 
int deltaLightOld = 0;                // Previous value to trigger a value change
int deltaDark = 0;                    // Difference between Dark threshold and actual value
int deltaDarkOld = 0;                 // Previous value to trigger a value change
int ThresHoldGettingDark = 25;        // Treshold value to indicate it's getting Dark and door should close
int ThresHoldGettingLight = 650;      // Treshold value to indicate it's getting Light and door should open

//--- Encoder input ---//
volatile int encoderPos = 0;          // Volatile int should be used for interrupt values
int encoderClosePos = 0;              // Variable to store the encoder value for Closed position
int encoderPosOld = 0;                // Previous value to trigger a value change

//--- Blynk command variables ---/
int buttonOpenValue;                  // Virtual Button to send Open command from Blynk
int buttonCloseValue;                 // Virtual Button to send Close command from Blynk
int switchOpenValue;                  // Virtual Button to send Open position from Blynk in case no reed switch is used
int switchCloseValue;                 // Virtual Button to send Close position from Blynk in case no reed switch is used
int operatingModeValue;               // Set mode to work in automatic mode using Photo resistor or manual operation

//--- Used for sequential programming steps during initialize calibration procedure ---
int step = 0;           // Squencer in void Initializing()

//--- The different states of the system ---
enum states {INITIALIZE, DOOR_ERROR, DOOR_IS_OPEN, CLOSING_DOOR, DOOR_IS_CLOSED, OPENING_DOOR};
String OldState;

//--- Holds the initial state of the system ---
states state;

//========================================================== //
//=========== Blynk connection with Read/Write ============== //
//========================================================== //
// You should get Auth Token in the Blynk App.
// Go to the Project Settings (nut icon).
char auth[] = "xxxxxxxxxxxxxxxxx";

// Your WiFi credentials.
// Set password to "" for open networks.
int StatusWifi = WL_IDLE_STATUS; // the Wifi radio's status
char ssid[] = "WiFi-SSID";
char pass[] = "Password";

int DisconnectCount = 0;    // Count number of disconnects
int ReCnctFlag;             // Reconnection Flag
int ReCnctCount = 0;        // Reconnection counter

//========================================================== //
//============== Initial setup at Startup ================= //
//========================================================== //

BlynkTimer timer;           // Start Timer based on SimpleTimer supporting to schedule 16 timers

void EncoderTimerEvent()    // Fast timer of 1 second to Push values to Blynk
{ 
  //--- Encoder ---//
  if(encoderPosOld != encoderPos){ // Check if encoder value has changed. If yes, send to Blynk.
  Blynk.virtualWrite(V2, encoderPos);
  encoderPosOld = encoderPos;
  Serial.print("Previous encoder position was: ");
  Serial.println(encoderPosOld);
  Serial.print("Actual encoder position is: ");
  Serial.println(encoderPos);
  }
}
  
void BlynkTimerEvent()    // Slow timer of 1 minute to Push values to Blynk
{
  //--- DHT22 ---//
  hum = dht.readHumidity();
  temp= dht.readTemperature();
  Blynk.virtualWrite(V3, temp); 
  Blynk.virtualWrite(V4, hum);

  //--- LUX ---//
  LightValue = analogRead(I_LightSensor);
  Blynk.virtualWrite(V1, LightValue);

  //--- Encoder ---//
  Blynk.virtualWrite(V2, encoderPos); // Only upon change (EncoderTimerEvent) messes up the graph in Blynk

  //--- Reconnect counter ---//
  Blynk.virtualWrite(V0, ReCnctCount);
}

// Pull requests from Blynk --> Only Pull when app is open since we don't need those as historical values!!
// Light Treshold -> Frequency is set in Blynk app and will only work when app is active in foreground!!

  BLYNK_READ(V11){                      // When Blynk requests a new value, calculate value and write back
    deltaLight = ThresHoldGettingLight - LightValue;
    Blynk.virtualWrite(V11, deltaLight);
  }

  BLYNK_READ(V12){                      // When Blynk requests a new value, calculate value and write back
  deltaDark = ThresHoldGettingDark - LightValue;
  Blynk.virtualWrite(V12, deltaDark);
  }
  
//--- Command's from Blynk to MKR1000 ---//
//--- These functions will be called every time a blynk Widget value is changed ---//

  BLYNK_WRITE(V21)
{
  ThresHoldGettingDark = param.asInt();
}

  BLYNK_WRITE(V22)
{
  ThresHoldGettingLight = param.asInt();
}

  BLYNK_WRITE(V7)
{
  buttonOpenValue = param.asInt();
}

  BLYNK_WRITE(V8)
{
  buttonCloseValue = param.asInt();
}

  BLYNK_WRITE(V9)
{
  switchOpenValue = param.asInt();
}

  BLYNK_WRITE(V10)
{
  switchCloseValue = param.asInt();
}

  BLYNK_WRITE(V20)
{
  operatingModeValue = param.asInt();
}

//========================================================== //
//================== Initial Setup LOOP ==================== //
//========================================================== //

void setup() 
{
  //Initialize serial and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println();

    //Check for the presence of the shield:
  /**************************************************************************************/
  if (WiFi.status() == WL_NO_SHIELD) {
    Serial.println("WiFi shield not present");
    /*Don't continue:*/
    while (true);
  }
  /**************************************************************************************/
  
//--- initialize digital pin LED_BUILTIN as an output ---
  pinMode(I_CloseDoorSwitch, INPUT_PULLUP);
  pinMode(I_OpenDoorSwitch, INPUT_PULLUP);
  pinMode(I_CloseDoorButton, INPUT_PULLUP);
  pinMode(I_OpenDoorButton, INPUT_PULLUP); 
  pinMode(O_MotorDriverOut1, OUTPUT);
  pinMode(O_MotorDriverOut2, OUTPUT);
  pinMode(encoderPinA, INPUT_PULLUP);
  pinMode(encoderPinB, INPUT_PULLUP);
  
 state = INITIALIZE;

  attachInterrupt(0, doEncoder, CHANGE);    // Use Interrupt to count reliable every encoder input

  Blynk.begin(auth, ssid, pass);
  dht.begin();
  
  timer.setInterval(1000L, EncoderTimerEvent);    // One second update timer for Blynk
  timer.setInterval(60000L, BlynkTimerEvent);     // One minute update timer for Blynk

}

  BLYNK_CONNECTED() {
    Serial.println("Connected");
    ReCnctCount = 0;
}


//========================================================== //
//========== Finite State Machine - Main LOOP ============== //
//========================================================== //
void loop() 
{ 

  timer.run();                                    // Starts Blynk Timer

// Reconnect routine tried from: https://community.blynk.cc/t/mega-esp-will-freeze-if-there-is-a-connection-problem/25124/2
// Not sure if this is actually needed or even working
if (Blynk.connected()) {
  Blynk.run();                                    // Starts Blynk connection
}
else if (ReCnctFlag == 0) {
  ReCnctFlag = 1;                                 // Set reconnection Flag
  Serial.println("Starting reconnection timer in 30 seconds...");
  timer.setTimeout(30000L, []() {                 // Lambda Reconnection Timer Function
      ReCnctFlag = 0;                             // Reset reconnection Flag
      ReCnctCount++;                              // Increment reconnection Counter
      Serial.print("Attempting reconnection #");
      Serial.println(ReCnctCount);
      Blynk.connect();                            // Try to reconnect to the server
    });
}

// --- Sequencer logic to track the State of the door and specific monitor events ---//
  switch (state)
  {
    case INITIALIZE:
      Initializing();
      break;

    case DOOR_IS_OPEN: 
      StopDoorMotor();
      CheckDownButton(); 
      CheckGettingDark();
      ChangeState("Door is OPEN");
    break;

    case CLOSING_DOOR:
      ClosingDoor(); 
      CheckCloseSwitch();
      CheckEncoderCloseValue();
      ChangeState("Closing door");
    break;   

    case DOOR_IS_CLOSED:
      StopDoorMotor();
      CheckUpButton(); 
      CheckGettingLight();
      ChangeState("Door is CLOSED");
    break; 

    case OPENING_DOOR:
      OpeningDoor();
      CheckOpenSwitch();
      CheckEncoderOpenValue();
      ChangeState("Opening door");
    break; 

    case DOOR_ERROR:
      ChangeState("Door Error");
    break;
  }
}

//--- Not sure if the previous routine within the Main LOOP is needed or not, so I use both for now ---//
BLYNK_DISCONNECTED()
{
  Serial.println("Blynk is disconnected!");

  if(Blynk.connected()!=true)
  {
    DisconnectCount++;
    Serial.println("Blynk not conected counter = ");
    Serial.println(DisconnectCount);
    Blynk.connect();
  }
}

//========================================================== //
//============== Finite State - Functions ================= //
//========================================================== //

// --------------- Initializing Function ---------------
void Initializing()

/* Still to Do:
 *  1. Record Time to Close
 *  2. Save the time in a variable and use it during Opening/Closing
 *  3. If time > preset value and encoder value is not reached -> ERROR
 *  4. When error, retry to open/close 2..3..?? times and then return critical error 
*/
/* Initializing Function:
 *  - After rebooting or resetting Arduino, first step will be to calibrate open/close position based on encoder value
 *  case 0: Sequencer will wait for your Open command to open the door or skip the step if the door is already in the Open position
 *  case 1: Door will open until Open switch is triggered or Virtual Open position is confirmed and EncoderPos value is set to 0
 *  case 2: Sequencer will wait for your Close command to close the door
 *  case 3: Door will close until Close switch is triggered or Virtual Open position is confirmed and encoderClosePos value is set
 */

{
  switch(step)
  {   
    case 0:
      ChangeState("Give Open Command");
      if(buttonOpenValue == HIGH or digitalRead(I_OpenDoorButton) == LOW)
      {
        OpeningDoor();
        step = 1;
      }
      else if(switchOpenValue == HIGH or digitalRead(I_OpenDoorSwitch) == LOW)
      {
        StopDoorMotor();
        encoderPos = 0;
        step = 2;  
      }
      break;

    case 1:
      ChangeState("Opening - Stop when Open");
      if(switchOpenValue == HIGH or digitalRead(I_OpenDoorSwitch) == LOW)
      {
        StopDoorMotor();
        encoderPos = 0;
        step = 2;         
      }
      break;

    case 2:
      ChangeState("Give Close Command");
      if(buttonCloseValue == HIGH or digitalRead(I_CloseDoorButton) == LOW)
      {
        ClosingDoor();
        step = 3;
      }
      break;

    case 3:
      ChangeState("Closing - Stop when Closed");
      if(switchCloseValue == HIGH or digitalRead(I_CloseDoorSwitch) == LOW)
        {
          StopDoorMotor();
          encoderClosePos = encoderPos;
          state = DOOR_IS_CLOSED;         
        }
        break;
    }
    
}

// --------------- MOTOR Functions --------------- //
void StopDoorMotor()
{
  digitalWrite(O_MotorDriverOut1, LOW);
  digitalWrite(O_MotorDriverOut2, LOW);
}

void ClosingDoor()
{
 digitalWrite(O_MotorDriverOut1, HIGH);
 digitalWrite(O_MotorDriverOut2, LOW);
}

void OpeningDoor()
{
 digitalWrite(O_MotorDriverOut1, LOW);
 digitalWrite(O_MotorDriverOut2, HIGH);
}

// --------------- Open/Close command Buttons --------------- //
void CheckDownButton()
{
  if(buttonCloseValue == HIGH or digitalRead(I_CloseDoorButton) == LOW) // Virtual value from Blynk or input button
  {
    state = CLOSING_DOOR;
  }  
}

void CheckUpButton()
{
  if(buttonOpenValue == HIGH or digitalRead(I_OpenDoorButton) == LOW) // Virtual value from Blynk or input button
  {
    state = OPENING_DOOR;
  }
}

// --------------- Open/Close Input Switches --------------- //
void CheckCloseSwitch()
{
  if(switchCloseValue == HIGH or digitalRead(I_CloseDoorSwitch) == LOW) // Virtual value from Blynk or input switch
    {
      state = DOOR_IS_CLOSED;
    }
}

void CheckOpenSwitch()
{
  if(switchOpenValue == HIGH or digitalRead(I_OpenDoorSwitch) == LOW) // Virtual value from Blynk or input switch
  {
    state = DOOR_IS_OPEN;
  }
}

// --- Stop Closing Door if encoder value >= Closed value registered during Initializing loop --- //
void CheckEncoderCloseValue()
{
  if(encoderPos >= encoderClosePos)
  {
    StopDoorMotor();
    state = DOOR_IS_CLOSED;    
  }
}

// --- Stop Opening Door if encoder value <= 0 which is calibrated during Initializing loop --- //
void CheckEncoderOpenValue()
{
  if(encoderPos <= 0)
  {
    StopDoorMotor();
    state = DOOR_IS_OPEN;    
  }
}

// --------------- Light Sensor --------------- //
void CheckGettingDark()
{
  if(operatingModeValue && LightValue < ThresHoldGettingDark)
  {
    state = CLOSING_DOOR;
  }
}

void CheckGettingLight()
{
  if(operatingModeValue && LightValue > ThresHoldGettingLight)
  {
    state = OPENING_DOOR;
  }
}

// --------------- Status String --------------- //
void ChangeState(String Status)
{
  if(OldState != Status)
  {
    Blynk.virtualWrite(V6, Status);     // Only push to Blynk if value is changed 
    OldState = Status;
  }
}

//========================================================== //
//=========== Interrupts - Detect fast changes ============== //
//========================================================== //
void doEncoder()
{
  if(digitalRead(encoderPinA) == digitalRead(encoderPinB))
  {
    encoderPos++;
  }
  else 
  {
    encoderPos--;
  }
}

Credits

BMic

BMic

0 projects • 11 followers

Comments