Control a Chicken Coop door with a Photon using Sun position

photon
Tags: #<Tag:0x00007fe21f34f400>

#1

Chickdoor - a Chicken Coop Controller

The Chickdoor project was created to allow our chicken coop door to open and close automatically. The coop is located a good distance from the house and would otherwise require going out in the field early every morning and every evening to open and close its door. Even a simple remote control would require you to remember to open or close the coop at the right time, so chickdoor calculates the open and close times from computations of the Sun’s elevation, while it also provides for manually overriding the computed actions. The solar calculation routine could easily be adapted to get accurate (~1 sec.) sunrise, sunset and twilight times.


It uses the Particle Photon with its related Relay-Shield board to power the linear actuator that operates the door. The Relay board also regulates the voltage from a “12V” storage battery down to what the Photon needs. A detailed overview may be found at
https://drive.google.com/file/d/1KjMlABmtM00hKPeYqIP-iuXN92Vxzoyz/view?usp=sharing

The code for chickdoor.ino

// -----------------------------------
// File 'chickdoor.ino'
// -----------------------------------
#include "Particle.h"
#include "math.h"

SYSTEM_THREAD(ENABLED);

/* RUNTIME = Linear actuator power-on time in seconds. Limit switch halts motor.
VCAL = battery Volts for 4095 counts as calibrated with a meter.
It also = the maximum battery Volts that won't over-voltage the Photon's input. */

// User defined constants
const bool USESMS = true; // Report event completions via SMS text message.
const char SMSADD[13] = "twilio_mysms"; // Webhook to twilio
const char BATTMSG[15] = "Battery low"; // Low battery message
const int RUNTIME = 10; // Use 10 seconds for testing, 90 for coop operation.
const double VCAL = 16.170; // Battery Volts full scale
const double LATITUDE = 40.1234;  // Degrees (+ to North)
const double LONGITUDE = -74.5678; // Degrees (+ to East)
const int TZ = -5; // Time zone, hours (+ to East). Eastern Std. time is -5 hours from UTC.
const float OPENELEV = -3; // degrees  Use -0.8333 deg. (-50 arc min) here to give sunrise time.
const float CLOSEELEV = -6; // degrees  Use -0.8333 deg. here to give sunset time.
const String TIMEFMT = " %a, %b %d %r"; // Time display format per 'strftime'

const int OPCLRYS = D3;  // Open/Close polarity relays, RY1 & RY2
const int POWERRY = D5;  // Power enable relay, RY3
const int ADCPIN = A0;  // To measure battery voltage
const double _2PI = 8 *atan(1); // = 2Pi
const double RAD2DEG = 45/atan(1); // = 180/Pi
const double DEG2RAD = atan(1)/45; //  = Pi/180;
const int SPM = 60; // Seconds Per Minute
const int MPH = 60; // Minutes Per Hour
const int SPH = 3600; // Seconds Per Hour
const int SPD = 86400; // Seconds Per Day
const double LAT = DEG2RAD *LATITUDE; // Latitude in radians

double nowtime = 0; // Current Unix UTC timestamp
double nowday = 0;  // Unix timestamp at 00:00 UTC, today
double opentime = 0; // next door opening time, Unix UTC
double closetime = 0; // next door closing time, Unix UTC
double savedopen = 0; // saved opentime while running 'test'
double savedclose = 0; // saved closetime while running 'test'
double strength = 0; // Wi-Fi signal strength, dBm
double quality = 0; // Wi-Fi signal quality, S/N Ratio, dB
double batVolts = 0; // measured battery voltage
bool isDST = false; // True if Daylight Saving Time is in effect.
bool warnSent = false; // True after Battery-Low warning sent, reset when voltage is > 12.2V
bool testing = false; // Indicates the 40-second test cycle is in progress.
bool findOpen = true; // Tells EventTime() whether to compute an opening or a closing time.
char mySSID[20] = ""; // SSID name of the connected Wi-Fi network
char status[20] = "Waiting for event"; // Reports the most recent event executed.
char opentimeTxt[40] = ""; // opentime as Local Time string.
char closetimeTxt[40] = ""; // closetime as Local Time string.
char nowstr[40] = ""; // Current time as Local Time string.

/* Select external antenna on startup
If no external antenna use WiFi.selectAntenna(ANT_INTERNAL) */
void startup()
{
    WiFi.selectAntenna(ANT_EXTERNAL);
}

void setup()
{
    // Digital pin configuration
    pinMode(OPCLRYS, OUTPUT);
    pinMode(POWERRY, OUTPUT);

    Particle.function("Open_Door", OpenDoor);
    Particle.function("Close_Door", CloseDoor);
    Particle.function("Lift_Door", LiftDoor);
    Particle.function("Skip_next", SkipNext);
//  Comment out "Reset" & "Test" if desired to prevent user access.
    Particle.function("Reset", DoReset);
    Particle.function("Test", DoTest);
    
    Particle.variable("Status", status);
    Particle.variable("Opentime", opentimeTxt);
    Particle.variable("Closetime", closetimeTxt);
    Particle.variable("Now", nowstr);
    Particle.variable("V_Battery", batVolts);
    Particle.variable("Wi-Fi_Network", mySSID);
    Particle.variable("Signal-dBm", strength);
    Particle.variable("Signal_SNR-dB", quality);

    // Make sure relays are off.
    digitalWrite(OPCLRYS, LOW);
    digitalWrite(POWERRY, LOW);
    batVolts = VBatt();

    waitUntil(Particle.connected);
}

void loop()
{
    nowtime = Time.now();
    // After door closes compute next day's open and close times.
    if ((nowtime > (closetime + 10)) && !testing)
        FindTimes();

    strcpy(nowstr, Time.format(nowtime + (TZ + isDST) *SPH, TIMEFMT));
    strcpy(mySSID, WiFi.SSID());
    WiFiSignal sig = WiFi.RSSI();
    strength = sig.getStrengthValue();
    quality = sig.getQualityValue();

    // At 01:00 Local Std. Time, sync the Photon's time.
    if (Time.hour(nowtime) == 01-TZ && Time.minute(nowtime) == 00)
    {   Particle.syncTime();
        waitUntil(Particle.syncTimeDone); // Will continue if no Cloud connection.
        isDST = IsDST();
        // in case 'isDST' has changed
        TimesTxt();
        delay(61 *1000);  // So we call syncTime() only once.
    }
    // Open door when 'nowtime' is 0 to 4 sec after 'opentime'.
    if (fabs(nowtime - opentime + 2) <= 2)
    {
        if (strcmp(status,"Open skipped") != 0)
        {
            if (strcmp(status,"Skipping next") != 0)
                OpenDoor("");
            else
            {
                // Skipped door opening. Reset 'Skipping Next' status
                strcpy(status, "Open skipped");
                PubStatus();
            }
        }
    }
    // Close door when 'nowtime' is 0 to 4 sec after 'closetime'.
    if (fabs(nowtime - closetime + 2) <= 2)
    {
        if (strcmp(status,"Close skipped") != 0)
        {
            if (strcmp(status,"Skipping next") != 0)
                CloseDoor("");
            else
            {
                // Skipped door closing. Reset 'Skipping Next' status
                strcpy(status, "Close skipped");
                PubStatus();
            }
        }
        // After test completes, reset opentime and closetime to saved values.
        if (testing)
        {
            opentime = savedopen;
            closetime = savedclose;
            TimesTxt();
            testing = false;
        }
    }

    if (strcmp(status,"Reset") == 0)
    {
        delay(1000);
        System.reset();
    }

    // Check for open or close request.
    if (strcmp(status,"Opening") == 0)
    {
        digitalWrite(POWERRY, LOW);
        delay(500);
        digitalWrite(OPCLRYS, HIGH);
        delay(200);
        digitalWrite(POWERRY, HIGH);
        //Report Status = "Opening"
        PubStatus();
        // Wait 'RUNTIME' seconds for the door to fully open
        delay(RUNTIME *1000);

        // Set relays OFF to save battery power
        digitalWrite(POWERRY, LOW);
        delay(500);
        digitalWrite(OPCLRYS, LOW);
        //Report Result = "Open complete"
        strcpy(status, "Open complete");
        if (USESMS)
            PubSMS();
        else
            PubStatus();
    }

    if (strcmp(status,"Closing") == 0)
    {
        digitalWrite(POWERRY, LOW);
        delay(500);
        digitalWrite(OPCLRYS, LOW);
        delay(200);
        digitalWrite(POWERRY, HIGH);
        //Report Status = "Closing"
        PubStatus();
        // Wait 'RUNTIME' seconds for the door to fully close
        delay(RUNTIME *1000);
        // Set relay OFF to save battery power
        digitalWrite(POWERRY, LOW);
        //Report Result = "Close complete"
        strcpy(status, "Close complete");
        if (USESMS)
            PubSMS();
        else
            PubStatus();
    }

    if (strcmp(status,"Lifting") == 0)
    {
        digitalWrite(POWERRY, LOW);
        delay(500);
        digitalWrite(OPCLRYS, LOW);
        delay(200);
        digitalWrite(POWERRY, HIGH);
        //Report Status = "Lifting"
        PubStatus();
        // Wait 10 seconds for the door to lift
        delay(10 *1000);
        // Set relay OFF to save battery power
        digitalWrite(POWERRY, LOW);
        //Report Result = "Lift complete"
        strcpy(status, "Lift complete");
        PubStatus();
    }

    if (Time.second(nowtime) == 00)
    {
        batVolts = VBatt();
        if (batVolts < 12.0)
        {
            if (!warnSent)
            {
                if (Particle.connected())
                    Particle.publish(SMSADD, BATTMSG, 60, PRIVATE);
                
                warnSent = true;
            }
        }
        else if (batVolts > 12.2)
            warnSent = false;
    }
}  // end of loop

// Publish status to the Cloud
int PubStatus()
{
    if (Particle.connected()) {
        Particle.publish("Status", status, 60, PRIVATE);
        return 1;
    }
    else
        return -1;
}

// Publish status vis SMS
int PubSMS()
{
    if (Particle.connected()) {
        Particle.publish(SMSADD, status, 60, PRIVATE);
        return 1;
    }
    else
        return -1;
}

// Open coop door
int OpenDoor(String command)
{
    strcpy(status, "Opening");
    return 1;
}

// Close coop door
int CloseDoor(String command)
{
    strcpy(status, "Closing");
    return 1;
}

// Lift coop door
int LiftDoor(String command)
{
    strcpy(status, "Lifting");
    return 1;
}

// Skip next event
int SkipNext(String command)
{
    strcpy(status, "Skipping next");
    PubStatus();
    return 1;
}

// Check battery voltage
double VBatt()
{
    // Average 10 readings
    int total = 0;
    for (int i = 0; i < 10; i++)
    {
        int reading = analogRead(ADCPIN);
        total += reading;
        delay(100);
    }
    return round(total * VCAL/40.950)/1000; // Report to nearest milliVolt.
}

/* For testing, 'opentime' is set to 'nowtime' +10 sec and
'closetime' to 10 sec after the end of the opening cycle. */
int DoTest(String command)
{
    savedopen = opentime;
    savedclose = closetime;
    opentime = nowtime + 10;
    closetime = opentime + RUNTIME + 10;
    TimesTxt();
    testing = true;
    return 1;
}

// Compute the UTC times for the next Open and Close events.
void FindTimes()
{   nowday =(int)(nowtime/SPD)*SPD;
    isDST = IsDST();
    strcpy(nowstr, Time.format(nowtime + (TZ + isDST) *SPH, TIMEFMT));
    if (closetime < (nowtime - SPD))  // If needed, make first approximations for times.
    {
        // opentime to 06:00 local
        opentime = nowday + (6 - TZ - isDST) *SPH; // UTC Unix
        // closetime to 18:00 local
        closetime = nowday + (18 - TZ - isDST) *SPH; // UTC Unix
    }
    else if (closetime < nowtime)
    {
        // Opentime and closetime have passed so increment initial event times by 1 day.
        opentime = opentime + SPD;
        closetime = closetime + SPD;
    }
    findOpen = true;
    opentime = EventTime();
    opentime = EventTime();
    findOpen = false;
    closetime = EventTime();
    closetime = EventTime();
    TimesTxt();
}

// Given 'findOpen' compute the UTC time to execute open or close.
double EventTime()
{
    double eotTime;
    double eventElev;

    if (findOpen)
    {
        eotTime = opentime;
        eventElev = OPENELEV;
    }
    else
    {
        eotTime = closetime;
        eventElev = CLOSEELEV;
    }
    // Calculating EOT, the Equation Of Time, related to the Earth's annual orbit around the Sun
    double JC=(eotTime/SPD-10957.5)/36525; // "Julian Century" as defined for Earth orbital calculations
    double GML=fmod(4.895063+JC*(628.3319668+JC*0.000005292),_2PI); // Sun Geometric Mean Longitude, radians
    double GMA=6.24+JC*(628.3); // Sun Geometric Mean Anomoly, radians
    double Ecc=0.016708634-JC*(0.000042037+0.0000001267*JC); // Earth orbit eccentricity
    //Sun's Equation of Center, radians
    double EqOC=(sin(GMA)*(0.03341611-JC*(0.000084073+0.000000244*JC))+sin((2*GMA))*(0.0003489-0.00000176*JC)+sin((3*GMA))*0.000005044);
    double STlong=GML+EqOC; // Sun's True Longitude, radians
    double SApLong=STlong+(-0.0000993-0.0000834*sin(2.1824-33.75704*JC)); // Sun's Apparent Longitude, radians
    double MObEc=(0.401425+(26+((21.488-JC*(46.815+JC*(0.00059-JC*0.001813))))/60.0)/3437.75); // Mean Obliquity of the Ecliptic, radians
    double ObCor=MObEc+0.00004468*cos((2.1824-33.75704*JC)); // Obliquity Correction, radians
    double Sdecl=(asin(sin(ObCor)*sin(SApLong))); // Sun's Declination, radians
    double Var_y=tan(ObCor/2)*tan(ObCor/2);
    // EOT is the difference between clock time and Sun time in minutes.
    double EOT=(4*RAD2DEG)*(Var_y*sin(2*GML)-2*Ecc*sin(GMA)+4*Ecc*Var_y*sin(GMA)*cos(2*GML)-0.5*Var_y*Var_y*sin(4*GML)-1.25*Ecc*Ecc*sin(2*GMA));

    // Calculations relating to the Earth's daily rotation
    double SNoon=SPD/2 - (4 *LONGITUDE + EOT - TZ *MPH) *SPM; // Solar Noon, seconds after midnight, UTC.
    // Hour Angle of the Sun's center for the event.
    double eventHA=(acos(cos(DEG2RAD*(90-eventElev))/(cos(LAT)*cos(Sdecl))-tan(LAT)*tan(Sdecl))); // Radians
    double eventTime=SNoon + (1-2 *findOpen) *RAD2DEG *eventHA *4 *SPM - TZ *SPH; // Seconds after midnight, UTC.
    return eventTime + nowday;  // UTC Unix date+time timestamp
}

// Create strings to report event times using format 'TIMEFMT'.
void TimesTxt()
{
    strcpy(opentimeTxt, Time.format(opentime + (TZ + isDST) *SPH, TIMEFMT)); //Local opentime as text
    strcpy(closetimeTxt, Time.format(closetime + (TZ + isDST) *SPH, TIMEFMT)); //Local closetime as text
}
int DoReset(String command)
{
    strcpy(status, "Reset");
    return 1;
}

// Returns true if times should be displayed as DST.
// Changes on the 2nd Sunday in March and the 1st Sunday in November.
bool IsDST()
{
    int dayOfMonth = Time.day(nowtime);
    int month = Time.month(nowtime);
    int dayOfWeek = Time.weekday(nowtime);
    int previousSunday = dayOfMonth - dayOfWeek + 1;
    if (month < 3 || month > 11)
        return false;
    if (month > 3 && month < 11)
        return true;
    if (month == 3)
        return previousSunday >= 8;
    return previousSunday <= 0;
}

// end of file

Or view it at:
https://go.particle.io/shared_apps/5cc5c17602577e00168e957e

Chickdoor has been running now for over a month and seems to be doing what it should.
After compiling, it is using < 30% of Flash memory and < 11% of RAM and executes loop() about 1200 times per second. I am still not sure enough about memory allocation to be certain that my frequent use of string variables isn’t messing up the stack, though I am guessing that it isn’t.

Apologies for all the commenting, but at 79, I need a little help to remember what I did last week, let alone last month. In time I will likely organize this on GitHub, but for now just learning C has been enough of a challenge. A few of the reasons for doing things the way I did are in the overview, but being a newbie at this stuff, I welcome any comments regarding what isn’t necessary or how I could do things in a more elegant fashion. Also please let me know if anything is missing or unclear in the documentation pages.

Packaging
The Chickdoor hardware is contained in a waterproof project box, with the construction details at:
https://drive.google.com/file/d/1IJKQfijR3NriY_r-s88aT5Vd6_F30jCU/view?usp=sharing

I am hoping that someone might be able to adapt my mounting bracket design to be able to be made on a 3-D printer; machining them isn’t easy.


#2

@brettn, cool project! Any reason you went with String(status) in you tests instead of using the c-string strcmp() function? Also, in your Particle.publish() calls, since status is alread a NULL-terminated char string, you don’t need to convert it to using String(). Simply use status since, by definition, is a string pointer.

BTW, thanks for sharing!!


#3

@peekay123 Yes, because I didn’t know any better; I just found something that seemed to work. And thanks for the suggestions.

OK I changed those and it’s still working fine.