Project in progress
Ceilometer

Ceilometer © GPL3+

Clouds meter <> establish the height of clouds through an IR measurement temperature of saturation.

  • 2,742 views
  • 12 comments
  • 29 respects

Components and supplies

Necessary tools and machines

Autodesk FUSION 360
Cura By DAGOMA

About this project

It's a kind of upgrade to my current Arduino's weather station. (Temperature, humidity, Wind speed and direction, rain fall, atmospherical pressure, sun power, luminosity)

I discovered this sensors and its alternative way of measurement on the following web page: https://chrisramsay.co.uk/tag/cloud-detection/

Used in airport, it's traditionally realize with a laser sensor for which the use is strictly regulated. The Infrared measurement is a good replacement and completely in accordance with the troposphere constant rule between temperature and altitude. I decided to follow Chris's approach and create a full sensor completely compatible with the outdoor conditions. That mean:

>water proof => germanium lens = IR transparency < protect the IR sensors

>heating system against ice => heater plug 12V-4W

>clean up system to remove water, ice or snow => waterproof servo to control a deflector

2.IR MLX90614 (FOV90° & 10°) + 1.IR MLX90614 (FOV90°) for reference compensation due to the Ge Lens + Complete 3D printing body.

In the following pix we could see the main body with the two sensors head with a copper bridge to defrost the Germanium lens. The part is the waterproof servo Hitec. A second MLX90614 is externalized without heating and without additional lens to assume a compensation and re adjust the altitude.

This is about the sensor <> obviously there is an additional component which must be place outside. (no radio communication) Every thing is recorded on SD-Card with RTC control. A Mega 2560 board has been required to implement the complete sketch too heavy for a simple UNO.

Tests are running. Heater is running very well and the safety loop also. Bodys are printed with PLA, so they are not really compatible with high temperature. The Heater can overeats 150°C under 12V, this why I decided to split the power supply level (aligned with the servo's needs) and define a safe threshold at 55°C.

First Tests and result 19&20 January 2019_______________________

24hr outdoor. I placed it outdoor with a full blue sky so that should provide the lower temperature and by conversion the biggest altitude close. This the main facts:

>I created a new control cover , higher to handle the Mega board because it was initially design to a Uno version

>Heater is running very well but high temperature on germanium lens create abnormal deviation to check the object temp. => program command has been changed to reduce this and maintain a lower temp and reduce the deviation.

>I tested rubber blades, but I should reduce the rotation speed <> even if the design on the Head and the Ref sensor casing have been designed to provide a nice slope, the 'shock' seem too strong and the blades extracted from the deflector.

>there are still glitches on measurement. I think my wiring is guilty because too 'wild' I placed a photo below => it's decided I'm going to use Eagle to create my own PCB, and probably replace the Darlington transistors to Finder relays.

>Results sounds good in appearance. The high altitude should be better if the sensor can be place over the roof... during this phase I placed it on my terrasse and the 90° angle integrate a part of the house... that create an offset of temperature. A part of the recording was also disturb by c... blades are not in place on the deflector, but there were no ice at the morning... that sound really good.

see the pix...

Code

celio_full_sketch.inoArduino
// IDE ADRUINO V1.8.7
// BOARD ARDUINO MEGA 2560 (no enough memory with UNO board)
//______________________________ SERVO
#include <Servo.h>
#define UPDATE_TIME 10
#define MAX_POS 180
#define MIN_POS 0
int servoPin = 9;
int pulse = 1900;
char s="1.5";
Servo HS64WP;
int pos=0; 
int ServoPwr_pin = 8;
int ServoPwr_led = 22;
//______________________________ THERMOCOUPLE
#include <Adafruit_MAX31855.h>
Adafruit_MAX31855  *thermocouples[1];
float Tc_HSU_start, Tc_HSU_end;
volatile float Tc_Heater;
float Tc_MAX31855;
//______________________________ IR SENSORs
#include <Wire.h>
#include <Adafruit_MLX90614.h>
float TA90H,TO90H,TA10H,TO10H,TA90R,TO90R,RO90;
Adafruit_MLX90614 mlx_fov10_Head = Adafruit_MLX90614(0x1B); // Head FOV90
Adafruit_MLX90614 mlx_fov90_Head = Adafruit_MLX90614(0x5A); // Head FOV10
Adafruit_MLX90614 mlx_fov90_Ref = Adafruit_MLX90614(0x3A); // Ref FOV90
//______________________________ SD-CARD
#include <SPI.h>
#include <SD.h>
const int chipSelect = 53;
//______________________________ REAL TIME CLOCK DS1307
#define DS1307_I2C_ADDRESS 0x68
volatile int top, top60, top10, topD,Heat_cnt;
byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
int SQW_RTC_led = 2;
//______________________________ RF433 MHZ
//#include <RH_ASK.h> => can't be used simutaneously with another SPI component. => should be externalyzed
// try to use RCSwitch or VirtualWire
//______________________________ HEATER 12V-4W
int HeaterPwr_pin = 7;
int HeaterPwr_led = 23;
//______________________________ GLOBAL VARIABLEs
boolean HeaterPwr_cmd, Heater_break, Idle_mode; 
float voltage, MinTamb, Alt_ref_raw, Alt_head_raw, Alt_head_corr, Alt_dyn_raw, Alt_dyn_corr, KDG90, KDG10;
String File_record = "Celio_A1";
volatile int Idle_cnt, Break_cnt, Clean_cnt;

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
volatile float TPL_HH = 55;         // (C) Thermal Protection Level High-High <> risk for PLA, absolute threshold to stop completely the main program.
volatile float TPL_H = 45;          // (C) Thermal Protection Level High <> permission to launch Start_up sequence in cas of several Power restart
volatile float PwrSupply_ref = 7.45;// (V) Power Supply Volatege Reference <> shoudl be control due to risk of deviation +/-
volatile float PwrSupply_db = 0.40; // (V) Dead band power supply for accessories <> below 7V the servo don't work correctly.
volatile int   HS64_offset = -12;   // (step) aligne the deflector with the MLX axle. 
volatile float TMNICE = 2.0;        // (C) thermal threshold to define the risk of ice and activate the Heater. Hysteresis is handle at +3C on pick-Up
volatile float KHTO = 120;          // (second) Heat TimeOut running => permit a safety break during heating. A n Breaker time is associated at the half
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup() {

pinMode(HeaterPwr_pin, OUTPUT);
pinMode(HeaterPwr_led, OUTPUT);
pinMode(ServoPwr_pin, OUTPUT);
pinMode(ServoPwr_led, OUTPUT);
pinMode(SQW_RTC_led,OUTPUT);
pinMode(3,INPUT_PULLUP);

thermocouples[0] = new Adafruit_MAX31855(4, 5, 6); 
attachInterrupt (digitalPinToInterrupt(3), Flash_RTC , RISING );

Serial.begin(9600);

mlx_fov10_Head.begin();  
mlx_fov90_Head.begin(); 
mlx_fov90_Ref.begin(); 

Wire.begin();                
Wire.beginTransmission(DS1307_I2C_ADDRESS); Wire.write(0x07); Wire.write(0x10);
Wire.endTransmission();

if (!SD.begin(chipSelect)) 
  {Serial.println("Card failed, or not present");return;}
else
  {Serial.println("card initialized.");
  File dataFile = SD.open(File_record, FILE_WRITE); 
    if (dataFile) 
    {              
      dataFile.print("Date")            ;dataFile.print(" ; ");             
      dataFile.print("Hour")            ;dataFile.print(" ; ");  
      dataFile.print("Tamb_FOV90_Head") ;dataFile.print(" ; ");   
      dataFile.print("Tamb_FOV10_Head") ;dataFile.print(" ; "); 
      dataFile.print("Tamb_FOV90_Ref")  ;dataFile.print(" ; ");
      dataFile.print("Tobj_FOV90_Head") ;dataFile.print(" ; ");
      dataFile.print("Tobj_FOV10_Head") ;dataFile.print(" ; ");
      dataFile.print("Tobj_FOV90_Ref")  ;dataFile.print(" ; ");   
      dataFile.print("Tc_Heater")       ;dataFile.print(" ; ");
      dataFile.print("Tc_MAX31855")     ;dataFile.print(" ; ");
      dataFile.print("Voltage")         ;dataFile.print(" ; ");  
      dataFile.print("Rate_FOV90")      ;dataFile.print(" ; ");  
      dataFile.print("Rate_FOV10")      ;dataFile.print(" ; ");  
      dataFile.print("Altitude_raw")    ;dataFile.print(" ; ");   
      dataFile.print("Altitude_corr")   ;dataFile.print(" ; "); 
      dataFile.print("Idle_mode")       ;dataFile.print(" ; ");
      dataFile.print("HeaterPwr_cmd")   ;dataFile.print(" ; ");      
      dataFile.print("Heater_break")    ;dataFile.print(" ; ");
      dataFile.print("Heat_count")      ;dataFile.print(" ; ");
      dataFile.println("File name")     ;dataFile.close(); 
    }}

StartUp();

HS64WP.attach(servoPin); 
for (pos = MIN_POS; pos <= MAX_POS+HS64_offset; pos += 1) { HS64WP.write(pos); } 
delay(UPDATE_TIME);Serial.println("Configuration Corrected Measurment");delay(1000);
top60 = 0;

}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void loop() {

Tc_Heater = thermocouples[0]->readCelsius();
Thermal_Prot();

Tc_MAX31855 = thermocouples[0]->readInternal();
TA90H = mlx_fov90_Head.readAmbientTempC();
TA10H = mlx_fov10_Head.readAmbientTempC();
TA90R = mlx_fov90_Ref.readAmbientTempC();
voltage = analogRead(A0) * (5.0 / 1023.0) * (1680.0/680.0); // (R1 = 1 kOhm , R2 = 680 Ohms)
TO90H = mlx_fov90_Head.readObjectTempC();
TO10H = mlx_fov10_Head.readObjectTempC();
TO90R = mlx_fov90_Ref.readObjectTempC();

KDG90 = TO90H / TO90R;
KDG10 = TO10H / TO90R;

MinTamb = min(TA90H,TA10H); // Mini Tambe sensor and not outdoor
Alt_ref_raw = max(0,(( TA90R - TO90R) / 0.6) * 100);
Alt_head_raw = max(0,(( TA90H - TO90H) / 0.6) * 100);
Alt_head_corr = Alt_head_raw * KDG90;
Alt_dyn_raw = max(0,(( TA10H - TO10H) / 0.6) * 100);
Alt_dyn_corr = Alt_dyn_raw * KDG10;

//[1] HEATING MANAGMENT___________________________________
if (MinTamb <= TMNICE && HeaterPwr_cmd == false && Heater_break == false) {HeaterPwr_cmd = true; } 
else if (MinTamb >= TMNICE + 3 or Heat_cnt >= KHTO) {HeaterPwr_cmd = false; Heat_cnt = 0; Heater_break = true;}

//[2] CLEANING MANAGMENT__________________________________

if ( ((TA90H - TO90H) <= 2.0) && ((TA10H - TO10H) <= 2.0) && ((TA90R - TO90R) <= 2.0) && (Idle_mode == false) )
  {Monitor(); CleanUp(); Clean_cnt = Clean_cnt + 1;}
else
  {Clean_cnt = 0;}

if (Clean_cnt >= 3) 
  {Serial.println("IDLE");for (pos = MAX_POS + HS64_offset; pos <= MIN_POS; pos -= 1) { HS64WP.write(pos); delay(UPDATE_TIME);} 
  Idle_mode = true;
  Alt_ref_raw = 0;
  Alt_head_raw = 0; 
  Alt_head_corr = 0; 
  Alt_dyn_raw = 0;    
  Alt_dyn_corr = 0;   
  }


//[3] RECORDING___________________________________________
if (top60 >= 10) { Record(); Monitor(); top60 = 0;}


//[4] ACTUATORS CONTROL __________________________________
if (HeaterPwr_cmd == true) {digitalWrite(HeaterPwr_pin, HIGH); digitalWrite(HeaterPwr_led, HIGH); } else {digitalWrite(HeaterPwr_pin, LOW); digitalWrite(HeaterPwr_led, LOW); }

}


//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

void CleanUp()
{

Serial.println("Clean-Up");
for (pos = MIN_POS; pos <= MAX_POS+HS64_offset; pos += 1) { HS64WP.write(pos); delay(UPDATE_TIME);} 
delay(2000);
  
}

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

byte bcdToDec(byte val) 
{ return ( (val/16*10) + (val%16) );}

void getDateDs1307(byte *second,byte *minute,byte *hour,byte *dayOfWeek,byte *dayOfMonth,byte *month,byte *year) //---
{
  Wire.beginTransmission(DS1307_I2C_ADDRESS);
  Wire.write(0);
  Wire.endTransmission();
  Wire.requestFrom(DS1307_I2C_ADDRESS, 7);

  *second     = (bcdToDec(Wire.read() & 0x7f));
  *minute     = (bcdToDec(Wire.read()));
  *hour       = (bcdToDec(Wire.read() & 0x3f));  // Need to change this if 12 hour am/pm
  *dayOfWeek  = bcdToDec(Wire.read());
  *dayOfMonth = bcdToDec(Wire.read());
  *month      = bcdToDec(Wire.read());
  *year       = bcdToDec(Wire.read());
}

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

void Flash_RTC ()
{
  
  top = top + 1;
  top60 = top60 + 1;

  digitalWrite(SQW_RTC_led,HIGH);
  delay(500); 
  digitalWrite(SQW_RTC_led, LOW); 

if (HeaterPwr_cmd == true) {Heat_cnt = Heat_cnt + 1;}
  
  switch(Heat_cnt) 
  {
    case 30:  CleanUp(); break;
    case 60:  CleanUp(); break;
    case 90:  CleanUp(); break;
    case 120: CleanUp(); break;    
  }

if (Heater_break == true) {Break_cnt = Break_cnt + 1;}
if (Break_cnt >= KHTO / 2) {Heater_break = false; Break_cnt = 0;}

if (Idle_mode == true) {Idle_cnt = Idle_cnt + 1;}
if (Idle_cnt >= 600) {Idle_mode = false; Idle_cnt = 0;Clean_cnt = 0;} // cleanUp reset required to permit a new cycle of Cleaning and avoid looping 
}

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

void Record() {

  getDateDs1307(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year);
   
  File dataFile = SD.open(File_record, FILE_WRITE); 
     
    if (dataFile) 
    {              
      dataFile.print(dayOfMonth);dataFile.print("/");dataFile.print(month);dataFile.print("/");dataFile.print(year);dataFile.print(" ; ");             
      dataFile.print(hour);dataFile.print(":");dataFile.print(minute);dataFile.print(":");dataFile.print(second);dataFile.print(" ; ");
      dataFile.print(TA90H,2)         ;dataFile.print(" ; ");   
      dataFile.print(TA10H,2)         ;dataFile.print(" ; "); 
      dataFile.print(TA90R,2)         ;dataFile.print(" ; ");
      dataFile.print(TO90H,2)         ;dataFile.print(" ; ");
      dataFile.print(TO10H,2)         ;dataFile.print(" ; ");
      dataFile.print(TO90R,2)         ;dataFile.print(" ; ");   
      dataFile.print(Tc_Heater,1)     ;dataFile.print(" ; ");
      dataFile.print(Tc_MAX31855,1)   ;dataFile.print(" ; ");
      dataFile.print(voltage,2)       ;dataFile.print(" ; ");  
      dataFile.print(KDG90,3)         ;dataFile.print(" ; ");  
      dataFile.print(KDG10,3)         ;dataFile.print(" ; ");  
      dataFile.print(Alt_ref_raw,1)   ;dataFile.print(" ; ");   
      dataFile.print(Alt_head_raw,1)  ;dataFile.print(" ; "); 
      dataFile.print(Alt_head_corr,1) ;dataFile.print(" ; "); 
      dataFile.print(Alt_dyn_raw,1)   ;dataFile.print(" ; "); 
      dataFile.print(Alt_dyn_corr,1)  ;dataFile.print(" ; "); 
      dataFile.print(Idle_mode)       ;dataFile.print(" ; ");
      dataFile.print(HeaterPwr_cmd)   ;dataFile.print(" ; ");      
      dataFile.print(Heater_break)    ;dataFile.print(" ; ");
      dataFile.print(Heat_cnt)        ;dataFile.print(" ; ");
      dataFile.println(File_record )  ;dataFile.close(); 
    }
  
}

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>


void Monitor() {

Serial.print(TA90H);Serial.print("C : ");
Serial.print(TA10H);Serial.print("C : ");
Serial.print(TA90R);Serial.print("C : >> ");
Serial.print(Tc_Heater);Serial.print("C << : ");
Serial.print(Tc_MAX31855);Serial.print("C : ");
Serial.print(top);Serial.print("- : ");
Serial.print(HeaterPwr_cmd);Serial.print(" : ");
Serial.print(voltage);Serial.print("V : ");
Serial.print(TO90H);Serial.print("C : ");
Serial.print(TO10H);Serial.print("C : ");
Serial.print(TO90R);Serial.print("C : ");
Serial.print(Alt_ref_raw);Serial.print("m : ");
Serial.print(Alt_head_raw);Serial.print("m : ");
Serial.print(Alt_head_corr);Serial.print("m : ");
Serial.print(Alt_dyn_raw);Serial.print("m : ");
Serial.print(Alt_dyn_corr);Serial.print("m : ");
Serial.print(KDG90);Serial.print("- : ");
Serial.print(KDG10);Serial.print("- : ");
Serial.print(top60);Serial.println("- : ");
  
}

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

int laps, SUHT_STOP;
float deltat, volt_max, volt_min;

void StartUp() {

Tc_Heater = thermocouples[0]->readCelsius();

if (Tc_Heater <= TPL_H) {

digitalWrite(HeaterPwr_pin, LOW);digitalWrite(HeaterPwr_led, LOW);
digitalWrite(ServoPwr_pin, LOW);digitalWrite(HeaterPwr_led, LOW);

Serial.println(">>>>>>>>>START-UP<<<<<<<<<<");

Serial.println("(1) HEATER TEST ______________");

Serial.print("Temp Tc Heater:");
Tc_HSU_start = thermocouples[0]->readCelsius();
volt_max = Tc_HSU_start;
volt_min = Tc_HSU_start;
Serial.print(Tc_HSU_start);Serial.println(" C");
delay(1000);
laps = 0;
Serial.println("-->HEATER ON");
  do {digitalWrite(HeaterPwr_pin, HIGH);digitalWrite(HeaterPwr_led, HIGH);
      laps = laps + 1 ;
      deltat=thermocouples[0]->readCelsius()-Tc_HSU_start;
      voltage = analogRead(A0) * (5.0 / 1023.0) * (1680.0/680.0);
      if (voltage > volt_max) {volt_max = voltage;} else {volt_max = volt_max;}
      if (voltage > volt_min) {volt_min = voltage;} else {volt_min = volt_min;}
      Serial.print(laps);Serial.print(" : ");
      Serial.print(voltage);Serial.print(" : ");
      Serial.print(deltat);Serial.println(" : ");
      if (volt_max - volt_min > (2 * PwrSupply_db)) {Serial.println("TRIP BOARD - POWER SUPPLY ISSUE"); delay(250); exit(0);}
      if (voltage < (PwrSupply_ref-PwrSupply_db)) {Serial.println("TRIP BOARD - POWER SUPPLY TOO LOW"); delay(250); exit(0);}
      if (laps == 40 && deltat <= 5.0) {Serial.println("TRIP BOARD - NO HEATING"); delay(250); exit(0);}
      if (thermocouples[0]->readCelsius() > TPL_HH) {Serial.println("TRIP BOARD - Too HOT"); delay(250); exit(0);}
      if (laps >=40 || deltat >= 5.0) {SUHT_STOP = 1;} else {SUHT_STOP = 0;}
      delay(1000); } while ( SUHT_STOP == 0); 

digitalWrite(HeaterPwr_pin, LOW); digitalWrite(HeaterPwr_led, LOW);Serial.println("-->HEATER OFF");} else {Serial.println("TRIP BOARD - Too HOT"); delay(250); exit(0);}

Serial.println("(2) SERVO TEST ______________");
HS64WP.attach(servoPin); 
digitalWrite(ServoPwr_pin, HIGH); digitalWrite(ServoPwr_led, HIGH); Serial.println("SERVO ON");
delay(1000);


for (pos = MIN_POS; pos <= MAX_POS+HS64_offset; pos += 1) { HS64WP.write(pos); delay(UPDATE_TIME);
      if (voltage > volt_max) {volt_max = voltage;} else {volt_max = volt_max;}
      if (voltage > volt_min) {volt_min = voltage;} else {volt_min = volt_min;}} 
      Serial.println("Configuration Corrected Measurment");delay(2000);
for (pos = MAX_POS+HS64_offset; pos >= 85+HS64_offset; pos -= 1) { HS64WP.write(pos); delay(UPDATE_TIME);
      if (voltage > volt_max) {volt_max = voltage;} else {volt_max = volt_max;}
      if (voltage > volt_min) {volt_min = voltage;} else {volt_min = volt_min;}} 
      Serial.println("Configuration Head Measurment only");delay(2000);
for (pos = 90+HS64_offset; pos >= MIN_POS; pos -= 1) { HS64WP.write(pos); delay(UPDATE_TIME);
      if (voltage > volt_max) {volt_max = voltage;} else {volt_max = volt_max;}
      if (voltage > volt_min) {volt_min = voltage;} else {volt_min = volt_min;}} 
      Serial.println("Safe location - Measurment OFF");delay(2000);


Serial.println("END START-UP ______________");

}

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

void Thermal_Prot() {if (Tc_Heater > TPL_HH) {Serial.println("TRIP BOARD - OVERHEAT");delay(250);exit(0);}}
Main ProgramArduino
Updated Revision with several improvements
// IDE ADRUINO V1.8.7
// BOARD ARDUINO MEGA 2560 (no enough memory with UNO board)
//______________________________ SERVO
#include <Servo.h>
#define UPDATE_TIME 10
#define MAX_POS 180
#define MIN_POS 0
int servoPin = 9;
int pulse = 1900;
char s="1.5";
Servo HS64WP;
int pos=0; 
int ServoPwr_pin = 8;
int ServoPwr_led = 23;
//______________________________ THERMOCOUPLE
#include <Adafruit_MAX31855.h>
Adafruit_MAX31855  *thermocouples[1];
float Tc_HSU_start, Tc_HSU_end;
volatile float Tc_Heater;
float Tc_MAX31855;
//______________________________ IR SENSORs
#include <Wire.h>
#include <Adafruit_MLX90614.h>
volatile float TA90H,TO90H,TA10H,TO10H,TA90R,TO90R,Text;
volatile float TA90H_int,TO90H_int,TA10H_int,TO10H_int,TA90R_int,TO90R_int;
Adafruit_MLX90614 mlx_fov10_Head = Adafruit_MLX90614(0x1B); // Head FOV90
Adafruit_MLX90614 mlx_fov90_Head = Adafruit_MLX90614(0x5A); // Head FOV10
Adafruit_MLX90614 mlx_fov90_Ref = Adafruit_MLX90614(0x3A); // Ref FOV90
//______________________________ SD-CARD
#include <SPI.h>
#include <SD.h>
const int chipSelect = 53;
//______________________________ REAL TIME CLOCK DS1307
#define DS1307_I2C_ADDRESS 0x68
volatile int top,Heat_cnt;
byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
int SQW_RTC_led = 24;
//______________________________ RF433 MHZ
//#include <RH_ASK.h> => can't be used simutaneously with another SPI component. => should be externalyzed
// try to use RCSwitch or VirtualWire
//______________________________ HEATER 12V-4W
int HeaterPwr_pin = 7;
int HeaterPwr_led = 22;
//______________________________ GLOBAL VARIABLEs
boolean HeaterPwr_cmd, Heater_break, Idle_mode; 
float MinTamb, Alt_ref_raw, Alt_head_raw, Alt_head_corr, Alt_dyn_raw, Alt_dyn_corr, KDG90, KDG10;
volatile float voltage, voltage_int;
String File_record = "Celio_A3";
volatile int Idle_cnt, Break_cnt, Clean_cnt;

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
volatile float TPL_HH = 55;         // (C) Thermal Protection Level High-High <> risk for PLA, absolute threshold to stop completely the main program.
volatile float TPL_H = 45;          // (C) Thermal Protection Level High <> permission to launch Start_up sequence in cas of several Power restart
volatile float PwrSupply_ref = 7.45;// (V) Power Supply Volatege Reference <> shoudl be control due to risk of deviation +/-
volatile float PwrSupply_db = 0.40; // (V) Dead band power supply for accessories <> below 7V the servo don't work correctly.
volatile int   HS64_offset = -2;   // (step) aligne the deflector with the MLX axle. 
volatile float TMNICE = 2.0;        // (C) thermal threshold to define the risk of ice and activate the Heater. Hysteresis is handle at +3C on pick-Up
volatile float KHTO = 120;          // (second) Heat TimeOut running => permit a safety break during heating. A n Breaker time is associated at the half
volatile float KIdle_dly = 30;      // (second) time delay to maintain the Idle mode
int            NBS = 10.0;            // (Numbers of top-1Hz <> NB seconds) define the recording frequency
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup() {

pinMode(HeaterPwr_pin, OUTPUT);
pinMode(HeaterPwr_led, OUTPUT);
pinMode(ServoPwr_pin, OUTPUT);
pinMode(ServoPwr_led, OUTPUT);
pinMode(SQW_RTC_led,OUTPUT);
pinMode(3,INPUT_PULLUP);

thermocouples[0] = new Adafruit_MAX31855(4, 5, 6); 
attachInterrupt (digitalPinToInterrupt(3), Flash_RTC , RISING );

Serial.begin(9600);

mlx_fov10_Head.begin();  
mlx_fov90_Head.begin(); 
mlx_fov90_Ref.begin(); 

Wire.begin();                
Wire.beginTransmission(DS1307_I2C_ADDRESS); Wire.write(0x07); Wire.write(0x10);
Wire.endTransmission();

if (!SD.begin(chipSelect)) 
  {Serial.println("Card failed, or not present"); exit(0);}
else
  {Serial.println("card initialized.");
  File dataFile = SD.open(File_record, FILE_WRITE); 
    if (dataFile) 
    {              
      dataFile.print("Date")            ;dataFile.print(" ; ");             
      dataFile.print("Hour")            ;dataFile.print(" ; ");  
      dataFile.print("Tamb_FOV90_Head") ;dataFile.print(" ; ");   
      dataFile.print("Tamb_FOV10_Head") ;dataFile.print(" ; "); 
      dataFile.print("Tamb_FOV90_Ref")  ;dataFile.print(" ; ");
      dataFile.print("Tobj_FOV90_Head") ;dataFile.print(" ; ");
      dataFile.print("Tobj_FOV10_Head") ;dataFile.print(" ; ");
      dataFile.print("Tobj_FOV90_Ref")  ;dataFile.print(" ; ");   
      dataFile.print("Tc_Heater")       ;dataFile.print(" ; ");
      dataFile.print("Tc_MAX31855")     ;dataFile.print(" ; ");
      dataFile.print("Voltage")         ;dataFile.print(" ; ");  
      dataFile.print("Rate_FOV90")      ;dataFile.print(" ; ");  
      dataFile.print("Rate_FOV10")      ;dataFile.print(" ; ");  
      dataFile.print("Alt_ref_raw")     ;dataFile.print(" ; ");   
      dataFile.print("Alt_head_raw")    ;dataFile.print(" ; ");  
      dataFile.print("Alt_head_corr")   ;dataFile.print(" ; ");  
      dataFile.print("Alt_dyn_raw")     ;dataFile.print(" ; "); 
      dataFile.print("Alt_dyn_corr")    ;dataFile.print(" ; "); 
      dataFile.print("Idle_mode")       ;dataFile.print(" ; ");
      dataFile.print("HeaterPwr_cmd")   ;dataFile.print(" ; ");      
      dataFile.print("Heater_break")    ;dataFile.print(" ; ");
      dataFile.print("Heat_count")      ;dataFile.print(" ; ");
      dataFile.println("File name")     ;dataFile.close(); 
    }}

StartUp();
Run();
top = 0;

}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void loop() {

Tc_Heater = thermocouples[0]->readCelsius();
Thermal_Prot();

HS64WP.attach(servoPin); 

Tc_MAX31855 = thermocouples[0]->readInternal();
TA90H = mlx_fov90_Head.readAmbientTempC();
TA10H = mlx_fov10_Head.readAmbientTempC();
TA90R = mlx_fov90_Ref.readAmbientTempC();
voltage = analogRead(A0) * (5.0 / 1023.0) * (1680.0/680.0); // (R1 = 1 kOhm , R2 = 680 Ohms)
TO90H = mlx_fov90_Head.readObjectTempC();
TO10H = mlx_fov10_Head.readObjectTempC();
TO90R = mlx_fov90_Ref.readObjectTempC();
Text = TA90R;

//[1] HEATING MANAGMENT___________________________________
MinTamb = min(TA90H,TA10H); // Mini Tambe sensor and not outdoor
if (MinTamb <= TMNICE && HeaterPwr_cmd == false && Heater_break == false) 
  {HeaterPwr_cmd = true; } 
else if ((MinTamb >= TMNICE + 3) or (Heat_cnt >= KHTO)) 
  {HeaterPwr_cmd = false; Heat_cnt = 0; Heater_break = true;}

switch(Heat_cnt) 
{
  case 30:  CleanUp(); Serial.println("CleanUp during Heating");break;
  case 60:  CleanUp(); Serial.println("CleanUp during Heating");break;
  case 90:  CleanUp(); Serial.println("CleanUp during Heating");break;
  case 120: CleanUp(); Serial.println("CleanUp during Heating");break;    
}

//[2] CLEANING MANAGMENT__________________________________

if ( (((TA90H - TO90H) <= 2.0) || ((TA10H - TO10H) <= 2.0) ) && (Idle_mode == false) )
  {Monitor(); Clean_cnt = Clean_cnt + 1;Serial.println(Clean_cnt);CleanUp();}
else
  {Clean_cnt = constrain((Clean_cnt -1),0,10000);}


if ((Clean_cnt >= 3) && (Idle_mode == false)) 
  {Serial.println("IDLE");
  Idle(); 
  Idle_mode = true;
  Alt_ref_raw = 0; Alt_head_raw = 0; Alt_head_corr = 0; Alt_dyn_raw = 0; Alt_dyn_corr = 0;   
  }



//[3] ACTUATORS CONTROL __________________________________
if (HeaterPwr_cmd == true) {digitalWrite(HeaterPwr_pin, HIGH); digitalWrite(HeaterPwr_led, HIGH); } else {digitalWrite(HeaterPwr_pin, LOW); digitalWrite(HeaterPwr_led, LOW); }


//[4] RECORDING___________________________________________
if (top >= NBS) 
{ 

TA90H = TA90H_int / NBS;
TO90H = TO90H_int / NBS;
TA10H = TA10H_int / NBS;
TO10H = TO10H_int / NBS;
TA90R = TA90R_int / NBS;
TO90R = TO90R_int / NBS;
voltage = voltage_int / NBS;

KDG90 = TO90H / TO90R;
KDG10 = TO10H / TO90R;

if (Idle_mode == true)
{
  Alt_ref_raw = 0; 
  Alt_head_raw = 0; 
  Alt_head_corr = 0; 
  Alt_dyn_raw = 0; 
  Alt_dyn_corr = 0;
}
else
{
  Alt_ref_raw = constrain(((( Text - TO90R) / 0.6) * 100),0,10000);
  Alt_head_raw = constrain(((( Text - TO90H) / 0.6) * 100),0,10000);
  Alt_head_corr = constrain(Alt_head_raw * KDG90,0,10000);
  Alt_dyn_raw = constrain(((( Text - TO10H) / 0.6) * 100),0,10000);
  Alt_dyn_corr = constrain(Alt_dyn_raw * KDG10,0,10000);
}

Record(); Monitor(); 
  
top = 0;

TA90H_int = 0;
TO90H_int = 0;
TA10H_int = 0;
TO10H_int = 0;
TA90R_int = 0;
TO90R_int = 0;
voltage_int = 0;

}

}

void CleanUp()
{

Serial.println("Clean-Up on run");
digitalWrite(ServoPwr_pin, HIGH); digitalWrite(ServoPwr_led, HIGH);
delay(500);
for (pos = MIN_POS; pos <= MAX_POS+HS64_offset; pos += 1) { HS64WP.write(pos); delay(UPDATE_TIME);} 
delay(1000);
//digitalWrite(ServoPwr_pin, LOW); digitalWrite(ServoPwr_led, LOW);
}

byte bcdToDec(byte val) 
{ return ( (val/16*10) + (val%16) );}

void getDateDs1307(byte *second,byte *minute,byte *hour,byte *dayOfWeek,byte *dayOfMonth,byte *month,byte *year) //---
{
  Wire.beginTransmission(DS1307_I2C_ADDRESS);
  Wire.write(0);
  Wire.endTransmission();
  Wire.requestFrom(DS1307_I2C_ADDRESS, 7);

  *second     = (bcdToDec(Wire.read() & 0x7f));
  *minute     = (bcdToDec(Wire.read()));
  *hour       = (bcdToDec(Wire.read() & 0x3f));  // Need to change this if 12 hour am/pm
  *dayOfWeek  = bcdToDec(Wire.read());
  *dayOfMonth = bcdToDec(Wire.read());
  *month      = bcdToDec(Wire.read());
  *year       = bcdToDec(Wire.read());
}

void Flash_RTC ()
{

  Thermal_Prot(); // already activated by the main loop , this crutial function is recall here to avoid infite loop condition in the main program.
  
  top = top + 1;


 digitalWrite(SQW_RTC_led,HIGH);
 delay(500);
 

if (HeaterPwr_cmd == true) {Heat_cnt = Heat_cnt + 1;}
  
if (Heater_break == true) {Break_cnt = Break_cnt + 1;}
if (Break_cnt >= KHTO / 2) {Heater_break = false; Break_cnt = 0;}

if (Idle_mode == true) {Idle_cnt = Idle_cnt + 1;Serial.println(Idle_cnt);}
if (Idle_cnt >= KIdle_dly) {Idle_mode = false; Idle_cnt = 0;Clean_cnt = 0;} // cleanUp reset required to permit a new cycle of Cleaning and avoid looping 

TA90H_int = TA90H_int + TA90H;
TO90H_int = TO90H_int + TO90H;
TA10H_int = TA10H_int + TA10H;
TO10H_int = TO10H_int + TO10H;
TA90R_int = TA90R_int + TA90R;
TO90R_int = TO90R_int + TO90R;
voltage_int = voltage_int + voltage;



 digitalWrite(SQW_RTC_led, LOW);
}

void Idle()
{

Serial.println("Return on idle");
digitalWrite(ServoPwr_pin, HIGH); digitalWrite(ServoPwr_led, HIGH);
delay(500);
for (pos = MAX_POS+HS64_offset; pos >= 0; pos -= 1) { HS64WP.write(pos); delay(UPDATE_TIME);} 
delay(1000);
digitalWrite(ServoPwr_pin, LOW); digitalWrite(ServoPwr_led, LOW);
  
}

void Record() {

  getDateDs1307(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year);
   
  File dataFile = SD.open(File_record, FILE_WRITE); 
     
    if (dataFile) 
    {              
      dataFile.print(dayOfMonth);dataFile.print("/");dataFile.print(month);dataFile.print("/");dataFile.print(year);dataFile.print(" ; ");             
      dataFile.print(hour);dataFile.print(":");dataFile.print(minute);dataFile.print(":");dataFile.print(second);dataFile.print(" ; ");
      dataFile.print(TA90H,2)         ;dataFile.print(" ; ");   
      dataFile.print(TA10H,2)         ;dataFile.print(" ; "); 
      dataFile.print(TA90R,2)         ;dataFile.print(" ; ");
      dataFile.print(TO90H,2)         ;dataFile.print(" ; ");
      dataFile.print(TO10H,2)         ;dataFile.print(" ; ");
      dataFile.print(TO90R,2)         ;dataFile.print(" ; ");   
      dataFile.print(Tc_Heater,1)     ;dataFile.print(" ; ");
      dataFile.print(Tc_MAX31855,1)   ;dataFile.print(" ; ");
      dataFile.print(voltage,2)       ;dataFile.print(" ; ");  
      dataFile.print(KDG90,3)         ;dataFile.print(" ; ");  
      dataFile.print(KDG10,3)         ;dataFile.print(" ; ");  
      dataFile.print(Alt_ref_raw,1)   ;dataFile.print(" ; ");   
      dataFile.print(Alt_head_raw,1)  ;dataFile.print(" ; "); 
      dataFile.print(Alt_head_corr,1) ;dataFile.print(" ; "); 
      dataFile.print(Alt_dyn_raw,1)   ;dataFile.print(" ; "); 
      dataFile.print(Alt_dyn_corr,1)  ;dataFile.print(" ; "); 
      dataFile.print(Idle_mode)       ;dataFile.print(" ; ");
      dataFile.print(HeaterPwr_cmd)   ;dataFile.print(" ; ");      
      dataFile.print(Heater_break)    ;dataFile.print(" ; ");
      dataFile.print(Heat_cnt)        ;dataFile.print(" ; ");
      dataFile.println(File_record )  ;dataFile.close(); 
    }
  
}

void Run()

{

Serial.println("Run");

digitalWrite(ServoPwr_pin, HIGH); digitalWrite(ServoPwr_led, HIGH);
delay(500);
for (pos = MIN_POS; pos <= MAX_POS+HS64_offset; pos += 1) { HS64WP.write(pos); delay(UPDATE_TIME);} 
delay(1000);
//digitalWrite(ServoPwr_pin, LOW); digitalWrite(ServoPwr_led, LOW);
  
}

void Monitor() {

Serial.print(TA90H);Serial.print("C : ");
Serial.print(TA10H);Serial.print("C : ");
Serial.print(TA90R);Serial.print("C : >> ");
Serial.print(Tc_Heater);Serial.print("C << : ");
Serial.print(Tc_MAX31855);Serial.print("C : ");
Serial.print(top);Serial.print("- : ");
Serial.print(HeaterPwr_cmd);Serial.print(" : ");
Serial.print(voltage);Serial.print("V : ");
Serial.print(TO90H);Serial.print("C : ");
Serial.print(TO10H);Serial.print("C : ");
Serial.print(TO90R);Serial.print("C : ");
Serial.print(Alt_ref_raw);Serial.print("m : ");
Serial.print(Alt_head_raw);Serial.print("m : ");
Serial.print(Alt_head_corr);Serial.print("m : ");
Serial.print(Alt_dyn_raw);Serial.print("m : ");
Serial.print(Alt_dyn_corr);Serial.print("m : ");
Serial.print(KDG90);Serial.print("- : ");
Serial.println(KDG10);

  
}

int laps, SUHT_STOP;
float deltat, volt_max, volt_min;

void StartUp() {

Tc_Heater = thermocouples[0]->readCelsius();

if (Tc_Heater <= TPL_H) {

digitalWrite(HeaterPwr_pin, LOW);digitalWrite(HeaterPwr_led, LOW);
digitalWrite(ServoPwr_pin, LOW);digitalWrite(HeaterPwr_led, LOW);

Serial.println(">>>>>>>>>START-UP<<<<<<<<<<");

Serial.println("(1) HEATER TEST ______________");

Serial.print("Temp Tc Heater:");
Tc_HSU_start = thermocouples[0]->readCelsius();
volt_max = Tc_HSU_start;
volt_min = Tc_HSU_start;
Serial.print(Tc_HSU_start);Serial.println(" C");
delay(1000);
laps = 0;
Serial.println("-->HEATER ON");
  do {digitalWrite(HeaterPwr_pin, HIGH);digitalWrite(HeaterPwr_led, HIGH);
      laps = laps + 1 ;
      deltat=thermocouples[0]->readCelsius()-Tc_HSU_start;
      voltage = analogRead(A0) * (5.0 / 1023.0) * (1680.0/680.0);
      if (voltage > volt_max) {volt_max = voltage;} else {volt_max = volt_max;}
      if (voltage > volt_min) {volt_min = voltage;} else {volt_min = volt_min;}
      Serial.print(laps);Serial.print(" : ");
      Serial.print(voltage);Serial.print(" : ");
      Serial.print(deltat);Serial.println(" : ");
      if (volt_max - volt_min > (2 * PwrSupply_db)) {Serial.println("TRIP BOARD - POWER SUPPLY ISSUE"); delay(250); exit(0);}
      if (voltage < (PwrSupply_ref-PwrSupply_db)) {Serial.println("TRIP BOARD - POWER SUPPLY TOO LOW"); delay(250); exit(0);}
      if (laps == 40 && deltat <= 5.0) {Serial.println("TRIP BOARD - NO HEATING"); delay(250); exit(0);}
      if (thermocouples[0]->readCelsius() > TPL_HH) {Serial.println("TRIP BOARD - Too HOT"); delay(250); exit(0);}
      if (laps >=40 || deltat >= 5.0) {SUHT_STOP = 1;} else {SUHT_STOP = 0;}
      delay(1000); } while ( SUHT_STOP == 0); 

digitalWrite(HeaterPwr_pin, LOW); digitalWrite(HeaterPwr_led, LOW);Serial.println("-->HEATER OFF");} else {Serial.println("TRIP BOARD - Too HOT"); delay(500); exit(0);}

Serial.println("(2) SERVO TEST ______________");
HS64WP.attach(servoPin); 
digitalWrite(ServoPwr_pin, HIGH); digitalWrite(ServoPwr_led, HIGH); Serial.println("SERVO ON");
delay(1000);


for (pos = MIN_POS; pos <= MAX_POS+HS64_offset; pos += 1) { HS64WP.write(pos); delay(UPDATE_TIME);
      if (voltage > volt_max) {volt_max = voltage;} else {volt_max = volt_max;}
      if (voltage > volt_min) {volt_min = voltage;} else {volt_min = volt_min;}} 
      Serial.println("Configuration Corrected Measurment");delay(2000);
for (pos = MAX_POS+HS64_offset; pos >= 85+HS64_offset; pos -= 1) { HS64WP.write(pos); delay(UPDATE_TIME);
      if (voltage > volt_max) {volt_max = voltage;} else {volt_max = volt_max;}
      if (voltage > volt_min) {volt_min = voltage;} else {volt_min = volt_min;}} 
      Serial.println("Configuration Head Measurment only");delay(2000);
for (pos = 90+HS64_offset; pos >= MIN_POS; pos -= 1) { HS64WP.write(pos); delay(UPDATE_TIME);
      if (voltage > volt_max) {volt_max = voltage;} else {volt_max = volt_max;}
      if (voltage > volt_min) {volt_min = voltage;} else {volt_min = volt_min;}} 
      Serial.println("Safe location - Measurment OFF");delay(2000);


Serial.println("END START-UP ______________");
digitalWrite(ServoPwr_pin, LOW); digitalWrite(ServoPwr_led, LOW); 

}

void Thermal_Prot() 

{

  
//if ((Tc_Heater > TPL_HH) || (voltage < (PwrSupply_ref-PwrSupply_db))) 
if (Tc_Heater > TPL_HH) 
  {Serial.println("TRIP BOARD - OVERHEAT or LOW-VOLTAGE");delay(500);exit(0);}

}

/*
Version Celio_A2
##############################################################################################################################################################
>manage Heater only with external temperature <> that mean Ambient temp from Reference MLX90614
>keep 2C of lower lever hysteresis and increase upper level from 3 to 5C
>recalculate altitudes from each sensors with raw and corrected result.
>re adjust Deflector's offset
>shutdown servo powersupply if not required = on idle 
>gain reactivity by calling the Thermal protection from the loop Flash RTC which is still workingat 1Hz (The main Loop can cumulate several delays)
>Change conditions for Clean Up replace AND between IR sensors by OR

Note: we temporaly remove de flash Led RTC which created disturbance with the Servo Signal pin.
The servo is hold powered on on High position (measurment) and just shutdonw on idle.
In all cases we withdrew the unstabilities 


Version Celio_A3
##############################################################################################################################################################
>Voltage recording unavailable... bad definition
>correct Altitude calculation an declare a new variable to avoid confusion TA90R=Text
>upgrade head name on record file

For next step
...>add voltage control in thermal loop... if no power supply => no protection = stop the main program (Exit0)
 */

Custom parts and enclosures

Technical Drawings
Electronic is not complicated, but the outdoor constraints required a really good conception of casing. These drawings define the several parts which constitute the full sensor.
3D STL File
Main Body
Printed on Dagoma Disco Easy 200 PLA White 1.75mm / 0.1mm
3D STL File
Servo Bearing
Printed on Dagoma Disco Easy 200 PLA White 1.75mm / 0.1mm
3D STL File
Controller Cover
Printed on Dagoma Disco Easy 200 PLA White 1.75mm / 0.1mm
3D STL Files
Controller Bearing
Printed on Dagoma Disco Easy 200 PLA White 1.75mm / 0.1mm
3D STL Files
Deflector
Printed on Dagoma Disco Easy 200 PLA White 1.75mm / 0.1mm
3D STL Files
Bottom Plug
Printed on Dagoma Disco Easy 200 PLA White 1.75mm / 0.1mm
3D STL File
Ref Bottom Plug
Printed on Dagoma Disco Easy 200 PLA White 1.75mm / 0.1mm

Comments

Similar projects you might like

Integrated Solar ChargeController, Inverter, PowerBank, Lamp

Project tutorial by Shahariar

  • 7,984 views
  • 25 comments
  • 31 respects

Solar Charged Battery Powered Arduino Uno

Project in progress by Igor Fonseca Albuquerque

  • 61,293 views
  • 46 comments
  • 127 respects

Air Surfer

Project tutorial by Anton

  • 28,047 views
  • 27 comments
  • 149 respects

WiDC: Wi-Fi-Controlled FPV Robot

Project tutorial by Igor Fonseca Albuquerque

  • 21,480 views
  • 10 comments
  • 87 respects

Arduino Weather Station

Project tutorial by woutvdr

  • 14,245 views
  • 7 comments
  • 44 respects
Add projectSign up / Login